DocArchiveManagerTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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.createHash('md5').update(data).digest('hex')
  165. this.request.put = sinon
  166. .stub()
  167. .callsArgWith(1, null, { statusCode: 200, headers: { etag: this.md5 } })
  168. return this.DocArchiveManager.archiveDoc(
  169. this.project_id,
  170. this.mongoDocs[0],
  171. (err) => {
  172. should.not.exist(err)
  173. return done()
  174. }
  175. )
  176. })
  177. return it('should return the error', function (done) {
  178. this.request.put = sinon.stub().callsArgWith(1, this.stubbedError, {
  179. statusCode: 400,
  180. headers: { etag: '' }
  181. })
  182. return this.DocArchiveManager.archiveDoc(
  183. this.project_id,
  184. this.mongoDocs[0],
  185. (err) => {
  186. should.exist(err)
  187. return done()
  188. }
  189. )
  190. })
  191. })
  192. describe('unarchiveDoc', function () {
  193. it('should use correct options', function (done) {
  194. this.request.get = sinon
  195. .stub()
  196. .callsArgWith(1, null, { statusCode: 200 }, this.mongoDocs[0].lines)
  197. this.request.del = sinon
  198. .stub()
  199. .callsArgWith(1, null, { statusCode: 204 }, {})
  200. return this.DocArchiveManager.unarchiveDoc(
  201. this.project_id,
  202. this.mongoDocs[0]._id,
  203. (err) => {
  204. const opts = this.request.get.args[0][0]
  205. assert.deepEqual(opts.aws, {
  206. key: this.settings.docstore.s3.key,
  207. secret: this.settings.docstore.s3.secret,
  208. bucket: this.settings.docstore.s3.bucket
  209. })
  210. opts.json.should.equal(true)
  211. opts.timeout.should.equal(30 * 1000)
  212. opts.uri.should.equal(
  213. `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`
  214. )
  215. return done()
  216. }
  217. )
  218. })
  219. it('should return the error', function (done) {
  220. this.request.get = sinon.stub().callsArgWith(1, this.stubbedError, {}, {})
  221. return this.DocArchiveManager.unarchiveDoc(
  222. this.project_id,
  223. this.mongoDocs[0],
  224. (err) => {
  225. should.exist(err)
  226. return done()
  227. }
  228. )
  229. })
  230. return it('should error if the doc lines are a string not an array', function (done) {
  231. this.request.get = sinon
  232. .stub()
  233. .callsArgWith(1, null, { statusCode: 200 }, 'this is a string')
  234. this.request.del = sinon.stub()
  235. return this.DocArchiveManager.unarchiveDoc(
  236. this.project_id,
  237. this.mongoDocs[0],
  238. (err) => {
  239. should.exist(err)
  240. this.request.del.called.should.equal(false)
  241. return done()
  242. }
  243. )
  244. })
  245. })
  246. describe('archiveAllDocs', function () {
  247. it('should archive all project docs which are not in s3', function (done) {
  248. this.MongoManager.getProjectsDocs = sinon
  249. .stub()
  250. .callsArgWith(3, null, this.mongoDocs)
  251. this.DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null)
  252. return this.DocArchiveManager.archiveAllDocs(this.project_id, (err) => {
  253. this.DocArchiveManager.archiveDoc
  254. .calledWith(this.project_id, this.mongoDocs[0])
  255. .should.equal(true)
  256. this.DocArchiveManager.archiveDoc
  257. .calledWith(this.project_id, this.mongoDocs[1])
  258. .should.equal(true)
  259. this.DocArchiveManager.archiveDoc
  260. .calledWith(this.project_id, this.mongoDocs[4])
  261. .should.equal(true)
  262. this.DocArchiveManager.archiveDoc
  263. .calledWith(this.project_id, this.mongoDocs[2])
  264. .should.equal(false)
  265. this.DocArchiveManager.archiveDoc
  266. .calledWith(this.project_id, this.mongoDocs[3])
  267. .should.equal(false)
  268. should.not.exist(err)
  269. return done()
  270. })
  271. })
  272. it('should return error if have no docs', function (done) {
  273. this.MongoManager.getProjectsDocs = sinon
  274. .stub()
  275. .callsArgWith(3, null, null)
  276. return this.DocArchiveManager.archiveAllDocs(this.project_id, (err) => {
  277. should.exist(err)
  278. return done()
  279. })
  280. })
  281. it('should return the error', function (done) {
  282. this.MongoManager.getProjectsDocs = sinon
  283. .stub()
  284. .callsArgWith(3, this.error, null)
  285. return this.DocArchiveManager.archiveAllDocs(this.project_id, (err) => {
  286. err.should.equal(this.error)
  287. return done()
  288. })
  289. })
  290. return describe('when most have been already put in s3', function () {
  291. beforeEach(function () {
  292. let numberOfDocs = 10 * 1000
  293. this.mongoDocs = []
  294. while (--numberOfDocs !== 0) {
  295. this.mongoDocs.push({ inS3: true, _id: ObjectId() })
  296. }
  297. this.MongoManager.getProjectsDocs = sinon
  298. .stub()
  299. .callsArgWith(3, null, this.mongoDocs)
  300. return (this.DocArchiveManager.archiveDoc = sinon
  301. .stub()
  302. .callsArgWith(2, null))
  303. })
  304. return it('should not throw and error', function (done) {
  305. return this.DocArchiveManager.archiveAllDocs(this.project_id, (err) => {
  306. should.not.exist(err)
  307. return done()
  308. })
  309. })
  310. })
  311. })
  312. describe('unArchiveAllDocs', function () {
  313. it('should unarchive all inS3 docs', function (done) {
  314. this.MongoManager.getArchivedProjectDocs = sinon
  315. .stub()
  316. .callsArgWith(1, null, this.archivedDocs)
  317. this.DocArchiveManager.unarchiveDoc = sinon.stub().callsArgWith(2, null)
  318. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, (err) => {
  319. for (const doc of Array.from(this.archivedDocs)) {
  320. this.DocArchiveManager.unarchiveDoc
  321. .calledWith(this.project_id, doc._id)
  322. .should.equal(true)
  323. }
  324. should.not.exist(err)
  325. return done()
  326. })
  327. })
  328. it('should return error if have no docs', function (done) {
  329. this.MongoManager.getArchivedProjectDocs = sinon
  330. .stub()
  331. .callsArgWith(1, null, null)
  332. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, (err) => {
  333. should.exist(err)
  334. return done()
  335. })
  336. })
  337. return it('should return the error', function (done) {
  338. this.MongoManager.getArchivedProjectDocs = sinon
  339. .stub()
  340. .callsArgWith(1, this.error, null)
  341. return this.DocArchiveManager.unArchiveAllDocs(this.project_id, (err) => {
  342. err.should.equal(this.error)
  343. return done()
  344. })
  345. })
  346. })
  347. describe('destroyAllDocs', function () {
  348. beforeEach(function () {
  349. this.request.del = sinon
  350. .stub()
  351. .callsArgWith(1, null, { statusCode: 204 }, {})
  352. this.MongoManager.getProjectsDocs = sinon
  353. .stub()
  354. .callsArgWith(3, null, this.mixedDocs)
  355. this.MongoManager.findDoc = sinon.stub().callsArgWith(3, null, null)
  356. this.MongoManager.destroyDoc = sinon.stub().yields()
  357. return Array.from(this.mixedDocs).map((doc) =>
  358. this.MongoManager.findDoc
  359. .withArgs(this.project_id, doc._id)
  360. .callsArgWith(3, null, doc)
  361. )
  362. })
  363. it('should destroy all the docs', function (done) {
  364. this.DocArchiveManager.destroyDoc = sinon.stub().callsArgWith(2, null)
  365. return this.DocArchiveManager.destroyAllDocs(this.project_id, (err) => {
  366. for (const doc of Array.from(this.mixedDocs)) {
  367. this.DocArchiveManager.destroyDoc
  368. .calledWith(this.project_id, doc._id)
  369. .should.equal(true)
  370. }
  371. should.not.exist(err)
  372. return done()
  373. })
  374. })
  375. it('should only the s3 docs from s3', function (done) {
  376. const docOpts = (doc) => {
  377. return JSON.parse(
  378. JSON.stringify({
  379. aws: {
  380. key: this.settings.docstore.s3.key,
  381. secret: this.settings.docstore.s3.secret,
  382. bucket: this.settings.docstore.s3.bucket
  383. },
  384. json: true,
  385. timeout: 30 * 1000,
  386. uri: `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${doc._id}`
  387. })
  388. )
  389. }
  390. return this.DocArchiveManager.destroyAllDocs(this.project_id, (err) => {
  391. let doc
  392. expect(err).not.to.exist
  393. for (doc of Array.from(this.archivedDocs)) {
  394. sinon.assert.calledWith(this.request.del, docOpts(doc))
  395. }
  396. for (doc of Array.from(this.unarchivedDocs)) {
  397. expect(this.request.del.calledWith(docOpts(doc))).to.equal(false)
  398. } // no notCalledWith
  399. return done()
  400. })
  401. })
  402. return it('should remove the docs from mongo', function (done) {
  403. this.DocArchiveManager.destroyAllDocs(this.project_id, (err) => {
  404. return expect(err).not.to.exist
  405. })
  406. for (const doc of Array.from(this.mixedDocs)) {
  407. sinon.assert.calledWith(this.MongoManager.destroyDoc, doc._id)
  408. }
  409. return done()
  410. })
  411. })
  412. describe('_s3DocToMongoDoc', function () {
  413. describe('with the old schema', function () {
  414. return it('should return the docs lines', function (done) {
  415. return this.DocArchiveManager._s3DocToMongoDoc(
  416. ['doc', 'lines'],
  417. (error, doc) => {
  418. expect(doc).to.deep.equal({
  419. lines: ['doc', 'lines']
  420. })
  421. return done()
  422. }
  423. )
  424. })
  425. })
  426. describe('with the new schema', function () {
  427. it('should return the doc lines and ranges', function (done) {
  428. this.RangeManager.jsonRangesToMongo = sinon
  429. .stub()
  430. .returns({ mongo: 'ranges' })
  431. return this.DocArchiveManager._s3DocToMongoDoc(
  432. {
  433. lines: ['doc', 'lines'],
  434. ranges: { json: 'ranges' },
  435. schema_v: 1
  436. },
  437. (error, doc) => {
  438. expect(doc).to.deep.equal({
  439. lines: ['doc', 'lines'],
  440. ranges: { mongo: 'ranges' }
  441. })
  442. return done()
  443. }
  444. )
  445. })
  446. return it('should return just the doc lines when there are no ranges', function (done) {
  447. return this.DocArchiveManager._s3DocToMongoDoc(
  448. {
  449. lines: ['doc', 'lines'],
  450. schema_v: 1
  451. },
  452. (error, doc) => {
  453. expect(doc).to.deep.equal({
  454. lines: ['doc', 'lines']
  455. })
  456. return done()
  457. }
  458. )
  459. })
  460. })
  461. return describe('with an unrecognised schema', function () {
  462. return it('should return an error', function (done) {
  463. return this.DocArchiveManager._s3DocToMongoDoc(
  464. {
  465. schema_v: 2
  466. },
  467. (error, doc) => {
  468. expect(error).to.exist
  469. return done()
  470. }
  471. )
  472. })
  473. })
  474. })
  475. return describe('_mongoDocToS3Doc', function () {
  476. describe('with a valid doc', function () {
  477. return it('should return the json version', function (done) {
  478. let doc
  479. return this.DocArchiveManager._mongoDocToS3Doc(
  480. (doc = {
  481. lines: ['doc', 'lines'],
  482. ranges: { mock: 'ranges' }
  483. }),
  484. (err, s3_doc) => {
  485. expect(s3_doc).to.equal(
  486. JSON.stringify({
  487. lines: ['doc', 'lines'],
  488. ranges: { mock: 'ranges' },
  489. schema_v: 1
  490. })
  491. )
  492. return done()
  493. }
  494. )
  495. })
  496. })
  497. describe('with null bytes in the result', function () {
  498. beforeEach(function () {
  499. this._stringify = JSON.stringify
  500. return (JSON.stringify = sinon.stub().returns('{"bad": "\u0000"}'))
  501. })
  502. afterEach(function () {
  503. return (JSON.stringify = this._stringify)
  504. })
  505. return it('should return an error', function (done) {
  506. return this.DocArchiveManager._mongoDocToS3Doc(
  507. {
  508. lines: ['doc', 'lines'],
  509. ranges: { mock: 'ranges' }
  510. },
  511. (err, s3_doc) => {
  512. expect(err).to.exist
  513. return done()
  514. }
  515. )
  516. })
  517. })
  518. return describe('without doc lines', function () {
  519. return it('should return an error', function (done) {
  520. return this.DocArchiveManager._mongoDocToS3Doc({}, (err, s3_doc) => {
  521. expect(err).to.exist
  522. return done()
  523. })
  524. })
  525. })
  526. })
  527. })