OutputFileArchiveManagerTests.js 6.5 KB

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