ArchiveManagerTests.js 18 KB

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