OutputFileArchiveManagerTests.js 6.6 KB

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