ProjectZipStreamManager.test.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import { vi } from 'vitest'
  2. // TODO: This file was created by bulk-decaffeinate.
  3. // Fix any style issues and re-enable lint.
  4. /*
  5. * decaffeinate suggestions:
  6. * DS101: Remove unnecessary use of Array.from
  7. * DS102: Remove unnecessary code created because of implicit returns
  8. * DS201: Simplify complex destructure assignments
  9. * DS205: Consider reworking code to avoid use of IIFEs
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  11. */
  12. import sinon from 'sinon'
  13. import { EventEmitter } from 'events'
  14. const modulePath =
  15. '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs'
  16. describe('ProjectZipStreamManager', function () {
  17. beforeEach(async function (ctx) {
  18. ctx.project_id = 'project-id-123'
  19. ctx.callback = sinon.stub()
  20. ctx.archive = {
  21. on() {},
  22. append: sinon.stub(),
  23. }
  24. ctx.logger = {
  25. error: sinon.stub(),
  26. info: sinon.stub(),
  27. debug: sinon.stub(),
  28. }
  29. vi.doMock('archiver', () => ({
  30. default: (ctx.archiver = sinon.stub().returns(ctx.archive)),
  31. }))
  32. vi.doMock('@overleaf/logger', () => ({
  33. default: ctx.logger,
  34. }))
  35. vi.doMock(
  36. '../../../../app/src/Features/Project/ProjectEntityHandler',
  37. () => ({
  38. default: (ctx.ProjectEntityHandler = {}),
  39. })
  40. )
  41. vi.doMock('../../../../app/src/Features/History/HistoryManager.js', () => ({
  42. default: (ctx.HistoryManager = {}),
  43. }))
  44. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  45. default: (ctx.ProjectGetter = {}),
  46. }))
  47. vi.doMock(
  48. '../../../../app/src/Features/FileStore/FileStoreHandler',
  49. () => ({
  50. default: (ctx.FileStoreHandler = {}),
  51. })
  52. )
  53. ctx.ProjectZipStreamManager = (await import(modulePath)).default
  54. })
  55. describe('createZipStreamForMultipleProjects', function () {
  56. describe('successfully', function () {
  57. beforeEach(async function (ctx) {
  58. await new Promise(resolve => {
  59. ctx.project_ids = ['project-1', 'project-2']
  60. ctx.zip_streams = {
  61. 'project-1': new EventEmitter(),
  62. 'project-2': new EventEmitter(),
  63. }
  64. ctx.project_names = {
  65. 'project-1': 'Project One Name',
  66. 'project-2': 'Project Two Name',
  67. }
  68. ctx.ProjectZipStreamManager.createZipStreamForProject = (
  69. projectId,
  70. callback
  71. ) => {
  72. callback(null, ctx.zip_streams[projectId])
  73. setTimeout(() => {
  74. return ctx.zip_streams[projectId].emit('end')
  75. })
  76. return 0
  77. }
  78. sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject')
  79. ctx.ProjectGetter.getProject = (projectId, fields, callback) => {
  80. return callback(null, { name: ctx.project_names[projectId] })
  81. }
  82. sinon.spy(ctx.ProjectGetter, 'getProject')
  83. ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects(
  84. ctx.project_ids,
  85. (...args) => {
  86. return ctx.callback(...Array.from(args || []))
  87. }
  88. )
  89. return (ctx.archive.finalize = () => resolve())
  90. })
  91. })
  92. it('should create a zip archive', function (ctx) {
  93. return ctx.archiver.calledWith('zip').should.equal(true)
  94. })
  95. it('should return a stream before any processing is done', function (ctx) {
  96. ctx.callback
  97. .calledWith(sinon.match.falsy, ctx.archive)
  98. .should.equal(true)
  99. return ctx.callback
  100. .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject)
  101. .should.equal(true)
  102. })
  103. it('should get a zip stream for all of the projects', function (ctx) {
  104. return Array.from(ctx.project_ids).map(projectId =>
  105. ctx.ProjectZipStreamManager.createZipStreamForProject
  106. .calledWith(projectId)
  107. .should.equal(true)
  108. )
  109. })
  110. it('should get the names of each project', function (ctx) {
  111. return Array.from(ctx.project_ids).map(projectId =>
  112. ctx.ProjectGetter.getProject
  113. .calledWith(projectId, { name: true })
  114. .should.equal(true)
  115. )
  116. })
  117. it('should add all of the projects to the zip', function (ctx) {
  118. return Array.from(ctx.project_ids).map(projectId =>
  119. ctx.archive.append
  120. .calledWith(ctx.zip_streams[projectId], {
  121. name: ctx.project_names[projectId] + '.zip',
  122. })
  123. .should.equal(true)
  124. )
  125. })
  126. })
  127. describe('with a project not existing', function () {
  128. beforeEach(async function (ctx) {
  129. await new Promise(resolve => {
  130. ctx.project_ids = ['project-1', 'wrong-id']
  131. ctx.project_names = {
  132. 'project-1': 'Project One Name',
  133. }
  134. ctx.zip_streams = {
  135. 'project-1': new EventEmitter(),
  136. }
  137. ctx.ProjectZipStreamManager.createZipStreamForProject = (
  138. projectId,
  139. callback
  140. ) => {
  141. callback(null, ctx.zip_streams[projectId])
  142. setTimeout(() => {
  143. ctx.zip_streams[projectId].emit('end')
  144. })
  145. }
  146. sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject')
  147. ctx.ProjectGetter.getProject = (projectId, fields, callback) => {
  148. const name = ctx.project_names[projectId]
  149. callback(null, name ? { name } : undefined)
  150. }
  151. sinon.spy(ctx.ProjectGetter, 'getProject')
  152. ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects(
  153. ctx.project_ids,
  154. ctx.callback
  155. )
  156. ctx.archive.finalize = () => resolve()
  157. })
  158. })
  159. it('should create a zip archive', function (ctx) {
  160. ctx.archiver.calledWith('zip').should.equal(true)
  161. })
  162. it('should return a stream before any processing is done', function (ctx) {
  163. ctx.callback
  164. .calledWith(sinon.match.falsy, ctx.archive)
  165. .should.equal(true)
  166. ctx.callback
  167. .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject)
  168. .should.equal(true)
  169. })
  170. it('should get the names of each project', function (ctx) {
  171. ctx.project_ids.map(projectId =>
  172. ctx.ProjectGetter.getProject
  173. .calledWith(projectId, { name: true })
  174. .should.equal(true)
  175. )
  176. })
  177. it('should get a zip stream only for the existing project', function (ctx) {
  178. ctx.ProjectZipStreamManager.createZipStreamForProject
  179. .calledWith('project-1')
  180. .should.equal(true)
  181. ctx.ProjectZipStreamManager.createZipStreamForProject
  182. .calledWith('wrong-id')
  183. .should.equal(false)
  184. })
  185. it('should only add the existing project to the zip', function (ctx) {
  186. sinon.assert.calledOnce(ctx.archive.append)
  187. ctx.archive.append
  188. .calledWith(ctx.zip_streams['project-1'], {
  189. name: ctx.project_names['project-1'] + '.zip',
  190. })
  191. .should.equal(true)
  192. })
  193. })
  194. })
  195. describe('createZipStreamForProject', function () {
  196. describe('successfully', function () {
  197. beforeEach(function (ctx) {
  198. ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon
  199. .stub()
  200. .callsArg(2)
  201. ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon
  202. .stub()
  203. .callsArg(2)
  204. ctx.archive.finalize = sinon.stub()
  205. return ctx.ProjectZipStreamManager.createZipStreamForProject(
  206. ctx.project_id,
  207. ctx.callback
  208. )
  209. })
  210. it('should create a zip archive', function (ctx) {
  211. return ctx.archiver.calledWith('zip').should.equal(true)
  212. })
  213. it('should return a stream before any processing is done', function (ctx) {
  214. ctx.callback
  215. .calledWith(sinon.match.falsy, ctx.archive)
  216. .should.equal(true)
  217. ctx.callback
  218. .calledBefore(ctx.ProjectZipStreamManager.addAllDocsToArchive)
  219. .should.equal(true)
  220. return ctx.callback
  221. .calledBefore(ctx.ProjectZipStreamManager.addAllFilesToArchive)
  222. .should.equal(true)
  223. })
  224. it('should add all of the project docs to the zip', function (ctx) {
  225. return ctx.ProjectZipStreamManager.addAllDocsToArchive
  226. .calledWith(ctx.project_id, ctx.archive)
  227. .should.equal(true)
  228. })
  229. it('should add all of the project files to the zip', function (ctx) {
  230. return ctx.ProjectZipStreamManager.addAllFilesToArchive
  231. .calledWith(ctx.project_id, ctx.archive)
  232. .should.equal(true)
  233. })
  234. it('should finalise the stream', function (ctx) {
  235. return ctx.archive.finalize.called.should.equal(true)
  236. })
  237. })
  238. describe('with an error adding docs', function () {
  239. beforeEach(function (ctx) {
  240. ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon
  241. .stub()
  242. .callsArgWith(2, new Error('something went wrong'))
  243. ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon
  244. .stub()
  245. .callsArg(2)
  246. ctx.archive.finalize = sinon.stub()
  247. ctx.ProjectZipStreamManager.createZipStreamForProject(
  248. ctx.project_id,
  249. ctx.callback
  250. )
  251. })
  252. it('should log out an error', function (ctx) {
  253. return ctx.logger.error
  254. .calledWith(sinon.match.any, 'error adding docs to zip stream')
  255. .should.equal(true)
  256. })
  257. it('should continue with the process', function (ctx) {
  258. ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal(
  259. true
  260. )
  261. ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal(
  262. true
  263. )
  264. return ctx.archive.finalize.called.should.equal(true)
  265. })
  266. })
  267. describe('with an error adding files', function () {
  268. beforeEach(function (ctx) {
  269. ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon
  270. .stub()
  271. .callsArg(2)
  272. ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon
  273. .stub()
  274. .callsArgWith(2, new Error('something went wrong'))
  275. ctx.archive.finalize = sinon.stub()
  276. return ctx.ProjectZipStreamManager.createZipStreamForProject(
  277. ctx.project_id,
  278. ctx.callback
  279. )
  280. })
  281. it('should log out an error', function (ctx) {
  282. return ctx.logger.error
  283. .calledWith(sinon.match.any, 'error adding files to zip stream')
  284. .should.equal(true)
  285. })
  286. it('should continue with the process', function (ctx) {
  287. ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal(
  288. true
  289. )
  290. ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal(
  291. true
  292. )
  293. return ctx.archive.finalize.called.should.equal(true)
  294. })
  295. })
  296. })
  297. describe('addAllDocsToArchive', function () {
  298. beforeEach(async function (ctx) {
  299. await new Promise(resolve => {
  300. ctx.docs = {
  301. '/main.tex': {
  302. lines: [
  303. '\\documentclass{article}',
  304. '\\begin{document}',
  305. 'Hello world',
  306. '\\end{document}',
  307. ],
  308. },
  309. '/chapters/chapter1.tex': {
  310. lines: ['chapter1', 'content'],
  311. },
  312. }
  313. ctx.ProjectEntityHandler.getAllDocs = sinon
  314. .stub()
  315. .callsArgWith(1, null, ctx.docs)
  316. return ctx.ProjectZipStreamManager.addAllDocsToArchive(
  317. ctx.project_id,
  318. ctx.archive,
  319. error => {
  320. ctx.callback(error)
  321. return resolve()
  322. }
  323. )
  324. })
  325. })
  326. it('should get the docs for the project', function (ctx) {
  327. return ctx.ProjectEntityHandler.getAllDocs
  328. .calledWith(ctx.project_id)
  329. .should.equal(true)
  330. })
  331. it('should add each doc to the archive', function (ctx) {
  332. return (() => {
  333. const result = []
  334. for (let path in ctx.docs) {
  335. const doc = ctx.docs[path]
  336. path = path.slice(1) // remove "/"
  337. result.push(
  338. ctx.archive.append
  339. .calledWith(doc.lines.join('\n'), { name: path })
  340. .should.equal(true)
  341. )
  342. }
  343. return result
  344. })()
  345. })
  346. })
  347. describe('addAllFilesToArchive', function () {
  348. beforeEach(function (ctx) {
  349. ctx.files = {
  350. '/image.png': {
  351. _id: 'file-id-1',
  352. hash: 'abc',
  353. },
  354. '/folder/picture.png': {
  355. _id: 'file-id-2',
  356. hash: 'def',
  357. },
  358. }
  359. ctx.streams = {
  360. abc: new EventEmitter(),
  361. def: new EventEmitter(),
  362. }
  363. ctx.ProjectEntityHandler.getAllFiles = sinon
  364. .stub()
  365. .callsArgWith(1, null, ctx.files)
  366. ctx.HistoryManager.requestBlobWithProjectId = (
  367. projectId,
  368. hash,
  369. callback
  370. ) => {
  371. return callback(null, { stream: ctx.streams[hash] })
  372. }
  373. sinon.spy(ctx.HistoryManager, 'requestBlobWithProjectId')
  374. ctx.ProjectZipStreamManager.addAllFilesToArchive(
  375. ctx.project_id,
  376. ctx.archive,
  377. ctx.callback
  378. )
  379. for (const hash in ctx.streams) {
  380. const stream = ctx.streams[hash]
  381. stream.emit('end')
  382. }
  383. })
  384. it('should get the files for the project', function (ctx) {
  385. return ctx.ProjectEntityHandler.getAllFiles
  386. .calledWith(ctx.project_id)
  387. .should.equal(true)
  388. })
  389. it('should get a stream for each file', function (ctx) {
  390. for (const path in ctx.files) {
  391. const file = ctx.files[path]
  392. ctx.HistoryManager.requestBlobWithProjectId
  393. .calledWith(ctx.project_id, file.hash)
  394. .should.equal(true)
  395. }
  396. })
  397. it('should add each file to the archive', function (ctx) {
  398. for (let path in ctx.files) {
  399. const file = ctx.files[path]
  400. path = path.slice(1) // remove "/"
  401. ctx.archive.append.should.have.been.calledWith(ctx.streams[file.hash], {
  402. name: path,
  403. })
  404. }
  405. })
  406. })
  407. })