ProjectZipStreamManager.test.mjs 14 KB

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