DocArchiveManagerTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  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. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. const { assert } = require('chai')
  16. const sinon = require('sinon')
  17. const chai = require('chai')
  18. const should = chai.should()
  19. const { expect } = chai
  20. const modulePath = '../../../app/js/DocArchiveManager.js'
  21. const SandboxedModule = require('sandboxed-module')
  22. const { ObjectId } = require('mongojs')
  23. const Errors = require('../../../app/js/Errors')
  24. const crypto = require('crypto')
  25. describe('DocArchiveManager', function() {
  26. beforeEach(function() {
  27. this.settings = {
  28. docstore: {
  29. s3: {
  30. secret: 'secret',
  31. key: 'this_key',
  32. bucket: 'doc-archive-unit-test'
  33. }
  34. }
  35. }
  36. this.request = {
  37. put: {},
  38. get: {},
  39. del: {}
  40. }
  41. this.archivedDocs = [
  42. {
  43. _id: ObjectId(),
  44. inS3: true,
  45. rev: 2
  46. },
  47. {
  48. _id: ObjectId(),
  49. inS3: true,
  50. rev: 4
  51. },
  52. {
  53. _id: ObjectId(),
  54. inS3: true,
  55. rev: 6
  56. }
  57. ]
  58. this.mongoDocs = [
  59. {
  60. _id: ObjectId(),
  61. lines: ['one', 'two', 'three'],
  62. rev: 2
  63. },
  64. {
  65. _id: ObjectId(),
  66. lines: ['aaa', 'bbb', 'ccc'],
  67. rev: 4
  68. },
  69. {
  70. _id: ObjectId(),
  71. inS3: true,
  72. rev: 6
  73. },
  74. {
  75. _id: ObjectId(),
  76. inS3: true,
  77. rev: 6
  78. },
  79. {
  80. _id: ObjectId(),
  81. lines: ['111', '222', '333'],
  82. rev: 6
  83. }
  84. ]
  85. this.unarchivedDocs = [
  86. {
  87. _id: ObjectId(),
  88. lines: ['wombat', 'potato', 'banana'],
  89. rev: 2
  90. },
  91. {
  92. _id: ObjectId(),
  93. lines: ['llama', 'turnip', 'apple'],
  94. rev: 4
  95. },
  96. {
  97. _id: ObjectId(),
  98. lines: ['elephant', 'swede', 'nectarine'],
  99. rev: 6
  100. }
  101. ]
  102. this.mixedDocs = this.archivedDocs.concat(this.unarchivedDocs)
  103. this.MongoManager = {
  104. markDocAsArchived: sinon.stub().callsArgWith(2, null),
  105. upsertIntoDocCollection: sinon.stub().callsArgWith(3, null),
  106. getProjectsDocs: sinon.stub().callsArgWith(3, null, this.mongoDocs),
  107. getArchivedProjectDocs: sinon.stub().callsArgWith(2, null, this.mongoDocs)
  108. }
  109. this.requires = {
  110. 'settings-sharelatex': this.settings,
  111. './MongoManager': this.MongoManager,
  112. request: this.request,
  113. './RangeManager': (this.RangeManager = {}),
  114. 'logger-sharelatex': {
  115. log() {},
  116. err() {}
  117. }
  118. }
  119. this.globals = { JSON }
  120. this.error = 'my errror'
  121. this.project_id = ObjectId().toString()
  122. this.stubbedError = new Errors.NotFoundError('Error in S3 request')
  123. return (this.DocArchiveManager = SandboxedModule.require(modulePath, {
  124. requires: this.requires,
  125. globals: this.globals
  126. }))
  127. })
  128. describe('archiveDoc', function() {
  129. it('should use correct options', function(done) {
  130. this.request.put = sinon
  131. .stub()
  132. .callsArgWith(1, null, { statusCode: 200, headers: { etag: '' } })
  133. return this.DocArchiveManager.archiveDoc(
  134. this.project_id,
  135. this.mongoDocs[0],
  136. err => {
  137. const opts = this.request.put.args[0][0]
  138. assert.deepEqual(opts.aws, {
  139. key: this.settings.docstore.s3.key,
  140. secret: this.settings.docstore.s3.secret,
  141. bucket: this.settings.docstore.s3.bucket
  142. })
  143. opts.body.should.equal(
  144. JSON.stringify({
  145. lines: this.mongoDocs[0].lines,
  146. ranges: this.mongoDocs[0].ranges,
  147. schema_v: 1
  148. })
  149. )
  150. opts.timeout.should.equal(30 * 1000)
  151. opts.uri.should.equal(
  152. `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`
  153. )
  154. return done()
  155. }
  156. )
  157. })
  158. it('should return no md5 error', function(done) {
  159. const data = JSON.stringify({
  160. lines: this.mongoDocs[0].lines,
  161. ranges: this.mongoDocs[0].ranges,
  162. schema_v: 1
  163. })
  164. this.md5 = crypto
  165. .createHash('md5')
  166. .update(data)
  167. .digest('hex')
  168. this.request.put = sinon
  169. .stub()
  170. .callsArgWith(1, null, { statusCode: 200, headers: { etag: this.md5 } })
  171. return this.DocArchiveManager.archiveDoc(
  172. this.project_id,
  173. this.mongoDocs[0],
  174. err => {
  175. should.not.exist(err)
  176. return done()
  177. }
  178. )
  179. })
  180. return it('should return the error', function(done) {
  181. this.request.put = sinon.stub().callsArgWith(1, this.stubbedError, {
  182. statusCode: 400,
  183. headers: { etag: '' }
  184. })
  185. return this.DocArchiveManager.archiveDoc(
  186. this.project_id,
  187. this.mongoDocs[0],
  188. err => {
  189. should.exist(err)
  190. return done()
  191. }
  192. )
  193. })
  194. })
  195. describe('unarchiveDoc', function() {
  196. it('should use correct options', function(done) {
  197. this.request.get = sinon
  198. .stub()
  199. .callsArgWith(1, null, { statusCode: 200 }, this.mongoDocs[0].lines)
  200. this.request.del = sinon
  201. .stub()
  202. .callsArgWith(1, null, { statusCode: 204 }, {})
  203. return this.DocArchiveManager.unarchiveDoc(
  204. this.project_id,
  205. this.mongoDocs[0]._id,
  206. err => {
  207. const opts = this.request.get.args[0][0]
  208. assert.deepEqual(opts.aws, {
  209. key: this.settings.docstore.s3.key,
  210. secret: this.settings.docstore.s3.secret,
  211. bucket: this.settings.docstore.s3.bucket
  212. })
  213. opts.json.should.equal(true)
  214. opts.timeout.should.equal(30 * 1000)
  215. opts.uri.should.equal(
  216. `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`
  217. )
  218. return done()
  219. }
  220. )
  221. })
  222. it('should return the error', function(done) {
  223. this.request.get = sinon.stub().callsArgWith(1, this.stubbedError, {}, {})
  224. return this.DocArchiveManager.unarchiveDoc(
  225. this.project_id,
  226. this.mongoDocs[0],
  227. err => {
  228. should.exist(err)
  229. return done()
  230. }
  231. )
  232. })
  233. return it('should error if the doc lines are a string not an array', function(done) {
  234. this.request.get = sinon
  235. .stub()
  236. .callsArgWith(1, null, { statusCode: 200 }, 'this is a string')
  237. this.request.del = sinon.stub()
  238. return this.DocArchiveManager.unarchiveDoc(
  239. this.project_id,
  240. this.mongoDocs[0],
  241. err => {
  242. should.exist(err)
  243. this.request.del.called.should.equal(false)
  244. return done()
  245. }
  246. )
  247. })
  248. })
  249. describe('archiveAllDocs', function() {
  250. it('should archive all project docs which are not in s3', function(done) {
  251. this.MongoManager.getProjectsDocs = sinon
  252. .stub()
  253. .callsArgWith(3, null, this.mongoDocs)
  254. this.DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null)
  255. return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
  256. this.DocArchiveManager.archiveDoc
  257. .calledWith(this.project_id, this.mongoDocs[0])
  258. .should.equal(true)
  259. this.DocArchiveManager.archiveDoc
  260. .calledWith(this.project_id, this.mongoDocs[1])
  261. .should.equal(true)
  262. this.DocArchiveManager.archiveDoc
  263. .calledWith(this.project_id, this.mongoDocs[4])
  264. .should.equal(true)
  265. this.DocArchiveManager.archiveDoc
  266. .calledWith(this.project_id, this.mongoDocs[2])
  267. .should.equal(false)
  268. this.DocArchiveManager.archiveDoc
  269. .calledWith(this.project_id, this.mongoDocs[3])
  270. .should.equal(false)
  271. should.not.exist(err)
  272. return done()
  273. })
  274. })
  275. it('should return error if have no docs', function(done) {
  276. this.MongoManager.getProjectsDocs = sinon
  277. .stub()
  278. .callsArgWith(3, null, null)
  279. return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
  280. should.exist(err)
  281. return done()
  282. })
  283. })
  284. it('should return the error', function(done) {
  285. this.MongoManager.getProjectsDocs = sinon
  286. .stub()
  287. .callsArgWith(3, this.error, null)
  288. return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
  289. err.should.equal(this.error)
  290. return done()
  291. })
  292. })
  293. return describe('when most have been already put in s3', function() {
  294. beforeEach(function() {
  295. let numberOfDocs = 10 * 1000
  296. this.mongoDocs = []
  297. while (--numberOfDocs !== 0) {
  298. this.mongoDocs.push({ inS3: true, _id: ObjectId() })
  299. }
  300. this.MongoManager.getProjectsDocs = sinon
  301. .stub()
  302. .callsArgWith(3, null, this.mongoDocs)
  303. return (this.DocArchiveManager.archiveDoc = sinon
  304. .stub()
  305. .callsArgWith(2, null))
  306. })
  307. return it('should not throw and error', function(done) {
  308. return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
  309. should.not.exist(err)
  310. return done()
  311. })
  312. })
  313. })
  314. })
  315. describe('unArchiveAllDocs', function() {
  316. it('should unarchive all inS3 docs', function(done) {
  317. this.MongoManager.getArchivedProjectDocs = sinon
  318. .stub()
  319. .callsArgWith(1, null, this.archivedDocs)
  320. this.DocArchiveManager.unarchiveDoc = sinon.stub().callsArgWith(2, null)
  321. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
  322. for (const doc of Array.from(this.archivedDocs)) {
  323. this.DocArchiveManager.unarchiveDoc
  324. .calledWith(this.project_id, doc._id)
  325. .should.equal(true)
  326. }
  327. should.not.exist(err)
  328. return done()
  329. })
  330. })
  331. it('should return error if have no docs', function(done) {
  332. this.MongoManager.getArchivedProjectDocs = sinon
  333. .stub()
  334. .callsArgWith(1, null, null)
  335. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
  336. should.exist(err)
  337. return done()
  338. })
  339. })
  340. return it('should return the error', function(done) {
  341. this.MongoManager.getArchivedProjectDocs = sinon
  342. .stub()
  343. .callsArgWith(1, this.error, null)
  344. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
  345. err.should.equal(this.error)
  346. return done()
  347. })
  348. })
  349. })
  350. describe('destroyAllDocs', function() {
  351. beforeEach(function() {
  352. this.request.del = sinon
  353. .stub()
  354. .callsArgWith(1, null, { statusCode: 204 }, {})
  355. this.MongoManager.getProjectsDocs = sinon
  356. .stub()
  357. .callsArgWith(3, null, this.mixedDocs)
  358. this.MongoManager.findDoc = sinon.stub().callsArgWith(3, null, null)
  359. this.MongoManager.destroyDoc = sinon.stub().yields()
  360. return Array.from(this.mixedDocs).map(doc =>
  361. this.MongoManager.findDoc
  362. .withArgs(this.project_id, doc._id)
  363. .callsArgWith(3, null, doc)
  364. )
  365. })
  366. it('should destroy all the docs', function(done) {
  367. this.DocArchiveManager.destroyDoc = sinon.stub().callsArgWith(2, null)
  368. return this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
  369. for (const doc of Array.from(this.mixedDocs)) {
  370. this.DocArchiveManager.destroyDoc
  371. .calledWith(this.project_id, doc._id)
  372. .should.equal(true)
  373. }
  374. should.not.exist(err)
  375. return done()
  376. })
  377. })
  378. it('should only the s3 docs from s3', function(done) {
  379. const docOpts = doc => {
  380. return JSON.parse(
  381. JSON.stringify({
  382. aws: {
  383. key: this.settings.docstore.s3.key,
  384. secret: this.settings.docstore.s3.secret,
  385. bucket: this.settings.docstore.s3.bucket
  386. },
  387. json: true,
  388. timeout: 30 * 1000,
  389. uri: `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${doc._id}`
  390. })
  391. )
  392. }
  393. return this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
  394. let doc
  395. expect(err).not.to.exist
  396. for (doc of Array.from(this.archivedDocs)) {
  397. sinon.assert.calledWith(this.request.del, docOpts(doc))
  398. }
  399. for (doc of Array.from(this.unarchivedDocs)) {
  400. expect(this.request.del.calledWith(docOpts(doc))).to.equal(false)
  401. } // no notCalledWith
  402. return done()
  403. })
  404. })
  405. return it('should remove the docs from mongo', function(done) {
  406. this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
  407. return expect(err).not.to.exist
  408. })
  409. for (const doc of Array.from(this.mixedDocs)) {
  410. sinon.assert.calledWith(this.MongoManager.destroyDoc, doc._id)
  411. }
  412. return done()
  413. })
  414. })
  415. describe('_s3DocToMongoDoc', function() {
  416. describe('with the old schema', function() {
  417. return it('should return the docs lines', function(done) {
  418. return this.DocArchiveManager._s3DocToMongoDoc(
  419. ['doc', 'lines'],
  420. (error, doc) => {
  421. expect(doc).to.deep.equal({
  422. lines: ['doc', 'lines']
  423. })
  424. return done()
  425. }
  426. )
  427. })
  428. })
  429. describe('with the new schema', function() {
  430. it('should return the doc lines and ranges', function(done) {
  431. this.RangeManager.jsonRangesToMongo = sinon
  432. .stub()
  433. .returns({ mongo: 'ranges' })
  434. return this.DocArchiveManager._s3DocToMongoDoc(
  435. {
  436. lines: ['doc', 'lines'],
  437. ranges: { json: 'ranges' },
  438. schema_v: 1
  439. },
  440. (error, doc) => {
  441. expect(doc).to.deep.equal({
  442. lines: ['doc', 'lines'],
  443. ranges: { mongo: 'ranges' }
  444. })
  445. return done()
  446. }
  447. )
  448. })
  449. return it('should return just the doc lines when there are no ranges', function(done) {
  450. return this.DocArchiveManager._s3DocToMongoDoc(
  451. {
  452. lines: ['doc', 'lines'],
  453. schema_v: 1
  454. },
  455. (error, doc) => {
  456. expect(doc).to.deep.equal({
  457. lines: ['doc', 'lines']
  458. })
  459. return done()
  460. }
  461. )
  462. })
  463. })
  464. return describe('with an unrecognised schema', function() {
  465. return it('should return an error', function(done) {
  466. return this.DocArchiveManager._s3DocToMongoDoc(
  467. {
  468. schema_v: 2
  469. },
  470. (error, doc) => {
  471. expect(error).to.exist
  472. return done()
  473. }
  474. )
  475. })
  476. })
  477. })
  478. return describe('_mongoDocToS3Doc', function() {
  479. describe('with a valid doc', function() {
  480. return it('should return the json version', function(done) {
  481. let doc
  482. return this.DocArchiveManager._mongoDocToS3Doc(
  483. (doc = {
  484. lines: ['doc', 'lines'],
  485. ranges: { mock: 'ranges' }
  486. }),
  487. (err, s3_doc) => {
  488. expect(s3_doc).to.equal(
  489. JSON.stringify({
  490. lines: ['doc', 'lines'],
  491. ranges: { mock: 'ranges' },
  492. schema_v: 1
  493. })
  494. )
  495. return done()
  496. }
  497. )
  498. })
  499. })
  500. describe('with null bytes in the result', function() {
  501. beforeEach(function() {
  502. this._stringify = JSON.stringify
  503. return (JSON.stringify = sinon.stub().returns('{"bad": "\u0000"}'))
  504. })
  505. afterEach(function() {
  506. return (JSON.stringify = this._stringify)
  507. })
  508. return it('should return an error', function(done) {
  509. return this.DocArchiveManager._mongoDocToS3Doc(
  510. {
  511. lines: ['doc', 'lines'],
  512. ranges: { mock: 'ranges' }
  513. },
  514. (err, s3_doc) => {
  515. expect(err).to.exist
  516. return done()
  517. }
  518. )
  519. })
  520. })
  521. return describe('without doc lines', function() {
  522. return it('should return an error', function(done) {
  523. return this.DocArchiveManager._mongoDocToS3Doc({}, (err, s3_doc) => {
  524. expect(err).to.exist
  525. return done()
  526. })
  527. })
  528. })
  529. })
  530. })