ArchiveManagerTests.js 18 KB

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