project_archive.test.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. 'use strict'
  2. const _ = require('lodash')
  3. const BPromise = require('bluebird')
  4. const { expect } = require('chai')
  5. const fs = BPromise.promisifyAll(require('node:fs'))
  6. const sinon = require('sinon')
  7. const stream = require('node:stream')
  8. const temp = require('temp')
  9. const cleanup = require('./support/cleanup')
  10. const fixtures = require('./support/fixtures')
  11. const testFiles = require('./support/test_files')
  12. const unzip = require('./support/unzip')
  13. const core = require('overleaf-editor-core')
  14. const File = core.File
  15. const Snapshot = core.Snapshot
  16. const storage = require('../../../../storage')
  17. const BlobStore = storage.BlobStore
  18. const ProjectArchive = storage.ProjectArchive
  19. describe('ProjectArchive', function () {
  20. beforeEach(cleanup.everything)
  21. beforeEach(fixtures.create)
  22. const projectId = '123'
  23. const blobStore = new BlobStore(projectId)
  24. let zipFilePath
  25. beforeEach(function () {
  26. zipFilePath = temp.path({ suffix: '.zip' })
  27. })
  28. afterEach(function () {
  29. return fs.unlinkAsync(zipFilePath).catch(() => {})
  30. })
  31. function makeMixedTestSnapshot(rounds) {
  32. const snapshot = new Snapshot()
  33. return blobStore.putFile(testFiles.path('graph.png')).then(() => {
  34. _.times(rounds, i => {
  35. snapshot.addFile('test' + i + '.txt', File.fromString('test'))
  36. snapshot.addFile(
  37. 'graph' + i + '.png',
  38. File.fromHash(testFiles.GRAPH_PNG_HASH)
  39. )
  40. })
  41. return snapshot
  42. })
  43. }
  44. function makeTextTestSnapshot(rounds) {
  45. const snapshot = new Snapshot()
  46. _.times(rounds, i => {
  47. snapshot.addFile('test' + i + '.txt', File.fromString('test'))
  48. })
  49. return snapshot
  50. }
  51. it('archives a small snapshot with binary and text data', function () {
  52. return makeMixedTestSnapshot(1)
  53. .then(snapshot => {
  54. const projectArchive = new ProjectArchive(snapshot, 25_000)
  55. return projectArchive.writeZip(blobStore, zipFilePath)
  56. })
  57. .then(() => {
  58. return unzip.getZipEntries(zipFilePath)
  59. })
  60. .then(zipEntries => {
  61. expect(zipEntries).to.have.length(2)
  62. zipEntries = _.sortBy(zipEntries, 'fileName')
  63. expect(zipEntries[0].fileName).to.equal('graph0.png')
  64. expect(zipEntries[0].uncompressedSize).to.equal(
  65. testFiles.GRAPH_PNG_BYTE_LENGTH
  66. )
  67. expect(zipEntries[1].fileName).to.equal('test0.txt')
  68. expect(zipEntries[1].uncompressedSize).to.equal(4)
  69. })
  70. })
  71. it('archives a larger snapshot with binary and text data', function () {
  72. return makeMixedTestSnapshot(10)
  73. .then(snapshot => {
  74. const projectArchive = new ProjectArchive(snapshot, 25_000)
  75. return projectArchive.writeZip(blobStore, zipFilePath)
  76. })
  77. .then(() => {
  78. return unzip.getZipEntries(zipFilePath)
  79. })
  80. .then(zipEntries => {
  81. expect(zipEntries).to.have.length(20)
  82. })
  83. })
  84. it('archives empty files', function () {
  85. const snapshot = new Snapshot()
  86. snapshot.addFile('test0', File.fromString(''))
  87. snapshot.addFile('test1', File.fromHash(File.EMPTY_FILE_HASH))
  88. return blobStore
  89. .putString('')
  90. .then(() => {
  91. const projectArchive = new ProjectArchive(snapshot, 25_000)
  92. return projectArchive.writeZip(blobStore, zipFilePath)
  93. })
  94. .then(() => {
  95. return unzip.getZipEntries(zipFilePath)
  96. })
  97. .then(zipEntries => {
  98. zipEntries = _.sortBy(zipEntries, 'fileName')
  99. expect(zipEntries[0].fileName).to.equal('test0')
  100. expect(zipEntries[0].uncompressedSize).to.equal(0)
  101. expect(zipEntries[1].fileName).to.equal('test1')
  102. expect(zipEntries[1].uncompressedSize).to.equal(0)
  103. })
  104. })
  105. describe('with a blob stream download error', function () {
  106. beforeEach(function () {
  107. const testStream = new stream.Readable({
  108. read: function () {
  109. testStream.destroy(new Error('test read error'))
  110. },
  111. })
  112. sinon.stub(blobStore, 'getStream').resolves(testStream)
  113. })
  114. afterEach(function () {
  115. blobStore.getStream.restore()
  116. })
  117. it('rejects with the error', function () {
  118. return makeMixedTestSnapshot(1)
  119. .then(snapshot => {
  120. const projectArchive = new ProjectArchive(snapshot, 25_000)
  121. return projectArchive.writeZip(blobStore, zipFilePath)
  122. })
  123. .then(() => {
  124. expect.fail()
  125. })
  126. .catch(err => {
  127. let message = err.message
  128. if (err instanceof ProjectArchive.DownloadError) {
  129. message = err.cause.message
  130. }
  131. expect(message).to.match(/test read error/)
  132. })
  133. })
  134. })
  135. describe('with zip write error', function () {
  136. beforeEach(function () {
  137. sinon.stub(fs, 'createWriteStream').callsFake(path => {
  138. const testStream = new stream.Writable({
  139. write: function (chunk, encoding, callback) {
  140. callback(new Error('test write error'))
  141. },
  142. })
  143. return testStream
  144. })
  145. })
  146. afterEach(function () {
  147. fs.createWriteStream.restore()
  148. })
  149. it('rejects with the error', function () {
  150. return makeMixedTestSnapshot(1)
  151. .then(snapshot => {
  152. const projectArchive = new ProjectArchive(snapshot, 25_000)
  153. return projectArchive.writeZip(blobStore, zipFilePath)
  154. })
  155. .then(() => {
  156. expect.fail()
  157. })
  158. .catch(err => {
  159. expect(err.message).to.equal('test write error')
  160. })
  161. })
  162. })
  163. describe('with a delayed file load', function () {
  164. beforeEach(function () {
  165. sinon.stub(File.prototype, 'load').callsFake(function () {
  166. return BPromise.delay(200).thenReturn(this)
  167. })
  168. })
  169. afterEach(function () {
  170. File.prototype.load.restore()
  171. })
  172. it('times out', function () {
  173. const snapshot = makeTextTestSnapshot(10)
  174. const projectArchive = new ProjectArchive(snapshot, 100)
  175. return projectArchive
  176. .writeZip(blobStore, zipFilePath)
  177. .then(() => {
  178. expect.fail()
  179. })
  180. .catch(err => {
  181. expect(err.name).to.equal('ArchiveTimeout')
  182. })
  183. })
  184. })
  185. })