OutputFileArchiveManagerTests.js 6.5 KB

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