diff --git a/services/web/app/src/Features/Project/ProjectRootDocManager.mjs b/services/web/app/src/Features/Project/ProjectRootDocManager.mjs index a640bf7927..e955b21f92 100644 --- a/services/web/app/src/Features/Project/ProjectRootDocManager.mjs +++ b/services/web/app/src/Features/Project/ProjectRootDocManager.mjs @@ -1,9 +1,3 @@ -/* eslint-disable - n/handle-callback-err, - max-len, - no-unused-vars, - no-useless-escape, -*/ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -18,321 +12,215 @@ import ProjectGetter from './ProjectGetter.mjs' import DocumentHelper from '../Documents/DocumentHelper.mjs' import Path from 'node:path' import fs from 'node:fs' -import async from 'async' +import pLimit from 'p-limit' import globby from 'globby' -import _ from 'lodash' -import { promisifyAll } from '@overleaf/promise-utils' +import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils' import logger from '@overleaf/logger' import { BackgroundTaskTracker } from '../../infrastructure/GracefulShutdown.mjs' const rootDocResets = new BackgroundTaskTracker('root doc resets') -const ProjectRootDocManager = { - setRootDocAutomaticallyInBackground(projectId) { - rootDocResets.add() - setTimeout(async () => { - try { - await ProjectRootDocManager.promises.setRootDocAutomatically(projectId) - } catch (err) { - logger.warn( - { err }, - 'failed to set root doc automatically in background' - ) - } finally { - rootDocResets.done() - } - }, 30 * 1000) - }, - setRootDocAutomatically(projectId, callback) { - if (callback == null) { - callback = function () {} +function setRootDocAutomaticallyInBackground(projectId) { + rootDocResets.add() + setTimeout(async () => { + try { + await ProjectRootDocManager.promises.setRootDocAutomatically(projectId) + } catch (err) { + logger.warn({ err }, 'failed to set root doc automatically in background') + } finally { + rootDocResets.done() } - return ProjectEntityHandler.getAllDocs(projectId, function (error, docs) { - if (error != null) { - return callback(error) - } + }, 30 * 1000) +} - const jobs = _.map( - docs, - (doc, path) => - function (cb) { - if ( - ProjectEntityUpdateHandler.isPathValidForRootDoc(path) && - DocumentHelper.contentHasDocumentclass(doc.lines) - ) { - async.setImmediate(function () { - cb(doc._id) - }) - } else { - async.setImmediate(function () { - cb(null) - }) - } - } +async function setRootDocAutomatically(projectId) { + const docs = await ProjectEntityHandler.promises.getAllDocs(projectId) + + for (const [path, doc] of Object.entries(docs)) { + if ( + ProjectEntityUpdateHandler.isPathValidForRootDoc(path) && + DocumentHelper.contentHasDocumentclass(doc.lines) && + doc._id + ) { + return await ProjectEntityUpdateHandler.promises.setRootDoc( + projectId, + doc._id ) + } + } +} - return async.series(jobs, function (rootDocId) { - if (rootDocId != null) { - return ProjectEntityUpdateHandler.setRootDoc( - projectId, - rootDocId, - callback - ) - } else { - return callback() +async function findRootDocFileFromDirectory(directoryPath) { + const unsortedFiles = await globby(['**/*.{tex,Rtex,Rnw}'], { + cwd: directoryPath, + followSymlinkedDirectories: false, + onlyFiles: true, + case: false, + }) + + // the search order is such that we prefer files closer to the project root, then + // we go by file size in ascending order, because people often have a main + // file that just includes a bunch of other files; then we go by name, in + // order to be deterministic + + const files = await _sortFileList(unsortedFiles, directoryPath) + let firstFileInRootFolder + let doc = null + + while (files.length > 0 && doc == null) { + const file = files.shift() + const content = await fs.promises.readFile( + Path.join(directoryPath, file), + 'utf8' + ) + const normalizedContent = (content || '').replace(/\r/g, '') + if (DocumentHelper.contentHasDocumentclass(normalizedContent)) { + doc = { path: file, content: normalizedContent } + } + + if (!firstFileInRootFolder && !file.includes('/')) { + firstFileInRootFolder = { path: file, content: normalizedContent } + } + } + + // if no doc was found, use the first file in the root folder as the main doc + if (!doc && firstFileInRootFolder) { + doc = firstFileInRootFolder + } + + return { path: doc?.path, content: doc?.content } +} + +async function setRootDocFromName(projectId, rootDocName) { + const docPaths = + await ProjectEntityHandler.promises.getAllDocPathsFromProjectById(projectId) + + let docId, path + // strip off leading and trailing quotes from rootDocName + rootDocName = rootDocName.replace(/^'|'$/g, '') + // prepend a slash for the root folder if not present + if (rootDocName[0] !== '/') { + rootDocName = `/${rootDocName}` + } + // find the root doc from the filename + let rootDocId = null + for (docId in docPaths) { + // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" + path = docPaths[docId] + if (path === rootDocName) { + rootDocId = docId + } + } + // try a basename match if there was no match + if (!rootDocId) { + for (docId in docPaths) { + path = docPaths[docId] + if (Path.basename(path) === Path.basename(rootDocName)) { + rootDocId = docId + } + } + } + // set the root doc id if we found a match + if (rootDocId != null) { + return await ProjectEntityUpdateHandler.promises.setRootDoc( + projectId, + rootDocId + ) + } +} + +async function ensureRootDocumentIsSet(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + rootDoc_id: 1, + }) + if (!project) { + throw new Error('project not found') + } + if (project.rootDoc_id != null) { + return + } + return await ProjectRootDocManager.promises.setRootDocAutomatically(projectId) +} + +/** + * @param {ObjectId | string} projectId + */ +async function ensureRootDocumentIsValid(projectId) { + const project = + await ProjectGetter.promises.getProjectWithoutDocLines(projectId) + if (!project) { + throw new Error('project not found') + } + if (project.rootDoc_id != null) { + const docPath = + await ProjectEntityHandler.promises.getDocPathFromProjectByDocId( + project, + project.rootDoc_id + ) + if (docPath) { + return + } + await ProjectEntityUpdateHandler.promises.unsetRootDoc(projectId) + } + return await ProjectRootDocManager.promises.setRootDocAutomatically(projectId) +} + +async function _sortFileList(listToSort, rootDirectory) { + const limit = pLimit(5) + const files = await Promise.all( + listToSort.map(filePath => + limit(async () => { + const fullPath = Path.join(rootDirectory, filePath) + const stat = await fs.promises.stat(fullPath) + return { + size: stat.size, + path: filePath, + elements: filePath.split(Path.sep).length, + name: Path.basename(filePath), } }) - }) - }, - - findRootDocFileFromDirectory(directoryPath, callback) { - if (callback == null) { - callback = function () {} - } - const filePathsPromise = globby(['**/*.{tex,Rtex,Rnw}'], { - cwd: directoryPath, - followSymlinkedDirectories: false, - onlyFiles: true, - case: false, - }) - - // the search order is such that we prefer files closer to the project root, then - // we go by file size in ascending order, because people often have a main - // file that just includes a bunch of other files; then we go by name, in - // order to be deterministic - filePathsPromise.then( - unsortedFiles => - ProjectRootDocManager._sortFileList( - unsortedFiles, - directoryPath, - function (err, files) { - if (err != null) { - return callback(err) - } - let firstFileInRootFolder - let doc = null - - return async.until( - cb => cb(null, doc != null || files.length === 0), - function (cb) { - const file = files.shift() - return fs.readFile( - Path.join(directoryPath, file), - 'utf8', - function (error, content) { - if (error != null) { - return cb(error) - } - content = (content || '').replace(/\r/g, '') - if (DocumentHelper.contentHasDocumentclass(content)) { - doc = { path: file, content } - } - - if (!firstFileInRootFolder && !file.includes('/')) { - firstFileInRootFolder = { path: file, content } - } - cb(null) - } - ) - }, - err => { - if (err) { - return callback(err) - } - - // if no doc was found, use the first file in the root folder as the main doc - if (!doc && firstFileInRootFolder) { - doc = firstFileInRootFolder - } - - callback(null, doc?.path, doc?.content) - } - ) - } - ), - err => callback(err) ) + ) + return files.sort(_rootDocSort).map(file => file.path) +} - // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha - return null - }, +function _rootDocSort(a, b) { + // sort first by folder depth + if (a.elements !== b.elements) { + return a.elements - b.elements + } + // ensure main.tex is at the start of each folder + if (a.name === 'main.tex' && b.name !== 'main.tex') { + return -1 + } + if (a.name !== 'main.tex' && b.name === 'main.tex') { + return 1 + } + // prefer smaller files + if (a.size !== b.size) { + return a.size - b.size + } + // otherwise, use the full path name + return a.path.localeCompare(b.path) +} - setRootDocFromName(projectId, rootDocName, callback) { - if (callback == null) { - callback = function () {} - } - return ProjectEntityHandler.getAllDocPathsFromProjectById( - projectId, - function (error, docPaths) { - let docId, path - if (error != null) { - return callback(error) - } - // strip off leading and trailing quotes from rootDocName - rootDocName = rootDocName.replace(/^\'|\'$/g, '') - // prepend a slash for the root folder if not present - if (rootDocName[0] !== '/') { - rootDocName = `/${rootDocName}` - } - // find the root doc from the filename - let rootDocId = null - for (docId in docPaths) { - // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" - path = docPaths[docId] - if (path === rootDocName) { - rootDocId = docId - } - } - // try a basename match if there was no match - if (!rootDocId) { - for (docId in docPaths) { - path = docPaths[docId] - if (Path.basename(path) === Path.basename(rootDocName)) { - rootDocId = docId - } - } - } - // set the root doc id if we found a match - if (rootDocId != null) { - return ProjectEntityUpdateHandler.setRootDoc( - projectId, - rootDocId, - callback - ) - } else { - return callback() - } - } - ) - }, - - ensureRootDocumentIsSet(projectId, callback) { - if (callback == null) { - callback = function () {} - } - return ProjectGetter.getProject( - projectId, - { rootDoc_id: 1 }, - function (error, project) { - if (error != null) { - return callback(error) - } - if (project == null) { - return callback(new Error('project not found')) - } - - if (project.rootDoc_id != null) { - return callback() - } else { - return ProjectRootDocManager.setRootDocAutomatically( - projectId, - callback - ) - } - } - ) - }, - - /** - * @param {ObjectId | string} project_id - * @param {Function} callback - */ - ensureRootDocumentIsValid(projectId, callback) { - ProjectGetter.getProjectWithoutDocLines( - projectId, - function (error, project) { - if (error != null) { - return callback(error) - } - if (project == null) { - return callback(new Error('project not found')) - } - - if (project.rootDoc_id != null) { - ProjectEntityHandler.getDocPathFromProjectByDocId( - project, - project.rootDoc_id, - (err, docPath) => { - if (docPath) return callback() - ProjectEntityUpdateHandler.unsetRootDoc(projectId, () => - ProjectRootDocManager.setRootDocAutomatically( - projectId, - callback - ) - ) - } - ) - } else { - return ProjectRootDocManager.setRootDocAutomatically( - projectId, - callback - ) - } - } - ) - }, - - _sortFileList(listToSort, rootDirectory, callback) { - if (callback == null) { - callback = function () {} - } - return async.mapLimit( - listToSort, - 5, - (filePath, cb) => - fs.stat(Path.join(rootDirectory, filePath), function (err, stat) { - if (err != null) { - return cb(err) - } - return cb(null, { - size: stat.size, - path: filePath, - elements: filePath.split(Path.sep).length, - name: Path.basename(filePath), - }) - }), - function (err, files) { - if (err != null) { - return callback(err) - } - - return callback( - null, - _.map( - files.sort(ProjectRootDocManager._rootDocSort), - file => file.path - ) - ) - } - ) - }, - - _rootDocSort(a, b) { - // sort first by folder depth - if (a.elements !== b.elements) { - return a.elements - b.elements - } - // ensure main.tex is at the start of each folder - if (a.name === 'main.tex' && b.name !== 'main.tex') { - return -1 - } - if (a.name !== 'main.tex' && b.name === 'main.tex') { - return 1 - } - // prefer smaller files - if (a.size !== b.size) { - return a.size - b.size - } - // otherwise, use the full path name - return a.path.localeCompare(b.path) +const ProjectRootDocManager = { + setRootDocAutomaticallyInBackground, + setRootDocAutomatically: callbackify(setRootDocAutomatically), + findRootDocFileFromDirectory: callbackifyMultiResult( + findRootDocFileFromDirectory, + ['path', 'content'] + ), + setRootDocFromName: callbackify(setRootDocFromName), + ensureRootDocumentIsSet: callbackify(ensureRootDocumentIsSet), + ensureRootDocumentIsValid: callbackify(ensureRootDocumentIsValid), + promises: { + setRootDocAutomatically, + findRootDocFileFromDirectory, + setRootDocFromName, + ensureRootDocumentIsSet, + ensureRootDocumentIsValid, }, } -ProjectRootDocManager.promises = promisifyAll(ProjectRootDocManager, { - without: ['_rootDocSort', 'setRootDocAutomaticallyInBackground'], - multiResult: { - findRootDocFileFromDirectory: ['path', 'content'], - }, -}) - export default ProjectRootDocManager diff --git a/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs b/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs index 4c56407603..48ae8394aa 100644 --- a/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs +++ b/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs @@ -19,31 +19,41 @@ describe('ProjectRootDocManager', function () { ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex' ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex' ctx.sl_req_id = 'sl-req-id-123' - ctx.callback = sinon.stub() ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex'] ctx.globby = sinon.stub().resolves(ctx.globbyFiles) ctx.fs = { - readFile: sinon.stub().callsArgWith(2, new Error('file not found')), - stat: sinon.stub().callsArgWith(1, null, { size: 100 }), + promises: { + readFile: sinon.stub().rejects(new Error('file not found')), + stat: sinon.stub().resolves({ size: 100 }), + }, + } + ctx.ProjectEntityHandler = { + promises: {}, + } + ctx.ProjectEntityUpdateHandler = { + promises: {}, + } + ctx.ProjectGetter = { + promises: {}, } vi.doMock( '../../../../app/src/Features/Project/ProjectEntityHandler', () => ({ - default: (ctx.ProjectEntityHandler = {}), + default: ctx.ProjectEntityHandler, }) ) vi.doMock( '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', () => ({ - default: (ctx.ProjectEntityUpdateHandler = {}), + default: ctx.ProjectEntityUpdateHandler, }) ) vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ - default: (ctx.ProjectGetter = {}), + default: ctx.ProjectGetter, })) vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({ @@ -66,7 +76,9 @@ describe('ProjectRootDocManager', function () { describe('setRootDocAutomatically', function () { beforeEach(function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon .stub() .returns(true) @@ -100,22 +112,22 @@ describe('ProjectRootDocManager', function () { lines: ['Hello world'], }, } - ctx.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.promises.getAllDocs = sinon .stub() - .callsArgWith(1, null, ctx.docs) + .resolves(ctx.docs) await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( ctx.project_id ) }) it('should check the docs of the project', function (ctx) { - ctx.ProjectEntityHandler.getAllDocs + ctx.ProjectEntityHandler.promises.getAllDocs .calledWith(ctx.project_id) .should.equal(true) }) it('should set the root doc to the doc containing a documentclass', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId2) .should.equal(true) }) @@ -133,16 +145,16 @@ describe('ProjectRootDocManager', function () { lines: ['\\documentclass{article}', '\\input{chapter1}'], }, } - ctx.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.promises.getAllDocs = sinon .stub() - .callsArgWith(1, null, ctx.docs) + .resolves(ctx.docs) await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( ctx.project_id ) }) it('should set the root doc to the doc containing a documentclass', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId2) .should.equal(true) }) @@ -160,37 +172,37 @@ describe('ProjectRootDocManager', function () { lines: ['%Example: \\documentclass{article}'], }, } - ctx.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.promises.getAllDocs = sinon .stub() - .callsArgWith(1, null, ctx.docs) + .resolves(ctx.docs) await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( ctx.project_id ) }) it('should not set the root doc to the doc containing a documentclass', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal( + false + ) }) }) }) describe('findRootDocFileFromDirectory', function () { beforeEach(function (ctx) { - ctx.fs.readFile - .withArgs('/foo/a.tex') - .callsArgWith(2, null, 'Hello World!') - ctx.fs.readFile + ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('Hello World!') + ctx.fs.promises.readFile .withArgs('/foo/b.tex') - .callsArgWith(2, null, "I'm a little teapot, get me out of here.") - ctx.fs.readFile + .resolves("I'm a little teapot, get me out of here.") + ctx.fs.promises.readFile .withArgs('/foo/main.tex') - .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory") - ctx.fs.readFile + .resolves("Help, I'm trapped in a unit testing factory") + ctx.fs.promises.readFile .withArgs('/foo/c.tex') - .callsArgWith(2, null, 'Tomato, tomahto.') - ctx.fs.readFile + .resolves('Tomato, tomahto.') + ctx.fs.promises.readFile .withArgs('/foo/a/a.tex') - .callsArgWith(2, null, 'Potato? Potahto. Potootee!') + .resolves('Potato? Potahto. Potootee!') ctx.documentclassContent = '% test\n\\documentclass\n% test' }) @@ -214,34 +226,34 @@ describe('ProjectRootDocManager', function () { ) expect(path).to.equal('a.tex') sinon.assert.callOrder( - ctx.fs.readFile.withArgs('/foo/a.tex'), - ctx.fs.readFile.withArgs('/foo/b.tex'), - ctx.fs.readFile.withArgs('/foo/c.tex'), - ctx.fs.readFile.withArgs('/foo/a/a.tex') + ctx.fs.promises.readFile.withArgs('/foo/a.tex'), + ctx.fs.promises.readFile.withArgs('/foo/b.tex'), + ctx.fs.promises.readFile.withArgs('/foo/c.tex'), + ctx.fs.promises.readFile.withArgs('/foo/a/a.tex') ) }) it('processes smaller files first', async function (ctx) { - ctx.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 }) + ctx.fs.promises.stat.withArgs('/foo/c.tex').resolves({ size: 1 }) const { path } = await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('c.tex') sinon.assert.callOrder( - ctx.fs.readFile.withArgs('/foo/c.tex'), - ctx.fs.readFile.withArgs('/foo/a.tex'), - ctx.fs.readFile.withArgs('/foo/b.tex'), - ctx.fs.readFile.withArgs('/foo/a/a.tex') + ctx.fs.promises.readFile.withArgs('/foo/c.tex'), + ctx.fs.promises.readFile.withArgs('/foo/a.tex'), + ctx.fs.promises.readFile.withArgs('/foo/b.tex'), + ctx.fs.promises.readFile.withArgs('/foo/a/a.tex') ) }) }) describe('when main.tex contains a documentclass', function () { beforeEach(function (ctx) { - ctx.fs.readFile + ctx.fs.promises.readFile .withArgs('/foo/main.tex') - .callsArgWith(2, null, ctx.documentclassContent) + .resolves(ctx.documentclassContent) }) it('returns main.tex', async function (ctx) { @@ -254,20 +266,22 @@ describe('ProjectRootDocManager', function () { }) it('processes main.text first and stops processing when it finds the content', async function (ctx) { - await ctx.ProjectRootDocManager.findRootDocFileFromDirectory('/foo') - expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(ctx.fs.readFile).not.to.be.calledWith('/foo/a.tex') + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + '/foo' + ) + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/a.tex') }) }) describe('when main.tex does not contain a line starting with \\documentclass', function () { beforeEach(function (ctx) { - ctx.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo') - ctx.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo') - ctx.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo') - ctx.fs.readFile + ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('foo') + ctx.fs.promises.readFile.withArgs('/foo/main.tex').resolves('foo') + ctx.fs.promises.readFile.withArgs('/foo/z.tex').resolves('foo') + ctx.fs.promises.readFile .withArgs('/foo/nested/chapter1a.tex') - .callsArgWith(2, null, 'foo') + .resolves('foo') }) it('returns the first .tex file from the root folder', async function (ctx) { @@ -308,9 +322,9 @@ describe('ProjectRootDocManager', function () { describe('when a.tex contains a documentclass', function () { beforeEach(function (ctx) { - ctx.fs.readFile + ctx.fs.promises.readFile .withArgs('/foo/a.tex') - .callsArgWith(2, null, ctx.documentclassContent) + .resolves(ctx.documentclassContent) }) it('returns a.tex', async function (ctx) { @@ -326,9 +340,9 @@ describe('ProjectRootDocManager', function () { await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) - expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex') - expect(ctx.fs.readFile).not.to.be.calledWith('/foo/b.tex') + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex') + expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/b.tex') }) }) @@ -343,17 +357,17 @@ describe('ProjectRootDocManager', function () { await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) - expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex') - expect(ctx.fs.readFile).to.be.calledWith('/foo/b.tex') + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex') + expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/b.tex') }) }) describe('when there is an error reading a file', function () { beforeEach(function (ctx) { - ctx.fs.readFile + ctx.fs.promises.readFile .withArgs('/foo/a.tex') - .callsArgWith(2, new Error('something went wrong')) + .rejects(new Error('something went wrong')) }) it('returns an error', async function (ctx) { @@ -375,10 +389,12 @@ describe('ProjectRootDocManager', function () { describe('setRootDocFromName', function () { describe('when there is a suitable root doc', function () { beforeEach(async function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, ctx.docPaths) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + .resolves(ctx.docPaths) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() await ctx.ProjectRootDocManager.promises.setRootDocFromName( ctx.project_id, '/main.tex' @@ -386,13 +402,13 @@ describe('ProjectRootDocManager', function () { }) it('should check the docs of the project', function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById .calledWith(ctx.project_id) .should.equal(true) }) it('should set the root doc to main.tex', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) @@ -400,10 +416,12 @@ describe('ProjectRootDocManager', function () { describe('when there is a suitable root doc but the leading slash is missing', function () { beforeEach(async function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, ctx.docPaths) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + .resolves(ctx.docPaths) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() await ctx.ProjectRootDocManager.promises.setRootDocFromName( ctx.project_id, 'main.tex' @@ -411,13 +429,13 @@ describe('ProjectRootDocManager', function () { }) it('should check the docs of the project', function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById .calledWith(ctx.project_id) .should.equal(true) }) it('should set the root doc to main.tex', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) @@ -425,10 +443,12 @@ describe('ProjectRootDocManager', function () { describe('when there is a suitable root doc with a basename match', function () { beforeEach(async function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, ctx.docPaths) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + .resolves(ctx.docPaths) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() await ctx.ProjectRootDocManager.promises.setRootDocFromName( ctx.project_id, 'chapter1a.tex' @@ -436,13 +456,13 @@ describe('ProjectRootDocManager', function () { }) it('should check the docs of the project', function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById .calledWith(ctx.project_id) .should.equal(true) }) it('should set the root doc using the basename', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId3.toString()) .should.equal(true) }) @@ -450,10 +470,12 @@ describe('ProjectRootDocManager', function () { describe('when there is a suitable root doc but the filename is in quotes', function () { beforeEach(async function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, ctx.docPaths) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + .resolves(ctx.docPaths) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() await ctx.ProjectRootDocManager.promises.setRootDocFromName( ctx.project_id, "'main.tex'" @@ -461,13 +483,13 @@ describe('ProjectRootDocManager', function () { }) it('should check the docs of the project', function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById .calledWith(ctx.project_id) .should.equal(true) }) it('should set the root doc to main.tex', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc + ctx.ProjectEntityUpdateHandler.promises.setRootDoc .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) @@ -475,10 +497,12 @@ describe('ProjectRootDocManager', function () { describe('when there is no suitable root doc', function () { beforeEach(async function (ctx) { - ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, ctx.docPaths) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + .resolves(ctx.docPaths) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon + .stub() + .resolves() await ctx.ProjectRootDocManager.promises.setRootDocFromName( ctx.project_id, 'other.tex' @@ -486,7 +510,9 @@ describe('ProjectRootDocManager', function () { }) it('should not set the root doc', function (ctx) { - ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal( + false + ) }) }) }) @@ -494,82 +520,65 @@ describe('ProjectRootDocManager', function () { describe('ensureRootDocumentIsSet', function () { beforeEach(function (ctx) { ctx.project = {} - ctx.ProjectGetter.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project) + ctx.ProjectRootDocManager.promises.setRootDocAutomatically = sinon .stub() - .callsArgWith(2, null, ctx.project) - ctx.ProjectRootDocManager.setRootDocAutomatically = sinon - .stub() - .callsArgWith(1, null) + .resolves() }) describe('when the root doc is set', function () { - beforeEach(function (ctx) { + beforeEach(async function (ctx) { ctx.project.rootDoc_id = ctx.docId2 - ctx.ProjectRootDocManager.ensureRootDocumentIsSet( - ctx.project_id, - ctx.callback + + await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet( + ctx.project_id ) }) it('should find the project fetching only the rootDoc_id field', function (ctx) { - ctx.ProjectGetter.getProject + ctx.ProjectGetter.promises.getProject .calledWith(ctx.project_id, { rootDoc_id: 1 }) .should.equal(true) }) it('should not try to update the project rootDoc_id', function (ctx) { - ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal( false ) }) - - it('should call the callback', function (ctx) { - ctx.callback.called.should.equal(true) - }) }) describe('when the root doc is not set', function () { - beforeEach(function (ctx) { - ctx.ProjectRootDocManager.ensureRootDocumentIsSet( - ctx.project_id, - ctx.callback + beforeEach(async function (ctx) { + await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet( + ctx.project_id ) }) it('should find the project with only the rootDoc_id field', function (ctx) { - ctx.ProjectGetter.getProject + ctx.ProjectGetter.promises.getProject .calledWith(ctx.project_id, { rootDoc_id: 1 }) .should.equal(true) }) it('should update the project rootDoc_id', function (ctx) { - ctx.ProjectRootDocManager.setRootDocAutomatically + ctx.ProjectRootDocManager.promises.setRootDocAutomatically .calledWith(ctx.project_id) .should.equal(true) }) - - it('should call the callback', function (ctx) { - ctx.callback.called.should.equal(true) - }) }) describe('when the project does not exist', function () { beforeEach(function (ctx) { - ctx.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - ctx.ProjectRootDocManager.ensureRootDocumentIsSet( - ctx.project_id, - ctx.callback - ) + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(null) }) - it('should call the callback with an error', function (ctx) { - ctx.callback - .calledWith( - sinon.match - .instanceOf(Error) - .and(sinon.match.has('message', 'project not found')) + it('should reject', async function (ctx) { + await expect( + ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet( + ctx.project_id ) - .should.equal(true) + ).to.be.rejectedWith('project not found') }) }) }) @@ -577,129 +586,110 @@ describe('ProjectRootDocManager', function () { describe('ensureRootDocumentIsValid', function () { beforeEach(function (ctx) { ctx.project = {} - ctx.ProjectGetter.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project) + ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon .stub() - .callsArgWith(2, null, ctx.project) - ctx.ProjectGetter.getProjectWithoutDocLines = sinon + .resolves(ctx.project) + ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon .stub() - .callsArgWith(1, null, ctx.project) - ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() - ctx.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() - ctx.ProjectRootDocManager.setRootDocAutomatically = sinon + .resolves() + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon .stub() - .callsArgWith(1, null) + .resolves() + ctx.ProjectRootDocManager.promises.setRootDocAutomatically = sinon + .stub() + .resolves(null) }) describe('when the root doc is set', function () { describe('when the root doc is valid', function () { - beforeEach(function (ctx) { + beforeEach(async function (ctx) { ctx.project.rootDoc_id = ctx.docId2 - ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon + ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon .stub() - .callsArgWith(2, null, ctx.docPaths[ctx.docId2]) - ctx.ProjectRootDocManager.ensureRootDocumentIsValid( - ctx.project_id, - ctx.callback + .resolves(ctx.docPaths[ctx.docId2]) + await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid( + ctx.project_id ) }) it('should find the project without doc lines', function (ctx) { - ctx.ProjectGetter.getProjectWithoutDocLines + ctx.ProjectGetter.promises.getProjectWithoutDocLines .calledWith(ctx.project_id) .should.equal(true) }) it('should not try to update the project rootDoc_id', function (ctx) { - ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal( false ) }) - - it('should call the callback', function (ctx) { - ctx.callback.called.should.equal(true) - }) }) describe('when the root doc is not valid', function () { - beforeEach(function (ctx) { + beforeEach(async function (ctx) { ctx.project.rootDoc_id = new ObjectId() - ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon + ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon .stub() - .callsArgWith(2, null, null) - ctx.ProjectRootDocManager.ensureRootDocumentIsValid( - ctx.project_id, - ctx.callback + .resolves(null) + await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid( + ctx.project_id ) }) it('should find the project without doc lines', function (ctx) { - ctx.ProjectGetter.getProjectWithoutDocLines + ctx.ProjectGetter.promises.getProjectWithoutDocLines .calledWith(ctx.project_id) .should.equal(true) }) it('should unset the root doc', function (ctx) { - ctx.ProjectEntityUpdateHandler.unsetRootDoc + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc .calledWith(ctx.project_id) .should.equal(true) }) it('should try to find a new rootDoc', function (ctx) { - ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal( true ) }) - - it('should call the callback', function (ctx) { - ctx.callback.called.should.equal(true) - }) }) }) describe('when the root doc is not set', function () { - beforeEach(function (ctx) { - ctx.ProjectRootDocManager.ensureRootDocumentIsValid( - ctx.project_id, - ctx.callback + beforeEach(async function (ctx) { + await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid( + ctx.project_id ) }) - it('should find the project without doc lines', function (ctx) { - ctx.ProjectGetter.getProjectWithoutDocLines + it('should find the project without doc lines', async function (ctx) { + ctx.ProjectGetter.promises.getProjectWithoutDocLines .calledWith(ctx.project_id) .should.equal(true) }) it('should update the project rootDoc_id', function (ctx) { - ctx.ProjectRootDocManager.setRootDocAutomatically + ctx.ProjectRootDocManager.promises.setRootDocAutomatically .calledWith(ctx.project_id) .should.equal(true) }) - - it('should call the callback', function (ctx) { - ctx.callback.called.should.equal(true) - }) }) describe('when the project does not exist', function () { - beforeEach(function (ctx) { - ctx.ProjectGetter.getProjectWithoutDocLines = sinon + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon .stub() - .callsArgWith(1, null, null) - ctx.ProjectRootDocManager.ensureRootDocumentIsValid( - ctx.project_id, - ctx.callback - ) + .resolves(null) }) - it('should call the callback with an error', function (ctx) { - ctx.callback - .calledWith( - sinon.match - .instanceOf(Error) - .and(sinon.match.has('message', 'project not found')) + it('should reject', async function (ctx) { + await expect( + ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid( + ctx.project_id ) - .should.equal(true) + ).to.be.rejectedWith('project not found') }) }) })