ProjectZipStreamManager.test.mjs 15 KB

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