From 1063dabf33f1abb931df6816370286b42bcfb9f7 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Wed, 11 Dec 2024 14:38:58 +0000 Subject: [PATCH] [web+document-updater] Allow appending to documents (#20745) Co-authored-by: David Powell GitOrigin-RevId: f66283926e7da3edf83ada9316c3a001287e1b42 --- services/document-updater/app.js | 1 + .../app/js/DocumentManager.js | 67 +++++++- .../document-updater/app/js/HttpController.js | 31 ++++ .../DocumentManager/DocumentManagerTests.js | 155 +++++++++++++++++- .../js/HttpController/HttpControllerTests.js | 99 ++++++++++- .../DocumentUpdater/DocumentUpdaterHandler.js | 19 +++ .../src/Features/Editor/EditorController.js | 20 +++ .../Project/ProjectEntityUpdateHandler.js | 21 +++ .../DocumentUpdaterHandlerTests.js | 92 +++++++++++ .../unit/src/Editor/EditorControllerTests.js | 53 ++++++ .../ProjectEntityUpdateHandlerTests.js | 103 ++++++++++++ 11 files changed, 649 insertions(+), 12 deletions(-) diff --git a/services/document-updater/app.js b/services/document-updater/app.js index ddb3d9ac77..9466c188ac 100644 --- a/services/document-updater/app.js +++ b/services/document-updater/app.js @@ -145,6 +145,7 @@ app.post( ) app.post('/project/:project_id/clearState', HttpController.clearProjectState) app.post('/project/:project_id/doc/:doc_id', HttpController.setDoc) +app.post('/project/:project_id/doc/:doc_id/append', HttpController.appendToDoc) app.post( '/project/:project_id/doc/:doc_id/flush', HttpController.flushDocIfLoaded diff --git a/services/document-updater/app/js/DocumentManager.js b/services/document-updater/app/js/DocumentManager.js index 2643c64a8d..1b5598aab8 100644 --- a/services/document-updater/app/js/DocumentManager.js +++ b/services/document-updater/app/js/DocumentManager.js @@ -9,6 +9,8 @@ const HistoryManager = require('./HistoryManager') const Errors = require('./Errors') const RangesManager = require('./RangesManager') const { extractOriginOrSource } = require('./Utils') +const { getTotalSizeOfLines } = require('./Limits') +const Settings = require('@overleaf/settings') const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change @@ -112,7 +114,41 @@ const DocumentManager = { } }, - async setDoc(projectId, docId, newLines, originOrSource, userId, undoing) { + async appendToDoc(projectId, docId, linesToAppend, originOrSource, userId) { + const { lines: currentLines } = await DocumentManager.getDoc( + projectId, + docId + ) + const currentLineSize = getTotalSizeOfLines(currentLines) + const addedSize = getTotalSizeOfLines(linesToAppend) + const newlineSize = '\n'.length + + if (currentLineSize + newlineSize + addedSize > Settings.max_doc_length) { + throw new Errors.FileTooLargeError( + 'doc would become too large if appending this text' + ) + } + + return await DocumentManager.setDoc( + projectId, + docId, + currentLines.concat(linesToAppend), + originOrSource, + userId, + false, + false + ) + }, + + async setDoc( + projectId, + docId, + newLines, + originOrSource, + userId, + undoing, + external + ) { if (newLines == null) { throw new Error('No lines were provided to setDoc') } @@ -150,10 +186,12 @@ const DocumentManager = { op, v: version, meta: { - type: 'external', user_id: userId, }, } + if (external) { + update.meta.type = 'external' + } if (origin) { update.meta.origin = origin } else if (source) { @@ -481,7 +519,15 @@ const DocumentManager = { ) }, - async setDocWithLock(projectId, docId, lines, source, userId, undoing) { + async setDocWithLock( + projectId, + docId, + lines, + source, + userId, + undoing, + external + ) { const UpdateManager = require('./UpdateManager') return await UpdateManager.promises.lockUpdatesAndDo( DocumentManager.setDoc, @@ -490,7 +536,20 @@ const DocumentManager = { lines, source, userId, - undoing + undoing, + external + ) + }, + + async appendToDocWithLock(projectId, docId, lines, source, userId) { + const UpdateManager = require('./UpdateManager') + return await UpdateManager.promises.lockUpdatesAndDo( + DocumentManager.appendToDoc, + projectId, + docId, + lines, + source, + userId ) }, diff --git a/services/document-updater/app/js/HttpController.js b/services/document-updater/app/js/HttpController.js index a3c7b6cd44..2d6e81eebb 100644 --- a/services/document-updater/app/js/HttpController.js +++ b/services/document-updater/app/js/HttpController.js @@ -144,6 +144,7 @@ function setDoc(req, res, next) { source, userId, undoing, + true, (error, result) => { timer.done() if (error) { @@ -155,6 +156,35 @@ function setDoc(req, res, next) { ) } +function appendToDoc(req, res, next) { + const docId = req.params.doc_id + const projectId = req.params.project_id + const { lines, source, user_id: userId } = req.body + const timer = new Metrics.Timer('http.appendToDoc') + DocumentManager.appendToDocWithLock( + projectId, + docId, + lines, + source, + userId, + (error, result) => { + timer.done() + if (error instanceof Errors.FileTooLargeError) { + logger.warn('refusing to append to file, it would become too large') + return res.sendStatus(422) + } + if (error) { + return next(error) + } + logger.debug( + { projectId, docId, lines, source, userId }, + 'appending to doc via http' + ) + res.json(result) + } + ) +} + function flushDocIfLoaded(req, res, next) { const docId = req.params.doc_id const projectId = req.params.project_id @@ -460,6 +490,7 @@ module.exports = { peekDoc, getProjectDocsAndFlushIfOld, clearProjectState, + appendToDoc, setDoc, flushDocIfLoaded, deleteDoc, diff --git a/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js b/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js index 75b7209b75..5dc3d1c88f 100644 --- a/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js +++ b/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js @@ -53,6 +53,9 @@ describe('DocumentManager', function () { acceptChanges: sinon.stub(), deleteComment: sinon.stub(), } + this.Settings = { + max_doc_length: 2 * 1024 * 1024, // 2mb + } this.DocumentManager = SandboxedModule.require(modulePath, { requires: { @@ -65,6 +68,7 @@ describe('DocumentManager', function () { './UpdateManager': this.UpdateManager, './RangesManager': this.RangesManager, './Errors': Errors, + '@overleaf/settings': this.Settings, }, }) this.project_id = 'project-id-123' @@ -446,7 +450,8 @@ describe('DocumentManager', function () { this.beforeLines, this.source, this.user_id, - false + false, + true ) }) @@ -480,7 +485,8 @@ describe('DocumentManager', function () { this.beforeLines, this.source, this.user_id, - false + false, + true ) }) @@ -513,7 +519,8 @@ describe('DocumentManager', function () { this.afterLines, this.source, this.user_id, - false + false, + true ) }) @@ -582,7 +589,8 @@ describe('DocumentManager', function () { this.afterLines, this.source, this.user_id, - false + false, + true ) }) @@ -618,7 +626,8 @@ describe('DocumentManager', function () { null, this.source, this.user_id, - false + false, + true ) ).to.be.rejectedWith('No lines were provided to setDoc') }) @@ -642,6 +651,7 @@ describe('DocumentManager', function () { this.afterLines, this.source, this.user_id, + true, true ) }) @@ -650,6 +660,77 @@ describe('DocumentManager', function () { this.ops.map(op => op.u.should.equal(true)) }) }) + + describe('with the external flag', function () { + beforeEach(async function () { + this.undoing = false + // Copy ops so we don't interfere with other tests + this.ops = [ + { i: 'foo', p: 4 }, + { d: 'bar', p: 42 }, + ] + this.DiffCodec.diffAsShareJsOp.returns(this.ops) + await this.DocumentManager.promises.setDoc( + this.project_id, + this.doc_id, + this.afterLines, + this.source, + this.user_id, + this.undoing, + true + ) + }) + + it('should add the external type to update metadata', function () { + this.UpdateManager.promises.applyUpdate + .calledWith(this.project_id, this.doc_id, { + doc: this.doc_id, + v: this.version, + op: this.ops, + meta: { + type: 'external', + source: this.source, + user_id: this.user_id, + }, + }) + .should.equal(true) + }) + }) + + describe('without the external flag', function () { + beforeEach(async function () { + this.undoing = false + // Copy ops so we don't interfere with other tests + this.ops = [ + { i: 'foo', p: 4 }, + { d: 'bar', p: 42 }, + ] + this.DiffCodec.diffAsShareJsOp.returns(this.ops) + await this.DocumentManager.promises.setDoc( + this.project_id, + this.doc_id, + this.afterLines, + this.source, + this.user_id, + this.undoing, + false + ) + }) + + it('should not add the external type to update metadata', function () { + this.UpdateManager.promises.applyUpdate + .calledWith(this.project_id, this.doc_id, { + doc: this.doc_id, + v: this.version, + op: this.ops, + meta: { + source: this.source, + user_id: this.user_id, + }, + }) + .should.equal(true) + }) + }) }) }) @@ -1136,4 +1217,68 @@ describe('DocumentManager', function () { }) }) }) + + describe('appendToDoc', function () { + describe('sucessfully', function () { + beforeEach(async function () { + this.lines = ['one', 'two', 'three'] + this.DocumentManager.promises.setDoc = sinon + .stub() + .resolves({ rev: '123' }) + this.DocumentManager.promises.getDoc = sinon.stub().resolves({ + lines: this.lines, + }) + this.result = await this.DocumentManager.promises.appendToDoc( + this.project_id, + this.doc_id, + ['four', 'five', 'six'], + this.source, + this.user_id + ) + }) + + it('should call setDoc with concatenated lines', function () { + this.DocumentManager.promises.setDoc + .calledWith( + this.project_id, + this.doc_id, + ['one', 'two', 'three', 'four', 'five', 'six'], + this.source, + this.user_id, + false, + false + ) + .should.equal(true) + }) + + it('should return output from setDoc', function () { + this.result.should.deep.equal({ rev: '123' }) + }) + }) + + describe('when doc would become too big', function () { + beforeEach(async function () { + this.Settings.max_doc_length = 100 + this.lines = ['one', 'two', 'three'] + this.DocumentManager.promises.setDoc = sinon + .stub() + .resolves({ rev: '123' }) + this.DocumentManager.promises.getDoc = sinon.stub().resolves({ + lines: this.lines, + }) + }) + + it('should fail with FileTooLarge error', async function () { + expect( + this.DocumentManager.promises.appendToDoc( + this.project_id, + this.doc_id, + ['x'.repeat(1000)], + this.source, + this.user_id + ) + ).to.eventually.be.rejectedWith(Errors.FileTooLargeError) + }) + }) + }) }) diff --git a/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js b/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js index c422b8c2c0..d6aa03ab52 100644 --- a/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js +++ b/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js @@ -209,7 +209,7 @@ describe('HttpController', function () { beforeEach(function () { this.DocumentManager.setDocWithLock = sinon .stub() - .callsArgWith(6, null, { rev: '123' }) + .callsArgWith(7, null, { rev: '123' }) this.HttpController.setDoc(this.req, this.res, this.next) }) @@ -221,7 +221,8 @@ describe('HttpController', function () { this.lines, this.source, this.user_id, - this.undoing + this.undoing, + true ) .should.equal(true) }) @@ -255,7 +256,7 @@ describe('HttpController', function () { beforeEach(function () { this.DocumentManager.setDocWithLock = sinon .stub() - .callsArgWith(6, new Error('oops')) + .callsArgWith(7, new Error('oops')) this.HttpController.setDoc(this.req, this.res, this.next) }) @@ -1103,4 +1104,96 @@ describe('HttpController', function () { }) }) }) + + describe('appendToDoc', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three'] + this.source = 'dropbox' + this.user_id = 'user-id-123' + this.req = { + headers: {}, + params: { + project_id: this.project_id, + doc_id: this.doc_id, + }, + query: {}, + body: { + lines: this.lines, + source: this.source, + user_id: this.user_id, + undoing: (this.undoing = true), + }, + } + }) + + describe('successfully', function () { + beforeEach(function () { + this.DocumentManager.appendToDocWithLock = sinon + .stub() + .callsArgWith(5, null, { rev: '123' }) + this.HttpController.appendToDoc(this.req, this.res, this.next) + }) + + it('should append to the doc', function () { + this.DocumentManager.appendToDocWithLock + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.source, + this.user_id + ) + .should.equal(true) + }) + + it('should return a json response with the document rev from web', function () { + this.res.json.calledWithMatch({ rev: '123' }).should.equal(true) + }) + + it('should log the request', function () { + this.logger.debug + .calledWith( + { + docId: this.doc_id, + projectId: this.project_id, + lines: this.lines, + source: this.source, + userId: this.user_id, + }, + 'appending to doc via http' + ) + .should.equal(true) + }) + + it('should time the request', function () { + this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.appendToDocWithLock = sinon + .stub() + .callsArgWith(5, new Error('oops')) + this.HttpController.appendToDoc(this.req, this.res, this.next) + }) + + it('should call next with the error', function () { + this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + }) + }) + + describe('when the payload is too large', function () { + beforeEach(function () { + this.DocumentManager.appendToDocWithLock = sinon + .stub() + .callsArgWith(5, new Errors.FileTooLargeError()) + this.HttpController.appendToDoc(this.req, this.res, this.next) + }) + + it('should send back a 422 response', function () { + this.res.sendStatus.calledWith(422).should.equal(true) + }) + }) + }) }) diff --git a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js index 7130b7131d..dbb8ce9c74 100644 --- a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js +++ b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js @@ -114,6 +114,23 @@ function setDocument(projectId, docId, userId, docLines, source, callback) { ) } +function appendToDocument(projectId, docId, userId, lines, source, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}/append`, + method: 'POST', + json: { + lines, + source, + user_id: userId, + }, + }, + projectId, + 'append-to-document', + callback + ) +} + function getProjectDocsIfMatch(projectId, projectStateHash, callback) { // If the project state hasn't changed, we can get all the latest // docs from redis via the docupdater. Otherwise we will need to @@ -533,6 +550,7 @@ module.exports = { deleteDoc, getDocument, setDocument, + appendToDocument, getProjectDocsIfMatch, clearProjectState, acceptChanges, @@ -566,5 +584,6 @@ module.exports = { blockProject: promisify(blockProject), unblockProject: promisify(unblockProject), updateProjectStructure: promisify(updateProjectStructure), + appendToDocument: promisify(appendToDocument), }, } diff --git a/services/web/app/src/Features/Editor/EditorController.js b/services/web/app/src/Features/Editor/EditorController.js index fd5a419c02..ea0153fe37 100644 --- a/services/web/app/src/Features/Editor/EditorController.js +++ b/services/web/app/src/Features/Editor/EditorController.js @@ -108,6 +108,26 @@ const EditorController = { ) }, + appendToDoc(projectId, docId, docLines, source, userId, callback) { + ProjectEntityUpdateHandler.appendToDoc( + projectId, + docId, + docLines, + source, + userId, + function (err, doc) { + if (err) { + OError.tag(err, 'error appending to doc', { + projectId, + docId, + }) + return callback(err) + } + callback(err, doc) + } + ) + }, + upsertDoc(projectId, folderId, docName, docLines, source, userId, callback) { ProjectEntityUpdateHandler.upsertDoc( projectId, diff --git a/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js index f72eddfee6..dd86dc4724 100644 --- a/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js +++ b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js @@ -515,6 +515,24 @@ const upsertDoc = wrapWithLock( } ) +const appendToDoc = wrapWithLock( + async (projectId, docId, lines, source, userId) => { + const { element } = await ProjectLocator.promises.findElement({ + project_id: projectId, + element_id: docId, + type: 'doc', + }) + + return await DocumentUpdaterHandler.promises.appendToDocument( + projectId, + element._id, + userId, + lines, + source + ) + } +) + const upsertFile = wrapWithLock({ async beforeLock( projectId, @@ -1212,6 +1230,8 @@ const ProjectEntityUpdateHandler = { upsertDoc: callbackifyMultiResult(upsertDoc, ['doc', 'isNew']), + appendToDoc: callbackify(appendToDoc), + upsertDocWithPath: callbackifyMultiResult(upsertDocWithPath, [ 'doc', 'isNew', @@ -1253,6 +1273,7 @@ const ProjectEntityUpdateHandler = { upsertDocWithPath, upsertFile, upsertFileWithPath, + appendToDocWithPath: appendToDoc, }, async _addDocAndSendToTpds(projectId, folderId, doc) { diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js index cec68eac72..b215d6b42c 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js @@ -1608,4 +1608,96 @@ describe('DocumentUpdaterHandler', function () { ) }) }) + + describe('appendToDocument', function () { + describe('successfully', function () { + beforeEach(function () { + this.body = { + rev: 1, + } + this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + this.handler.appendToDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + it('should append to the document in the document updater', function () { + this.request + .calledWith({ + url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/append`, + json: { + lines: this.lines, + source: this.source, + user_id: this.user_id, + }, + method: 'POST', + timeout: 30 * 1000, + }) + .should.equal(true) + }) + + it('should call the callback with no error', function () { + this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function () { + beforeEach(function () { + this.request.callsArgWith( + 1, + new Error('something went wrong'), + null, + null + ) + this.handler.appendToDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + it('should return an error to the callback', function () { + this.callback + .calledWith(sinon.match.instanceOf(Error)) + .should.equal(true) + }) + }) + + describe('when the document updater returns a failure error code', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.handler.appendToDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + it('should return the callback with an error', function () { + this.callback + .calledWith( + sinon.match + .instanceOf(Error) + .and( + sinon.match.has( + 'message', + 'document updater returned a failure status code: 500' + ) + ) + ) + .should.equal(true) + }) + }) + }) }) diff --git a/services/web/test/unit/src/Editor/EditorControllerTests.js b/services/web/test/unit/src/Editor/EditorControllerTests.js index 208e152486..798f2f9499 100644 --- a/services/web/test/unit/src/Editor/EditorControllerTests.js +++ b/services/web/test/unit/src/Editor/EditorControllerTests.js @@ -14,6 +14,7 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') +const OError = require('@overleaf/o-error') const modulePath = require('path').join( __dirname, @@ -1026,4 +1027,56 @@ describe('EditorController', function () { }) }) }) + + describe('appendToDoc', function () { + describe('on success', function () { + beforeEach(function () { + this.docId = 'doc-1' + this.ProjectEntityUpdateHandler.appendToDoc = sinon + .stub() + .yields(null, { rev: '1' }) + this.EditorController.appendToDoc( + this.project_id, + this.docId, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + it('appends to the doc using the project entity handler', function () { + this.ProjectEntityUpdateHandler.appendToDoc + .calledWith(this.project_id, this.docId, this.docLines, this.source) + .should.equal(true) + }) + }) + + describe('on error', function () { + beforeEach(function () { + this.docId = 'doc-1' + this.ProjectEntityUpdateHandler.appendToDoc = sinon + .stub() + .yields(new Error('foo')) + this.EditorController.appendToDoc( + this.project_id, + this.docId, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + it('tries to append to the doc using the project entity handler', function () { + this.ProjectEntityUpdateHandler.appendToDoc + .calledWith(this.project_id, this.docId, this.docLines, this.source) + .should.equal(true) + }) + + it('tags the error', function () { + this.callback.calledWith(sinon.match.instanceOf(OError)) + }) + }) + }) }) diff --git a/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js b/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js index 93ec104cea..de4a67be93 100644 --- a/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js +++ b/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js @@ -3167,4 +3167,107 @@ describe('ProjectEntityUpdateHandler', function () { }) }) }) + + describe('appendToDoc', function () { + describe('when document cannot be found', function () { + beforeEach(function (done) { + this.appendedLines = ['5678', 'def'] + this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + this.ProjectLocator.promises.findElement = sinon.stub() + this.ProjectLocator.promises.findElement + .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) + .rejects(new Errors.NotFoundError()) + this.ProjectEntityUpdateHandler.appendToDoc( + projectId, + docId, + this.appendedLines, + this.source, + userId, + (...args) => { + this.callback(...args) + done() + } + ) + }) + + it('should not talk to DocumentUpdaterHandler', function () { + this.DocumentUpdaterHandler.promises.appendToDocument.should.not.have + .been.called + }) + + it('should throw the error', function () { + this.callback.should.have.been.calledWith( + sinon.match.instanceOf(Errors.NotFoundError) + ) + }) + }) + + describe('when document is found', function () { + beforeEach(function (done) { + this.appendedLines = ['5678', 'def'] + this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + this.DocumentUpdaterHandler.promises.appendToDocument + .withArgs(projectId, docId, userId, this.appendedLines, this.source) + .resolves({ rev: 1 }) + this.ProjectLocator.promises.findElement = sinon.stub() + this.ProjectLocator.promises.findElement + .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) + .resolves({ element: { _id: docId } }) + this.ProjectEntityUpdateHandler.appendToDoc( + projectId, + docId, + this.appendedLines, + this.source, + userId, + (...args) => { + this.callback(...args) + done() + } + ) + }) + + it('should forward call to DocumentUpdaterHandler.appendToDocument', function () { + this.DocumentUpdaterHandler.promises.appendToDocument.should.have.been.calledWith( + projectId, + docId, + userId, + this.appendedLines, + this.source + ) + }) + + it('should return the response from DocumentUpdaterHandler', function () { + this.callback.should.have.been.calledWith(null, { rev: 1 }) + }) + }) + + describe('when DocumentUpdater throws an error', function () { + beforeEach(function (done) { + this.appendedLines = ['5678', 'def'] + this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + this.DocumentUpdaterHandler.promises.appendToDocument.rejects( + new Error() + ) + this.ProjectLocator.promises.findElement = sinon.stub() + this.ProjectLocator.promises.findElement + .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) + .resolves({ element: { _id: docId } }) + this.ProjectEntityUpdateHandler.appendToDoc( + projectId, + docId, + this.appendedLines, + this.source, + userId, + (...args) => { + this.callback(...args) + done() + } + ) + }) + + it('should return the response from DocumentUpdaterHandler', function () { + this.callback.should.have.been.calledWith(sinon.match.instanceOf(Error)) + }) + }) + }) })