import { beforeEach, describe, expect, it, vi } from 'vitest' import sinon from 'sinon' import OError from '@overleaf/o-error' import mongodb from 'mongodb-legacy' const modulePath = '../../../../app/src/Features/Editor/EditorController' const { ObjectId } = mongodb describe('EditorController', function () { beforeEach(async function (ctx) { ctx.project_id = 'test-project-id' ctx.source = 'dropbox' ctx.user_id = new ObjectId() ctx.doc = { _id: (ctx.doc_id = 'test-doc-id') } ctx.docName = 'doc.tex' ctx.docLines = ['1234', 'dskl'] ctx.file = { _id: (ctx.file_id = 'dasdkjk') } ctx.fileName = 'file.png' ctx.fsPath = '/folder/file.png' ctx.linkedFileData = { provider: 'url' } ctx.newFile = { _id: 'new-file-id' } ctx.folder_id = '123ksajdn' ctx.folder = { _id: ctx.folder_id } ctx.folderName = 'folder' ctx.callback = sinon.stub() vi.doMock( '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', () => ({ default: (ctx.ProjectEntityUpdateHandler = {}), }) ) vi.doMock( '../../../../app/src/Features/Project/ProjectOptionsHandler', () => ({ default: (ctx.ProjectOptionsHandler = { setCompiler: sinon.stub().yields(), setImageName: sinon.stub().yields(), setSpellCheckLanguage: sinon.stub().yields(), }), }) ) vi.doMock( '../../../../app/src/Features/Project/ProjectDetailsHandler', () => ({ default: (ctx.ProjectDetailsHandler = { setProjectDescription: sinon.stub().yields(), renameProject: sinon.stub().yields(), setPublicAccessLevel: sinon.stub().yields(), }), }) ) vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ default: (ctx.ProjectDeleter = { deleteProject: sinon.stub(), }), })) vi.doMock( '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', () => ({ default: (ctx.DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().yields(), setDocument: sinon.stub().yields(), }), }) ) vi.doMock( '../../../../app/src/Features/Editor/EditorRealTimeController', () => ({ default: (ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), }), }) ) ctx.EditorController = (await import(modulePath)).default }) describe('addDoc', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.addDocWithRanges = sinon .stub() .yields(null, ctx.doc, ctx.folder_id) return ctx.EditorController.addDoc( ctx.project_id, ctx.folder_id, ctx.docName, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('should add the doc using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.addDocWithRanges .calledWith( ctx.project_id, ctx.folder_id, ctx.docName, ctx.docLines, {}, ctx.user_id, ctx.source ) .should.equal(true) }) it('should send the update out to the users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewDoc', ctx.folder_id, ctx.doc, ctx.source, ctx.user_id ) .should.equal(true) }) it('calls the callback', function (ctx) { return ctx.callback.calledWith(null, ctx.doc).should.equal(true) }) }) describe('addFile', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.addFile = sinon .stub() .yields(null, ctx.file, ctx.folder_id) return ctx.EditorController.addFile( ctx.project_id, ctx.folder_id, ctx.fileName, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('should add the folder using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.addFile .calledWith( ctx.project_id, ctx.folder_id, ctx.fileName, ctx.fsPath, ctx.linkedFileData, ctx.user_id, ctx.source ) .should.equal(true) }) it('should send the update of a new folder out to the users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFile', ctx.folder_id, ctx.file, ctx.source, ctx.linkedFileData, ctx.user_id ) .should.equal(true) }) it('calls the callback', function (ctx) { return ctx.callback.calledWith(null, ctx.file).should.equal(true) }) }) describe('upsertDoc', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertDoc = sinon .stub() .yields(null, ctx.doc, false) return ctx.EditorController.upsertDoc( ctx.project_id, ctx.folder_id, ctx.docName, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('upserts the doc using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.upsertDoc .calledWith( ctx.project_id, ctx.folder_id, ctx.docName, ctx.docLines, ctx.source ) .should.equal(true) }) it('returns the doc', function (ctx) { return ctx.callback.calledWith(null, ctx.doc).should.equal(true) }) describe('doc does not exist', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertDoc = sinon .stub() .yields(null, ctx.doc, true) return ctx.EditorController.upsertDoc( ctx.project_id, ctx.folder_id, ctx.docName, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('sends an update out to users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewDoc', ctx.folder_id, ctx.doc, ctx.source, ctx.user_id ) .should.equal(true) }) }) }) describe('upsertFile', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertFile = sinon .stub() .yields(null, ctx.newFile, false, ctx.file) return ctx.EditorController.upsertFile( ctx.project_id, ctx.folder_id, ctx.fileName, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('upserts the file using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.upsertFile .calledWith( ctx.project_id, ctx.folder_id, ctx.fileName, ctx.fsPath, ctx.linkedFileData, ctx.user_id, ctx.source ) .should.equal(true) }) it('returns the file', function (ctx) { return ctx.callback.calledWith(null, ctx.newFile).should.equal(true) }) describe('file does not exist', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertFile = sinon .stub() .yields(null, ctx.file, true) return ctx.EditorController.upsertFile( ctx.project_id, ctx.folder_id, ctx.fileName, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('should send the update out to users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFile', ctx.folder_id, ctx.file, ctx.source, ctx.linkedFileData, ctx.user_id ) .should.equal(true) }) }) }) describe('upsertDocWithPath', function () { beforeEach(function (ctx) { ctx.docPath = '/folder/doc' ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() .yields(null, ctx.doc, false, [], ctx.folder) return ctx.EditorController.upsertDocWithPath( ctx.project_id, ctx.docPath, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('upserts the doc using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.upsertDocWithPath .calledWith(ctx.project_id, ctx.docPath, ctx.docLines, ctx.source) .should.equal(true) }) describe('doc does not exist', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() .yields(null, ctx.doc, true, [], ctx.folder) return ctx.EditorController.upsertDocWithPath( ctx.project_id, ctx.docPath, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('should send the update for the doc out to users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewDoc', ctx.folder_id, ctx.doc, ctx.source, ctx.user_id ) .should.equal(true) }) }) describe('folders required for doc do not exist', function () { beforeEach(function (ctx) { const folders = [ (ctx.folderA = { _id: 2, parentFolder_id: 1 }), (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() .yields(null, ctx.doc, true, folders, ctx.folderB) return ctx.EditorController.upsertDocWithPath( ctx.project_id, ctx.docPath, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('should send the update for each folder to users in the project', function (ctx) { ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFolder', ctx.folderA.parentFolder_id, ctx.folderA ) .should.equal(true) return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFolder', ctx.folderB.parentFolder_id, ctx.folderB ) .should.equal(true) }) }) }) describe('upsertFileWithPath', function () { beforeEach(function (ctx) { ctx.filePath = '/folder/file' ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() .yields(null, ctx.newFile, false, ctx.file, [], ctx.folder) return ctx.EditorController.upsertFileWithPath( ctx.project_id, ctx.filePath, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('upserts the file using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.upsertFileWithPath .calledWith( ctx.project_id, ctx.filePath, ctx.fsPath, ctx.linkedFileData, ctx.user_id, ctx.source ) .should.equal(true) }) describe('file does not exist', function () { beforeEach(function (ctx) { ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() .yields(null, ctx.file, true, undefined, [], ctx.folder) return ctx.EditorController.upsertFileWithPath( ctx.project_id, ctx.filePath, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('should send the update for the file out to users in the project', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFile', ctx.folder_id, ctx.file, ctx.source, ctx.linkedFileData, ctx.user_id ) .should.equal(true) }) }) describe('folders required for file do not exist', function () { beforeEach(function (ctx) { const folders = [ (ctx.folderA = { _id: 2, parentFolder_id: 1 }), (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() .yields(null, ctx.file, true, undefined, folders, ctx.folderB) return ctx.EditorController.upsertFileWithPath( ctx.project_id, ctx.filePath, ctx.fsPath, ctx.linkedFileData, ctx.source, ctx.user_id, ctx.callback ) }) it('should send the update for each folder to users in the project', function (ctx) { ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFolder', ctx.folderA.parentFolder_id, ctx.folderA ) .should.equal(true) return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveNewFolder', ctx.folderB.parentFolder_id, ctx.folderB ) .should.equal(true) }) }) }) describe('addFolder', function () { beforeEach(function (ctx) { ctx.EditorController._notifyProjectUsersOfNewFolder = sinon .stub() .yields() ctx.ProjectEntityUpdateHandler.addFolder = sinon .stub() .yields(null, ctx.folder, ctx.folder_id) return ctx.EditorController.addFolder( ctx.project_id, ctx.folder_id, ctx.folderName, ctx.source, ctx.user_id, ctx.callback ) }) it('should add the folder using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.addFolder .calledWith(ctx.project_id, ctx.folder_id, ctx.folderName, ctx.user_id) .should.equal(true) }) it('should notifyProjectUsersOfNewFolder', function (ctx) { return ctx.EditorController._notifyProjectUsersOfNewFolder .calledWith(ctx.project_id, ctx.folder_id, ctx.folder, ctx.user_id) .should.equal(true) }) it('should return the folder in the callback', function (ctx) { return ctx.callback.calledWith(null, ctx.folder).should.equal(true) }) }) describe('mkdirp', function () { beforeEach(function (ctx) { ctx.path = 'folder1/folder2' ctx.folders = [ (ctx.folderA = { _id: 2, parentFolder_id: 1 }), (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] ctx.userId = new ObjectId().toString() ctx.EditorController._notifyProjectUsersOfNewFolders = sinon .stub() .yields() ctx.ProjectEntityUpdateHandler.mkdirp = sinon .stub() .yields(null, ctx.folders, ctx.folder) return ctx.EditorController.mkdirp( ctx.project_id, ctx.path, ctx.userId, ctx.callback ) }) it('should create the folder using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.mkdirp .calledWith(ctx.project_id, ctx.path, ctx.userId) .should.equal(true) }) it('should notifyProjectUsersOfNewFolder', function (ctx) { return ctx.EditorController._notifyProjectUsersOfNewFolders.calledWith( ctx.project_id, ctx.folders ) }) it('should return the folder in the callback', function (ctx) { return ctx.callback .calledWith(null, ctx.folders, ctx.folder) .should.equal(true) }) }) describe('deleteEntity', function () { beforeEach(function (ctx) { ctx.entity_id = 'entity_id_here' ctx.type = 'doc' ctx.ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() return ctx.EditorController.deleteEntity( ctx.project_id, ctx.entity_id, ctx.type, ctx.source, ctx.user_id, ctx.callback ) }) it('should delete the folder using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.deleteEntity .calledWith( ctx.project_id, ctx.entity_id, ctx.type, ctx.user_id, ctx.source ) .should.equal(true) }) it('notify users an entity has been deleted', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'removeEntity', ctx.entity_id, ctx.source) .should.equal(true) }) }) describe('deleteEntityWithPath', function () { beforeEach(function (ctx) { ctx.entity_id = 'entity_id_here' ctx.ProjectEntityUpdateHandler.deleteEntityWithPath = sinon .stub() .yields(null, ctx.entity_id) ctx.path = 'folder1/folder2' return ctx.EditorController.deleteEntityWithPath( ctx.project_id, ctx.path, ctx.source, ctx.user_id, ctx.callback ) }) it('should delete the folder using the project entity handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.deleteEntityWithPath .calledWith(ctx.project_id, ctx.path, ctx.user_id, ctx.source) .should.equal(true) }) it('notify users an entity has been deleted', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'removeEntity', ctx.entity_id, ctx.source) .should.equal(true) }) }) describe('updateProjectDescription', function () { beforeEach(function (ctx) { ctx.description = 'new description' return ctx.EditorController.updateProjectDescription( ctx.project_id, ctx.description, ctx.callback ) }) it('should send the new description to the project details handler', function (ctx) { return ctx.ProjectDetailsHandler.setProjectDescription .calledWith(ctx.project_id, ctx.description) .should.equal(true) }) it('should notify the other clients about the updated description', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'projectDescriptionUpdated', ctx.description ) .should.equal(true) }) }) describe('deleteProject', function () { beforeEach(function (ctx) { ctx.err = 'errro' ctx.ProjectDeleter.deleteProject.callsArgWith(2, ctx.err) }) it('should call the project handler', async function (ctx) { await new Promise(resolve => { ctx.EditorController.deleteProject(ctx.project_id, err => { err.should.equal(ctx.err) ctx.ProjectDeleter.deleteProject .calledWith(ctx.project_id, { deletedReason: 'user', }) .should.equal(true) resolve() }) }) }) }) describe('renameEntity', function () { beforeEach(async function (ctx) { await new Promise(resolve => { ctx.entity_id = 'entity_id_here' ctx.entityType = 'doc' ctx.newName = 'bobsfile.tex' ctx.ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() return ctx.EditorController.renameEntity( ctx.project_id, ctx.entity_id, ctx.entityType, ctx.newName, ctx.user_id, ctx.source, resolve ) }) }) it('should call the project handler', function (ctx) { return ctx.ProjectEntityUpdateHandler.renameEntity .calledWith( ctx.project_id, ctx.entity_id, ctx.entityType, ctx.newName, ctx.user_id, ctx.source ) .should.equal(true) }) it('should emit the update to the room', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveEntityRename', ctx.entity_id, ctx.newName ) .should.equal(true) }) }) describe('moveEntity', function () { beforeEach(function (ctx) { ctx.entity_id = 'entity_id_here' ctx.entityType = 'doc' ctx.ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() return ctx.EditorController.moveEntity( ctx.project_id, ctx.entity_id, ctx.folder_id, ctx.entityType, ctx.user_id, ctx.source, ctx.callback ) }) it('should call the ProjectEntityUpdateHandler', function (ctx) { return ctx.ProjectEntityUpdateHandler.moveEntity .calledWith( ctx.project_id, ctx.entity_id, ctx.folder_id, ctx.entityType, ctx.user_id, ctx.source ) .should.equal(true) }) it('should emit the update to the room', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'reciveEntityMove', ctx.entity_id, ctx.folder_id ) .should.equal(true) }) it('calls the callback', function (ctx) { return ctx.callback.called.should.equal(true) }) }) describe('renameProject', function () { beforeEach(function (ctx) { ctx.err = 'errro' ctx.newName = 'new name here' return ctx.EditorController.renameProject( ctx.project_id, ctx.newName, ctx.callback ) }) it('should call the EditorController', function (ctx) { return ctx.ProjectDetailsHandler.renameProject .calledWith(ctx.project_id, ctx.newName) .should.equal(true) }) it('should emit the update to the room', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'projectNameUpdated', ctx.newName) .should.equal(true) }) }) describe('setCompiler', function () { beforeEach(function (ctx) { ctx.compiler = 'latex' return ctx.EditorController.setCompiler( ctx.project_id, ctx.compiler, ctx.callback ) }) it('should send the new compiler and project id to the project options handler', function (ctx) { ctx.ProjectOptionsHandler.setCompiler .calledWith(ctx.project_id, ctx.compiler) .should.equal(true) return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'compilerUpdated', ctx.compiler) .should.equal(true) }) }) describe('setImageName', function () { beforeEach(function (ctx) { ctx.imageName = 'texlive-1234.5' return ctx.EditorController.setImageName( ctx.project_id, ctx.imageName, ctx.callback ) }) it('should send the new imageName and project id to the project options handler', function (ctx) { ctx.ProjectOptionsHandler.setImageName .calledWith(ctx.project_id, ctx.imageName) .should.equal(true) return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'imageNameUpdated', ctx.imageName) .should.equal(true) }) }) describe('setSpellCheckLanguage', function () { beforeEach(function (ctx) { ctx.languageCode = 'fr' return ctx.EditorController.setSpellCheckLanguage( ctx.project_id, ctx.languageCode, ctx.callback ) }) it('should send the new languageCode and project id to the project options handler', function (ctx) { ctx.ProjectOptionsHandler.setSpellCheckLanguage .calledWith(ctx.project_id, ctx.languageCode) .should.equal(true) return ctx.EditorRealTimeController.emitToRoom .calledWith( ctx.project_id, 'spellCheckLanguageUpdated', ctx.languageCode ) .should.equal(true) }) }) describe('setPublicAccessLevel', function () { describe('when setting to private', function () { beforeEach(function (ctx) { ctx.newAccessLevel = 'private' ctx.ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields() return ctx.EditorController.setPublicAccessLevel( ctx.project_id, ctx.newAccessLevel, ctx.callback ) }) it('should set the access level', function (ctx) { return ctx.ProjectDetailsHandler.setPublicAccessLevel .calledWith(ctx.project_id, ctx.newAccessLevel) .should.equal(true) }) it('should broadcast the access level change', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'project:publicAccessLevel:changed') .should.equal(true) }) it('should not ensure tokens are present for project', function (ctx) { return ctx.ProjectDetailsHandler.ensureTokensArePresent .calledWith(ctx.project_id) .should.equal(false) }) }) describe('when setting to tokenBased', function () { beforeEach(function (ctx) { ctx.newAccessLevel = 'tokenBased' ctx.tokens = { readOnly: 'aaa', readAndWrite: '42bbb' } ctx.ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields() return ctx.EditorController.setPublicAccessLevel( ctx.project_id, ctx.newAccessLevel, ctx.callback ) }) it('should set the access level', function (ctx) { return ctx.ProjectDetailsHandler.setPublicAccessLevel .calledWith(ctx.project_id, ctx.newAccessLevel) .should.equal(true) }) it('should broadcast the access level change', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'project:publicAccessLevel:changed') .should.equal(true) }) it('should ensure tokens are present for project', function (ctx) { return ctx.ProjectDetailsHandler.ensureTokensArePresent .calledWith(ctx.project_id) .should.equal(true) }) }) }) describe('setRootDoc', function () { beforeEach(function (ctx) { ctx.newRootDocID = '21312321321' ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() return ctx.EditorController.setRootDoc( ctx.project_id, ctx.newRootDocID, ctx.callback ) }) it('should call the ProjectEntityUpdateHandler', function (ctx) { return ctx.ProjectEntityUpdateHandler.setRootDoc .calledWith(ctx.project_id, ctx.newRootDocID) .should.equal(true) }) it('should emit the update to the room', function (ctx) { return ctx.EditorRealTimeController.emitToRoom .calledWith(ctx.project_id, 'rootDocUpdated', ctx.newRootDocID) .should.equal(true) }) }) describe('setMainBibliographyDoc', function () { describe('on success', function () { beforeEach(async function (ctx) { await new Promise(resolve => { ctx.mainBibliographyId = 'bib-doc-id' ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon .stub() .yields() ctx.callback = sinon.stub().callsFake(resolve) ctx.EditorController.setMainBibliographyDoc( ctx.project_id, ctx.mainBibliographyId, ctx.callback ) }) }) it('should forward the call to the ProjectEntityUpdateHandler', function (ctx) { expect( ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc ).to.have.been.calledWith(ctx.project_id, ctx.mainBibliographyId) }) it('should emit the update to the room', function (ctx) { expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( ctx.project_id, 'mainBibliographyDocUpdated', ctx.mainBibliographyId ) }) it('should return nothing', function (ctx) { expect(ctx.callback).to.have.been.calledWithExactly() }) }) describe('on error', function () { beforeEach(async function (ctx) { await new Promise(resolve => { ctx.mainBibliographyId = 'bib-doc-id' ctx.error = new Error('oh no') ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon .stub() .yields(ctx.error) ctx.callback = sinon.stub().callsFake(() => resolve()) ctx.EditorController.setMainBibliographyDoc( ctx.project_id, ctx.mainBibliographyId, ctx.callback ) }) }) it('should forward the call to the ProjectEntityUpdateHandler', function (ctx) { expect( ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc ).to.have.been.calledWith(ctx.project_id, ctx.mainBibliographyId) }) it('should return the error', function (ctx) { expect(ctx.callback).to.have.been.calledWithExactly(ctx.error) }) it('should not emit the update to the room', function (ctx) { expect(ctx.EditorRealTimeController.emitToRoom).to.not.have.been.called }) }) }) describe('appendToDoc', function () { describe('on success', function () { beforeEach(function (ctx) { ctx.docId = 'doc-1' ctx.ProjectEntityUpdateHandler.appendToDoc = sinon .stub() .yields(null, { rev: '1' }) ctx.EditorController.appendToDoc( ctx.project_id, ctx.docId, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('appends to the doc using the project entity handler', function (ctx) { ctx.ProjectEntityUpdateHandler.appendToDoc .calledWith(ctx.project_id, ctx.docId, ctx.docLines, ctx.source) .should.equal(true) }) }) describe('on error', function () { beforeEach(function (ctx) { ctx.docId = 'doc-1' ctx.ProjectEntityUpdateHandler.appendToDoc = sinon .stub() .yields(new Error('foo')) ctx.EditorController.appendToDoc( ctx.project_id, ctx.docId, ctx.docLines, ctx.source, ctx.user_id, ctx.callback ) }) it('tries to append to the doc using the project entity handler', function (ctx) { ctx.ProjectEntityUpdateHandler.appendToDoc .calledWith(ctx.project_id, ctx.docId, ctx.docLines, ctx.source) .should.equal(true) }) it('tags the error', function (ctx) { ctx.callback.calledWith(sinon.match.instanceOf(OError)) }) }) }) })