From ee094a21ebbaa92ab859f27f8eb3c0ff46eb03d7 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:43:54 -0400 Subject: [PATCH] Merge pull request #28401 from overleaf/em-promisify-document-updater-handler Promisify DocumentUpdaterHandler GitOrigin-RevId: 8793f30ef51f5cd5886d0f74773f4b952761c2d4 --- .../DocumentUpdater/DocumentUpdaterHandler.js | 669 +++----- .../DocumentUpdaterHandlerTests.js | 1394 +++++++---------- 2 files changed, 793 insertions(+), 1270 deletions(-) diff --git a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js index 6ec7cda3c9..0b19e8681b 100644 --- a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js +++ b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js @@ -1,271 +1,210 @@ -const request = require('request').defaults({ timeout: 30 * 1000 }) const OError = require('@overleaf/o-error') const settings = require('@overleaf/settings') +const { + fetchJson, + fetchNothing, + fetchString, + RequestFailedError, +} = require('@overleaf/fetch-utils') const _ = require('lodash') -const async = require('async') const logger = require('@overleaf/logger') -const metrics = require('@overleaf/metrics') -const { promisify, callbackify } = require('util') -const { promisifyMultiResult } = require('@overleaf/promise-utils') +const { callbackifyAll } = require('@overleaf/promise-utils') const ProjectGetter = require('../Project/ProjectGetter') const Modules = require('../../infrastructure/Modules') -function getProjectLastUpdatedAt(projectId, callback) { - _makeRequest( - { - path: `/project/${projectId}/last_updated_at`, - method: 'GET', - json: true, - }, - projectId, - 'project.redis.last_updated_at', - (err, body) => { - if (err || !body?.lastUpdatedAt) return callback(err, null) - callback(null, new Date(body.lastUpdatedAt)) - } +const REQUEST_TIMEOUT_MS = 30 * 1000 +const RESYNC_TIMEOUT_MS = 6 * 60 * 1000 +const BASE_URL = settings.apis.documentupdater.url + +async function getProjectLastUpdatedAt(projectId) { + const body = await fetchJson( + `${BASE_URL}/project/${projectId}/last_updated_at`, + { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) } ) + return body.lastUpdatedAt != null ? new Date(body.lastUpdatedAt) : null } /** * @param {string} projectId */ -function flushProjectToMongo(projectId, callback) { - _makeRequest( - { - path: `/project/${projectId}/flush`, - method: 'POST', - }, - projectId, - 'flushing.mongo.project', - callback - ) -} - -function flushMultipleProjectsToMongo(projectIds, callback) { - const jobs = projectIds.map(projectId => callback => { - flushProjectToMongo(projectId, callback) +async function flushProjectToMongo(projectId) { + await fetchNothing(`${BASE_URL}/project/${projectId}/flush`, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }) - async.series(jobs, callback) +} + +async function flushMultipleProjectsToMongo(projectIds) { + for (const projectId of projectIds) { + await flushProjectToMongo(projectId) + } } /** * @param {string} projectId */ -function flushProjectToMongoAndDelete(projectId, callback) { - _makeRequest( - { - path: `/project/${projectId}`, - method: 'DELETE', - }, - projectId, - 'flushing.mongo.project', - callback - ) +async function flushProjectToMongoAndDelete(projectId) { + await fetchNothing(`${BASE_URL}/project/${projectId}`, { + method: 'DELETE', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) } /** * @param {string} projectId * @param {string} docId - * @param {Callback} callback */ -function flushDocToMongo(projectId, docId, callback) { - _makeRequest( - { - path: `/project/${projectId}/doc/${docId}/flush`, - method: 'POST', - }, - projectId, - 'flushing.mongo.doc', - callback - ) +async function flushDocToMongo(projectId, docId) { + await fetchNothing(`${BASE_URL}/project/${projectId}/doc/${docId}/flush`, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) } -function deleteDoc(projectId, docId, ignoreFlushErrors, callback) { - if (typeof ignoreFlushErrors === 'function') { - callback = ignoreFlushErrors - ignoreFlushErrors = false - } - let path = `/project/${projectId}/doc/${docId}` +async function deleteDoc(projectId, docId, ignoreFlushErrors = false) { + const url = new URL(`${BASE_URL}/project/${projectId}/doc/${docId}`) if (ignoreFlushErrors) { - path += '?ignore_flush_errors=true' + url.searchParams.set('ignore_flush_errors', 'true') } - const method = 'DELETE' - _makeRequest( - { - path, - method, - }, - projectId, - 'delete.mongo.doc', - callback - ) + await fetchNothing(url, { + method: 'DELETE', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) } -function getComment(projectId, docId, commentId, callback) { - _makeRequest( - { - path: `/project/${projectId}/doc/${docId}/comment/${commentId}`, - json: true, - }, - projectId, - 'get-comment', - function (error, comment) { - if (error) { - return callback(error) - } - callback(null, comment) - } +async function getComment(projectId, docId, commentId) { + const comment = await fetchJson( + `${BASE_URL}/project/${projectId}/doc/${docId}/comment/${commentId}`, + { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) } ) + return comment } -function getDocument(projectId, docId, fromVersion, callback) { - _makeRequest( - { - path: `/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`, - json: true, - }, - projectId, - 'get-document', - function (error, doc) { - if (error) { - return callback(error) - } - callback(null, doc.lines, doc.version, doc.ranges, doc.ops) - } - ) +async function getDocument(projectId, docId, fromVersion) { + const url = new URL(`${BASE_URL}/project/${projectId}/doc/${docId}`) + url.searchParams.set('fromVersion', fromVersion) + const doc = await fetchJson(url, { + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + return { + lines: doc.lines, + version: doc.version, + ranges: doc.ranges, + ops: doc.ops, + } } /** * Get a document with its history ranges * @param {string} projectId * @param {string} docId - * @param {Callback} callback */ -function getDocumentWithHistoryRanges(projectId, docId, callback) { - _makeRequest( - { - path: `/project/${projectId}/doc/${docId}?historyRanges=true`, - json: true, - }, - projectId, - 'get-document-with-history-ranges', - function (error, doc) { - if (error) { - return callback(error) - } - callback(null, doc) - } +async function getDocumentWithHistoryRanges(projectId, docId) { + const doc = await fetchJson( + `${BASE_URL}/project/${projectId}/doc/${docId}?historyRanges=true`, + { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) } ) + return doc } -function setDocument(projectId, docId, userId, docLines, source, callback) { - _makeRequest( +async function setDocument(projectId, docId, userId, docLines, source) { + const maybeJson = await fetchString( + `${BASE_URL}/project/${projectId}/doc/${docId}`, { - path: `/project/${projectId}/doc/${docId}`, method: 'POST', json: { lines: docLines, source, user_id: userId, }, - }, - projectId, - 'set-document', - callback + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) + + // The set document endpoint sometimes returns json and sometimes just an + // empty response + try { + const { rev, modified } = JSON.parse(maybeJson) + return { rev, modified } + } catch (err) { + return undefined + } } -function appendToDocument(projectId, docId, userId, lines, source, callback) { - _makeRequest( +async function appendToDocument(projectId, docId, userId, lines, source) { + const maybeJson = await fetchString( + `${BASE_URL}/project/${projectId}/doc/${docId}/append`, { - path: `/project/${projectId}/doc/${docId}/append`, method: 'POST', json: { lines, source, user_id: userId, }, - }, - projectId, - 'append-to-document', - callback + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) + + // The append to document endpoint sometimes returns json and sometimes just an + // empty response + try { + const { rev, modified } = JSON.parse(maybeJson) + return { rev, modified } + } catch (err) { + return undefined + } } -function getProjectDocsIfMatch(projectId, projectStateHash, callback) { +async function getProjectDocsIfMatch(projectId, projectStateHash) { // If the project state hasn't changed, we can get all the latest // docs from redis via the docupdater. Otherwise we will need to // fall back to getting them from mongo. - const timer = new metrics.Timer('get-project-docs') - const url = `${settings.apis.documentupdater.url}/project/${projectId}/get_and_flush_if_old?state=${projectStateHash}` - request.post(url, function (error, res, body) { - timer.done() - if (error) { - OError.tag(error, 'error getting project docs from doc updater', { - url, - projectId, - }) - return callback(error) - } - if (res.statusCode === 409) { + const url = new URL(`${BASE_URL}/project/${projectId}/get_and_flush_if_old`) + url.searchParams.set('state', projectStateHash) + + let docs + try { + docs = await fetchJson(url, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + } catch (err) { + if (err instanceof RequestFailedError && err.response.status === 409) { // HTTP response code "409 Conflict" // Docupdater has checked the projectStateHash and found that // it has changed. This means that the docs currently in redis // aren't the only change to the project and the full set of // docs/files should be retreived from docstore/filestore // instead. - callback() - } else if (res.statusCode >= 200 && res.statusCode < 300) { - let docs - try { - docs = JSON.parse(body) - } catch (error1) { - return callback(OError.tag(error1)) - } - callback(null, docs) + return undefined } else { - callback( - new OError( - `doc updater returned a non-success status code: ${res.statusCode}`, - { - projectId, - url, - } - ) - ) + throw err } - }) + } + return docs } -function clearProjectState(projectId, callback) { - _makeRequest( - { - path: `/project/${projectId}/clearState`, - method: 'POST', - }, - projectId, - 'clear-project-state', - callback - ) +async function clearProjectState(projectId) { + await fetchNothing(`${BASE_URL}/project/${projectId}/clearState`, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) } /** * @param {string} projectId * @param {string} docId * @param {string[]} changeIds - * @param {Callback} callback */ async function acceptChanges(projectId, docId, changeIds) { - await _makeRequestAsync( + await fetchNothing( + `${BASE_URL}/project/${projectId}/doc/${docId}/change/accept`, { - path: `/project/${projectId}/doc/${docId}/change/accept`, - json: { change_ids: changeIds }, method: 'POST', - }, - projectId, - 'accept-changes' + json: { change_ids: changeIds }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) - await Modules.promises.hooks.fire('changesAccepted', projectId, docId) } @@ -273,18 +212,33 @@ async function acceptChanges(projectId, docId, changeIds) { * @param {string} projectId * @param {string} docId * @param {string[]} changeIds - * @param {Callback} callback */ -function rejectChanges(projectId, docId, changeIds, userId, callback) { - _makeRequest( +async function rejectChanges(projectId, docId, changeIds, userId) { + const { rejectedChangeIds } = await fetchJson( + `${BASE_URL}/project/${projectId}/doc/${docId}/change/reject`, { - path: `/project/${projectId}/doc/${docId}/change/reject`, + method: 'POST', json: { change_ids: changeIds, user_id: userId }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } + ) + return { rejectedChangeIds } +} + +/** + * @param {string} projectId + * @param {string} docId + * @param {string} threadId + * @param {string} userId + */ +async function resolveThread(projectId, docId, threadId, userId) { + await fetchNothing( + `${BASE_URL}/project/${projectId}/doc/${docId}/comment/${threadId}/resolve`, + { method: 'POST', - }, - projectId, - 'reject-changes', - callback + json: { user_id: userId }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) } @@ -293,67 +247,35 @@ function rejectChanges(projectId, docId, changeIds, userId, callback) { * @param {string} docId * @param {string} threadId * @param {string} userId - * @param {Callback} callback */ -function resolveThread(projectId, docId, threadId, userId, callback) { - _makeRequest( +async function reopenThread(projectId, docId, threadId, userId) { + await fetchNothing( + `${BASE_URL}/project/${projectId}/doc/${docId}/comment/${threadId}/reopen`, { - path: `/project/${projectId}/doc/${docId}/comment/${threadId}/resolve`, method: 'POST', - json: { - user_id: userId, - }, - }, - projectId, - 'resolve-thread', - callback + json: { user_id: userId }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) } -/** - * @param {string} projectId - * @param {string} docId - * @param {string} threadId - * @param {string} userId - * @param {Callback} callback - */ -function reopenThread(projectId, docId, threadId, userId, callback) { - _makeRequest( +async function deleteThread(projectId, docId, threadId, userId) { + await fetchNothing( + `${BASE_URL}/project/${projectId}/doc/${docId}/comment/${threadId}`, { - path: `/project/${projectId}/doc/${docId}/comment/${threadId}/reopen`, - method: 'POST', - json: { - user_id: userId, - }, - }, - projectId, - 'reopen-thread', - callback - ) -} - -function deleteThread(projectId, docId, threadId, userId, callback) { - _makeRequest( - { - path: `/project/${projectId}/doc/${docId}/comment/${threadId}`, method: 'DELETE', - json: { - user_id: userId, - }, - }, - projectId, - 'delete-thread', - callback + json: { user_id: userId }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + } ) } -function resyncProjectHistory( +async function resyncProjectHistory( projectId, projectHistoryId, docs, files, - opts, - callback + opts ) { docs = docs.map(doc => ({ doc: doc.doc._id, @@ -362,9 +284,7 @@ function resyncProjectHistory( // Files without a hash likely do not have a blob. Abort. for (const { file } of files) { if (!file.hash) { - return callback( - new OError('found file with missing hash', { projectId, file }) - ) + throw new OError('found file with missing hash', { projectId, file }) } } files = files.map(file => ({ @@ -382,192 +302,119 @@ function resyncProjectHistory( if (opts.resyncProjectStructureOnly) { body.resyncProjectStructureOnly = opts.resyncProjectStructureOnly } - _makeRequest( - { - path: `/project/${projectId}/history/resync`, - json: body, - method: 'POST', - timeout: 6 * 60 * 1000, // allow 6 minutes for resync - }, - projectId, - 'resync-project-history', - callback - ) + await fetchNothing(`${BASE_URL}/project/${projectId}/history/resync`, { + json: body, + method: 'POST', + signal: AbortSignal.timeout(RESYNC_TIMEOUT_MS), // allow 6 minutes for resync + }) } /** * Block a project from being loaded in docupdater * * @param {string} projectId - * @param {Callback} callback */ -function blockProject(projectId, callback) { - _makeRequest( - { path: `/project/${projectId}/block`, method: 'POST', json: true }, - projectId, - 'block-project', - (err, body) => { - if (err) { - return callback(err) - } - callback(null, body.blocked) - } - ) +async function blockProject(projectId) { + const body = await fetchJson(`${BASE_URL}/project/${projectId}/block`, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + return body.blocked } /** * Unblock a previously blocked project * * @param {string} projectId - * @param {Callback} callback */ -function unblockProject(projectId, callback) { - _makeRequest( - { path: `/project/${projectId}/unblock`, method: 'POST', json: true }, - projectId, - 'unblock-project', - (err, body) => { - if (err) { - return callback(err) - } - callback(null, body.wasBlocked) - } - ) +async function unblockProject(projectId) { + const body = await fetchJson(`${BASE_URL}/project/${projectId}/unblock`, { + method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + return body.wasBlocked } -function updateProjectStructure( +async function updateProjectStructure( projectId, projectHistoryId, userId, changes, - source, - callback + source ) { if ( settings.apis.project_history == null || !settings.apis.project_history.sendProjectStructureOps ) { - return callback() + return } - ProjectGetter.getProjectWithoutLock( + const project = await ProjectGetter.promises.getProjectWithoutLock( projectId, - { overleaf: true }, - (err, project) => { - if (err) { - return callback(err) - } - const historyRangesSupport = _.get( - project, - 'overleaf.history.rangesSupportEnabled', - false - ) - const { - deletes: docDeletes, - adds: docAdds, - renames: docRenames, - } = _getUpdates( - 'doc', - changes.oldDocs, - changes.newDocs, - historyRangesSupport - ) - for (const newEntity of changes.newFiles || []) { - if (!newEntity.file.hash) { - // Files without a hash likely do not have a blob. Abort. - return callback( - new OError('found file with missing hash', { newEntity }) - ) - } - } - const { - deletes: fileDeletes, - adds: fileAdds, - renames: fileRenames, - } = _getUpdates( - 'file', - changes.oldFiles, - changes.newFiles, - historyRangesSupport - ) - const updates = [].concat( - docDeletes, - fileDeletes, - docAdds, - fileAdds, - docRenames, - fileRenames - ) - const projectVersion = - changes && changes.newProject && changes.newProject.version - - if (updates.length < 1) { - return callback() - } - - if (projectVersion == null) { - logger.warn( - { projectId, changes, projectVersion }, - 'did not receive project version in changes' - ) - return callback(new Error('did not receive project version in changes')) - } - - _makeRequest( - { - path: `/project/${projectId}`, - json: { - updates, - userId, - version: projectVersion, - projectHistoryId, - source, - }, - method: 'POST', - }, - projectId, - 'update-project-structure', - callback - ) - } + { overleaf: true } ) -} + const historyRangesSupport = _.get( + project, + 'overleaf.history.rangesSupportEnabled', + false + ) + const { + deletes: docDeletes, + adds: docAdds, + renames: docRenames, + } = _getUpdates('doc', changes.oldDocs, changes.newDocs, historyRangesSupport) + for (const newEntity of changes.newFiles || []) { + if (!newEntity.file.hash) { + // Files without a hash likely do not have a blob. Abort. + throw new OError('found file with missing hash', { newEntity }) + } + } + const { + deletes: fileDeletes, + adds: fileAdds, + renames: fileRenames, + } = _getUpdates( + 'file', + changes.oldFiles, + changes.newFiles, + historyRangesSupport + ) + const updates = [].concat( + docDeletes, + fileDeletes, + docAdds, + fileAdds, + docRenames, + fileRenames + ) + const projectVersion = + changes && changes.newProject && changes.newProject.version -function _makeRequest(options, projectId, metricsKey, callback) { - const timer = new metrics.Timer(metricsKey) - request( - { - url: `${settings.apis.documentupdater.url}${options.path}`, - json: options.json, - method: options.method || 'GET', - timeout: options.timeout || 30 * 1000, + if (updates.length < 1) { + return + } + + if (projectVersion == null) { + logger.warn( + { projectId, changes, projectVersion }, + 'did not receive project version in changes' + ) + throw new Error('did not receive project version in changes') + } + + await fetchNothing(`${BASE_URL}/project/${projectId}`, { + method: 'POST', + json: { + updates, + userId, + version: projectVersion, + projectHistoryId, + source, }, - function (error, res, body) { - timer.done() - if (error) { - logger.warn( - { error, projectId }, - 'error making request to document updater' - ) - callback(error) - } else if (res.statusCode >= 200 && res.statusCode < 300) { - callback(null, body) - } else { - error = new Error( - `document updater returned a failure status code: ${res.statusCode}` - ) - logger.warn( - { error, projectId }, - `document updater returned failure status code: ${res.statusCode}` - ) - callback(error) - } - } - ) + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) } -const _makeRequestAsync = promisify(_makeRequest) - function _getUpdates( entityType, oldEntities, @@ -662,7 +509,7 @@ function buildFileMetadataForHistory(file) { return metadata } -module.exports = { +const DocumentUpdaterHandler = { flushProjectToMongo, flushMultipleProjectsToMongo, flushProjectToMongoAndDelete, @@ -675,7 +522,7 @@ module.exports = { appendToDocument, getProjectDocsIfMatch, clearProjectState, - acceptChanges: callbackify(acceptChanges), + acceptChanges, rejectChanges, resolveThread, reopenThread, @@ -685,33 +532,13 @@ module.exports = { unblockProject, updateProjectStructure, getDocumentWithHistoryRanges, - promises: { - flushProjectToMongo: promisify(flushProjectToMongo), - flushMultipleProjectsToMongo: promisify(flushMultipleProjectsToMongo), - flushProjectToMongoAndDelete: promisify(flushProjectToMongoAndDelete), - flushDocToMongo: promisify(flushDocToMongo), - deleteDoc: promisify(deleteDoc), - getComment: promisify(getComment), - getDocument: promisifyMultiResult(getDocument, [ - 'lines', - 'version', - 'ranges', - 'ops', - ]), - setDocument: promisify(setDocument), - getProjectDocsIfMatch: promisify(getProjectDocsIfMatch), - getProjectLastUpdatedAt: promisify(getProjectLastUpdatedAt), - clearProjectState: promisify(clearProjectState), - acceptChanges, - rejectChanges: promisify(rejectChanges), - resolveThread: promisify(resolveThread), - reopenThread: promisify(reopenThread), - deleteThread: promisify(deleteThread), - resyncProjectHistory: promisify(resyncProjectHistory), - blockProject: promisify(blockProject), - unblockProject: promisify(unblockProject), - updateProjectStructure: promisify(updateProjectStructure), - appendToDocument: promisify(appendToDocument), - getDocumentWithHistoryRanges: promisify(getDocumentWithHistoryRanges), - }, +} + +module.exports = { + ...callbackifyAll(DocumentUpdaterHandler, { + multiResult: { + getDocument: ['lines', 'version', 'ranges', 'ops'], + }, + }), + promises: DocumentUpdaterHandler, } diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js index 95bba9c1d7..3335ea7152 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js @@ -3,6 +3,7 @@ const SandboxedModule = require('sandboxed-module') const path = require('path') const { expect } = require('chai') const { ObjectId } = require('mongodb-legacy') +const nock = require('nock') const modulePath = path.join( __dirname, '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler' @@ -18,7 +19,6 @@ describe('DocumentUpdaterHandler', function () { this.user_id = 'mock-user-id-123' this.project = { _id: this.project_id } - this.request = sinon.stub() this.projectEntityHandler = {} this.settings = { apis: { @@ -32,22 +32,25 @@ describe('DocumentUpdaterHandler', function () { moduleImportSequence: [], } this.source = 'dropbox' + this.docUpdaterMock = nock(this.settings.apis.documentupdater.url) + + this.ProjectGetter = { + promises: { + getProjectWithoutLock: sinon.stub(), + }, + } + this.ProjectGetter.promises.getProjectWithoutLock + .withArgs(this.project_id) + .resolves(this.project) this.handler = SandboxedModule.require(modulePath, { requires: { - request: { - defaults: () => { - return this.request - }, - }, '@overleaf/settings': this.settings, '../Project/ProjectEntityHandler': this.projectEntityHandler, '../../models/Project': { Project: (this.Project = {}), }, - '../Project/ProjectGetter': (this.ProjectGetter = { - getProjectWithoutLock: sinon.stub(), - }), + '../Project/ProjectGetter': this.ProjectGetter, '../../Features/Project/ProjectLocator': {}, '@overleaf/metrics': { Timer: class { @@ -63,70 +66,45 @@ describe('DocumentUpdaterHandler', function () { }, }, }) - this.ProjectGetter.getProjectWithoutLock - .withArgs(this.project_id) - .yields(null, this.project) + }) + + afterEach(function () { + nock.cleanAll() }) describe('flushProjectToMongo', function () { describe('successfully', function () { beforeEach(async function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') + this.docUpdaterMock.post(`/project/${this.project_id}/flush`).reply(204) await this.handler.promises.flushProjectToMongo(this.project_id) }) it('should flush the document from the document updater', function () { - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/flush`, - method: 'POST', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/flush`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushProjectToMongo(this.project_id) - } catch (err) { - error = err - } - - expect(error).to.exist + await expect(this.handler.promises.flushProjectToMongo(this.project_id)) + .to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock.post(`/project/${this.project_id}/flush`).reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushProjectToMongo(this.project_id) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect(this.handler.promises.flushProjectToMongo(this.project_id)) + .to.be.rejected }) }) }) @@ -134,68 +112,40 @@ describe('DocumentUpdaterHandler', function () { describe('flushProjectToMongoAndDelete', function () { describe('successfully', function () { beforeEach(async function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') + this.docUpdaterMock.delete(`/project/${this.project_id}`).reply(204) await this.handler.promises.flushProjectToMongoAndDelete( this.project_id ) }) it('should delete the project from the document updater', function () { - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}`, - method: 'DELETE', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .delete(`/project/${this.project_id}`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushProjectToMongoAndDelete( - this.project_id - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + await expect( + this.handler.promises.flushProjectToMongoAndDelete(this.project_id) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock.delete(`/project/${this.project_id}`).reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushProjectToMongoAndDelete( - this.project_id - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect( + this.handler.promises.flushProjectToMongoAndDelete(this.project_id) + ).to.be.rejected }) }) }) @@ -203,7 +153,9 @@ describe('DocumentUpdaterHandler', function () { describe('flushDocToMongo', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + .reply(204) }) it('should flush the document from the document updater', async function () { @@ -211,63 +163,35 @@ describe('DocumentUpdaterHandler', function () { this.project_id, this.doc_id ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/flush`, - method: 'POST', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushDocToMongo( - this.project_id, - this.doc_id - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + await expect( + this.handler.promises.flushDocToMongo(this.project_id, this.doc_id) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.flushDocToMongo( - this.project_id, - this.doc_id - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect( + this.handler.promises.flushDocToMongo(this.project_id, this.doc_id) + ).to.be.rejected }) }) }) @@ -275,96 +199,70 @@ describe('DocumentUpdaterHandler', function () { describe('deleteDoc', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') + this.docUpdaterMock + .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + .reply(204) }) it('should delete the document from the document updater', async function () { await this.handler.promises.deleteDoc(this.project_id, this.doc_id) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`, - method: 'DELETE', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.deleteDoc(this.project_id, this.doc_id) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + await expect( + this.handler.promises.deleteDoc(this.project_id, this.doc_id) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.deleteDoc(this.project_id, this.doc_id) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect( + this.handler.promises.deleteDoc(this.project_id, this.doc_id) + ).to.be.rejected }) }) describe("with 'ignoreFlushErrors' option", function () { - beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') - }) - it('when option is true, should send a `ignore_flush_errors=true` URL query to document-updater', async function () { + this.docUpdaterMock + .delete( + `/project/${this.project_id}/doc/${this.doc_id}?ignore_flush_errors=true` + ) + .reply(204) await this.handler.promises.deleteDoc( this.project_id, this.doc_id, true ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?ignore_flush_errors=true`, - method: 'DELETE', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.true }) it("when option is false, shouldn't send any URL query to document-updater", async function () { + this.docUpdaterMock + .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + .reply(204) await this.handler.promises.deleteDoc( this.project_id, this.doc_id, false ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`, - method: 'DELETE', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.true }) }) }) @@ -372,7 +270,13 @@ describe('DocumentUpdaterHandler', function () { describe('setDocument', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 204 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}`, { + lines: this.lines, + source: this.source, + user_id: this.user_id, + }) + .reply(204) }) it('should set the document in the document updater', async function () { @@ -383,198 +287,146 @@ describe('DocumentUpdaterHandler', function () { this.lines, this.source ) - this.request - .calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`, - json: { - lines: this.lines, - source: this.source, - user_id: this.user_id, - }, - method: 'POST', - timeout: 30 * 1000, - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}`, { + lines: this.lines, + source: this.source, + user_id: this.user_id, + }) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.setDocument( + await expect( + this.handler.promises.setDocument( this.project_id, this.doc_id, this.user_id, this.lines, this.source ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}`, { + lines: this.lines, + source: this.source, + user_id: this.user_id, + }) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.setDocument( + await expect( + this.handler.promises.setDocument( this.project_id, this.doc_id, this.user_id, this.lines, this.source ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) }) describe('getComment', function () { - describe('successfully', function () { - beforeEach(function () { - this.comment = { - id: 'mock-comment-id-1', - } - this.body = this.comment - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) - }) - - it('should get the comment from the document updater', async function () { - await this.handler.promises.getComment( - this.project_id, - this.doc_id, - this.comment.id + beforeEach(function () { + this.comment = { id: new ObjectId().toString() } + this.docUpdaterMock + .get( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.comment.id}` ) - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.comment.id}` - this.request - .calledWith({ - url, - method: 'GET', - json: true, - timeout: 30 * 1000, - }) - .should.equal(true) - }) + .reply(200, this.comment) + }) + + it('should get the comment from the document updater', async function () { + const body = await this.handler.promises.getComment( + this.project_id, + this.doc_id, + this.comment.id + ) + expect(body).to.deep.equal(this.comment) }) }) describe('getDocument', function () { + beforeEach(function () { + this.doc = { + lines: this.lines, + version: this.version, + ops: ['mock-op-1', 'mock-op-2'], + ranges: { mock: 'ranges' }, + } + this.fromVersion = 2 + }) + describe('successfully', function () { - let getDocumentResponse - beforeEach(async function () { - this.body = { - lines: this.lines, - version: this.version, - ops: (this.ops = ['mock-op-1', 'mock-op-2']), - ranges: (this.ranges = { mock: 'ranges' }), - } - this.fromVersion = 2 - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) - getDocumentResponse = await this.handler.promises.getDocument( + beforeEach(function () { + this.docUpdaterMock + .get( + `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + ) + .reply(200, this.doc) + }) + + it('should return the lines and version', async function () { + const doc = await this.handler.promises.getDocument( this.project_id, this.doc_id, this.fromVersion ) - }) - - it('should get the document from the document updater', function () { - this.request - .calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`, - method: 'GET', - json: true, - timeout: 30 * 1000, - }) - .should.equal(true) - }) - - it('should call the callback with the lines and version', function () { - expect(getDocumentResponse).to.eql({ - lines: this.lines, - version: this.version, - ranges: this.ranges, - ops: this.ops, - }) + expect(doc).to.deep.equal(this.doc) }) }) describe('when the document updater API returns an error', function () { beforeEach(function () { - this.request.callsArgWith( - 1, - new Error('something went wrong'), - null, - null - ) + this.docUpdaterMock + .get( + `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + ) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.getDocument( + await expect( + this.handler.promises.getDocument( this.project_id, this.doc_id, this.fromVersion ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .get( + `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + ) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.getDocument( + await expect( + this.handler.promises.getDocument( this.project_id, this.doc_id, this.fromVersion ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) }) @@ -582,66 +434,58 @@ describe('DocumentUpdaterHandler', function () { describe('getProjectDocsIfMatch', function () { beforeEach(function () { this.project_state_hash = '1234567890abcdef' + this.doc0 = { + _id: this.doc_id, + lines: this.lines, + v: this.version, + } + this.docs = [this.doc0, this.doc0, this.doc0] }) describe('successfully', function () { - let getProjectDocsIfMatchResponse - beforeEach(async function () { - this.doc0 = { - _id: this.doc_id, - lines: this.lines, - v: this.version, - } - this.docs = [this.doc0, this.doc0, this.doc0] - this.body = JSON.stringify(this.docs) - this.request.post = sinon - .stub() - .callsArgWith(1, null, { statusCode: 200 }, this.body) - getProjectDocsIfMatchResponse = - await this.handler.promises.getProjectDocsIfMatch( - this.project_id, - this.project_state_hash + beforeEach(function () { + this.docUpdaterMock + .post( + `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` ) + .reply(200, this.docs) }) - it('should get the documents from the document updater', function () { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` - this.request.post.calledWith(url).should.equal(true) - }) - - it('should call the callback with the documents', function () { - expect(getProjectDocsIfMatchResponse).to.eql(this.docs) + it('should call the callback with the documents', async function () { + const docs = await this.handler.promises.getProjectDocsIfMatch( + this.project_id, + this.project_state_hash + ) + expect(docs).to.deep.equal(this.docs) }) }) describe('when the document updater API returns an error', function () { beforeEach(function () { - this.request.post = sinon - .stub() - .callsArgWith(1, new Error('something went wrong'), null, null) + this.docUpdaterMock + .post( + `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` + ) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.getProjectDocsIfMatch( + await expect( + this.handler.promises.getProjectDocsIfMatch( this.project_id, this.project_state_hash ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a conflict error code', function () { beforeEach(function () { - this.request.post = sinon - .stub() - .callsArgWith(1, null, { statusCode: 409 }, 'Conflict') + this.docUpdaterMock + .post( + `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` + ) + .reply(409) }) it('should return no documents', async function () { @@ -657,63 +501,40 @@ describe('DocumentUpdaterHandler', function () { describe('clearProjectState', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 200 }) + this.docUpdaterMock + .post(`/project/${this.project_id}/clearState`) + .reply(200) }) it('should clear the project state from the document updater', async function () { await this.handler.promises.clearProjectState(this.project_id) - - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`, - method: 'POST', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/clearState`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.clearProjectState(this.project_id) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + await expect(this.handler.promises.clearProjectState(this.project_id)) + .to.be.rejected }) }) describe('when the document updater returns an error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, null) + this.docUpdaterMock + .post(`/project/${this.project_id}/clearState`) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.clearProjectState(this.project_id) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect(this.handler.promises.clearProjectState(this.project_id)) + .to.be.rejected }) }) }) @@ -725,7 +546,14 @@ describe('DocumentUpdaterHandler', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + this.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/change/accept`, + { + change_ids: [this.change_id], + } + ) + .reply(200) }) it('should accept the change in the document updater', async function () { @@ -734,69 +562,39 @@ describe('DocumentUpdaterHandler', function () { this.doc_id, [this.change_id] ) - this.request - .calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`, - json: { - change_ids: [this.change_id], - }, - method: 'POST', - timeout: 30 * 1000, - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/change/accept`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.acceptChanges( - this.project_id, - this.doc_id, - [this.change_id] - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + await expect( + this.handler.promises.acceptChanges(this.project_id, this.doc_id, [ + this.change_id, + ]) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/change/accept`) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.acceptChanges( - this.project_id, - this.doc_id, - [this.change_id] - ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + await expect( + this.handler.promises.acceptChanges(this.project_id, this.doc_id, [ + this.change_id, + ]) + ).to.be.rejected }) }) }) @@ -808,7 +606,11 @@ describe('DocumentUpdaterHandler', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + this.docUpdaterMock + .delete( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + ) + .reply(200) }) it('should delete the thread in the document updater', async function () { @@ -818,67 +620,49 @@ describe('DocumentUpdaterHandler', function () { this.thread_id, this.user_id ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`, - method: 'DELETE', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .delete( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + ) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.deleteThread( + await expect( + this.handler.promises.deleteThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .delete( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + ) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.deleteThread( + await expect( + this.handler.promises.deleteThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) }) @@ -890,7 +674,11 @@ describe('DocumentUpdaterHandler', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + this.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + ) + .reply(200) }) it('should resolve the thread in the document updater', async function () { @@ -900,67 +688,49 @@ describe('DocumentUpdaterHandler', function () { this.thread_id, this.user_id ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve`, - method: 'POST', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + ) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.resolveThread( + await expect( + this.handler.promises.resolveThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + ) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.resolveThread( + await expect( + this.handler.promises.resolveThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) }) @@ -972,7 +742,11 @@ describe('DocumentUpdaterHandler', function () { describe('successfully', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + this.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + ) + .reply(200) }) it('should reopen the thread in the document updater', async function () { @@ -982,67 +756,49 @@ describe('DocumentUpdaterHandler', function () { this.thread_id, this.user_id ) - this.request - .calledWithMatch({ - url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen`, - method: 'POST', - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + ) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.reopenThread( + await expect( + this.handler.promises.reopenThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post( + `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + ) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.reopenThread( + await expect( + this.handler.promises.reopenThread( this.project_id, this.doc_id, this.thread_id, this.user_id ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) }) @@ -1058,7 +814,7 @@ describe('DocumentUpdaterHandler', function () { this.settings.apis.project_history.sendProjectStructureOps = false }) - it('does not make a web request', async function () { + it('returns early', async function () { await this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, @@ -1066,35 +822,32 @@ describe('DocumentUpdaterHandler', function () { {}, this.source ) - this.request.called.should.equal(false) }) }) describe('with project history enabled', function () { beforeEach(function () { this.settings.apis.project_history.sendProjectStructureOps = true - this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}` - this.request.callsArgWith(1, null, { statusCode: 204 }, '') }) describe('when an entity has changed name', function () { it('should send the structure update to the document updater', async function () { - this.docIdA = new ObjectId() - this.docIdB = new ObjectId() - this.changes = { + const docIdA = new ObjectId() + const docIdB = new ObjectId() + const changes = { oldDocs: [ - { path: '/old_a', doc: { _id: this.docIdA } }, - { path: '/old_b', doc: { _id: this.docIdB } }, + { path: '/old_a', doc: { _id: docIdA } }, + { path: '/old_b', doc: { _id: docIdB } }, ], // create new instances of the same ObjectIds so that == doesn't pass newDocs: [ { path: '/old_a', - doc: { _id: new ObjectId(this.docIdA.toString()) }, + doc: { _id: new ObjectId(docIdA.toString()) }, }, { path: '/new_b', - doc: { _id: new ObjectId(this.docIdB.toString()) }, + doc: { _id: new ObjectId(docIdB.toString()) }, }, ], newProject: { version: this.version }, @@ -1103,51 +856,44 @@ describe('DocumentUpdaterHandler', function () { const updates = [ { type: 'rename-doc', - id: this.docIdB.toString(), + id: docIdB.toString(), pathname: '/old_b', newPathname: '/new_b', }, ] + this.docUpdaterMock + .post(`/project/${this.project_id}`, { + updates, + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId, + source: this.source, + }) + .reply(204) await this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, this.user_id, - this.changes, + changes, this.source ) - - this.request - .calledWith({ - url: this.url, - method: 'POST', - json: { - updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, - }, - timeout: 30 * 1000, - }) - .should.equal(true) + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('when a doc has been added', function () { it('should send the structure update to the document updater', async function () { - this.docId = new ObjectId() - this.changes = { - newDocs: [ - { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }, - ], + const docId = new ObjectId() + const changes = { + newDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], newProject: { version: this.version }, } const updates = [ { type: 'add-doc', - id: this.docId.toString(), + id: docId.toString(), pathname: '/foo', docLines: 'a\nb', historyRangesSupport: false, @@ -1158,37 +904,35 @@ describe('DocumentUpdaterHandler', function () { }, ] - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source - ) - - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { + this.docUpdaterMock + .post(`/project/${this.project_id}`, { updates, userId: this.user_id, version: this.version, projectHistoryId: this.projectHistoryId, source: this.source, - }, - timeout: 30 * 1000, - }) + }) + .reply(204) + await this.handler.promises.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + changes, + this.source + ) + + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('when a file has been added', function () { it('should send the structure update to the document updater', async function () { - this.fileId = new ObjectId() - this.changes = { + const fileId = new ObjectId() + const changes = { newFiles: [ { path: '/bar', - file: { _id: this.fileId, hash: '12345' }, + file: { _id: fileId, hash: '12345' }, }, ], newProject: { version: this.version }, @@ -1197,7 +941,7 @@ describe('DocumentUpdaterHandler', function () { const updates = [ { type: 'add-file', - id: this.fileId.toString(), + id: fileId.toString(), pathname: '/bar', docLines: undefined, historyRangesSupport: false, @@ -1208,87 +952,83 @@ describe('DocumentUpdaterHandler', function () { }, ] - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source - ) - - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { + this.docUpdaterMock + .post(`/project/${this.project_id}`, { updates, userId: this.user_id, version: this.version, projectHistoryId: this.projectHistoryId, source: this.source, - }, - timeout: 30 * 1000, - }) + }) + .reply(204) + + await this.handler.promises.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + changes, + this.source + ) + + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('when an entity has been deleted', function () { it('should end the structure update to the document updater', async function () { - this.docId = new ObjectId() - this.changes = { - oldDocs: [ - { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }, - ], + const docId = new ObjectId() + const changes = { + oldDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], newProject: { version: this.version }, } const updates = [ { type: 'rename-doc', - id: this.docId.toString(), + id: docId.toString(), pathname: '/foo', newPathname: '', }, ] - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source - ) - - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { + this.docUpdaterMock + .post(`/project/${this.project_id}`, { updates, userId: this.user_id, version: this.version, projectHistoryId: this.projectHistoryId, source: this.source, - }, - timeout: 30 * 1000, - }) + }) + .reply(204) + + await this.handler.promises.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + changes, + this.source + ) + + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('when a file is converted to a doc', function () { it('should send the delete first', async function () { - this.docId = new ObjectId() - this.fileId = new ObjectId() - this.changes = { + const docId = new ObjectId() + const fileId = new ObjectId() + const changes = { oldFiles: [ { path: '/foo.doc', - file: { _id: this.fileId }, + file: { _id: fileId }, }, ], newDocs: [ { path: '/foo.doc', docLines: 'hello there', - doc: { _id: this.docId }, + doc: { _id: docId }, }, ], newProject: { version: this.version }, @@ -1297,13 +1037,13 @@ describe('DocumentUpdaterHandler', function () { const updates = [ { type: 'rename-file', - id: this.fileId.toString(), + id: fileId.toString(), pathname: '/foo.doc', newPathname: '', }, { type: 'add-doc', - id: this.docId.toString(), + id: docId.toString(), pathname: '/foo.doc', docLines: 'hello there', historyRangesSupport: false, @@ -1314,57 +1054,43 @@ describe('DocumentUpdaterHandler', function () { }, ] - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source - ) - - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { + this.docUpdaterMock + .post(`/project/${this.project_id}`, { updates, userId: this.user_id, version: this.version, projectHistoryId: this.projectHistoryId, source: this.source, - }, - timeout: 30 * 1000, - }) + }) + .reply(204) + + await this.handler.promises.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + changes, + this.source + ) + + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('when the project version is missing', function () { it('should call the callback with an error', async function () { - this.docId = new ObjectId() - this.changes = { - oldDocs: [ - { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } }, - ], + const docId = new ObjectId() + const changes = { + oldDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], } - - let error - - try { - await this.handler.promises.updateProjectStructure( + await expect( + this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, this.user_id, - this.changes, + changes, this.source ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'did not receive project version in changes' - ) + ).to.be.rejectedWith('did not receive project version in changes') }) }) @@ -1417,6 +1143,16 @@ describe('DocumentUpdaterHandler', function () { }, ] + this.docUpdaterMock + .post(`/project/${this.project_id}`, { + updates, + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId, + source: this.source, + }) + .reply(204) + await this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, @@ -1425,24 +1161,13 @@ describe('DocumentUpdaterHandler', function () { this.source ) - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { - updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, - }, - timeout: 30 * 1000, - }) + expect(this.docUpdaterMock.isDone()).to.be.true }) it('should include flag when history ranges support is enabled', async function () { - this.ProjectGetter.getProjectWithoutLock + this.ProjectGetter.promises.getProjectWithoutLock .withArgs(this.project_id) - .yields(null, { + .resolves({ _id: this.project_id, overleaf: { history: { rangesSupportEnabled: true } }, }) @@ -1461,6 +1186,16 @@ describe('DocumentUpdaterHandler', function () { }, ] + this.docUpdaterMock + .post(`/project/${this.project_id}`, { + updates, + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId, + source: this.source, + }) + .reply(204) + await this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, @@ -1469,34 +1204,13 @@ describe('DocumentUpdaterHandler', function () { this.source ) - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { - updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, - }, - timeout: 30 * 1000, - }) + expect(this.docUpdaterMock.isDone()).to.be.true }) }) describe('with filestore disabled', function () { - it('should add files without URL and with createdBlob', async function () { + beforeEach(function () { this.fileId = new ObjectId() - this.changes = { - newFiles: [ - { - path: '/bar', - file: { _id: this.fileId, hash: '12345' }, - }, - ], - newProject: { version: this.version }, - } - const updates = [ { type: 'add-file', @@ -1511,6 +1225,28 @@ describe('DocumentUpdaterHandler', function () { }, ] + this.docUpdaterMock + .post(`/project/${this.project_id}`, { + updates, + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId, + source: this.source, + }) + .reply(204) + }) + + it('should add files without URL and with createdBlob', async function () { + this.changes = { + newFiles: [ + { + path: '/bar', + file: { _id: this.fileId, hash: '12345' }, + }, + ], + newProject: { version: this.version }, + } + await this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, @@ -1519,19 +1255,9 @@ describe('DocumentUpdaterHandler', function () { this.source ) - this.request.should.have.been.calledWith({ - url: this.url, - method: 'POST', - json: { - updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, - }, - timeout: 30 * 1000, - }) + expect(this.docUpdaterMock.isDone()).to.be.true }) + it('should flag files without hash', async function () { this.fileId = new ObjectId() this.changes = { @@ -1544,19 +1270,15 @@ describe('DocumentUpdaterHandler', function () { newProject: { version: this.version }, } - let error - try { - await this.handler.promises.updateProjectStructure( + await expect( + this.handler.promises.updateProjectStructure( this.project_id, this.projectHistoryId, this.user_id, this.changes, this.source ) - } catch (err) { - error = err - } - expect(error).to.exist + ).to.be.rejected }) }) }) @@ -1571,9 +1293,20 @@ describe('DocumentUpdaterHandler', function () { { doc: { _id: docId2 }, path: 'references.bib' }, ] const files = [] - this.request.yields(null, { statusCode: 200 }) const projectId = new ObjectId() const projectHistoryId = 99 + + this.docUpdaterMock + .post(`/project/${projectId}/history/resync`, { + docs: [ + { doc: docId1.toString(), path: 'main.tex' }, + { doc: docId2.toString(), path: 'references.bib' }, + ], + files: [], + projectHistoryId, + }) + .reply(200) + await this.handler.promises.resyncProjectHistory( projectId, projectHistoryId, @@ -1581,20 +1314,10 @@ describe('DocumentUpdaterHandler', function () { files, {} ) - this.request.should.have.been.calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`, - method: 'POST', - json: { - docs: [ - { doc: docId1, path: 'main.tex' }, - { doc: docId2, path: 'references.bib' }, - ], - files: [], - projectHistoryId, - }, - timeout: 6 * 60 * 1000, - }) + + expect(this.docUpdaterMock.isDone()).to.be.true }) + it('should add files', async function () { const fileId1 = new ObjectId() const fileId2 = new ObjectId() @@ -1632,9 +1355,47 @@ describe('DocumentUpdaterHandler', function () { }, ] const docs = [] - this.request.yields(null, { statusCode: 200 }) const projectId = new ObjectId() const projectHistoryId = 99 + + this.docUpdaterMock + .post(`/project/${projectId}/history/resync`, { + docs: [], + files: [ + { + file: fileId1.toString(), + _hash: '42', + path: '1.png', + createdBlob: true, + }, + { + file: fileId2.toString(), + _hash: '1337', + path: '1.bib', + createdBlob: true, + metadata: { + importedAt: fileCreated2.toISOString(), + provider: 'references-provider', + }, + }, + { + file: fileId3.toString(), + _hash: '21', + path: 'bar.txt', + createdBlob: true, + metadata: { + importedAt: fileCreated3.toISOString(), + provider: 'project_output_file', + source_project_id: otherProjectId.toString(), + source_output_file_path: 'foo/bar.txt', + // build_id and clsiServerId are omitted + }, + }, + ], + projectHistoryId, + }) + .reply(200) + await this.handler.promises.resyncProjectHistory( projectId, projectHistoryId, @@ -1642,48 +1403,10 @@ describe('DocumentUpdaterHandler', function () { files, {} ) - this.request.should.have.been.calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`, - method: 'POST', - json: { - docs: [], - files: [ - { - file: fileId1, - _hash: '42', - path: '1.png', - createdBlob: true, - metadata: undefined, - }, - { - file: fileId2, - _hash: '1337', - path: '1.bib', - createdBlob: true, - metadata: { - importedAt: fileCreated2, - provider: 'references-provider', - }, - }, - { - file: fileId3, - _hash: '21', - path: 'bar.txt', - createdBlob: true, - metadata: { - importedAt: fileCreated3, - provider: 'project_output_file', - source_project_id: otherProjectId, - source_output_file_path: 'foo/bar.txt', - // build_id and clsiServerId are omitted - }, - }, - ], - projectHistoryId, - }, - timeout: 6 * 60 * 1000, - }) + + expect(this.docUpdaterMock.isDone()).to.be.true }) + it('should add files without URL', async function () { const fileId1 = new ObjectId() const fileId2 = new ObjectId() @@ -1721,46 +1444,36 @@ describe('DocumentUpdaterHandler', function () { }, ] const docs = [] - this.request.yields(null, { statusCode: 200 }) const projectId = new ObjectId() const projectHistoryId = 99 - await this.handler.promises.resyncProjectHistory( - projectId, - projectHistoryId, - docs, - files, - {} - ) - this.request.should.have.been.calledWith({ - url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`, - method: 'POST', - json: { + + this.docUpdaterMock + .post(`/project/${projectId}/history/resync`, { docs: [], files: [ { - file: fileId1, + file: fileId1.toString(), _hash: '42', path: '1.png', createdBlob: true, - metadata: undefined, }, { - file: fileId2, + file: fileId2.toString(), _hash: '1337', path: '1.bib', createdBlob: true, metadata: { - importedAt: fileCreated2, + importedAt: fileCreated2.toISOString(), provider: 'references-provider', }, }, { - file: fileId3, + file: fileId3.toString(), _hash: '21', path: 'bar.txt', createdBlob: true, metadata: { - importedAt: fileCreated3, + importedAt: fileCreated3.toISOString(), provider: 'project_output_file', source_project_id: otherProjectId, source_output_file_path: 'foo/bar.txt', @@ -1769,10 +1482,18 @@ describe('DocumentUpdaterHandler', function () { }, ], projectHistoryId, - }, - timeout: 6 * 60 * 1000, - }) + }) + .reply(200) + await this.handler.promises.resyncProjectHistory( + projectId, + projectHistoryId, + docs, + files, + {} + ) + expect(this.docUpdaterMock.isDone()).to.be.true }) + it('should flag files with missing hashes', async function () { const fileId1 = new ObjectId() const fileId2 = new ObjectId() @@ -1809,7 +1530,6 @@ describe('DocumentUpdaterHandler', function () { }, ] const docs = [] - this.request.yields(null, { statusCode: 200 }) const projectId = new ObjectId() const projectHistoryId = 99 await expect( @@ -1827,10 +1547,14 @@ 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.body = { rev: 1 } + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/append`, { + lines: this.lines, + source: this.source, + user_id: this.user_id, + }) + .reply(200) }) it('should append to the document in the document updater', async function () { @@ -1841,75 +1565,47 @@ describe('DocumentUpdaterHandler', function () { this.lines, this.source ) - 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) + expect(this.docUpdaterMock.isDone()).to.be.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.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/append`) + .replyWithError('boom') }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.appendToDocument( + await expect( + this.handler.promises.appendToDocument( this.project_id, this.doc_id, this.user_id, this.lines, this.source ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) + ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { beforeEach(function () { - this.request.callsArgWith(1, null, { statusCode: 500 }, '') + this.docUpdaterMock + .post(`/project/${this.project_id}/doc/${this.doc_id}/append`) + .reply(500) }) it('should reject with an error', async function () { - let error - - try { - await this.handler.promises.appendToDocument( + await expect( + this.handler.promises.appendToDocument( this.project_id, this.doc_id, this.user_id, this.lines, this.source ) - } catch (err) { - error = err - } - - expect(error).to.be.instanceOf(Error) - expect(error).to.have.property( - 'message', - 'document updater returned a failure status code: 500' - ) + ).to.be.rejected }) }) })