/* eslint-disable camelcase, no-dupe-keys, no-return-assign, no-unused-vars, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { assert, expect } = require('chai') const modulePath = require('path').join(__dirname, '../../../app/js/DocManager') const { ObjectId } = require('mongodb') const Errors = require('../../../app/js/Errors') describe('DocManager', function () { beforeEach(function () { this.DocManager = SandboxedModule.require(modulePath, { requires: { './MongoManager': (this.MongoManager = {}), './DocArchiveManager': (this.DocArchiveManager = {}), './RangeManager': (this.RangeManager = { jsonRangesToMongo(r) { return r }, shouldUpdateRanges: sinon.stub().returns(false), }), '@overleaf/settings': (this.settings = { docstore: {} }), './Errors': Errors, }, }) this.doc_id = ObjectId().toString() this.project_id = ObjectId().toString() this.another_project_id = ObjectId().toString() this.callback = sinon.stub() return (this.stubbedError = new Error('blew up')) }) describe('getFullDoc', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub() return (this.doc = { _id: this.doc_id, lines: ['2134'], }) }) it('should call get doc with a quick filter', function (done) { this.DocManager._getDoc.callsArgWith(3, null, this.doc) return this.DocManager.getFullDoc( this.project_id, this.doc_id, (err, doc) => { if (err) return done(err) doc.should.equal(this.doc) this.DocManager._getDoc .calledWith(this.project_id, this.doc_id, { lines: true, rev: true, deleted: true, version: true, ranges: true, inS3: true, }) .should.equal(true) return done() } ) }) return it('should return error when get doc errors', function (done) { this.DocManager._getDoc.callsArgWith(3, 'error') return this.DocManager.getFullDoc( this.project_id, this.doc_id, (err, exist) => { err.should.equal('error') return done() } ) }) }) describe('getRawDoc', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub() return (this.doc = { lines: ['2134'] }) }) it('should call get doc with a quick filter', function (done) { this.DocManager._getDoc.callsArgWith(3, null, this.doc) return this.DocManager.getDocLines( this.project_id, this.doc_id, (err, doc) => { if (err) return done(err) doc.should.equal(this.doc) this.DocManager._getDoc .calledWith(this.project_id, this.doc_id, { lines: true, inS3: true, }) .should.equal(true) return done() } ) }) return it('should return error when get doc errors', function (done) { this.DocManager._getDoc.callsArgWith(3, 'error') return this.DocManager.getDocLines( this.project_id, this.doc_id, (err, exist) => { err.should.equal('error') return done() } ) }) }) describe('getDoc', function () { beforeEach(function () { this.project = { name: 'mock-project' } this.doc = { _id: this.doc_id, project_id: this.project_id, lines: ['mock-lines'], } this.version = 42 this.MongoManager.findDoc = sinon.stub() return (this.MongoManager.getDocVersion = sinon .stub() .yields(null, this.version)) }) describe('when using a filter', function () { beforeEach(function () { return this.MongoManager.findDoc.yields(null, this.doc) }) it('should error if inS3 is not set to true', function (done) { return this.DocManager._getDoc( this.project_id, this.doc_id, { inS3: false }, err => { expect(err).to.exist return done() } ) }) it('should always get inS3 even when no filter is passed', function (done) { return this.DocManager._getDoc( this.project_id, this.doc_id, undefined, err => { this.MongoManager.findDoc.called.should.equal(false) expect(err).to.exist return done() } ) }) return it('should not error if inS3 is set to true', function (done) { return this.DocManager._getDoc( this.project_id, this.doc_id, { inS3: true }, err => { expect(err).to.not.exist return done() } ) }) }) describe('when the doc is in the doc collection', function () { beforeEach(function () { this.MongoManager.findDoc.yields(null, this.doc) return this.DocManager._getDoc( this.project_id, this.doc_id, { version: true, inS3: true }, this.callback ) }) it('should get the doc from the doc collection', function () { return this.MongoManager.findDoc .calledWith(this.project_id, this.doc_id) .should.equal(true) }) it('should get the doc version from the docOps collection', function () { return this.MongoManager.getDocVersion .calledWith(this.doc_id) .should.equal(true) }) return it('should return the callback with the doc with the version', function () { this.callback.called.should.equal(true) const doc = this.callback.args[0][1] doc.lines.should.equal(this.doc.lines) return doc.version.should.equal(this.version) }) }) describe('without the version filter', function () { beforeEach(function () { this.MongoManager.findDoc.yields(null, this.doc) return this.DocManager._getDoc( this.project_id, this.doc_id, { version: false, inS3: true }, this.callback ) }) return it('should not get the doc version from the docOps collection', function () { return this.MongoManager.getDocVersion.called.should.equal(false) }) }) describe('when MongoManager.findDoc errors', function () { beforeEach(function () { this.MongoManager.findDoc.yields(this.stubbedError) return this.DocManager._getDoc( this.project_id, this.doc_id, { version: true, inS3: true }, this.callback ) }) return it('should return the error', function () { return this.callback.calledWith(this.stubbedError).should.equal(true) }) }) describe('when the doc is archived', function () { beforeEach(function () { this.doc = { _id: this.doc_id, project_id: this.project_id, lines: ['mock-lines'], inS3: true, } this.MongoManager.findDoc.yields(null, this.doc) this.DocArchiveManager.unarchiveDoc = ( project_id, doc_id, callback ) => { this.doc.inS3 = false return callback() } sinon.spy(this.DocArchiveManager, 'unarchiveDoc') return this.DocManager._getDoc( this.project_id, this.doc_id, { version: true, inS3: true }, this.callback ) }) it('should call the DocArchive to unarchive the doc', function () { return this.DocArchiveManager.unarchiveDoc .calledWith(this.project_id, this.doc_id) .should.equal(true) }) it('should look up the doc twice', function () { return this.MongoManager.findDoc.calledTwice.should.equal(true) }) return it('should return the doc', function () { return this.callback.calledWith(null, this.doc).should.equal(true) }) }) return describe('when the doc does not exist in the docs collection', function () { beforeEach(function () { this.MongoManager.findDoc = sinon.stub().yields(null, null) return this.DocManager._getDoc( this.project_id, this.doc_id, { version: true, inS3: true }, this.callback ) }) return it('should return a NotFoundError', function () { return this.callback .calledWith( sinon.match.has( 'message', `No such doc: ${this.doc_id} in project ${this.project_id}` ) ) .should.equal(true) }) }) }) describe('getAllNonDeletedDocs', function () { describe('when the project exists', function () { beforeEach(function () { this.docs = [ { _id: this.doc_id, project_id: this.project_id, lines: ['mock-lines'], }, ] this.MongoManager.getProjectsDocs = sinon .stub() .callsArgWith(3, null, this.docs) this.DocArchiveManager.unArchiveAllDocs = sinon .stub() .callsArgWith(1, null, this.docs) this.filter = { lines: true } return this.DocManager.getAllNonDeletedDocs( this.project_id, this.filter, this.callback ) }) it('should get the project from the database', function () { return this.MongoManager.getProjectsDocs .calledWith(this.project_id, { include_deleted: false }, this.filter) .should.equal(true) }) return it('should return the docs', function () { return this.callback.calledWith(null, this.docs).should.equal(true) }) }) return describe('when there are no docs for the project', function () { beforeEach(function () { this.MongoManager.getProjectsDocs = sinon .stub() .callsArgWith(3, null, null) this.DocArchiveManager.unArchiveAllDocs = sinon .stub() .callsArgWith(1, null) return this.DocManager.getAllNonDeletedDocs( this.project_id, this.filter, this.callback ) }) return it('should return a NotFoundError', function () { return this.callback .calledWith( sinon.match.has('message', `No docs for project ${this.project_id}`) ) .should.equal(true) }) }) }) describe('patchDoc', function () { describe('when the doc exists', function () { beforeEach(function () { this.lines = ['mock', 'doc', 'lines'] this.rev = 77 this.MongoManager.findDoc = sinon .stub() .yields(null, { _id: ObjectId(this.doc_id) }) this.MongoManager.patchDoc = sinon.stub().yields(null) this.DocArchiveManager.archiveDoc = sinon.stub().yields(null) this.meta = {} }) describe('standard path', function () { beforeEach(function (done) { this.callback = sinon.stub().callsFake(done) this.DocManager.patchDoc( this.project_id, this.doc_id, this.meta, this.callback ) }) it('should get the doc', function () { expect(this.MongoManager.findDoc).to.have.been.calledWith( this.project_id, this.doc_id ) }) it('should persist the meta', function () { expect(this.MongoManager.patchDoc).to.have.been.calledWith( this.project_id, this.doc_id, this.meta ) }) it('should return the callback', function () { expect(this.callback).to.have.been.calledWith(null) }) }) describe('background flush disabled and deleting a doc', function () { beforeEach(function (done) { this.settings.docstore.archiveOnSoftDelete = false this.meta.deleted = true this.callback = sinon.stub().callsFake(done) this.DocManager.patchDoc( this.project_id, this.doc_id, this.meta, this.callback ) }) it('should not flush the doc out of mongo', function () { expect(this.DocArchiveManager.archiveDoc).to.not.have.been.called }) }) describe('background flush enabled and not deleting a doc', function () { beforeEach(function (done) { this.settings.docstore.archiveOnSoftDelete = false this.meta.deleted = false this.callback = sinon.stub().callsFake(done) this.DocManager.patchDoc( this.project_id, this.doc_id, this.meta, this.callback ) }) it('should not flush the doc out of mongo', function () { expect(this.DocArchiveManager.archiveDoc).to.not.have.been.called }) }) describe('background flush enabled and deleting a doc', function () { beforeEach(function () { this.settings.docstore.archiveOnSoftDelete = true this.meta.deleted = true }) describe('when the background flush succeeds', function () { beforeEach(function (done) { this.DocArchiveManager.archiveDoc = sinon.stub().yields(null) this.callback = sinon.stub().callsFake(done) this.DocManager.patchDoc( this.project_id, this.doc_id, this.meta, this.callback ) }) it('should not log a warning', function () { expect(this.logger.warn).to.not.have.been.called }) it('should flush the doc out of mongo', function () { expect(this.DocArchiveManager.archiveDoc).to.have.been.calledWith( this.project_id, this.doc_id ) }) }) describe('when the background flush fails', function () { beforeEach(function (done) { this.err = new Error('foo') this.DocArchiveManager.archiveDoc = sinon.stub().yields(this.err) this.callback = sinon.stub().callsFake(done) this.DocManager.patchDoc( this.project_id, this.doc_id, this.meta, this.callback ) }) it('should log a warning', function () { expect(this.logger.warn).to.have.been.calledWith( sinon.match({ project_id: this.project_id, doc_id: this.doc_id, err: this.err, }), 'archiving a single doc in the background failed' ) }) it('should not fail the delete process', function () { expect(this.callback).to.have.been.calledWith(null) }) }) }) }) describe('when the doc does not exist', function () { beforeEach(function () { this.MongoManager.findDoc = sinon.stub().yields(null) this.DocManager.patchDoc( this.project_id, this.doc_id, {}, this.callback ) }) it('should return a NotFoundError', function () { expect(this.callback).to.have.been.calledWith( sinon.match.has( 'message', `No such project/doc to delete: ${this.project_id}/${this.doc_id}` ) ) }) }) }) return describe('updateDoc', function () { beforeEach(function () { this.oldDocLines = ['old', 'doc', 'lines'] this.newDocLines = ['new', 'doc', 'lines'] this.originalRanges = { changes: [ { id: ObjectId().toString(), op: { i: 'foo', p: 3 }, meta: { user_id: ObjectId().toString(), ts: new Date().toString(), }, }, ], } this.newRanges = { changes: [ { id: ObjectId().toString(), op: { i: 'bar', p: 6 }, meta: { user_id: ObjectId().toString(), ts: new Date().toString(), }, }, ], } this.version = 42 this.doc = { _id: this.doc_id, project_id: this.project_id, lines: this.oldDocLines, rev: (this.rev = 5), version: this.version, ranges: this.originalRanges, } this.MongoManager.upsertIntoDocCollection = sinon.stub().callsArg(3) this.MongoManager.setDocVersion = sinon.stub().yields() return (this.DocManager._getDoc = sinon.stub()) }) describe('when only the doc lines have changed', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback ) }) it('should get the existing doc', function () { return this.DocManager._getDoc .calledWith(this.project_id, this.doc_id, { version: true, rev: true, lines: true, version: true, ranges: true, inS3: true, }) .should.equal(true) }) it('should upsert the document to the doc collection', function () { return this.MongoManager.upsertIntoDocCollection .calledWith(this.project_id, this.doc_id, { lines: this.newDocLines }) .should.equal(true) }) it('should not update the version', function () { return this.MongoManager.setDocVersion.called.should.equal(false) }) return it('should return the callback with the new rev', function () { return this.callback .calledWith(null, true, this.rev + 1) .should.equal(true) }) }) describe('when the doc ranges have changed', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc) this.RangeManager.shouldUpdateRanges.returns(true) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.oldDocLines, this.version, this.newRanges, this.callback ) }) it('should upsert the ranges', function () { return this.MongoManager.upsertIntoDocCollection .calledWith(this.project_id, this.doc_id, { ranges: this.newRanges }) .should.equal(true) }) it('should not update the version', function () { return this.MongoManager.setDocVersion.called.should.equal(false) }) return it('should return the callback with the new rev', function () { return this.callback .calledWith(null, true, this.rev + 1) .should.equal(true) }) }) describe('when only the version has changed', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.oldDocLines, this.version + 1, this.originalRanges, this.callback ) }) it('should not change the lines or ranges', function () { return this.MongoManager.upsertIntoDocCollection.called.should.equal( false ) }) it('should update the version', function () { return this.MongoManager.setDocVersion .calledWith(this.doc_id, this.version + 1) .should.equal(true) }) return it('should return the callback with the old rev', function () { return this.callback.calledWith(null, true, this.rev).should.equal(true) }) }) describe('when the doc has not changed at all', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.oldDocLines, this.version, this.originalRanges, this.callback ) }) it('should not update the ranges or lines', function () { return this.MongoManager.upsertIntoDocCollection.called.should.equal( false ) }) it('should not update the version', function () { return this.MongoManager.setDocVersion.called.should.equal(false) }) return it('should return the callback with the old rev and modified == false', function () { return this.callback .calledWith(null, false, this.rev) .should.equal(true) }) }) describe('when the version is null', function () { beforeEach(function () { return this.DocManager.updateDoc( this.project_id, this.doc_id, this.newDocLines, null, this.originalRanges, this.callback ) }) return it('should return an error', function () { return this.callback .calledWith( sinon.match.has('message', 'no lines, version or ranges provided') ) .should.equal(true) }) }) describe('when the lines are null', function () { beforeEach(function () { return this.DocManager.updateDoc( this.project_id, this.doc_id, null, this.version, this.originalRanges, this.callback ) }) return it('should return an error', function () { return this.callback .calledWith( sinon.match.has('message', 'no lines, version or ranges provided') ) .should.equal(true) }) }) describe('when the ranges are null', function () { beforeEach(function () { return this.DocManager.updateDoc( this.project_id, this.doc_id, this.newDocLines, this.version, null, this.callback ) }) return it('should return an error', function () { return this.callback .calledWith( sinon.match.has('message', 'no lines, version or ranges provided') ) .should.equal(true) }) }) describe('when there is a generic error getting the doc', function () { beforeEach(function () { this.error = new Error('doc could not be found') this.DocManager._getDoc = sinon .stub() .callsArgWith(3, this.error, null, null) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback ) }) it('should not upsert the document to the doc collection', function () { return this.MongoManager.upsertIntoDocCollection.called.should.equal( false ) }) return it('should return the callback with the error', function () { return this.callback.calledWith(this.error).should.equal(true) }) }) describe('when the doc lines have not changed', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.oldDocLines.slice(), this.version, this.originalRanges, this.callback ) }) it('should not update the doc', function () { return this.MongoManager.upsertIntoDocCollection.called.should.equal( false ) }) return it('should return the callback with the existing rev', function () { return this.callback .calledWith(null, false, this.rev) .should.equal(true) }) }) return describe('when the doc does not exist', function () { beforeEach(function () { this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, null, null) return this.DocManager.updateDoc( this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback ) }) it('should upsert the document to the doc collection', function () { return this.MongoManager.upsertIntoDocCollection .calledWith(this.project_id, this.doc_id, { lines: this.newDocLines, ranges: this.originalRanges, }) .should.equal(true) }) it('should set the version', function () { return this.MongoManager.setDocVersion .calledWith(this.doc_id, this.version) .should.equal(true) }) return it('should return the callback with the new rev', function () { return this.callback.calledWith(null, true, 1).should.equal(true) }) }) }) })