ArchiveManagerTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /* eslint-disable
  2. n/handle-callback-err,
  3. max-len,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS206: Consider reworking classes to avoid initClass
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. const sinon = require('sinon')
  16. const { expect } = require('chai')
  17. const modulePath = '../../../../app/src/Features/Uploads/ArchiveManager.js'
  18. const ArchiveErrors = require('../../../../app/src/Features/Uploads/ArchiveErrors')
  19. const SandboxedModule = require('sandboxed-module')
  20. const events = require('events')
  21. describe('ArchiveManager', function () {
  22. beforeEach(function () {
  23. let Timer
  24. this.metrics = {
  25. Timer: (Timer = (function () {
  26. Timer = class Timer {
  27. static initClass() {
  28. this.prototype.done = sinon.stub()
  29. }
  30. }
  31. Timer.initClass()
  32. return Timer
  33. })()),
  34. }
  35. this.zipfile = new events.EventEmitter()
  36. this.zipfile.readEntry = sinon.stub()
  37. this.zipfile.close = sinon.stub()
  38. this.ArchiveManager = SandboxedModule.require(modulePath, {
  39. requires: {
  40. '@overleaf/settings': {},
  41. yauzl: (this.yauzl = {
  42. open: sinon.stub().callsArgWith(2, null, this.zipfile),
  43. }),
  44. '@overleaf/metrics': this.metrics,
  45. fs: (this.fs = { mkdir: sinon.stub().yields() }),
  46. './ArchiveErrors': ArchiveErrors,
  47. },
  48. })
  49. return (this.callback = sinon.stub())
  50. })
  51. describe('extractZipArchive', function () {
  52. beforeEach(function () {
  53. this.source = '/path/to/zip/source.zip'
  54. this.destination = '/path/to/zip/destination'
  55. return (this.ArchiveManager._isZipTooLarge = sinon
  56. .stub()
  57. .callsArgWith(1, null, false))
  58. })
  59. describe('successfully', function () {
  60. beforeEach(function (done) {
  61. this.readStream = new events.EventEmitter()
  62. this.readStream.pipe = sinon.stub()
  63. this.zipfile.openReadStream = sinon
  64. .stub()
  65. .callsArgWith(1, null, this.readStream)
  66. this.writeStream = new events.EventEmitter()
  67. this.fs.createWriteStream = sinon.stub().returns(this.writeStream)
  68. this.ArchiveManager.extractZipArchive(
  69. this.source,
  70. this.destination,
  71. done
  72. )
  73. // entry contains a single file
  74. this.zipfile.emit('entry', { fileName: 'testfile.txt' })
  75. this.readStream.emit('end')
  76. return this.zipfile.emit('end')
  77. })
  78. it('should run yauzl', function () {
  79. return this.yauzl.open.calledWith(this.source).should.equal(true)
  80. })
  81. it('should time the unzip', function () {
  82. return this.metrics.Timer.prototype.done.called.should.equal(true)
  83. })
  84. })
  85. describe('with a zipfile containing an empty directory', function () {
  86. beforeEach(function (done) {
  87. this.readStream = new events.EventEmitter()
  88. this.readStream.pipe = sinon.stub()
  89. this.zipfile.openReadStream = sinon
  90. .stub()
  91. .callsArgWith(1, null, this.readStream)
  92. this.writeStream = new events.EventEmitter()
  93. this.fs.createWriteStream = sinon.stub().returns(this.writeStream)
  94. this.ArchiveManager.extractZipArchive(
  95. this.source,
  96. this.destination,
  97. error => {
  98. this.callback(error)
  99. done()
  100. }
  101. )
  102. // entry contains a single, empty directory
  103. this.zipfile.emit('entry', { fileName: 'testdir/' })
  104. this.readStream.emit('end')
  105. return this.zipfile.emit('end')
  106. })
  107. it('should return the callback with an error', function () {
  108. return sinon.assert.calledWithExactly(
  109. this.callback,
  110. sinon.match.instanceOf(ArchiveErrors.EmptyZipFileError)
  111. )
  112. })
  113. })
  114. describe('with an empty zipfile', function () {
  115. beforeEach(function (done) {
  116. this.ArchiveManager.extractZipArchive(
  117. this.source,
  118. this.destination,
  119. error => {
  120. this.callback(error)
  121. return done()
  122. }
  123. )
  124. return this.zipfile.emit('end')
  125. })
  126. it('should return the callback with an error', function () {
  127. return sinon.assert.calledWithExactly(
  128. this.callback,
  129. sinon.match.instanceOf(ArchiveErrors.EmptyZipFileError)
  130. )
  131. })
  132. })
  133. describe('with an error in the zip file header', function () {
  134. beforeEach(function (done) {
  135. this.yauzl.open = sinon
  136. .stub()
  137. .callsArgWith(2, new ArchiveErrors.InvalidZipFileError())
  138. return this.ArchiveManager.extractZipArchive(
  139. this.source,
  140. this.destination,
  141. error => {
  142. this.callback(error)
  143. return done()
  144. }
  145. )
  146. })
  147. it('should return the callback with an error', function () {
  148. return sinon.assert.calledWithExactly(
  149. this.callback,
  150. sinon.match.instanceOf(ArchiveErrors.InvalidZipFileError)
  151. )
  152. })
  153. })
  154. describe('with a zip that is too large', function () {
  155. beforeEach(function (done) {
  156. this.ArchiveManager._isZipTooLarge = sinon
  157. .stub()
  158. .callsArgWith(1, null, true)
  159. return this.ArchiveManager.extractZipArchive(
  160. this.source,
  161. this.destination,
  162. error => {
  163. this.callback(error)
  164. return done()
  165. }
  166. )
  167. })
  168. it('should return the callback with an error', function () {
  169. return sinon.assert.calledWithExactly(
  170. this.callback,
  171. sinon.match.instanceOf(ArchiveErrors.ZipContentsTooLargeError)
  172. )
  173. })
  174. it('should not call yauzl.open', function () {
  175. return this.yauzl.open.called.should.equal(false)
  176. })
  177. })
  178. describe('with an error in the extracted files', function () {
  179. beforeEach(function (done) {
  180. this.ArchiveManager.extractZipArchive(
  181. this.source,
  182. this.destination,
  183. error => {
  184. this.callback(error)
  185. return done()
  186. }
  187. )
  188. return this.zipfile.emit('error', new Error('Something went wrong'))
  189. })
  190. it('should return the callback with an error', function () {
  191. return this.callback.should.have.been.calledWithExactly(
  192. sinon.match
  193. .instanceOf(Error)
  194. .and(sinon.match.has('message', 'Something went wrong'))
  195. )
  196. })
  197. })
  198. describe('with a relative extracted file path', function () {
  199. beforeEach(function (done) {
  200. this.zipfile.openReadStream = sinon.stub()
  201. this.ArchiveManager.extractZipArchive(
  202. this.source,
  203. this.destination,
  204. error => {
  205. this.callback(error)
  206. return done()
  207. }
  208. )
  209. this.zipfile.emit('entry', { fileName: '../testfile.txt' })
  210. return this.zipfile.emit('end')
  211. })
  212. it('should not write try to read the file entry', function () {
  213. return this.zipfile.openReadStream.called.should.equal(false)
  214. })
  215. })
  216. describe('with an unnormalized extracted file path', function () {
  217. beforeEach(function (done) {
  218. this.zipfile.openReadStream = sinon.stub()
  219. this.ArchiveManager.extractZipArchive(
  220. this.source,
  221. this.destination,
  222. error => {
  223. this.callback(error)
  224. return done()
  225. }
  226. )
  227. this.zipfile.emit('entry', { fileName: 'foo/./testfile.txt' })
  228. return this.zipfile.emit('end')
  229. })
  230. it('should not try to read the file entry', function () {
  231. return this.zipfile.openReadStream.called.should.equal(false)
  232. })
  233. })
  234. describe('with backslashes in the path', function () {
  235. beforeEach(function (done) {
  236. this.readStream = new events.EventEmitter()
  237. this.readStream.pipe = sinon.stub()
  238. this.writeStream = new events.EventEmitter()
  239. this.fs.createWriteStream = sinon.stub().returns(this.writeStream)
  240. this.zipfile.openReadStream = sinon
  241. .stub()
  242. .callsArgWith(1, null, this.readStream)
  243. this.ArchiveManager.extractZipArchive(
  244. this.source,
  245. this.destination,
  246. error => {
  247. this.callback(error)
  248. return done()
  249. }
  250. )
  251. this.zipfile.emit('entry', { fileName: 'wombat\\foo.tex' })
  252. this.zipfile.emit('entry', { fileName: 'potato\\bar.tex' })
  253. return this.zipfile.emit('end')
  254. })
  255. it('should read the file entry with its original path', function () {
  256. this.zipfile.openReadStream.should.be.calledWith({
  257. fileName: 'wombat\\foo.tex',
  258. })
  259. return this.zipfile.openReadStream.should.be.calledWith({
  260. fileName: 'potato\\bar.tex',
  261. })
  262. })
  263. it('should treat the backslashes as a directory separator when creating the directory', function () {
  264. this.fs.mkdir.should.be.calledWith(`${this.destination}/wombat`, {
  265. recursive: true,
  266. })
  267. this.fs.mkdir.should.be.calledWith(`${this.destination}/potato`, {
  268. recursive: true,
  269. })
  270. })
  271. it('should treat the backslashes as a directory separator when creating the file', function () {
  272. this.fs.createWriteStream.should.be.calledWith(
  273. `${this.destination}/wombat/foo.tex`
  274. )
  275. return this.fs.createWriteStream.should.be.calledWith(
  276. `${this.destination}/potato/bar.tex`
  277. )
  278. })
  279. })
  280. describe('with a directory entry', function () {
  281. beforeEach(function (done) {
  282. this.zipfile.openReadStream = sinon.stub()
  283. this.ArchiveManager.extractZipArchive(
  284. this.source,
  285. this.destination,
  286. error => {
  287. this.callback(error)
  288. return done()
  289. }
  290. )
  291. this.zipfile.emit('entry', { fileName: 'testdir/' })
  292. return this.zipfile.emit('end')
  293. })
  294. it('should not try to read the entry', function () {
  295. return this.zipfile.openReadStream.called.should.equal(false)
  296. })
  297. })
  298. describe('with an error opening the file read stream', function () {
  299. beforeEach(function (done) {
  300. this.zipfile.openReadStream = sinon
  301. .stub()
  302. .callsArgWith(1, new Error('Something went wrong'))
  303. this.writeStream = new events.EventEmitter()
  304. this.ArchiveManager.extractZipArchive(
  305. this.source,
  306. this.destination,
  307. error => {
  308. this.callback(error)
  309. return done()
  310. }
  311. )
  312. this.zipfile.emit('entry', { fileName: 'testfile.txt' })
  313. return this.zipfile.emit('end')
  314. })
  315. it('should return the callback with an error', function () {
  316. return this.callback.should.have.been.calledWithExactly(
  317. sinon.match
  318. .instanceOf(Error)
  319. .and(sinon.match.has('message', 'Something went wrong'))
  320. )
  321. })
  322. it('should close the zipfile', function () {
  323. return this.zipfile.close.called.should.equal(true)
  324. })
  325. })
  326. describe('with an error in the file read stream', function () {
  327. beforeEach(function (done) {
  328. this.readStream = new events.EventEmitter()
  329. this.readStream.pipe = sinon.stub()
  330. this.zipfile.openReadStream = sinon
  331. .stub()
  332. .callsArgWith(1, null, this.readStream)
  333. this.writeStream = new events.EventEmitter()
  334. this.fs.createWriteStream = sinon.stub().returns(this.writeStream)
  335. this.ArchiveManager.extractZipArchive(
  336. this.source,
  337. this.destination,
  338. error => {
  339. this.callback(error)
  340. return done()
  341. }
  342. )
  343. this.zipfile.emit('entry', { fileName: 'testfile.txt' })
  344. this.readStream.emit('error', new Error('Something went wrong'))
  345. return this.zipfile.emit('end')
  346. })
  347. it('should return the callback with an error', function () {
  348. return this.callback.should.have.been.calledWithExactly(
  349. sinon.match
  350. .instanceOf(Error)
  351. .and(sinon.match.has('message', 'Something went wrong'))
  352. )
  353. })
  354. it('should close the zipfile', function () {
  355. return this.zipfile.close.called.should.equal(true)
  356. })
  357. })
  358. describe('with an error in the file write stream', function () {
  359. beforeEach(function (done) {
  360. this.readStream = new events.EventEmitter()
  361. this.readStream.pipe = sinon.stub()
  362. this.readStream.unpipe = sinon.stub()
  363. this.readStream.destroy = sinon.stub()
  364. this.zipfile.openReadStream = sinon
  365. .stub()
  366. .callsArgWith(1, null, this.readStream)
  367. this.writeStream = new events.EventEmitter()
  368. this.fs.createWriteStream = sinon.stub().returns(this.writeStream)
  369. this.ArchiveManager.extractZipArchive(
  370. this.source,
  371. this.destination,
  372. error => {
  373. this.callback(error)
  374. return done()
  375. }
  376. )
  377. this.zipfile.emit('entry', { fileName: 'testfile.txt' })
  378. this.writeStream.emit('error', new Error('Something went wrong'))
  379. return this.zipfile.emit('end')
  380. })
  381. it('should return the callback with an error', function () {
  382. return this.callback.should.have.been.calledWithExactly(
  383. sinon.match
  384. .instanceOf(Error)
  385. .and(sinon.match.has('message', 'Something went wrong'))
  386. )
  387. })
  388. it('should unpipe from the readstream', function () {
  389. return this.readStream.unpipe.called.should.equal(true)
  390. })
  391. it('should destroy the readstream', function () {
  392. return this.readStream.destroy.called.should.equal(true)
  393. })
  394. it('should close the zipfile', function () {
  395. return this.zipfile.close.called.should.equal(true)
  396. })
  397. })
  398. })
  399. describe('_isZipTooLarge', function () {
  400. it('should return false with small output', function (done) {
  401. this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => {
  402. isTooLarge.should.equal(false)
  403. return done()
  404. })
  405. this.zipfile.emit('entry', { uncompressedSize: 109042 })
  406. return this.zipfile.emit('end')
  407. })
  408. it('should return true with large bytes', function (done) {
  409. this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => {
  410. isTooLarge.should.equal(true)
  411. return done()
  412. })
  413. this.zipfile.emit('entry', { uncompressedSize: 109e16 })
  414. return this.zipfile.emit('end')
  415. })
  416. it('should return error on no data', function (done) {
  417. this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => {
  418. expect(error).to.exist
  419. return done()
  420. })
  421. this.zipfile.emit('entry', {})
  422. return this.zipfile.emit('end')
  423. })
  424. it("should return error if it didn't get a number", function (done) {
  425. this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => {
  426. expect(error).to.exist
  427. return done()
  428. })
  429. this.zipfile.emit('entry', { uncompressedSize: 'random-error' })
  430. return this.zipfile.emit('end')
  431. })
  432. it('should return error if there is no data', function (done) {
  433. this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => {
  434. expect(error).to.exist
  435. return done()
  436. })
  437. return this.zipfile.emit('end')
  438. })
  439. })
  440. describe('findTopLevelDirectory', function () {
  441. beforeEach(function () {
  442. this.fs.readdir = sinon.stub()
  443. this.fs.stat = sinon.stub()
  444. return (this.directory = 'test/directory')
  445. })
  446. describe('with multiple files', function () {
  447. beforeEach(function () {
  448. this.fs.readdir.callsArgWith(1, null, ['multiple', 'files'])
  449. return this.ArchiveManager.findTopLevelDirectory(
  450. this.directory,
  451. this.callback
  452. )
  453. })
  454. it('should find the files in the directory', function () {
  455. return this.fs.readdir.calledWith(this.directory).should.equal(true)
  456. })
  457. it('should return the original directory', function () {
  458. return this.callback.calledWith(null, this.directory).should.equal(true)
  459. })
  460. })
  461. describe('with a single file (not folder)', function () {
  462. beforeEach(function () {
  463. this.fs.readdir.callsArgWith(1, null, ['foo.tex'])
  464. this.fs.stat.callsArgWith(1, null, {
  465. isDirectory() {
  466. return false
  467. },
  468. })
  469. return this.ArchiveManager.findTopLevelDirectory(
  470. this.directory,
  471. this.callback
  472. )
  473. })
  474. it('should check if the file is a directory', function () {
  475. return this.fs.stat
  476. .calledWith(this.directory + '/foo.tex')
  477. .should.equal(true)
  478. })
  479. it('should return the original directory', function () {
  480. return this.callback.calledWith(null, this.directory).should.equal(true)
  481. })
  482. })
  483. describe('with a single top-level folder', function () {
  484. beforeEach(function () {
  485. this.fs.readdir.callsArgWith(1, null, ['folder'])
  486. this.fs.stat.callsArgWith(1, null, {
  487. isDirectory() {
  488. return true
  489. },
  490. })
  491. return this.ArchiveManager.findTopLevelDirectory(
  492. this.directory,
  493. this.callback
  494. )
  495. })
  496. it('should check if the file is a directory', function () {
  497. return this.fs.stat
  498. .calledWith(this.directory + '/folder')
  499. .should.equal(true)
  500. })
  501. it('should return the child directory', function () {
  502. return this.callback
  503. .calledWith(null, this.directory + '/folder')
  504. .should.equal(true)
  505. })
  506. })
  507. })
  508. })