diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js index 8798859831..d1d29f5d35 100644 --- a/services/web/app/src/Features/Compile/CompileManager.js +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -9,254 +9,219 @@ const ClsiManager = require('./ClsiManager') const Metrics = require('@overleaf/metrics') const { RateLimiter } = require('../../infrastructure/RateLimiter') const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache') +const { + callbackify, + callbackifyMultiResult, +} = require('@overleaf/promise-utils') + +function instrumentWithTimer(fn, key) { + return async (...args) => { + const timer = new Metrics.Timer(key) + try { + return await fn(...args) + } finally { + timer.done() + } + } +} + +async function compile(projectId, userId, options = {}) { + const recentlyCompiled = await CompileManager._checkIfRecentlyCompiled( + projectId, + userId + ) + if (recentlyCompiled) { + return { status: 'too-recently-compiled', outputFiles: [] } + } + + try { + const canCompile = await CompileManager._checkIfAutoCompileLimitHasBeenHit( + options.isAutoCompile, + 'everyone' + ) + if (!canCompile) { + return { status: 'autocompile-backoff', outputFiles: [] } + } + } catch (error) { + return { status: 'autocompile-backoff', outputFiles: [] } + } + + await ProjectRootDocManager.promises.ensureRootDocumentIsSet(projectId) + + const limits = + await CompileManager.promises.getProjectCompileLimits(projectId) + for (const key in limits) { + const value = limits[key] + options[key] = value + } + + try { + const canCompile = await CompileManager._checkCompileGroupAutoCompileLimit( + options.isAutoCompile, + limits.compileGroup + ) + if (!canCompile) { + return { status: 'autocompile-backoff', outputFiles: [] } + } + } catch (error) { + return { message: 'autocompile-backoff', outputFiles: [] } + } + + // only pass userId down to clsi if this is a per-user compile + const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId + const { + status, + outputFiles, + clsiServerId, + validationProblems, + stats, + timings, + outputUrlPrefix, + } = await ClsiManager.promises.sendRequest(projectId, compileAsUser, options) + + return { + status, + outputFiles, + clsiServerId, + limits, + validationProblems, + stats, + timings, + outputUrlPrefix, + } +} + +const instrumentedCompile = instrumentWithTimer(compile, 'editor.compile') + +async function getProjectCompileLimits(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + owner_ref: 1, + }) + + const owner = await UserGetter.promises.getUser(project.owner_ref, { + _id: 1, + alphaProgram: 1, + analyticsId: 1, + betaProgram: 1, + features: 1, + }) + + const ownerFeatures = (owner && owner.features) || {} + // put alpha users into their own compile group + if (owner && owner.alphaProgram) { + ownerFeatures.compileGroup = 'alpha' + } + const analyticsId = await UserAnalyticsIdCache.get(owner._id) + + const compileGroup = + ownerFeatures.compileGroup || Settings.defaultFeatures.compileGroup + const limits = { + timeout: + ownerFeatures.compileTimeout || Settings.defaultFeatures.compileTimeout, + compileGroup, + compileBackendClass: compileGroup === 'standard' ? 'n2d' : 'c2d', + ownerAnalyticsId: analyticsId, + } + return limits +} + +async function wordCount(projectId, userId, file, clsiserverid) { + const limits = + await CompileManager.promises.getProjectCompileLimits(projectId) + return await ClsiManager.promises.wordCount( + projectId, + userId, + file, + limits, + clsiserverid + ) +} + +async function stopCompile(projectId, userId) { + const limits = + await CompileManager.promises.getProjectCompileLimits(projectId) + + return await ClsiManager.promises.stopCompile(projectId, userId, limits) +} + +async function deleteAuxFiles(projectId, userId, clsiserverid) { + const limits = + await CompileManager.promises.getProjectCompileLimits(projectId) + + return await ClsiManager.promises.deleteAuxFiles( + projectId, + userId, + limits, + clsiserverid + ) +} module.exports = CompileManager = { - compile(projectId, userId, options = {}, _callback) { - const timer = new Metrics.Timer('editor.compile') - const callback = function (...args) { - timer.done() - _callback(...args) - } - - CompileManager._checkIfRecentlyCompiled( - projectId, - userId, - function (error, recentlyCompiled) { - if (error) { - return callback(error) - } - if (recentlyCompiled) { - return callback(null, 'too-recently-compiled', []) - } - - CompileManager._checkIfAutoCompileLimitHasBeenHit( - options.isAutoCompile, - 'everyone', - function (err, canCompile) { - if (err || !canCompile) { - return callback(null, 'autocompile-backoff', []) - } - - ProjectRootDocManager.ensureRootDocumentIsSet( - projectId, - function (error) { - if (error) { - return callback(error) - } - CompileManager.getProjectCompileLimits( - projectId, - function (error, limits) { - if (error) { - return callback(error) - } - for (const key in limits) { - const value = limits[key] - options[key] = value - } - // Put a lower limit on autocompiles for free users, based on compileGroup - CompileManager._checkCompileGroupAutoCompileLimit( - options.isAutoCompile, - limits.compileGroup, - function (err, canCompile) { - if (err || !canCompile) { - return callback(null, 'autocompile-backoff', []) - } - // only pass userId down to clsi if this is a per-user compile - const compileAsUser = Settings.disablePerUserCompiles - ? undefined - : userId - ClsiManager.sendRequest( - projectId, - compileAsUser, - options, - function ( - error, - status, - outputFiles, - clsiServerId, - validationProblems, - stats, - timings, - outputUrlPrefix - ) { - if (error) { - return callback(error) - } - callback( - null, - status, - outputFiles, - clsiServerId, - limits, - validationProblems, - stats, - timings, - outputUrlPrefix - ) - } - ) - } - ) - } - ) - } - ) - } - ) - } - ) + promises: { + compile: instrumentedCompile, + deleteAuxFiles, + getProjectCompileLimits, + stopCompile, + wordCount, }, + compile: callbackifyMultiResult(instrumentedCompile, [ + 'status', + 'outputFiles', + 'clsiServerId', + 'limits', + 'validationProblems', + 'stats', + 'timings', + 'outputUrlPrefix', + ]), - stopCompile(projectId, userId, callback) { - CompileManager.getProjectCompileLimits(projectId, function (error, limits) { - if (error) { - return callback(error) - } - ClsiManager.stopCompile(projectId, userId, limits, callback) - }) - }, + stopCompile: callbackify(stopCompile), - deleteAuxFiles(projectId, userId, clsiserverid, callback) { - CompileManager.getProjectCompileLimits(projectId, function (error, limits) { - if (error) { - return callback(error) - } - ClsiManager.deleteAuxFiles( - projectId, - userId, - limits, - clsiserverid, - callback - ) - }) - }, + deleteAuxFiles: callbackify(deleteAuxFiles), - getProjectCompileLimits(projectId, callback) { - ProjectGetter.getProject( - projectId, - { owner_ref: 1 }, - function (error, project) { - if (error) { - return callback(error) - } - UserGetter.getUser( - project.owner_ref, - { - _id: 1, - alphaProgram: 1, - analyticsId: 1, - betaProgram: 1, - features: 1, - }, - function (err, owner) { - if (err) { - return callback(err) - } - const ownerFeatures = (owner && owner.features) || {} - // put alpha users into their own compile group - if (owner && owner.alphaProgram) { - ownerFeatures.compileGroup = 'alpha' - } - UserAnalyticsIdCache.callbacks.get( - owner._id, - function (err, analyticsId) { - if (err) { - return callback(err) - } - const compileGroup = - ownerFeatures.compileGroup || - Settings.defaultFeatures.compileGroup - const limits = { - timeout: - ownerFeatures.compileTimeout || - Settings.defaultFeatures.compileTimeout, - compileGroup, - compileBackendClass: - compileGroup === 'standard' ? 'n2d' : 'c2d', - ownerAnalyticsId: analyticsId, - } - callback(null, limits) - } - ) - } - ) - } - ) - }, + getProjectCompileLimits: callbackify(getProjectCompileLimits), COMPILE_DELAY: 1, // seconds - _checkIfRecentlyCompiled(projectId, userId, callback) { + async _checkIfRecentlyCompiled(projectId, userId) { const key = `compile:${projectId}:${userId}` - rclient.set( - key, - true, - 'EX', - this.COMPILE_DELAY, - 'NX', - function (error, ok) { - if (error) { - return callback(error) - } - if (ok === 'OK') { - callback(null, false) - } else { - callback(null, true) - } - } - ) + const ok = await rclient.set(key, true, 'EX', this.COMPILE_DELAY, 'NX') + return ok !== 'OK' }, - _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) { + async _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup) { if (!isAutoCompile) { - return callback(null, true) + return true } if (compileGroup === 'standard') { // apply extra limits to the standard compile group - CompileManager._checkIfAutoCompileLimitHasBeenHit( + return await CompileManager._checkIfAutoCompileLimitHasBeenHit( isAutoCompile, - compileGroup, - callback + compileGroup ) } else { Metrics.inc(`auto-compile-${compileGroup}`) - callback(null, true) + return true } }, // always allow priority group users to compile - _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) { + async _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup) { if (!isAutoCompile) { - return callback(null, true) + return true } Metrics.inc(`auto-compile-${compileGroup}`) const rateLimiter = getAutoCompileRateLimiter(compileGroup) - rateLimiter - .consume('global', 1, { method: 'global' }) - .then(() => { - callback(null, true) - }) - .catch(() => { - // Don't differentiate between errors and rate limits. Silently trigger - // the rate limit if there's an error consuming the points. - Metrics.inc(`auto-compile-${compileGroup}-limited`) - callback(null, false) - }) + try { + await rateLimiter.consume('global', 1, { method: 'global' }) + return true + } catch (e) { + // Don't differentiate between errors and rate limits. Silently trigger + // the rate limit if there's an error consuming the points. + Metrics.inc(`auto-compile-${compileGroup}-limited`) + return false + } }, - wordCount(projectId, userId, file, clsiserverid, callback) { - CompileManager.getProjectCompileLimits(projectId, function (error, limits) { - if (error) { - return callback(error) - } - ClsiManager.wordCount( - projectId, - userId, - file, - limits, - clsiserverid, - callback - ) - }) - }, + wordCount: callbackify(wordCount), } const autoCompileRateLimiters = new Map() diff --git a/services/web/test/unit/src/Compile/CompileManagerTests.js b/services/web/test/unit/src/Compile/CompileManagerTests.js index 8a6a613db8..7fcbcee335 100644 --- a/services/web/test/unit/src/Compile/CompileManagerTests.js +++ b/services/web/test/unit/src/Compile/CompileManagerTests.js @@ -29,18 +29,21 @@ describe('CompileManager', function () { rateLimit: { autoCompile: {} }, }), '../../infrastructure/RedisWrapper': { - client: () => (this.rclient = { auth() {} }), + client: () => + (this.rclient = { + auth() {}, + }), }, - '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}), - '../Project/ProjectGetter': (this.ProjectGetter = {}), - '../User/UserGetter': (this.UserGetter = {}), - './ClsiManager': (this.ClsiManager = {}), + '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = { + promises: {}, + }), + '../Project/ProjectGetter': (this.ProjectGetter = { promises: {} }), + '../User/UserGetter': (this.UserGetter = { promises: {} }), + './ClsiManager': (this.ClsiManager = { promises: {} }), '../../infrastructure/RateLimiter': this.RateLimiter, '@overleaf/metrics': this.Metrics, '../Analytics/UserAnalyticsIdCache': (this.UserAnalyticsIdCache = { - callbacks: { - get: sinon.stub().yields(null, 'abc'), - }, + get: sinon.stub().resolves('abc'), }), }, }) @@ -57,36 +60,42 @@ describe('CompileManager', function () { beforeEach(function () { this.CompileManager._checkIfRecentlyCompiled = sinon .stub() - .callsArgWith(2, null, false) - this.ProjectRootDocManager.ensureRootDocumentIsSet = sinon + .resolves(false) + this.ProjectRootDocManager.promises.ensureRootDocumentIsSet = sinon .stub() - .callsArgWith(1, null) - this.CompileManager.getProjectCompileLimits = sinon + .resolves() + this.CompileManager.promises.getProjectCompileLimits = sinon .stub() - .callsArgWith(1, null, this.limits) - this.ClsiManager.sendRequest = sinon - .stub() - .callsArgWith( - 3, - null, - (this.status = 'mock-status'), - (this.outputFiles = 'mock output files'), - (this.output = 'mock output') - ) + .resolves(this.limits) + this.ClsiManager.promises.sendRequest = sinon.stub().resolves({ + status: (this.status = 'mock-status'), + outputFiles: (this.outputFiles = []), + clsiServerId: (this.output = 'mock output'), + }) }) describe('succesfully', function () { - beforeEach(function () { - this.CompileManager._checkIfAutoCompileLimitHasBeenHit = ( + let result + beforeEach(async function () { + this.CompileManager._checkIfAutoCompileLimitHasBeenHit = async ( isAutoCompile, - compileGroup, - cb - ) => cb(null, true) - this.CompileManager.compile( + compileGroup + ) => true + this.ProjectGetter.promises.getProject = sinon + .stub() + .resolves( + (this.project = { owner_ref: (this.owner_id = 'owner-id-123') }) + ) + this.UserGetter.promises.getUser = sinon.stub().resolves( + (this.user = { + features: { compileTimeout: '20s', compileGroup: 'standard' }, + analyticsId: 'abc', + }) + ) + result = await this.CompileManager.promises.compile( this.project_id, this.user_id, - {}, - this.callback + {} ) }) @@ -97,19 +106,19 @@ describe('CompileManager', function () { }) it('should ensure that the root document is set', function () { - this.ProjectRootDocManager.ensureRootDocumentIsSet + this.ProjectRootDocManager.promises.ensureRootDocumentIsSet .calledWith(this.project_id) .should.equal(true) }) it('should get the project compile limits', function () { - this.CompileManager.getProjectCompileLimits + this.CompileManager.promises.getProjectCompileLimits .calledWith(this.project_id) .should.equal(true) }) it('should run the compile with the compile limits', function () { - this.ClsiManager.sendRequest + this.ClsiManager.promises.sendRequest .calledWith(this.project_id, this.user_id, { timeout: this.limits.timeout, compileGroup: 'standard', @@ -117,10 +126,10 @@ describe('CompileManager', function () { .should.equal(true) }) - it('should call the callback with the output', function () { - this.callback - .calledWith(null, this.status, this.outputFiles, this.output) - .should.equal(true) + it('should resolve with the output', function () { + expect(result).to.haveOwnProperty('status', this.status) + expect(result).to.haveOwnProperty('clsiServerId', this.output) + expect(result).to.haveOwnProperty('outputFiles', this.outputFiles) }) it('should time the compile', function () { @@ -130,26 +139,24 @@ describe('CompileManager', function () { describe('when the project has been recently compiled', function () { it('should return', function (done) { - this.CompileManager._checkIfAutoCompileLimitHasBeenHit = ( + this.CompileManager._checkIfAutoCompileLimitHasBeenHit = async ( isAutoCompile, - compileGroup, - cb - ) => cb(null, true) + compileGroup + ) => true this.CompileManager._checkIfRecentlyCompiled = sinon .stub() - .callsArgWith(2, null, true) - this.CompileManager.compile( - this.project_id, - this.user_id, - {}, - (err, status) => { - if (err) { - return done(err) - } + .resolves(true) + this.CompileManager.promises + .compile(this.project_id, this.user_id, {}) + .then(({ status }) => { status.should.equal('too-recently-compiled') done() - } - ) + }) + .catch(error => { + // Catch any errors and fail the test + true.should.equal(false) + done(error) + }) }) }) @@ -157,60 +164,51 @@ describe('CompileManager', function () { it('should return', function (done) { this.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon .stub() - .callsArgWith(2, null, false) - this.CompileManager.compile( - this.project_id, - this.user_id, - {}, - (err, status) => { - if (err) { - return done(err) - } - status.should.equal('autocompile-backoff') + .resolves(false) + this.CompileManager.promises + .compile(this.project_id, this.user_id, {}) + .then(({ status }) => { + expect(status).to.equal('autocompile-backoff') done() - } - ) + }) + .catch(err => done(err)) }) }) }) describe('getProjectCompileLimits', function () { - beforeEach(function (done) { + beforeEach(async function () { this.features = { compileTimeout: (this.timeout = 42), compileGroup: (this.group = 'priority'), } - this.ProjectGetter.getProject = sinon + this.ProjectGetter.promises.getProject = sinon .stub() - .callsArgWith( - 2, - null, + .resolves( (this.project = { owner_ref: (this.owner_id = 'owner-id-123') }) ) - this.UserGetter.getUser = sinon + this.UserGetter.promises.getUser = sinon .stub() - .callsArgWith( - 2, - null, - (this.user = { features: this.features, analyticsId: 'abc' }) - ) - this.CompileManager.getProjectCompileLimits( - this.project_id, - (err, res) => { - this.callback(err, res) - done() - } - ) + .resolves((this.user = { features: this.features, analyticsId: 'abc' })) + try { + const result = + await this.CompileManager.promises.getProjectCompileLimits( + this.project_id + ) + this.callback(null, result) + } catch (error) { + this.callback(error) + } }) it('should look up the owner of the project', function () { - this.ProjectGetter.getProject + this.ProjectGetter.promises.getProject .calledWith(this.project_id, { owner_ref: 1 }) .should.equal(true) }) it("should look up the owner's features", function () { - this.UserGetter.getUser + this.UserGetter.promises.getUser .calledWith(this.project.owner_ref, { _id: 1, alphaProgram: 1, @@ -239,12 +237,12 @@ describe('CompileManager', function () { compileTimeout: 42, compileGroup: 'standard', } - this.ProjectGetter.getProject = sinon + this.ProjectGetter.promises.getProject = sinon .stub() - .yields(null, { owner_ref: 'owner-id-123' }) - this.UserGetter.getUser = sinon + .resolves({ owner_ref: 'owner-id-123' }) + this.UserGetter.promises.getUser = sinon .stub() - .yields(null, { features: this.features, analyticsId: 'abc' }) + .resolves({ features: this.features, analyticsId: 'abc' }) }) describe('with priority compile', function () { @@ -265,47 +263,45 @@ describe('CompileManager', function () { }) describe('deleteAuxFiles', function () { - beforeEach(function () { - this.CompileManager.getProjectCompileLimits = sinon + let result + + beforeEach(async function () { + this.CompileManager.promises.getProjectCompileLimits = sinon .stub() - .callsArgWith( - 1, - null, - (this.limits = { compileGroup: 'mock-compile-group' }) - ) - this.ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3) - this.CompileManager.deleteAuxFiles( + .resolves((this.limits = { compileGroup: 'mock-compile-group' })) + this.ClsiManager.promises.deleteAuxFiles = sinon.stub().resolves('test') + result = await this.CompileManager.promises.deleteAuxFiles( this.project_id, - this.user_id, - this.callback + this.user_id ) }) it('should look up the compile group to use', function () { - this.CompileManager.getProjectCompileLimits + this.CompileManager.promises.getProjectCompileLimits .calledWith(this.project_id) .should.equal(true) }) it('should delete the aux files', function () { - this.ClsiManager.deleteAuxFiles + this.ClsiManager.promises.deleteAuxFiles .calledWith(this.project_id, this.user_id, this.limits) .should.equal(true) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should resolve', function () { + expect(result).not.to.be.undefined }) }) describe('_checkIfRecentlyCompiled', function () { describe('when the key exists in redis', function () { - beforeEach(function () { - this.rclient.set = sinon.stub().callsArgWith(5, null, null) - this.CompileManager._checkIfRecentlyCompiled( + let result + + beforeEach(async function () { + this.rclient.set = sinon.stub().resolves(null) + result = await this.CompileManager._checkIfRecentlyCompiled( this.project_id, - this.user_id, - this.callback + this.user_id ) }) @@ -321,18 +317,19 @@ describe('CompileManager', function () { .should.equal(true) }) - it('should call the callback with true', function () { - this.callback.calledWith(null, true).should.equal(true) + it('should resolve with true', function () { + result.should.equal(true) }) }) describe('when the key does not exist in redis', function () { - beforeEach(function () { - this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK') - this.CompileManager._checkIfRecentlyCompiled( + let result + + beforeEach(async function () { + this.rclient.set = sinon.stub().resolves('OK') + result = await this.CompileManager._checkIfRecentlyCompiled( this.project_id, - this.user_id, - this.callback + this.user_id ) }) @@ -348,105 +345,86 @@ describe('CompileManager', function () { .should.equal(true) }) - it('should call the callback with false', function () { - this.callback.calledWith(null, false).should.equal(true) + it('should resolve with false', function () { + result.should.equal(false) }) }) }) describe('_checkIfAutoCompileLimitHasBeenHit', function () { - it('should be able to compile if it is not an autocompile', function (done) { - this.CompileManager._checkIfAutoCompileLimitHasBeenHit( - false, - 'everyone', - (err, canCompile) => { - if (err) { - return done(err) - } - canCompile.should.equal(true) - done() - } - ) + it('should be able to compile if it is not an autocompile', async function () { + const canCompile = + await this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + false, + 'everyone' + ) + expect(canCompile).to.equal(true) }) - it('should be able to compile if rate limit has remaining', function (done) { - this.CompileManager._checkIfAutoCompileLimitHasBeenHit( - true, - 'everyone', - (err, canCompile) => { - if (err) { - return done(err) - } - expect(this.rateLimiter.consume).to.have.been.calledWith('global') - canCompile.should.equal(true) - done() - } - ) + it('should be able to compile if rate limit has remaining', async function () { + const canCompile = + await this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone' + ) + + expect(this.rateLimiter.consume).to.have.been.calledWith('global') + expect(canCompile).to.equal(true) }) - it('should be not able to compile if rate limit has no remianing', function (done) { + it('should be not able to compile if rate limit has no remianing', async function () { this.rateLimiter.consume.rejects({ remainingPoints: 0 }) - this.CompileManager._checkIfAutoCompileLimitHasBeenHit( - true, - 'everyone', - (err, canCompile) => { - if (err) { - return done(err) - } - canCompile.should.equal(false) - done() - } - ) + const canCompile = + await this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone' + ) + + expect(canCompile).to.equal(false) }) - it('should return false if there is an error in the rate limit', function (done) { + it('should return false if there is an error in the rate limit', async function () { this.rateLimiter.consume.rejects(new Error('BOOM!')) - this.CompileManager._checkIfAutoCompileLimitHasBeenHit( - true, - 'everyone', - (err, canCompile) => { - if (err) { - return done(err) - } - canCompile.should.equal(false) - done() - } - ) + const canCompile = + await this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone' + ) + + expect(canCompile).to.equal(false) }) }) describe('wordCount', function () { - beforeEach(function () { - this.CompileManager.getProjectCompileLimits = sinon + let result + const wordCount = 1 + + beforeEach(async function () { + this.CompileManager.promises.getProjectCompileLimits = sinon .stub() - .callsArgWith( - 1, - null, - (this.limits = { compileGroup: 'mock-compile-group' }) - ) - this.ClsiManager.wordCount = sinon.stub().callsArg(4) - this.CompileManager.wordCount( + .resolves((this.limits = { compileGroup: 'mock-compile-group' })) + this.ClsiManager.promises.wordCount = sinon.stub().resolves(wordCount) + result = await this.CompileManager.promises.wordCount( this.project_id, this.user_id, - false, - this.callback + false ) }) it('should look up the compile group to use', function () { - this.CompileManager.getProjectCompileLimits + this.CompileManager.promises.getProjectCompileLimits .calledWith(this.project_id) .should.equal(true) }) it('should call wordCount for project', function () { - this.ClsiManager.wordCount + this.ClsiManager.promises.wordCount .calledWith(this.project_id, this.user_id, false, this.limits) .should.equal(true) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should resolve with the wordCount from the ClsiManager', function () { + expect(result).to.equal(wordCount) }) }) })