OutputFileArchiveManager.test.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { vi, assert, expect, describe, afterEach, beforeEach, it } from 'vitest'
  2. import sinon from 'sinon'
  3. import path from 'node:path'
  4. const MODULE_PATH = path.join(
  5. import.meta.dirname,
  6. '../../../app/js/OutputFileArchiveManager'
  7. )
  8. describe('OutputFileArchiveManager', () => {
  9. const userId = 'user-id-123'
  10. const projectId = 'project-id-123'
  11. const buildId = 'build-id-123'
  12. afterEach(() => {
  13. sinon.restore()
  14. })
  15. beforeEach(async ctx => {
  16. ctx.OutputFileFinder = {
  17. promises: {
  18. findOutputFiles: sinon.stub().resolves({ outputFiles: [] }),
  19. },
  20. }
  21. ctx.OutputCacheManger = {
  22. path: sinon.stub().callsFake((build, path) => {
  23. return `${build}/${path}`
  24. }),
  25. }
  26. ctx.archive = {
  27. append: sinon.stub(),
  28. finalize: sinon.stub().resolves(),
  29. on: sinon.stub(),
  30. }
  31. ctx.archiver = sinon.stub().returns(ctx.archive)
  32. ctx.outputDir = '/output/dir'
  33. ctx.fs = {
  34. open: sinon.stub().callsFake(file => ({
  35. createReadStream: sinon.stub().returns(`handle: ${file}`),
  36. })),
  37. }
  38. vi.doMock('../../../app/js/OutputFileFinder', () => ({
  39. default: ctx.OutputFileFinder,
  40. }))
  41. vi.doMock('../../../app/js/OutputCacheManager', () => ({
  42. default: ctx.OutputCacheManger,
  43. }))
  44. vi.doMock('archiver', () => ({
  45. default: ctx.archiver,
  46. }))
  47. vi.doMock('node:fs/promises', () => ctx.fs)
  48. vi.doMock('@overleaf/settings', () => ({
  49. default: {
  50. path: {
  51. outputDir: ctx.outputDir,
  52. },
  53. },
  54. }))
  55. ctx.OutputFileArchiveManager = (await import(MODULE_PATH)).default
  56. })
  57. describe('when the output cache directory contains only exportable files', () => {
  58. beforeEach(async ctx => {
  59. ctx.OutputFileFinder.promises.findOutputFiles.resolves({
  60. outputFiles: [
  61. { path: 'file_1' },
  62. { path: 'file_2' },
  63. { path: 'file_3' },
  64. { path: 'file_4' },
  65. ],
  66. })
  67. await ctx.OutputFileArchiveManager.archiveFilesForBuild(
  68. projectId,
  69. userId,
  70. buildId
  71. )
  72. })
  73. it('creates a zip archive', ctx => {
  74. sinon.assert.calledWith(ctx.archiver, 'zip')
  75. })
  76. it('listens to errors from the archive', ctx => {
  77. sinon.assert.calledWith(ctx.archive.on, 'error', sinon.match.func)
  78. })
  79. it('adds all the output files to the archive', ctx => {
  80. expect(ctx.archive.append.callCount).to.equal(4)
  81. sinon.assert.calledWith(
  82. ctx.archive.append,
  83. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
  84. sinon.match({ name: 'file_1' })
  85. )
  86. sinon.assert.calledWith(
  87. ctx.archive.append,
  88. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
  89. sinon.match({ name: 'file_2' })
  90. )
  91. sinon.assert.calledWith(
  92. ctx.archive.append,
  93. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
  94. sinon.match({ name: 'file_3' })
  95. )
  96. sinon.assert.calledWith(
  97. ctx.archive.append,
  98. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
  99. sinon.match({ name: 'file_4' })
  100. )
  101. })
  102. it('finalizes the archive after all files are appended', ctx => {
  103. sinon.assert.called(ctx.archive.finalize)
  104. expect(ctx.archive.finalize.calledBefore(ctx.archive.append)).to.be.false
  105. })
  106. })
  107. describe('when the directory includes files ignored by web', () => {
  108. beforeEach(async ctx => {
  109. ctx.OutputFileFinder.promises.findOutputFiles.resolves({
  110. outputFiles: [
  111. { path: 'file_1' },
  112. { path: 'file_2' },
  113. { path: 'file_3' },
  114. { path: 'file_4' },
  115. { path: 'output.pdf' },
  116. ],
  117. })
  118. await ctx.OutputFileArchiveManager.archiveFilesForBuild(
  119. projectId,
  120. userId,
  121. buildId
  122. )
  123. })
  124. it('only includes the non-ignored files in the archive', ctx => {
  125. expect(ctx.archive.append.callCount).to.equal(4)
  126. sinon.assert.calledWith(
  127. ctx.archive.append,
  128. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
  129. sinon.match({ name: 'file_1' })
  130. )
  131. sinon.assert.calledWith(
  132. ctx.archive.append,
  133. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
  134. sinon.match({ name: 'file_2' })
  135. )
  136. sinon.assert.calledWith(
  137. ctx.archive.append,
  138. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
  139. sinon.match({ name: 'file_3' })
  140. )
  141. sinon.assert.calledWith(
  142. ctx.archive.append,
  143. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
  144. sinon.match({ name: 'file_4' })
  145. )
  146. })
  147. })
  148. describe('when one of the files is called output.pdf', () => {
  149. beforeEach(async ctx => {
  150. ctx.OutputFileFinder.promises.findOutputFiles.resolves({
  151. outputFiles: [
  152. { path: 'file_1' },
  153. { path: 'file_2' },
  154. { path: 'file_3' },
  155. { path: 'file_4' },
  156. { path: 'output.pdf' },
  157. ],
  158. })
  159. await ctx.OutputFileArchiveManager.archiveFilesForBuild(
  160. projectId,
  161. userId,
  162. buildId
  163. )
  164. })
  165. it('does not include that file in the archive', ctx => {
  166. expect(ctx.archive.append.callCount).to.equal(4)
  167. sinon.assert.calledWith(
  168. ctx.archive.append,
  169. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
  170. sinon.match({ name: 'file_1' })
  171. )
  172. sinon.assert.calledWith(
  173. ctx.archive.append,
  174. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
  175. sinon.match({ name: 'file_2' })
  176. )
  177. sinon.assert.calledWith(
  178. ctx.archive.append,
  179. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
  180. sinon.match({ name: 'file_3' })
  181. )
  182. sinon.assert.calledWith(
  183. ctx.archive.append,
  184. `handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
  185. sinon.match({ name: 'file_4' })
  186. )
  187. })
  188. })
  189. describe('when the output directory cannot be accessed', () => {
  190. beforeEach(async ctx => {
  191. ctx.OutputFileFinder.promises.findOutputFiles.rejects({
  192. code: 'ENOENT',
  193. })
  194. })
  195. it('rejects with a NotFoundError', async ctx => {
  196. try {
  197. await ctx.OutputFileArchiveManager.archiveFilesForBuild(
  198. projectId,
  199. userId,
  200. buildId
  201. )
  202. assert.fail('should have thrown a NotFoundError')
  203. } catch (err) {
  204. expect(err).to.haveOwnProperty('name', 'NotFoundError')
  205. }
  206. })
  207. it('does not create an archive', ctx => {
  208. expect(ctx.archiver.called).to.be.false
  209. })
  210. })
  211. })