From 20619a740ac11d2a866a17311a7cad7e31fb2d4b Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Wed, 30 Apr 2025 09:08:59 +0100 Subject: [PATCH] Update test files with vitest compat changes GitOrigin-RevId: 494f906089d250268a5ff8c8a2150ff2692c37e2 --- .../unit/src/LaunchpadController.test.mjs | 1152 +++++------ .../unit/src/UserActivateController.test.mjs | 178 +- .../Analytics/AnalyticsController.test.mjs | 125 +- .../AnalyticsUTMTrackingMiddleware.test.mjs | 149 +- .../BetaProgramController.test.mjs | 253 ++- .../BetaProgram/BetaProgramHandler.test.mjs | 169 +- .../CollaboratorsController.test.mjs | 597 +++--- .../CollaboratorsInviteController.test.mjs | 1787 +++++++++-------- .../CollaboratorsInviteHandler.test.mjs | 852 ++++---- .../src/Contact/ContactController.test.mjs | 169 +- .../src/Cooldown/CooldownMiddleware.test.mjs | 140 +- .../DocumentUpdaterController.test.mjs | 100 +- .../src/Documents/DocumentController.test.mjs | 223 +- .../ProjectDownloadsController.test.mjs | 161 +- .../ProjectZipStreamManager.test.mjs | 514 ++--- .../src/Exports/ExportsController.test.mjs | 278 +-- .../unit/src/Exports/ExportsHandler.test.mjs | 933 ++++----- .../FileStore/FileStoreController.test.mjs | 229 ++- .../LinkedFilesController.test.mjs | 260 ++- .../unit/src/Metadata/MetaController.test.mjs | 77 +- .../unit/src/Metadata/MetaHandler.test.mjs | 95 +- .../NotificationsController.test.mjs | 80 +- .../PasswordResetController.test.mjs | 752 ++++--- .../PasswordResetHandler.test.mjs | 720 ++++--- .../src/Project/DocLinesComparitor.test.mjs | 42 +- .../src/Project/ProjectApiController.test.mjs | 76 +- .../Project/ProjectListController.test.mjs | 873 ++++---- .../unit/src/Referal/ReferalConnect.test.mjs | 203 +- .../src/Referal/ReferalController.test.mjs | 12 +- .../unit/src/Referal/ReferalHandler.test.mjs | 54 +- .../References/ReferencesController.test.mjs | 249 +-- .../src/References/ReferencesHandler.test.mjs | 440 ++-- .../SubscriptionGroupController.test.mjs | 1231 ++++++------ .../TeamInvitesController.test.mjs | 308 +-- .../unit/src/Tags/TagsController.test.mjs | 431 ++-- .../TpdsController.test.mjs | 723 ++++--- .../TpdsUpdateHandler.test.mjs | 389 ++-- .../TokenAccessController.test.mjs | 1340 ++++++------ .../Uploads/ProjectUploadController.test.mjs | 337 ++-- .../src/User/UserPagesController.test.mjs | 684 ++++--- .../UserMembershipController.test.mjs | 478 +++-- .../ServeStaticWrapper.test.mjs | 43 +- 42 files changed, 9712 insertions(+), 8194 deletions(-) diff --git a/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs index e1ca25a75a..89bc165305 100644 --- a/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs +++ b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs @@ -1,187 +1,215 @@ -import { fileURLToPath } from 'node:url' +import { vi } from 'vitest' import * as path from 'node:path' -import { strict as esmock } from 'esmock' import { expect } from 'chai' import sinon from 'sinon' -import Settings from '@overleaf/settings' import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js' -const __dirname = fileURLToPath(new URL('.', import.meta.url)) const modulePath = path.join( - __dirname, + import.meta.dirname, '../../../app/src/LaunchpadController.mjs' ) describe('LaunchpadController', function () { // esmock doesn't work well with CommonJS dependencies, global imports for // @overleaf/settings aren't working until that module is migrated to ESM. In the - // meantime, the workaroung is to set and restore settings values - let oldSettingsAdminPrivilegeAvailable + // meantime, the workaround is to set and restore settings values - beforeEach(async function () { - this.user = { + beforeEach(async function (ctx) { + ctx.user = { _id: '323123', first_name: 'fn', last_name: 'ln', save: sinon.stub().callsArgWith(0), } - oldSettingsAdminPrivilegeAvailable = Settings.adminPrivilegeAvailable - Settings.adminPrivilegeAvailable = true + ctx.User = {} - this.User = {} - this.LaunchpadController = await esmock(modulePath, { - '@overleaf/metrics': (this.Metrics = {}), - '../../../../../app/src/Features/User/UserRegistrationHandler.js': - (this.UserRegistrationHandler = { + ctx.Settings = { + adminPrivilegeAvailable: true, + } + + vi.doMock('@overleaf/settings', () => ({ default: ctx.Settings })) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.Metrics = {}), + })) + + vi.doMock( + '../../../../../app/src/Features/User/UserRegistrationHandler.js', + () => ({ + default: (ctx.UserRegistrationHandler = { promises: {}, }), - '../../../../../app/src/Features/Email/EmailHandler.js': - (this.EmailHandler = { promises: {} }), - '../../../../../app/src/Features/User/UserGetter.js': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../../app/src/Features/Email/EmailHandler.js', () => ({ + default: (ctx.EmailHandler = { promises: {} }), + })) + + vi.doMock('../../../../../app/src/Features/User/UserGetter.js', () => ({ + default: (ctx.UserGetter = { promises: {}, }), - '../../../../../app/src/models/User.js': { User: this.User }, - '../../../../../app/src/Features/Authentication/AuthenticationController.js': - (this.AuthenticationController = {}), - '../../../../../app/src/Features/Authentication/AuthenticationManager.js': - (this.AuthenticationManager = {}), - '../../../../../app/src/Features/Authentication/SessionManager.js': - (this.SessionManager = { + })) + + vi.doMock('../../../../../app/src/models/User.js', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../../app/src/Features/Authentication/AuthenticationController.js', + () => ({ + default: (ctx.AuthenticationController = {}), + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Authentication/AuthenticationManager.js', + () => ({ + default: (ctx.AuthenticationManager = {}), + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: (ctx.SessionManager = { getSessionUser: sinon.stub(), }), - }) + }) + ) - this.email = 'bob@smith.com' + ctx.LaunchpadController = (await import(modulePath)).default - this.req = { + ctx.email = 'bob@smith.com' + + ctx.req = { query: {}, body: {}, session: {}, } - this.res = new MockResponse() - this.res.locals = { + ctx.res = new MockResponse() + ctx.res.locals = { translate(key) { return key }, } - this.next = sinon.stub() - }) - - afterEach(function () { - Settings.adminPrivilegeAvailable = oldSettingsAdminPrivilegeAvailable + ctx.next = sinon.stub() }) describe('launchpadPage', function () { - beforeEach(function () { - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists - this.AuthenticationController.setRedirectInSession = sinon.stub() + beforeEach(function (ctx) { + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists + ctx.AuthenticationController.setRedirectInSession = sinon.stub() }) describe('when the user is not logged in', function () { - beforeEach(function () { - this.SessionManager.getSessionUser = sinon.stub().returns(null) + beforeEach(function (ctx) { + ctx.SessionManager.getSessionUser = sinon.stub().returns(null) }) describe('when there are no admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should render the launchpad page', function () { - const viewPath = path.join(__dirname, '../../../app/views/launchpad') - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith(viewPath, { - adminUserExists: false, - authMethod: 'local', - }) - .should.equal(true) + it('should render the launchpad page', function (ctx) { + const viewPath = path.join( + import.meta.dirname, + '../../../app/views/launchpad' + ) + ctx.res.render.callCount.should.equal(1) + expect(ctx.res.render).to.have.been.calledWith(viewPath, { + adminUserExists: false, + authMethod: 'local', + }) }) }) describe('when there is at least one admin', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should redirect to login page', function () { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( + it('should redirect to login page', function (ctx) { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( 1 ) - this.res.redirect.calledWith('/login').should.equal(true) + ctx.res.redirect.calledWith('/login').should.equal(true) }) - it('should not render the launchpad page', function () { - this.res.render.callCount.should.equal(0) + it('should not render the launchpad page', function (ctx) { + ctx.res.render.callCount.should.equal(0) }) }) }) describe('when the user is logged in', function () { - beforeEach(function () { - this.user = { + beforeEach(function (ctx) { + ctx.user = { _id: 'abcd', email: 'abcd@example.com', } - this.SessionManager.getSessionUser.returns(this.user) - this._atLeastOneAdminExists.resolves(true) + ctx.SessionManager.getSessionUser.returns(ctx.user) + ctx._atLeastOneAdminExists.resolves(true) }) describe('when the user is an admin', function () { - beforeEach(async function () { - this.UserGetter.promises.getUser = sinon + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .resolves({ isAdmin: true }) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should render the launchpad page', function () { - const viewPath = path.join(__dirname, '../../../app/views/launchpad') - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith(viewPath, { - wsUrl: undefined, - adminUserExists: true, - authMethod: 'local', - }) - .should.equal(true) + it('should render the launchpad page', function (ctx) { + const viewPath = path.join( + import.meta.dirname, + '../../../app/views/launchpad' + ) + ctx.res.render.callCount.should.equal(1) + expect(ctx.res.render).to.have.been.calledWith(viewPath, { + wsUrl: undefined, + adminUserExists: true, + authMethod: 'local', + }) }) }) describe('when the user is not an admin', function () { - beforeEach(async function () { - this.UserGetter.promises.getUser = sinon + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .resolves({ isAdmin: false }) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should redirect to restricted page', function () { - this.res.redirect.callCount.should.equal(1) - this.res.redirect.calledWith('/restricted').should.equal(true) + it('should redirect to restricted page', function (ctx) { + ctx.res.redirect.callCount.should.equal(1) + ctx.res.redirect.calledWith('/restricted').should.equal(true) }) }) }) @@ -189,100 +217,92 @@ describe('LaunchpadController', function () { describe('_atLeastOneAdminExists', function () { describe('when there are no admins', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon.stub().resolves(null) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(null) }) - it('should callback with false', async function () { - const exists = await this.LaunchpadController._atLeastOneAdminExists() + it('should callback with false', async function (ctx) { + const exists = await ctx.LaunchpadController._atLeastOneAdminExists() expect(exists).to.equal(false) }) }) describe('when there are some admins', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon - .stub() - .resolves({ _id: 'abcd' }) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves({ _id: 'abcd' }) }) - it('should callback with true', async function () { - const exists = await this.LaunchpadController._atLeastOneAdminExists() + it('should callback with true', async function (ctx) { + const exists = await ctx.LaunchpadController._atLeastOneAdminExists() expect(exists).to.equal(true) }) }) describe('when getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.LaunchpadController._atLeastOneAdminExists()).rejected + it('should produce an error', async function (ctx) { + await expect(ctx.LaunchpadController._atLeastOneAdminExists()).rejected }) }) }) describe('sendTestEmail', function () { - beforeEach(function () { - this.EmailHandler.promises.sendEmail = sinon.stub().resolves() - this.req.body.email = 'someone@example.com' + beforeEach(function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon.stub().resolves() + ctx.req.body.email = 'someone@example.com' }) - it('should produce a 200 response', async function () { - await this.LaunchpadController.sendTestEmail( - this.req, - this.res, - this.next - ) - this.res.json.calledWith({ message: 'email_sent' }).should.equal(true) + it('should produce a 200 response', async function (ctx) { + await ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.res.json.calledWith({ message: 'email_sent' }).should.equal(true) }) - it('should not call next with an error', function () { - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(0) }) - it('should have called sendEmail', async function () { - await this.LaunchpadController.sendTestEmail( - this.req, - this.res, - this.next - ) - this.EmailHandler.promises.sendEmail.callCount.should.equal(1) - this.EmailHandler.promises.sendEmail + it('should have called sendEmail', async function (ctx) { + await ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.EmailHandler.promises.sendEmail.callCount.should.equal(1) + ctx.EmailHandler.promises.sendEmail .calledWith('testEmail') .should.equal(true) }) describe('when sendEmail produces an error', function () { - beforeEach(function () { - this.EmailHandler.promises.sendEmail = sinon + beforeEach(function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon .stub() .rejects(new Error('woops')) }) - it('should call next with an error', function (done) { - this.next = sinon.stub().callsFake(err => { - expect(err).to.be.instanceof(Error) - this.next.callCount.should.equal(1) - done() + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(err => { + expect(err).to.be.instanceof(Error) + ctx.next.callCount.should.equal(1) + resolve() + }) + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) }) - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) }) }) describe('when no email address is supplied', function () { - beforeEach(function () { - this.req.body.email = undefined + beforeEach(function (ctx) { + ctx.req.body.email = undefined }) - it('should produce a 400 response', function () { - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) - this.res.status.calledWith(400).should.equal(true) - this.res.json + it('should produce a 400 response', function (ctx) { + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json .calledWith({ message: 'no email address supplied', }) @@ -292,67 +312,63 @@ describe('LaunchpadController', function () { }) describe('registerAdmin', function () { - beforeEach(function () { - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists + beforeEach(function (ctx) { + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists }) describe('when all goes well', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json).to.have.been.calledWith({ redir: '/launchpad' }) + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json).to.have.been.calledWith({ redir: '/launchpad' }) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne.callCount.should.equal(1) - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne.callCount.should.equal(1) + ctx.User.updateOne .calledWithMatch( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -362,390 +378,345 @@ describe('LaunchpadController', function () { }) describe('when no email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when no password is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = undefined - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = undefined + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when an invalid email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'invalid password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'invalid password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon .stub() .returns(new Error('bad email')) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(400).should.equal(true) - this.res.json.calledWith({ + it('should send a 400 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json.calledWith({ message: { type: 'error', text: 'bad email' }, }) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when an invalid password is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'invalid password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'invalid password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon .stub() .returns(new Error('bad password')) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(400).should.equal(true) - this.res.json.calledWith({ + it('should send a 400 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json.calledWith({ message: { type: 'error', text: 'bad password' }, }) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when there are already existing admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 403 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(403).should.equal(true) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when checking admins produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.rejects(new Error('woops')) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.rejects(new Error('woops')) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when registerNewUser produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() .rejects(new Error('woops')) - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should not call update', function () { - this.User.updateOne.callCount.should.equal(0) + it('should not call update', function (ctx) { + ctx.User.updateOne.callCount.should.equal(0) }) }) describe('when user update produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon.stub().returns({ + .resolves(ctx.user) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub().rejects(new Error('woops')), }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) }) describe('when overleaf', function () { - let oldSettingsOverleaf - - beforeEach(async function () { - oldSettingsOverleaf = Settings.overleaf - Settings.overleaf = { one: 1 } - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx.Settings.overleaf = { one: 1 } + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - this.UserGetter.promises.getUser = sinon - .stub() - .resolves({ _id: '1234' }) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + ctx.UserGetter.promises.getUser = sinon.stub().resolves({ _id: '1234' }) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - afterEach(async function () { - Settings.overleaf = oldSettingsOverleaf + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json).to.have.been.calledWith({ redir: '/launchpad' }) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json).to.have.been.calledWith({ redir: '/launchpad' }) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) - }) - - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne .calledWith( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -756,76 +727,69 @@ describe('LaunchpadController', function () { }) describe('registerExternalAuthAdmin', function () { - let oldSettingsLDAP - - beforeEach(function () { - oldSettingsLDAP = Settings.ldap - Settings.ldap = { one: 1 } - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists - }) - - afterEach(function () { - Settings.ldap = oldSettingsLDAP + beforeEach(function (ctx) { + ctx.Settings.ldap = { one: 1 } + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists }) describe('when all goes well', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.lastCall.args[0].email).to.equal(this.email) + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.lastCall.args[0].email).to.equal(ctx.email) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne.callCount.should.equal(1) - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne.callCount.should.equal(1) + ctx.User.updateOne .calledWith( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -833,240 +797,240 @@ describe('LaunchpadController', function () { .should.equal(true) }) - it('should have set a redirect in session', function () { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( + it('should have set a redirect in session', function (ctx) { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( 1 ) - this.AuthenticationController.setRedirectInSession - .calledWith(this.req, '/launchpad') + ctx.AuthenticationController.setRedirectInSession + .calledWith(ctx.req, '/launchpad') .should.equal(true) }) }) describe('when the authMethod is invalid', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin( + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin( 'NOTAVALIDAUTHMETHOD' - )(this.req, this.res, this.next) + )(ctx.req, ctx.res, ctx.next) }) - it('should send a 403 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(403).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when no email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when there are already existing admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 403 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(403).should.equal(true) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when checking admins produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.rejects(new Error('woops')) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.rejects(new Error('woops')) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when registerNewUser produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() .rejects(new Error('woops')) - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) }) - it('should not call update', function () { - this.User.updateOne.callCount.should.equal(0) + it('should not call update', function (ctx) { + ctx.User.updateOne.callCount.should.equal(0) }) }) describe('when user update produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon.stub().returns({ + .resolves(ctx.user) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub().rejects(new Error('woops')), }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) diff --git a/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs b/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs index 7c4382a720..9019e525d7 100644 --- a/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs +++ b/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs @@ -1,132 +1,162 @@ +import { vi } from 'vitest' import Path from 'node:path' -import { fileURLToPath } from 'node:url' -import { strict as esmock } from 'esmock' import sinon from 'sinon' -const __dirname = Path.dirname(fileURLToPath(import.meta.url)) - const MODULE_PATH = '../../../app/src/UserActivateController.mjs' -const VIEW_PATH = Path.join(__dirname, '../../../app/views/user/activate') +const VIEW_PATH = Path.join( + import.meta.dirname, + '../../../app/views/user/activate' +) describe('UserActivateController', function () { - beforeEach(async function () { - this.user = { - _id: (this.user_id = 'kwjewkl'), + beforeEach(async function (ctx) { + ctx.user = { + _id: (ctx.user_id = 'kwjewkl'), features: {}, email: 'joe@example.com', } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub(), }, } - this.UserRegistrationHandler = { promises: {} } - this.ErrorController = { notFound: sinon.stub() } - this.SplitTestHandler = { + ctx.UserRegistrationHandler = { promises: {} } + ctx.ErrorController = { notFound: sinon.stub() } + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, } - this.UserActivateController = await esmock(MODULE_PATH, { - '../../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../../app/src/Features/User/UserRegistrationHandler.js': - this.UserRegistrationHandler, - '../../../../../app/src/Features/Errors/ErrorController.js': - this.ErrorController, - '../../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) - this.req = { + + vi.doMock('../../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../../app/src/Features/User/UserRegistrationHandler.js', + () => ({ + default: ctx.UserRegistrationHandler, + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Errors/ErrorController.js', + () => ({ + default: ctx.ErrorController, + }) + ) + + vi.doMock( + '../../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.UserActivateController = (await import(MODULE_PATH)).default + ctx.req = { body: {}, query: {}, session: { - user: this.user, + user: ctx.user, }, } - this.res = { + ctx.res = { json: sinon.stub(), } }) describe('activateAccountPage', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon.stub().resolves(this.user) - this.req.query.user_id = this.user_id - this.req.query.token = this.token = 'mock-token-123' + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user) + ctx.req.query.user_id = ctx.user_id + ctx.req.query.token = ctx.token = 'mock-token-123' }) - it('should 404 without a user_id', async function (done) { - delete this.req.query.user_id - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a user_id', async function (ctx) { + delete ctx.req.query.user_id + return new Promise(resolve => { + ctx.ErrorController.notFound = () => resolve() + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 404 without a token', function (done) { - delete this.req.query.token - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a token', function (ctx) { + return new Promise(resolve => { + delete ctx.req.query.token + ctx.ErrorController.notFound = resolve + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 404 without a valid user_id', function (done) { - this.UserGetter.promises.getUser = sinon.stub().resolves(null) - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a valid user_id', function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(null) + ctx.ErrorController.notFound = resolve + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 403 for complex user_id', function (done) { - this.ErrorController.forbidden = () => done() - this.req.query.user_id = { first_name: 'X' } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 403 for complex user_id', function (ctx) { + return new Promise(resolve => { + ctx.ErrorController.forbidden = resolve + ctx.req.query.user_id = { first_name: 'X' } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should redirect activated users to login', function (done) { - this.user.loginCount = 1 - this.res.redirect = url => { - sinon.assert.calledWith(this.UserGetter.promises.getUser, this.user_id) - url.should.equal('/login') - return done() - } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should redirect activated users to login', function (ctx) { + return new Promise(resolve => { + ctx.user.loginCount = 1 + ctx.res.redirect = url => { + sinon.assert.calledWith(ctx.UserGetter.promises.getUser, ctx.user_id) + url.should.equal('/login') + resolve() + } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('render the activation page if the user has not logged in before', function (done) { - this.user.loginCount = 0 - this.res.render = (page, opts) => { - page.should.equal(VIEW_PATH) - opts.email.should.equal(this.user.email) - opts.token.should.equal(this.token) - return done() - } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('render the activation page if the user has not logged in before', function (ctx) { + return new Promise(resolve => { + ctx.user.loginCount = 0 + ctx.res.render = (page, opts) => { + page.should.equal(VIEW_PATH) + opts.email.should.equal(ctx.user.email) + opts.token.should.equal(ctx.token) + resolve() + } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) }) describe('register', function () { - beforeEach(async function () { - this.UserRegistrationHandler.promises.registerNewUserAndSendActivationEmail = + beforeEach(async function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUserAndSendActivationEmail = sinon.stub().resolves({ - user: this.user, - setNewPasswordUrl: (this.url = 'mock/url'), + user: ctx.user, + setNewPasswordUrl: (ctx.url = 'mock/url'), }) - this.req.body.email = this.user.email = this.email = 'email@example.com' - await this.UserActivateController.register(this.req, this.res) + ctx.req.body.email = ctx.user.email = ctx.email = 'email@example.com' + await ctx.UserActivateController.register(ctx.req, ctx.res) }) - it('should register the user and send them an email', function () { + it('should register the user and send them an email', function (ctx) { sinon.assert.calledWith( - this.UserRegistrationHandler.promises + ctx.UserRegistrationHandler.promises .registerNewUserAndSendActivationEmail, - this.email + ctx.email ) }) - it('should return the user and activation url', function () { - this.res.json + it('should return the user and activation url', function (ctx) { + ctx.res.json .calledWith({ - email: this.email, - setNewPasswordUrl: this.url, + email: ctx.email, + setNewPasswordUrl: ctx.url, }) .should.equal(true) }) diff --git a/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs b/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs index cba0e935db..4019f2bce9 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs +++ b/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockResponse from '../helpers/MockResponse.js' const modulePath = new URL( @@ -7,37 +7,52 @@ const modulePath = new URL( ).pathname describe('AnalyticsController', function () { - beforeEach(async function () { - this.SessionManager = { getLoggedInUserId: sinon.stub() } + beforeEach(async function (ctx) { + ctx.SessionManager = { getLoggedInUserId: sinon.stub() } - this.AnalyticsManager = { + ctx.AnalyticsManager = { updateEditingSession: sinon.stub(), recordEventForSession: sinon.stub(), } - this.Features = { + ctx.Features = { hasFeature: sinon.stub().returns(true), } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - this.AnalyticsManager, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '../../../../app/src/infrastructure/Features.js': this.Features, - '../../../../app/src/infrastructure/GeoIpLookup.js': (this.GeoIpLookup = { + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features.js', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/infrastructure/GeoIpLookup.js', () => ({ + default: (ctx.GeoIpLookup = { promises: { getDetails: sinon.stub().resolves(), }, }), - }) + })) - this.res = new MockResponse() + ctx.controller = (await import(modulePath)).default + + ctx.res = new MockResponse() }) describe('updateEditingSession', function () { - beforeEach(function () { - this.req = { + beforeEach(function (ctx) { + ctx.req = { params: { projectId: 'a project id', }, @@ -48,34 +63,36 @@ describe('AnalyticsController', function () { }, }, } - this.GeoIpLookup.promises.getDetails = sinon + ctx.GeoIpLookup.promises.getDetails = sinon .stub() .resolves({ country_code: 'XY' }) }) - it('delegates to the AnalyticsManager', function (done) { - this.SessionManager.getLoggedInUserId.returns('1234') - this.res.callback = () => { - sinon.assert.calledWith( - this.AnalyticsManager.updateEditingSession, - '1234', - 'a project id', - 'XY', - { editorType: 'abc' } - ) - done() - } - this.controller.updateEditingSession(this.req, this.res) + it('delegates to the AnalyticsManager', function (ctx) { + return new Promise(resolve => { + ctx.SessionManager.getLoggedInUserId.returns('1234') + ctx.res.callback = () => { + sinon.assert.calledWith( + ctx.AnalyticsManager.updateEditingSession, + '1234', + 'a project id', + 'XY', + { editorType: 'abc' } + ) + resolve() + } + ctx.controller.updateEditingSession(ctx.req, ctx.res) + }) }) }) describe('recordEvent', function () { - beforeEach(function () { + beforeEach(function (ctx) { const body = { foo: 'stuff', _csrf: 'atoken123', } - this.req = { + ctx.req = { params: { event: 'i_did_something', }, @@ -84,30 +101,34 @@ describe('AnalyticsController', function () { session: {}, } - this.expectedData = Object.assign({}, body) - delete this.expectedData._csrf + ctx.expectedData = Object.assign({}, body) + delete ctx.expectedData._csrf }) - it('should use the session', function (done) { - this.controller.recordEvent(this.req, this.res) - sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, - this.req.params.event, - this.expectedData - ) - done() + it('should use the session', function (ctx) { + return new Promise(resolve => { + ctx.controller.recordEvent(ctx.req, ctx.res) + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, + ctx.req.params.event, + ctx.expectedData + ) + resolve() + }) }) - it('should remove the CSRF token before sending to the manager', function (done) { - this.controller.recordEvent(this.req, this.res) - sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, - this.req.params.event, - this.expectedData - ) - done() + it('should remove the CSRF token before sending to the manager', function (ctx) { + return new Promise(resolve => { + ctx.controller.recordEvent(ctx.req, ctx.res) + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, + ctx.req.params.event, + ctx.expectedData + ) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs b/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs index 461a2a70d1..fff5224b48 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs +++ b/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' @@ -10,83 +10,90 @@ const MODULE_PATH = new URL( ).pathname describe('AnalyticsUTMTrackingMiddleware', function () { - beforeEach(async function () { - this.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55' - this.userId = '61795fcb013504bb7b663092' + beforeEach(async function (ctx) { + ctx.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55' + ctx.userId = '61795fcb013504bb7b663092' - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub().returns() - this.req.session = { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub().returns() + ctx.req.session = { user: { - _id: this.userId, - analyticsId: this.analyticsId, + _id: ctx.userId, + analyticsId: ctx.analyticsId, }, } - this.AnalyticsUTMTrackingMiddleware = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - (this.AnalyticsManager = { + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: (ctx.AnalyticsManager = { recordEventForSession: sinon.stub().resolves(), setUserPropertyForSessionInBackground: sinon.stub(), }), - '@overleaf/settings': { + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: { siteUrl: 'https://www.overleaf.com', }, - }) + })) - this.middleware = this.AnalyticsUTMTrackingMiddleware.recordUTMTags() + ctx.AnalyticsUTMTrackingMiddleware = (await import(MODULE_PATH)).default + + ctx.middleware = ctx.AnalyticsUTMTrackingMiddleware.recordUTMTags() }) describe('without UTM tags in query', function () { - beforeEach(function () { - this.req.url = '/project' - this.middleware(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.url = '/project' + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is not redirected', function () { - assert.isFalse(this.res.redirected) + it('user is not redirected', function (ctx) { + assert.isFalse(ctx.res.redirected) }) - it('next middleware is executed', function () { - sinon.assert.calledOnce(this.next) + it('next middleware is executed', function (ctx) { + sinon.assert.calledOnce(ctx.next) }) - it('no event or user property is recorded', function () { - sinon.assert.notCalled(this.AnalyticsManager.recordEventForSession) + it('no event or user property is recorded', function (ctx) { + sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForSession) sinon.assert.notCalled( - this.AnalyticsManager.setUserPropertyForSessionInBackground + ctx.AnalyticsManager.setUserPropertyForSessionInBackground ) }) }) describe('with all UTM tags in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_source=Organic&utm_medium=Facebook&utm_campaign=Some%20Campaign&utm_content=foo-bar&utm_term=overridden' - this.req.query = { + ctx.req.query = { utm_source: 'Organic', utm_medium: 'Facebook', utm_campaign: 'Some Campaign', utm_content: 'foo-bar', utm_term: 'overridden', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -99,10 +106,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'Organic;Facebook;Some Campaign;foo-bar' ) @@ -110,30 +117,30 @@ describe('AnalyticsUTMTrackingMiddleware', function () { }) describe('with some UTM tags in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_medium=Facebook&utm_campaign=Some%20Campaign&utm_term=foo' - this.req.query = { + ctx.req.query = { utm_medium: 'Facebook', utm_campaign: 'Some Campaign', utm_term: 'foo', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -144,10 +151,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'N/A;Facebook;Some Campaign;foo' ) @@ -155,30 +162,30 @@ describe('AnalyticsUTMTrackingMiddleware', function () { }) describe('with some UTM tags and additional parameters in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_medium=Facebook&utm_campaign=Some%20Campaign&other_param=some-value' - this.req.query = { + ctx.req.query = { utm_medium: 'Facebook', utm_campaign: 'Some Campaign', other_param: 'some-value', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project?other_param=some-value', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project?other_param=some-value', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -188,10 +195,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'N/A;Facebook;Some Campaign;N/A' ) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs index 78747b8880..e2160cca08 100644 --- a/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs +++ b/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import path from 'node:path' import sinon from 'sinon' import { expect } from 'chai' @@ -13,185 +13,228 @@ const modulePath = path.join( ) describe('BetaProgramController', function () { - beforeEach(async function () { - this.user = { - _id: (this.user_id = 'a_simple_id'), + beforeEach(async function (ctx) { + ctx.user = { + _id: (ctx.user_id = 'a_simple_id'), email: 'user@example.com', features: {}, betaProgram: false, } - this.req = { + ctx.req = { query: {}, session: { - user: this.user, + user: ctx.user, }, } - this.SplitTestSessionHandler = { + ctx.SplitTestSessionHandler = { promises: { sessionMaintenance: sinon.stub(), }, } - this.BetaProgramController = await esmock.strict(modulePath, { - '../../../../app/src/Features/SplitTests/SplitTestSessionHandler': - this.SplitTestSessionHandler, - '../../../../app/src/Features/BetaProgram/BetaProgramHandler': - (this.BetaProgramHandler = { + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestSessionHandler', + () => ({ + default: ctx.SplitTestSessionHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/BetaProgram/BetaProgramHandler', + () => ({ + default: (ctx.BetaProgramHandler = { promises: { optIn: sinon.stub().resolves(), optOut: sinon.stub().resolves(), }, }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(), }, }), - '@overleaf/settings': (this.settings = { + })) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { languages: {}, }), - '../../../../app/src/Features/Authentication/AuthenticationController': - (this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.user._id), + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: (ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), }), - }) - this.res = new MockResponse() - this.next = sinon.stub() + }) + ) + + ctx.BetaProgramController = (await import(modulePath)).default + ctx.res = new MockResponse() + ctx.next = sinon.stub() }) describe('optIn', function () { - it("should redirect to '/beta/participate'", function (done) { - this.res.callback = () => { - this.res.redirectedTo.should.equal('/beta/participate') - done() - } - this.BetaProgramController.optIn(this.req, this.res, done) + it("should redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.redirectedTo.should.equal('/beta/participate') + resolve() + } + ctx.BetaProgramController.optIn(ctx.req, ctx.res, resolve) + }) }) - it('should not call next with an error', function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(0) }) - it('should call BetaProgramHandler.optIn', function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.BetaProgramHandler.promises.optIn.callCount.should.equal(1) + it('should call BetaProgramHandler.optIn', function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.BetaProgramHandler.promises.optIn.callCount.should.equal(1) }) - it('should invoke the session maintenance', function (done) { - this.res.callback = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req - ) - done() - } - this.BetaProgramController.optIn(this.req, this.res, done) + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req + ) + resolve() + } + ctx.BetaProgramController.optIn(ctx.req, ctx.res, resolve) + }) }) describe('when BetaProgramHandler.opIn produces an error', function () { - beforeEach(function () { - this.BetaProgramHandler.promises.optIn.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.BetaProgramHandler.promises.optIn.throws(new Error('woops')) }) - it("should not redirect to '/beta/participate'", function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.res.redirect.callCount.should.equal(0) + it("should not redirect to '/beta/participate'", function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.res.redirect.callCount.should.equal(0) }) - it('should produce an error', function (done) { - this.BetaProgramController.optIn(this.req, this.res, err => { - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, err => { + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) }) describe('optOut', function () { - it("should redirect to '/beta/participate'", function (done) { - this.res.callback = () => { - expect(this.res.redirectedTo).to.equal('/beta/participate') - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it("should redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.redirectedTo).to.equal('/beta/participate') + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should not call next with an error', function (done) { - this.res.callback = () => { - this.next.callCount.should.equal(0) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should not call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.next.callCount.should.equal(0) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should call BetaProgramHandler.optOut', function (done) { - this.res.callback = () => { - this.BetaProgramHandler.promises.optOut.callCount.should.equal(1) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should call BetaProgramHandler.optOut', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.BetaProgramHandler.promises.optOut.callCount.should.equal(1) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should invoke the session maintenance', function (done) { - this.res.callback = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req, - null - ) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req, + null + ) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) describe('when BetaProgramHandler.optOut produces an error', function () { - beforeEach(function () { - this.BetaProgramHandler.promises.optOut.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.BetaProgramHandler.promises.optOut.throws(new Error('woops')) }) - it("should not redirect to '/beta/participate'", function (done) { - this.BetaProgramController.optOut(this.req, this.res, error => { - expect(error).to.exist - expect(this.res.redirected).to.equal(false) - done() + it("should not redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optOut(ctx.req, ctx.res, error => { + expect(error).to.exist + expect(ctx.res.redirected).to.equal(false) + resolve() + }) }) }) - it('should produce an error', function (done) { - this.BetaProgramController.optOut(this.req, this.res, error => { - expect(error).to.exist - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optOut(ctx.req, ctx.res, error => { + expect(error).to.exist + resolve() + }) }) }) }) }) describe('optInPage', function () { - beforeEach(function () { - this.UserGetter.promises.getUser.resolves(this.user) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser.resolves(ctx.user) }) - it('should render the opt-in page', function (done) { - this.res.callback = () => { - expect(this.res.renderedTemplate).to.equal('beta_program/opt_in') - done() - } - this.BetaProgramController.optInPage(this.req, this.res, done) + it('should render the opt-in page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedTemplate).to.equal('beta_program/opt_in') + resolve() + } + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, resolve) + }) }) describe('when UserGetter.getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUser.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser.throws(new Error('woops')) }) - it('should not render the opt-in page', function () { - this.BetaProgramController.optInPage(this.req, this.res, this.next) - this.res.render.callCount.should.equal(0) + it('should not render the opt-in page', function (ctx) { + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, ctx.next) + ctx.res.render.callCount.should.equal(0) }) - it('should produce an error', function (done) { - this.BetaProgramController.optInPage(this.req, this.res, error => { - expect(error).to.exist - expect(error).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, error => { + expect(error).to.exist + expect(error).to.be.instanceof(Error) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs index 2b72271fd5..14438a8ed7 100644 --- a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs +++ b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import path from 'node:path' import sinon from 'sinon' @@ -13,128 +13,155 @@ const modulePath = path.join( ) describe('BetaProgramHandler', function () { - beforeEach(async function () { - this.user_id = 'some_id' - this.user = { - _id: this.user_id, + beforeEach(async function (ctx) { + ctx.user_id = 'some_id' + ctx.user = { + _id: ctx.user_id, email: 'user@example.com', features: {}, betaProgram: false, save: sinon.stub().callsArgWith(0, null), } - this.handler = await esmock.strict(modulePath, { - '@overleaf/metrics': { + + vi.doMock('@overleaf/metrics', () => ({ + default: { inc: sinon.stub(), }, - '../../../../app/src/Features/User/UserUpdater': (this.UserUpdater = { + })) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: (ctx.UserUpdater = { promises: { updateUser: sinon.stub().resolves(), }, }), - '../../../../app/src/Features/Analytics/AnalyticsManager': - (this.AnalyticsManager = { + })) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: (ctx.AnalyticsManager = { setUserPropertyForUserInBackground: sinon.stub(), }), - }) + }) + ) + + ctx.handler = (await import(modulePath)).default }) describe('optIn', function () { - beforeEach(function () { - this.user.betaProgram = false - this.call = callback => { - this.handler.optIn(this.user_id, callback) + beforeEach(function (ctx) { + ctx.user.betaProgram = false + ctx.call = callback => { + ctx.handler.optIn(ctx.user_id, callback) } }) - it('should call userUpdater', function (done) { - this.call(err => { - expect(err).to.not.exist - this.UserUpdater.promises.updateUser.callCount.should.equal(1) - done() + it('should call userUpdater', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + ctx.UserUpdater.promises.updateUser.callCount.should.equal(1) + resolve() + }) }) }) - it('should set beta-program user property to true', function (done) { - this.call(err => { - expect(err).to.not.exist - sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForUserInBackground, - this.user_id, - 'beta-program', - true - ) - done() + it('should set beta-program user property to true', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + sinon.assert.calledWith( + ctx.AnalyticsManager.setUserPropertyForUserInBackground, + ctx.user_id, + 'beta-program', + true + ) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(err => { - expect(err).to.not.exist - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + resolve() + }) }) }) describe('when userUpdater produces an error', function () { - beforeEach(function () { - this.UserUpdater.promises.updateUser.rejects() + beforeEach(function (ctx) { + ctx.UserUpdater.promises.updateUser.rejects() }) - it('should produce an error', function (done) { - this.call(err => { - expect(err).to.exist - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) }) describe('optOut', function () { - beforeEach(function () { - this.user.betaProgram = true - this.call = callback => { - this.handler.optOut(this.user_id, callback) + beforeEach(function (ctx) { + ctx.user.betaProgram = true + ctx.call = callback => { + ctx.handler.optOut(ctx.user_id, callback) } }) - it('should call userUpdater', function (done) { - this.call(err => { - expect(err).to.not.exist - this.UserUpdater.promises.updateUser.callCount.should.equal(1) - done() + it('should call userUpdater', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + ctx.UserUpdater.promises.updateUser.callCount.should.equal(1) + resolve() + }) }) }) - it('should set beta-program user property to false', function (done) { - this.call(err => { - expect(err).to.not.exist - sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForUserInBackground, - this.user_id, - 'beta-program', - false - ) - done() + it('should set beta-program user property to false', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + sinon.assert.calledWith( + ctx.AnalyticsManager.setUserPropertyForUserInBackground, + ctx.user_id, + 'beta-program', + false + ) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(err => { - expect(err).to.not.exist - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + resolve() + }) }) }) describe('when userUpdater produces an error', function () { - beforeEach(function () { - this.UserUpdater.promises.updateUser.rejects() + beforeEach(function (ctx) { + ctx.UserUpdater.promises.updateUser.rejects() }) - it('should produce an error', function (done) { - this.call(err => { - expect(err).to.exist - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs index 27460da148..9bb9c4b3c0 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import mongodb from 'mongodb-legacy' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockRequest from '../helpers/MockRequest.js' @@ -11,425 +11,510 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsController.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('CollaboratorsController', function () { - beforeEach(async function () { - this.res = new MockResponse() - this.req = new MockRequest() + beforeEach(async function (ctx) { + ctx.res = new MockResponse() + ctx.req = new MockRequest() - this.user = { _id: new ObjectId() } - this.projectId = new ObjectId() - this.callback = sinon.stub() + ctx.user = { _id: new ObjectId() } + ctx.projectId = new ObjectId() + ctx.callback = sinon.stub() - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { removeUserFromProject: sinon.stub().resolves(), setCollaboratorPrivilegeLevel: sinon.stub().resolves(), }, createTokenHashPrefix: sinon.stub().returns('abc123'), } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getAllInvitedMembers: sinon.stub(), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.HttpErrorHandler = { + ctx.HttpErrorHandler = { forbidden: sinon.stub(), notFound: sinon.stub(), } - this.TagsHandler = { + ctx.TagsHandler = { promises: { removeProjectFromAllTags: sinon.stub().resolves(), }, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.user), - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.user), + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.OwnershipTransferHandler = { + ctx.OwnershipTransferHandler = { promises: { transferOwnership: sinon.stub().resolves(), }, } - this.TokenAccessHandler = { + ctx.TokenAccessHandler = { getRequestToken: sinon.stub().returns('access-token'), } - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { addEntryInBackground: sinon.stub(), } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves({ owner_ref: this.user._id }), + getProject: sinon.stub().resolves({ owner_ref: ctx.user._id }), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAddXEditCollaborators: sinon.stub().resolves(), canChangeCollaboratorPrivilegeLevel: sinon.stub().resolves(true), }, } - this.CollaboratorsController = await esmock.strict(MODULE_PATH, { - 'mongodb-legacy': { ObjectId }, - '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js': - this.CollaboratorsHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js': - this.CollaboratorsGetter, - '../../../../app/src/Features/Collaborators/OwnershipTransferHandler.js': - this.OwnershipTransferHandler, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Errors/HttpErrorHandler.js': - this.HttpErrorHandler, - '../../../../app/src/Features/Tags/TagsHandler.js': this.TagsHandler, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '../../../../app/src/Features/TokenAccess/TokenAccessHandler.js': - this.TokenAccessHandler, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/SplitTests/SplitTestHandler.js': - this.SplitTestHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/OwnershipTransferHandler.js', + () => ({ + default: ctx.OwnershipTransferHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Errors/HttpErrorHandler.js', + () => ({ + default: ctx.HttpErrorHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler.js', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler.js', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.js', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + ctx.CollaboratorsController = (await import(MODULE_PATH)).default }) describe('removeUserFromProject', function () { - beforeEach(function (done) { - this.req.params = { - Project_id: this.projectId, - user_id: this.user._id, - } - this.res.sendStatus = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { + Project_id: ctx.projectId, + user_id: ctx.user._id, + } + ctx.res.sendStatus = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsController.removeUserFromProject(ctx.req, ctx.res) }) - this.CollaboratorsController.removeUserFromProject(this.req, this.res) }) - it('should from the user from the project', function () { + it('should from the user from the project', function (ctx) { expect( - this.CollaboratorsHandler.promises.removeUserFromProject - ).to.have.been.calledWith(this.projectId, this.user._id) + ctx.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(ctx.projectId, ctx.user._id) }) - it('should emit a userRemovedFromProject event to the proejct', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should emit a userRemovedFromProject event to the proejct', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'userRemovedFromProject', - this.user._id + ctx.user._id ) }) - it('should send the back a success response', function () { - this.res.sendStatus.calledWith(204).should.equal(true) + it('should send the back a success response', function (ctx) { + ctx.res.sendStatus.calledWith(204).should.equal(true) }) - it('should have called emitToRoom', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should have called emitToRoom', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'project:membership:changed' ) }) - it('should write a project audit log', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should write a project audit log', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'remove-collaborator', - this.user._id, - this.req.ip, - { userId: this.user._id } + ctx.user._id, + ctx.req.ip, + { userId: ctx.user._id } ) }) }) describe('removeSelfFromProject', function () { - beforeEach(function (done) { - this.req.params = { Project_id: this.projectId } - this.res.sendStatus = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { Project_id: ctx.projectId } + ctx.res.sendStatus = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsController.removeSelfFromProject(ctx.req, ctx.res) }) - this.CollaboratorsController.removeSelfFromProject(this.req, this.res) }) - it('should remove the logged in user from the project', function () { + it('should remove the logged in user from the project', function (ctx) { expect( - this.CollaboratorsHandler.promises.removeUserFromProject - ).to.have.been.calledWith(this.projectId, this.user._id) + ctx.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(ctx.projectId, ctx.user._id) }) - it('should emit a userRemovedFromProject event to the proejct', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should emit a userRemovedFromProject event to the proejct', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'userRemovedFromProject', - this.user._id + ctx.user._id ) }) - it('should remove the project from all tags', function () { + it('should remove the project from all tags', function (ctx) { expect( - this.TagsHandler.promises.removeProjectFromAllTags - ).to.have.been.calledWith(this.user._id, this.projectId) + ctx.TagsHandler.promises.removeProjectFromAllTags + ).to.have.been.calledWith(ctx.user._id, ctx.projectId) }) - it('should return a success code', function () { - this.res.sendStatus.calledWith(204).should.equal(true) + it('should return a success code', function (ctx) { + ctx.res.sendStatus.calledWith(204).should.equal(true) }) - it('should write a project audit log', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should write a project audit log', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'leave-project', - this.user._id, - this.req.ip + ctx.user._id, + ctx.req.ip ) }) }) describe('getAllMembers', function () { - beforeEach(function (done) { - this.req.params = { Project_id: this.projectId } - this.res.json = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { Project_id: ctx.projectId } + ctx.res.json = sinon.spy(() => { + resolve() + }) + ctx.next = sinon.stub() + ctx.members = [{ a: 1 }] + ctx.CollaboratorsGetter.promises.getAllInvitedMembers.resolves( + ctx.members + ) + ctx.CollaboratorsController.getAllMembers(ctx.req, ctx.res, ctx.next) }) - this.next = sinon.stub() - this.members = [{ a: 1 }] - this.CollaboratorsGetter.promises.getAllInvitedMembers.resolves( - this.members - ) - this.CollaboratorsController.getAllMembers(this.req, this.res, this.next) }) - it('should not produce an error', function () { - this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should produce a json response', function () { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith({ members: this.members }).should.equal(true) + it('should produce a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith({ members: ctx.members }).should.equal(true) }) - it('should call CollaboratorsGetter.getAllInvitedMembers', function () { - expect(this.CollaboratorsGetter.promises.getAllInvitedMembers).to.have - .been.calledOnce + it('should call CollaboratorsGetter.getAllInvitedMembers', function (ctx) { + expect(ctx.CollaboratorsGetter.promises.getAllInvitedMembers).to.have.been + .calledOnce }) describe('when CollaboratorsGetter.getAllInvitedMembers produces an error', function () { - beforeEach(function (done) { - this.res.json = sinon.stub() - this.next = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.json = sinon.stub() + ctx.next = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsGetter.promises.getAllInvitedMembers.rejects( + new Error('woops') + ) + ctx.CollaboratorsController.getAllMembers(ctx.req, ctx.res, ctx.next) }) - this.CollaboratorsGetter.promises.getAllInvitedMembers.rejects( - new Error('woops') - ) - this.CollaboratorsController.getAllMembers( - this.req, - this.res, - this.next - ) }) - it('should produce an error', function () { - expect(this.next).to.have.been.calledOnce - expect(this.next).to.have.been.calledWithMatch( + it('should produce an error', function (ctx) { + expect(ctx.next).to.have.been.calledOnce + expect(ctx.next).to.have.been.calledWithMatch( sinon.match.instanceOf(Error) ) }) - it('should not produce a json response', function () { - this.res.json.callCount.should.equal(0) + it('should not produce a json response', function (ctx) { + ctx.res.json.callCount.should.equal(0) }) }) }) describe('setCollaboratorInfo', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - user_id: this.user._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + user_id: ctx.user._id, } - this.req.body = { privilegeLevel: 'readOnly' } + ctx.req.body = { privilegeLevel: 'readOnly' } }) - it('should set the collaborator privilege level', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith(this.projectId, this.user._id, 'readOnly') - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) - }) - - it('should return a 404 when the project or collaborator is not found', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - done() + it('should set the collaborator privilege level', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.have.been.calledWith(ctx.projectId, ctx.user._id, 'readOnly') + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) }) - - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( - new Errors.NotFoundError() - ) - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) - it('should pass the error to the next handler when setting the privilege level fails', function (done) { - this.next = sinon.spy(err => { - expect(err).instanceOf(Error) - done() - }) + it('should return a 404 when the project or collaborator is not found', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + resolve() + }) - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( - new Error() - ) - this.CollaboratorsController.setCollaboratorInfo( - this.req, - this.res, - this.next - ) + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( + new Errors.NotFoundError() + ) + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) + }) + + it('should pass the error to the next handler when setting the privilege level fails', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.spy(err => { + expect(err).instanceOf(Error) + resolve() + }) + + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( + new Error() + ) + ctx.CollaboratorsController.setCollaboratorInfo( + ctx.req, + ctx.res, + ctx.next + ) + }) }) describe('when setting privilege level to readAndWrite', function () { - beforeEach(function () { - this.req.body = { privilegeLevel: 'readAndWrite' } + beforeEach(function (ctx) { + ctx.req.body = { privilegeLevel: 'readAndWrite' } }) describe('when owner can add new edit collaborators', function () { - it('should set privilege level after checking collaborators can be added', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect( - this.LimitationsManager.promises - .canChangeCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.projectId, - this.user._id, - 'readAndWrite' - ) - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) + it('should set privilege level after checking collaborators can be added', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect( + ctx.LimitationsManager.promises + .canChangeCollaboratorPrivilegeLevel + ).to.have.been.calledWith( + ctx.projectId, + ctx.user._id, + 'readAndWrite' + ) + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) }) }) describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel.resolves( false ) }) - it('should return a 403 if trying to set a new edit collaborator', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect( - this.LimitationsManager.promises - .canChangeCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.projectId, - this.user._id, - 'readAndWrite' - ) - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.not.have.been.called - done() + it('should return a 403 if trying to set a new edit collaborator', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.forbidden = sinon.spy((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect( + ctx.LimitationsManager.promises + .canChangeCollaboratorPrivilegeLevel + ).to.have.been.calledWith( + ctx.projectId, + ctx.user._id, + 'readAndWrite' + ) + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.not.have.been.called + resolve() + }) + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) }) - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) }) }) describe('when setting privilege level to readOnly', function () { - beforeEach(function () { - this.req.body = { privilegeLevel: 'readOnly' } + beforeEach(function (ctx) { + ctx.req.body = { privilegeLevel: 'readOnly' } }) describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves( false ) }) - it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect(this.LimitationsManager.promises.canAddXEditCollaborators).to - .not.have.been.called - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith(this.projectId, this.user._id, 'readOnly') - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) + it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect(ctx.LimitationsManager.promises.canAddXEditCollaborators) + .to.not.have.been.called + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.have.been.calledWith(ctx.projectId, ctx.user._id, 'readOnly') + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) }) }) }) }) describe('transferOwnership', function () { - beforeEach(function () { - this.req.body = { user_id: this.user._id.toString() } + beforeEach(function (ctx) { + ctx.req.body = { user_id: ctx.user._id.toString() } }) - it('returns 204 on success', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - done() - } - this.CollaboratorsController.transferOwnership(this.req, this.res) - }) - - it('returns 404 if the project does not exist', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect(message).to.match(/project not found/) - done() + it('returns 204 on success', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + resolve() + } + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) }) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.ProjectNotFoundError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) }) - it('returns 404 if the user does not exist', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect(message).to.match(/user not found/) - done() + it('returns 404 if the project does not exist', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect(message).to.match(/project not found/) + resolve() + }) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.ProjectNotFoundError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) }) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.UserNotFoundError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) }) - it('invokes HTTP forbidden error handler if the user is not a collaborator', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy(() => done()) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.UserNotCollaboratorError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) + it('returns 404 if the user does not exist', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect(message).to.match(/user not found/) + resolve() + }) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.UserNotFoundError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) + }) + }) + + it('invokes HTTP forbidden error handler if the user is not a collaborator', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.forbidden = sinon.spy(() => resolve()) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.UserNotCollaboratorError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs index 3e7d4c3daa..d948e69ed4 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import mongodb from 'mongodb-legacy' @@ -12,419 +12,488 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsInviteController.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('CollaboratorsInviteController', function () { - beforeEach(async function () { - this.projectId = 'project-id-123' - this.token = 'some-opaque-token' - this.tokenHmac = 'some-hmac-token' - this.targetEmail = 'user@example.com' - this.privileges = 'readAndWrite' - this.projectOwner = { + beforeEach(async function (ctx) { + ctx.projectId = 'project-id-123' + ctx.token = 'some-opaque-token' + ctx.tokenHmac = 'some-hmac-token' + ctx.targetEmail = 'user@example.com' + ctx.privileges = 'readAndWrite' + ctx.projectOwner = { _id: 'project-owner-id', email: 'project-owner@example.com', } - this.currentUser = { + ctx.currentUser = { _id: 'current-user-id', email: 'current-user@example.com', } - this.invite = { + ctx.invite = { _id: new ObjectId(), - token: this.token, - tokenHmac: this.tokenHmac, - sendingUserId: this.currentUser._id, - projectId: this.projectId, - email: this.targetEmail, - privileges: this.privileges, + token: ctx.token, + tokenHmac: ctx.tokenHmac, + sendingUserId: ctx.currentUser._id, + projectId: ctx.projectId, + email: ctx.targetEmail, + privileges: ctx.privileges, createdAt: new Date(), } - this.inviteReducedData = _.pick(this.invite, ['_id', 'email', 'privileges']) - this.project = { - _id: this.projectId, - owner_ref: this.projectOwner._id, + ctx.inviteReducedData = _.pick(ctx.invite, ['_id', 'email', 'privileges']) + ctx.project = { + _id: ctx.projectId, + owner_ref: ctx.projectOwner._id, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.currentUser), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.currentUser), } - this.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() } + ctx.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() } - this.rateLimiter = { + ctx.rateLimiter = { consume: sinon.stub().resolves(), } - this.RateLimiter = { - RateLimiter: sinon.stub().returns(this.rateLimiter), + ctx.RateLimiter = { + RateLimiter: sinon.stub().returns(ctx.rateLimiter), } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { allowedNumberOfCollaboratorsForUser: sinon.stub(), canAddXEditCollaborators: sinon.stub().resolves(true), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { getUserByAnyEmail: sinon.stub(), getUser: sinon.stub(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProject: sinon.stub(), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { isUserInvitedMemberOfProject: sinon.stub(), }, } - this.CollaboratorsInviteHandler = { + ctx.CollaboratorsInviteHandler = { promises: { - inviteToProject: sinon.stub().resolves(this.inviteReducedData), - generateNewInvite: sinon.stub().resolves(this.invite), - revokeInvite: sinon.stub().resolves(this.invite), + inviteToProject: sinon.stub().resolves(ctx.inviteReducedData), + generateNewInvite: sinon.stub().resolves(ctx.invite), + revokeInvite: sinon.stub().resolves(ctx.invite), acceptInvite: sinon.stub(), }, } - this.CollaboratorsInviteGetter = { + ctx.CollaboratorsInviteGetter = { promises: { getAllInvites: sinon.stub(), - getInviteByToken: sinon.stub().resolves(this.invite), + getInviteByToken: sinon.stub().resolves(ctx.invite), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.settings = {} + ctx.settings = {} - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, addEntryInBackground: sinon.stub(), } - this.AuthenticationController = { + ctx.AuthenticationController = { setRedirectInSession: sinon.stub(), } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.CollaboratorsInviteController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - '../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js': - this.CollaboratorsGetter, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs': - this.CollaboratorsInviteHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js': - this.CollaboratorsInviteGetter, - '../../../../app/src/Features/Editor/EditorRealTimeController.js': - this.EditorRealTimeController, - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - this.AnalyticsManger, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '@overleaf/settings': this.settings, - '../../../../app/src/infrastructure/RateLimiter': this.RateLimiter, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) - this.res = new MockResponse() - this.req = new MockRequest() - this.next = sinon.stub() + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs', + () => ({ + default: ctx.CollaboratorsInviteHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js', + () => ({ + default: ctx.CollaboratorsInviteGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController.js', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: ctx.AnalyticsManger, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/infrastructure/RateLimiter', + () => ctx.RateLimiter + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.CollaboratorsInviteController = (await import(MODULE_PATH)).default + + ctx.res = new MockResponse() + ctx.req = new MockRequest() + ctx.next = sinon.stub() }) describe('getAllInvites', function () { - beforeEach(function () { - this.fakeInvites = [ + beforeEach(function (ctx) { + ctx.fakeInvites = [ { _id: new ObjectId(), one: 1 }, { _id: new ObjectId(), two: 2 }, ] - this.req.params = { Project_id: this.projectId } + ctx.req.params = { Project_id: ctx.projectId } }) describe('when all goes well', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvites - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.getAllInvites( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvites + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.getAllInvites( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce an error', function () { - this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should produce a list of invite objects', function () { - this.res.json.callCount.should.equal(1) - this.res.json - .calledWith({ invites: this.fakeInvites }) - .should.equal(true) + it('should produce a list of invite objects', function (ctx) { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith({ invites: ctx.fakeInvites }).should.equal(true) }) - it('should have called CollaboratorsInviteHandler.getAllInvites', function () { - this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( + it('should have called CollaboratorsInviteHandler.getAllInvites', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getAllInvites - .calledWith(this.projectId) + ctx.CollaboratorsInviteGetter.promises.getAllInvites + .calledWith(ctx.projectId) .should.equal(true) }) }) describe('when CollaboratorsInviteHandler.getAllInvites produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getAllInvites.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.getAllInvites( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.getAllInvites( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - this.next.firstCall.args[0].should.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.firstCall.args[0].should.be.instanceof(Error) }) }) }) describe('inviteToProject', function () { - beforeEach(function () { - this.req.params = { Project_id: this.projectId } - this.req.body = { - email: this.targetEmail, - privileges: this.privileges, + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.projectId } + ctx.req.body = { + email: ctx.targetEmail, + privileges: ctx.privileges, } - this.ProjectGetter.promises.getProject.resolves({ - owner_ref: this.project.owner_ref, + ctx.ProjectGetter.promises.getProject.resolves({ + owner_ref: ctx.project.owner_ref, }) }) describe('when all goes well', function (done) { - beforeEach(async function () { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(async function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) - await this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res + await ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res ) }) - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, + it('should produce json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ + invite: ctx.inviteReducedData, }) }) - it('should have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXEditCollaborators - .calledWith(this.projectId) + ctx.LimitationsManager.promises.canAddXEditCollaborators + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('adds a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'send-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when the user is not allowed to add more edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - false - ) + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false) }) describe('readAndWrite collaborator', function () { - beforeEach(function (done) { - this.privileges = 'readAndWrite' - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.privileges = 'readAndWrite' + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce json response without an invite', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should produce json response without an invite', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, }) }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should not have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 0 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.currentUser, this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.currentUser, ctx.targetEmail) .should.equal(false) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('readOnly collaborator (always allowed)', function () { - beforeEach(function (done) { - this.req.body = { - email: this.targetEmail, - privileges: (this.privileges = 'readOnly'), - } - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) - }) - - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + email: ctx.targetEmail, + privileges: (ctx.privileges = 'readOnly'), + } + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) }) }) - it('should not have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should produce json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ + invite: ctx.inviteReducedData, + }) + }) + + it('should not have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 0 ) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('adds a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'send-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) @@ -432,808 +501,834 @@ describe('CollaboratorsInviteController', function () { }) describe('when inviteToProject produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteHandler.promises.inviteToProject.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteHandler.promises.inviteToProject.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) }) - it('should have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXEditCollaborators - .calledWith(this.projectId) + ctx.LimitationsManager.promises.canAddXEditCollaborators + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) }) describe('when _checkShouldInviteEmail disallows the invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(false) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(false) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce json response with no invite, and an error property', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should produce json response with no invite, and an error property', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, error: 'cannot_invite_non_user', }) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when _checkShouldInviteEmail produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .rejects(new Error('woops')) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .rejects(new Error('woops')) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when the user invites themselves to the project', function () { - beforeEach(function () { - this.req.body.email = this.currentUser.email - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(function (ctx) { + ctx.req.body.email = ctx.currentUser.email + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should reject action, return json response with error code', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should reject action, return json response with error code', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, error: 'cannot_invite_self', }) }) - it('should not have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should not have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 0 ) }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should not have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 0 ) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) - it('should not have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + it('should not have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) }) }) describe('when _checkRateLimit returns false', function () { - beforeEach(async function () { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(async function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(false) - await this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next + await ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 429 response', function () { - this.res.sendStatus.calledWith(429).should.equal(true) + it('should send a 429 response', function (ctx) { + ctx.res.sendStatus.calledWith(429).should.equal(true) }) - it('should not call inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.called.should.equal( + it('should not call inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.called.should.equal( false ) }) - it('should not call emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.called.should.equal(false) + it('should not call emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.called.should.equal(false) }) }) }) describe('viewInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - token: this.token, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + token: ctx.token, } - this.fakeProject = { - _id: this.projectId, + ctx.fakeProject = { + _id: ctx.projectId, name: 'some project', - owner_ref: this.invite.sendingUserId, + owner_ref: ctx.invite.sendingUserId, collaberator_refs: [], readOnly_refs: [], } - this.owner = { - _id: this.fakeProject.owner_ref, + ctx.owner = { + _id: ctx.fakeProject.owner_ref, first_name: 'John', last_name: 'Doe', email: 'john@example.com', } - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( false ) - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( - this.invite + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( + ctx.invite ) - this.ProjectGetter.promises.getProject.resolves(this.fakeProject) - this.UserGetter.promises.getUser.resolves(this.owner) + ctx.ProjectGetter.promises.getProject.resolves(ctx.fakeProject) + ctx.UserGetter.promises.getUser.resolves(ctx.owner) }) describe('when the token is valid', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render.calledWith('project/invite/show').should.equal(true) + it('should render the view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/show').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getInviteByToken - .calledWith(this.fakeProject._id, this.invite.token) + ctx.CollaboratorsInviteGetter.promises.getInviteByToken + .calledWith(ctx.fakeProject._id, ctx.invite.token) .should.equal(true) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) - this.ProjectGetter.promises.getProject - .calledWith(this.projectId) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.projectId) .should.equal(true) }) }) describe('when not logged in', function () { - beforeEach(function (done) { - this.SessionManager.getSessionUser.returns(null) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.SessionManager.getSessionUser.returns(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not check member status', function () { - expect(this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject) - .to.not.have.been.called + it('should not check member status', function (ctx) { + expect(ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject).to + .not.have.been.called }) - it('should set redirect back to invite', function () { + it('should set redirect back to invite', function (ctx) { expect( - this.AuthenticationController.setRedirectInSession - ).to.have.been.calledWith(this.req) + ctx.AuthenticationController.setRedirectInSession + ).to.have.been.calledWith(ctx.req) }) - it('should redirect to the register page', function () { - expect(this.res.render).to.not.have.been.called - expect(this.res.redirect).to.have.been.calledOnce - expect(this.res.redirect).to.have.been.calledWith('/register') + it('should redirect to the register page', function (ctx) { + expect(ctx.res.render).to.not.have.been.called + expect(ctx.res.redirect).to.have.been.calledOnce + expect(ctx.res.redirect).to.have.been.calledWith('/register') }) }) describe('when user is already a member of the project', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - true - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + true + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should redirect to the project page', function () { - this.res.redirect.callCount.should.equal(1) - this.res.redirect - .calledWith(`/project/${this.projectId}`) + it('should redirect to the project page', function (ctx) { + ctx.res.redirect.callCount.should.equal(1) + ctx.res.redirect + .calledWith(`/project/${ctx.projectId}`) .should.equal(true) }) - it('should not call next with an error', function () { - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should not call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 0 ) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when isUserInvitedMemberOfProject produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should not call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 0 ) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when the getInviteByToken produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when the getInviteByToken does not produce an invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when User.getUser produces an error', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUser.rejects(new Error('woops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser.rejects(new Error('woops')) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when User.getUser does not find a user', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUser.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when getProject produces an error', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.rejects(new Error('woops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.rejects(new Error('woops')) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) }) }) describe('when Project.getUser does not find a user', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) }) }) }) describe('generateNewInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - invite_id: this.invite._id.toString(), + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + invite_id: ctx.invite._id.toString(), } - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) }) describe('when generateNewInvite does not produce an error', function () { describe('and returns an invite object', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(201).should.equal(true) + it('should produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(201).should.equal(true) }) - it('should have called generateNewInvite', function () { - this.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( + it('should have called generateNewInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( 1 ) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should check the rate limit', function () { - this.CollaboratorsInviteController._checkRateLimit.callCount.should.equal( + it('should check the rate limit', function (ctx) { + ctx.CollaboratorsInviteController._checkRateLimit.callCount.should.equal( 1 ) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'resend-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('and returns a null invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.generateNewInvite.resolves( - null - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.resolves( + null + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should produce a 404 response when invite is null', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.should.have.been.calledWith(404) + it('should produce a 404 response when invite is null', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.should.have.been.calledWith(404) }) }) }) describe('when generateNewInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.generateNewInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(0) + it('should not produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called generateNewInvite', function () { - this.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( + it('should have called generateNewInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( 1 ) }) @@ -1241,79 +1336,83 @@ describe('CollaboratorsInviteController', function () { }) describe('revokeInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - invite_id: this.invite._id.toString(), + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + invite_id: ctx.invite._id.toString(), } }) describe('when revokeInvite does not produce an error', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.revokeInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.revokeInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce a 204 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.should.have.been.calledWith(204) + it('should produce a 204 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.should.have.been.calledWith(204) }) - it('should have called revokeInvite', function () { - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'revoke-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when revokeInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.revokeInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.revokeInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.revokeInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(0) + it('should not produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called revokeInvite', function () { - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) }) @@ -1321,188 +1420,196 @@ describe('CollaboratorsInviteController', function () { }) describe('acceptInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - token: this.token, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + token: ctx.token, } }) describe('when acceptInvite does not produce an error', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should redirect to project page', function (ctx) { + ctx.res.redirect.should.have.been.calledOnce + ctx.res.redirect.should.have.been.calledWith( + `/project/${ctx.projectId}` ) }) - it('should redirect to project page', function () { - this.res.redirect.should.have.been.calledOnce - this.res.redirect.should.have.been.calledWith( - `/project/${this.projectId}` + it('should have called acceptInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.should.have.been.calledWith( + ctx.invite, + ctx.projectId, + ctx.currentUser ) }) - it('should have called acceptInvite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.should.have.been.calledWith( - this.invite, - this.projectId, - this.currentUser - ) - }) - - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.should.have.been.calledOnce - this.EditorRealTimeController.emitToRoom.should.have.been.calledWith( - this.projectId, + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledOnce + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledWith( + ctx.projectId, 'project:membership:changed' ) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( + ctx.projectId, 'accept-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when the invite is not found', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('throws a NotFoundError', function () { - expect(this.next).to.have.been.calledWith( + it('throws a NotFoundError', function (ctx) { + expect(ctx.next).to.have.been.calledWith( sinon.match.instanceOf(Errors.NotFoundError) ) }) }) describe('when acceptInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.acceptInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not redirect to project page', function () { - this.res.redirect.callCount.should.equal(0) + it('should not redirect to project page', function (ctx) { + ctx.res.redirect.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called acceptInvite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.callCount.should.equal( + it('should have called acceptInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.callCount.should.equal( 1 ) }) }) describe('when the project audit log entry fails', function () { - beforeEach(function (done) { - this.ProjectAuditLogHandler.promises.addEntry.rejects(new Error('oops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectAuditLogHandler.promises.addEntry.rejects( + new Error('oops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not accept the invite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.should.not.have + it('should not accept the invite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.should.not.have .been.called }) }) }) describe('_checkShouldInviteEmail', function () { - beforeEach(function () { - this.email = 'user@example.com' + beforeEach(function (ctx) { + ctx.email = 'user@example.com' }) describe('when we should be restricting to existing accounts', function () { - beforeEach(function () { - this.settings.restrictInvitesToExistingAccounts = true - this.call = () => - this.CollaboratorsInviteController._checkShouldInviteEmail(this.email) + beforeEach(function (ctx) { + ctx.settings.restrictInvitesToExistingAccounts = true + ctx.call = () => + ctx.CollaboratorsInviteController._checkShouldInviteEmail(ctx.email) }) describe('when user account is present', function () { - beforeEach(function () { - this.user = { _id: new ObjectId().toString() } - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.user = { _id: new ObjectId().toString() } + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) }) - it('should callback with `true`', async function () { + it('should callback with `true`', async function (ctx) { const shouldAllow = - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) expect(shouldAllow).to.equal(true) }) }) describe('when user account is absent', function () { - beforeEach(function () { - this.user = null - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.user = null + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) }) - it('should callback with `false`', async function () { + it('should callback with `false`', async function (ctx) { const shouldAllow = - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) expect(shouldAllow).to.equal(false) }) - it('should have called getUser', async function () { - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + it('should have called getUser', async function (ctx) { + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.email, { _id: 1 }) + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.email, { _id: 1 }) .should.equal(true) }) }) describe('when getUser produces an error', function () { - beforeEach(function () { - this.user = null - this.UserGetter.promises.getUserByAnyEmail.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.user = null + ctx.UserGetter.promises.getUserByAnyEmail.rejects(new Error('woops')) }) - it('should callback with an error', async function () { + it('should callback with an error', async function (ctx) { await expect( - this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email - ) + ctx.CollaboratorsInviteController._checkShouldInviteEmail(ctx.email) ).to.be.rejected }) }) @@ -1510,67 +1617,57 @@ describe('CollaboratorsInviteController', function () { }) describe('_checkRateLimit', function () { - beforeEach(function () { - this.settings.restrictInvitesToExistingAccounts = false - this.currentUserId = '32312313' - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + beforeEach(function (ctx) { + ctx.settings.restrictInvitesToExistingAccounts = false + ctx.currentUserId = '32312313' + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(17) }) - it('should callback with `true` when rate limit under', async function () { - const result = await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId + it('should callback with `true` when rate limit under', async function (ctx) { + const result = await ctx.CollaboratorsInviteController._checkRateLimit( + ctx.currentUserId ) + expect(ctx.rateLimiter.consume).to.have.been.calledWith(ctx.currentUserId) result.should.equal(true) }) - it('should callback with `false` when rate limit hit', async function () { - this.rateLimiter.consume.rejects({ remainingPoints: 0 }) - const result = await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId + it('should callback with `false` when rate limit hit', async function (ctx) { + ctx.rateLimiter.consume.rejects({ remainingPoints: 0 }) + const result = await ctx.CollaboratorsInviteController._checkRateLimit( + ctx.currentUserId ) + expect(ctx.rateLimiter.consume).to.have.been.calledWith(ctx.currentUserId) result.should.equal(false) }) - it('should allow 10x the collaborators', async function () { - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + it('should allow 10x the collaborators', async function (ctx) { + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 170) ) }) - it('should allow 200 requests when collaborators is -1', async function () { - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + it('should allow 200 requests when collaborators is -1', async function (ctx) { + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(-1) - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 200) ) }) - it('should allow 10 requests when user has no collaborators set', async function () { - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + it('should allow 10 requests when user has no collaborators set', async function (ctx) { + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(null) - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 10) ) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs index f386648552..ec8f453536 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import mongodb from 'mongodb-legacy' import Crypto from 'crypto' @@ -10,8 +10,8 @@ const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs' describe('CollaboratorsInviteHandler', function () { - beforeEach(async function () { - this.ProjectInvite = class ProjectInvite { + beforeEach(async function (ctx) { + ctx.ProjectInvite = class ProjectInvite { constructor(options) { if (options == null) { options = {} @@ -23,120 +23,174 @@ describe('CollaboratorsInviteHandler', function () { } } } - this.ProjectInvite.prototype.save = sinon.stub() - this.ProjectInvite.findOne = sinon.stub() - this.ProjectInvite.find = sinon.stub() - this.ProjectInvite.deleteOne = sinon.stub() - this.ProjectInvite.findOneAndDelete = sinon.stub() - this.ProjectInvite.countDocuments = sinon.stub() + ctx.ProjectInvite.prototype.save = sinon.stub() + ctx.ProjectInvite.findOne = sinon.stub() + ctx.ProjectInvite.find = sinon.stub() + ctx.ProjectInvite.deleteOne = sinon.stub() + ctx.ProjectInvite.findOneAndDelete = sinon.stub() + ctx.ProjectInvite.countDocuments = sinon.stub() - this.Crypto = { + ctx.Crypto = { randomBytes: sinon.stub().callsFake(Crypto.randomBytes), } - this.settings = {} - this.CollaboratorsEmailHandler = { promises: {} } - this.CollaboratorsHandler = { + ctx.settings = {} + ctx.CollaboratorsEmailHandler = { promises: {} } + ctx.CollaboratorsHandler = { promises: { addUserIdToProject: sinon.stub(), }, } - this.UserGetter = { promises: { getUser: sinon.stub() } } - this.ProjectGetter = { promises: { getProject: sinon.stub().resolves() } } - this.NotificationsBuilder = { promises: {} } - this.tokenHmac = 'jkhajkefhaekjfhkfg' - this.CollaboratorsInviteHelper = { - generateToken: sinon.stub().returns(this.Crypto.randomBytes(24)), - hashInviteToken: sinon.stub().returns(this.tokenHmac), + ctx.UserGetter = { promises: { getUser: sinon.stub() } } + ctx.ProjectGetter = { promises: { getProject: sinon.stub().resolves() } } + ctx.NotificationsBuilder = { promises: {} } + ctx.tokenHmac = 'jkhajkefhaekjfhkfg' + ctx.CollaboratorsInviteHelper = { + generateToken: sinon.stub().returns(ctx.Crypto.randomBytes(24)), + hashInviteToken: sinon.stub().returns(ctx.tokenHmac), } - this.CollaboratorsInviteGetter = { + ctx.CollaboratorsInviteGetter = { promises: { getAllInvites: sinon.stub(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignmentForUser: sinon.stub().resolves(), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAcceptEditCollaboratorInvite: sinon.stub().resolves(), }, } - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, addEntryInBackground: sinon.stub(), } - this.logger = { + ctx.logger = { debug: sinon.stub(), warn: sinon.stub(), err: sinon.stub(), } - this.CollaboratorsInviteHandler = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/models/ProjectInvite.js': { - ProjectInvite: this.ProjectInvite, - }, - '@overleaf/logger': this.logger, - '../../../../app/src/Features/Collaborators/CollaboratorsEmailHandler.mjs': - this.CollaboratorsEmailHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js': - this.CollaboratorsHandler, - '../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/Notifications/NotificationsBuilder.js': - this.NotificationsBuilder, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHelper.js': - this.CollaboratorsInviteHelper, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter': - this.CollaboratorsInviteGetter, - '../../../../app/src/Features/SplitTests/SplitTestHandler.js': - this.SplitTestHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - crypto: this.CryptogetAssignmentForUser, - }) + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) - this.projectId = new ObjectId() - this.sendingUserId = new ObjectId() - this.sendingUser = { - _id: this.sendingUserId, + vi.doMock('../../../../app/src/models/ProjectInvite.js', () => ({ + ProjectInvite: ctx.ProjectInvite, + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsEmailHandler.mjs', + () => ({ + default: ctx.CollaboratorsEmailHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder.js', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHelper.js', + () => ({ + default: ctx.CollaboratorsInviteHelper, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter', + () => ({ + default: ctx.CollaboratorsInviteGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.js', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock('crypto', () => ({ + default: ctx.CryptogetAssignmentForUser, + })) + + ctx.CollaboratorsInviteHandler = (await import(MODULE_PATH)).default + + ctx.projectId = new ObjectId() + ctx.sendingUserId = new ObjectId() + ctx.sendingUser = { + _id: ctx.sendingUserId, name: 'Bob', } - this.email = 'user@example.com' - this.userId = new ObjectId() - this.user = { - _id: this.userId, + ctx.email = 'user@example.com' + ctx.userId = new ObjectId() + ctx.user = { + _id: ctx.userId, email: 'someone@example.com', } - this.inviteId = new ObjectId() - this.token = 'hnhteaosuhtaeosuahs' - this.privileges = 'readAndWrite' - this.fakeInvite = { - _id: this.inviteId, - email: this.email, - token: this.token, - tokenHmac: this.tokenHmac, - sendingUserId: this.sendingUserId, - projectId: this.projectId, - privileges: this.privileges, + ctx.inviteId = new ObjectId() + ctx.token = 'hnhteaosuhtaeosuahs' + ctx.privileges = 'readAndWrite' + ctx.fakeInvite = { + _id: ctx.inviteId, + email: ctx.email, + token: ctx.token, + tokenHmac: ctx.tokenHmac, + sendingUserId: ctx.sendingUserId, + projectId: ctx.projectId, + privileges: ctx.privileges, createdAt: new Date(), } }) describe('inviteToProject', function () { - beforeEach(function () { - this.ProjectInvite.prototype.save.callsFake(async function () { + beforeEach(function (ctx) { + ctx.ProjectInvite.prototype.save.callsFake(async function () { Object.defineProperty(this, 'toObject', { value: function () { return this @@ -147,191 +201,193 @@ describe('CollaboratorsInviteHandler', function () { }) return this }) - this.CollaboratorsInviteHandler.promises._sendMessages = sinon + ctx.CollaboratorsInviteHandler.promises._sendMessages = sinon .stub() .resolves() - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.inviteToProject( - this.projectId, - this.sendingUser, - this.email, - this.privileges + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.inviteToProject( + ctx.projectId, + ctx.sendingUser, + ctx.email, + ctx.privileges ) } }) describe('when all goes well', function () { - it('should produce the invite object', async function () { - const invite = await this.call() + it('should produce the invite object', async function (ctx) { + const invite = await ctx.call() expect(invite).to.not.equal(null) expect(invite).to.not.equal(undefined) expect(invite).to.be.instanceof(Object) expect(invite).to.have.all.keys(['_id', 'email', 'privileges']) }) - it('should have generated a random token', async function () { - await this.call() - this.Crypto.randomBytes.callCount.should.equal(1) + it('should have generated a random token', async function (ctx) { + await ctx.call() + ctx.Crypto.randomBytes.callCount.should.equal(1) }) - it('should have generated a HMAC token', async function () { - await this.call() - this.CollaboratorsInviteHelper.hashInviteToken.callCount.should.equal(1) + it('should have generated a HMAC token', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHelper.hashInviteToken.callCount.should.equal(1) }) - it('should have called ProjectInvite.save', async function () { - await this.call() - this.ProjectInvite.prototype.save.callCount.should.equal(1) + it('should have called ProjectInvite.save', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.prototype.save.callCount.should.equal(1) }) - it('should have called _sendMessages', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._sendMessages.callCount.should.equal( + it('should have called _sendMessages', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._sendMessages.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._sendMessages - .calledWith(this.projectId, this.sendingUser) + ctx.CollaboratorsInviteHandler.promises._sendMessages + .calledWith(ctx.projectId, ctx.sendingUser) .should.equal(true) }) }) describe('when saving model produces an error', function () { - beforeEach(function () { - this.ProjectInvite.prototype.save.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectInvite.prototype.save.rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('_sendMessages', function () { - beforeEach(function () { - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon .stub() .resolves() - this.CollaboratorsInviteHandler.promises._trySendInviteNotification = - sinon.stub().resolves() - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._sendMessages( - this.projectId, - this.sendingUser, - this.fakeInvite + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification = sinon + .stub() + .resolves() + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._sendMessages( + ctx.projectId, + ctx.sendingUser, + ctx.fakeInvite ) } }) describe('when all goes well', function () { - it('should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', async function () { - await this.call() - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite.callCount.should.equal( + it('should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite.callCount.should.equal( 1 ) - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite - .calledWith(this.projectId, this.fakeInvite.email, this.fakeInvite) + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite + .calledWith(ctx.projectId, ctx.fakeInvite.email, ctx.fakeInvite) .should.equal(true) }) - it('should call _trySendInviteNotification', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._trySendInviteNotification.callCount.should.equal( + it('should call _trySendInviteNotification', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._trySendInviteNotification - .calledWith(this.projectId, this.sendingUser, this.fakeInvite) + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification + .calledWith(ctx.projectId, ctx.sendingUser, ctx.fakeInvite) .should.equal(true) }) }) describe('when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', function () { - beforeEach(function () { - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = - sinon.stub().rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon + .stub() + .rejects(new Error('woops')) }) - it('should not produce an error', async function () { - await expect(this.call()).to.be.fulfilled - expect(this.logger.err).to.be.calledOnce + it('should not produce an error', async function (ctx) { + await expect(ctx.call()).to.be.fulfilled + expect(ctx.logger.err).to.be.calledOnce }) }) describe('when _trySendInviteNotification produces an error', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises._trySendInviteNotification = + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification = sinon.stub().rejects(new Error('woops')) }) - it('should not produce an error', async function () { - await expect(this.call()).to.be.fulfilled - expect(this.logger.err).to.be.calledOnce + it('should not produce an error', async function (ctx) { + await expect(ctx.call()).to.be.fulfilled + expect(ctx.logger.err).to.be.calledOnce }) }) }) describe('revokeInviteForUser', function () { - beforeEach(function () { - this.targetInvite = { + beforeEach(function (ctx) { + ctx.targetInvite = { _id: new ObjectId(), email: 'fake2@example.org', two: 2, } - this.fakeInvites = [ + ctx.fakeInvites = [ { _id: new ObjectId(), email: 'fake1@example.org', one: 1 }, - this.targetInvite, + ctx.targetInvite, ] - this.fakeInvitesWithoutUser = [ + ctx.fakeInvitesWithoutUser = [ { _id: new ObjectId(), email: 'fake1@example.org', one: 1 }, { _id: new ObjectId(), email: 'fake3@example.org', two: 2 }, ] - this.targetEmail = [{ email: 'fake2@example.org' }] + ctx.targetEmail = [{ email: 'fake2@example.org' }] - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvites + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvites ) - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() - .resolves(this.targetInvite) + .resolves(ctx.targetInvite) - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.revokeInviteForUser( - this.projectId, - this.targetEmail + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.revokeInviteForUser( + ctx.projectId, + ctx.targetEmail ) } }) describe('for a valid user', function () { - it('should have called CollaboratorsInviteGetter.getAllInvites', async function () { - await this.call() - this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( + it('should have called CollaboratorsInviteGetter.getAllInvites', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getAllInvites - .calledWith(this.projectId) + ctx.CollaboratorsInviteGetter.promises.getAllInvites + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.revokeInvite - .calledWith(this.projectId, this.targetInvite._id) + ctx.CollaboratorsInviteHandler.promises.revokeInvite + .calledWith(ctx.projectId, ctx.targetInvite._id) .should.equal(true) }) }) describe('for a user without an invite in the project', function () { - beforeEach(function () { - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvitesWithoutUser + beforeEach(function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvitesWithoutUser ) }) - it('should not have called CollaboratorsInviteHandler.revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should not have called CollaboratorsInviteHandler.revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 0 ) }) @@ -339,142 +395,142 @@ describe('CollaboratorsInviteHandler', function () { }) describe('revokeInvite', function () { - beforeEach(function () { - this.ProjectInvite.findOneAndDelete.returns({ - exec: sinon.stub().resolves(this.fakeInvite), + beforeEach(function (ctx) { + ctx.ProjectInvite.findOneAndDelete.returns({ + exec: sinon.stub().resolves(ctx.fakeInvite), }) - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = sinon.stub().resolves() - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.revokeInvite( - this.projectId, - this.inviteId + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.revokeInvite( + ctx.projectId, + ctx.inviteId ) } }) describe('when all goes well', function () { - it('should call ProjectInvite.findOneAndDelete', async function () { - await this.call() - this.ProjectInvite.findOneAndDelete.should.have.been.calledOnce - this.ProjectInvite.findOneAndDelete.should.have.been.calledWith({ - projectId: this.projectId, - _id: this.inviteId, + it('should call ProjectInvite.findOneAndDelete', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.findOneAndDelete.should.have.been.calledOnce + ctx.ProjectInvite.findOneAndDelete.should.have.been.calledWith({ + projectId: ctx.projectId, + _id: ctx.inviteId, }) }) - it('should call _tryCancelInviteNotification', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification.callCount.should.equal( + it('should call _tryCancelInviteNotification', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification - .calledWith(this.inviteId) + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification + .calledWith(ctx.inviteId) .should.equal(true) }) - it('should return the deleted invite', async function () { - const invite = await this.call() - expect(invite).to.deep.equal(this.fakeInvite) + it('should return the deleted invite', async function (ctx) { + const invite = await ctx.call() + expect(invite).to.deep.equal(ctx.fakeInvite) }) }) describe('when remove produces an error', function () { - beforeEach(function () { - this.ProjectInvite.findOneAndDelete.returns({ + beforeEach(function (ctx) { + ctx.ProjectInvite.findOneAndDelete.returns({ exec: sinon.stub().rejects(new Error('woops')), }) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('generateNewInvite', function () { - beforeEach(function () { - this.fakeInviteToProjectObject = { + beforeEach(function (ctx) { + ctx.fakeInviteToProjectObject = { _id: new ObjectId(), - email: this.email, - privileges: this.privileges, + email: ctx.email, + privileges: ctx.privileges, } - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() - .resolves(this.fakeInvite) - this.CollaboratorsInviteHandler.promises.inviteToProject = sinon + .resolves(ctx.fakeInvite) + ctx.CollaboratorsInviteHandler.promises.inviteToProject = sinon .stub() - .resolves(this.fakeInviteToProjectObject) - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.generateNewInvite( - this.projectId, - this.sendingUser, - this.inviteId + .resolves(ctx.fakeInviteToProjectObject) + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.generateNewInvite( + ctx.projectId, + ctx.sendingUser, + ctx.inviteId ) } }) describe('when all goes well', function () { - it('should call revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should call revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.revokeInvite - .calledWith(this.projectId, this.inviteId) + ctx.CollaboratorsInviteHandler.promises.revokeInvite + .calledWith(ctx.projectId, ctx.inviteId) .should.equal(true) }) - it('should have called inviteToProject', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.sendingUser, - this.fakeInvite.email, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUser, + ctx.fakeInvite.email, + ctx.fakeInvite.privileges ) .should.equal(true) }) - it('should return the invite', async function () { - const invite = await this.call() - expect(invite).to.deep.equal(this.fakeInviteToProjectObject) + it('should return the invite', async function (ctx) { + const invite = await ctx.call() + expect(invite).to.deep.equal(ctx.fakeInviteToProjectObject) }) }) describe('when revokeInvite produces an error', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should not have called inviteToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when findOne does not find an invite', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() .resolves(null) }) - it('should not have called inviteToProject', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) @@ -482,91 +538,91 @@ describe('CollaboratorsInviteHandler', function () { }) describe('acceptInvite', function () { - beforeEach(function () { - this.fakeProject = { - _id: this.projectId, - owner_ref: this.sendingUserId, + beforeEach(function (ctx) { + ctx.fakeProject = { + _id: ctx.projectId, + owner_ref: ctx.sendingUserId, } - this.ProjectGetter.promises.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon .stub() - .resolves(this.fakeProject) - this.CollaboratorsHandler.promises.addUserIdToProject.resolves() - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = + .resolves(ctx.fakeProject) + ctx.CollaboratorsHandler.promises.addUserIdToProject.resolves() + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = sinon.stub().resolves() - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) - this.ProjectInvite.deleteOne.returns({ exec: sinon.stub().resolves() }) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises.acceptInvite( - this.fakeInvite, - this.projectId, - this.user + ctx.ProjectInvite.deleteOne.returns({ exec: sinon.stub().resolves() }) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises.acceptInvite( + ctx.fakeInvite, + ctx.projectId, + ctx.user ) } }) describe('when all goes well', function () { - it('should add readAndWrite invitees to the project as normal', async function () { - await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + it('should add readAndWrite invitees to the project as normal', async function (ctx) { + await ctx.call() + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) }) - it('should have called ProjectInvite.deleteOne', async function () { - await this.call() - this.ProjectInvite.deleteOne.callCount.should.equal(1) - this.ProjectInvite.deleteOne - .calledWith({ _id: this.inviteId }) + it('should have called ProjectInvite.deleteOne', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.deleteOne.callCount.should.equal(1) + ctx.ProjectInvite.deleteOne + .calledWith({ _id: ctx.inviteId }) .should.equal(true) }) }) describe('when the invite is for readOnly access', function () { - beforeEach(function () { - this.fakeInvite.privileges = 'readOnly' + beforeEach(function (ctx) { + ctx.fakeInvite.privileges = 'readOnly' }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject .calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) .should.equal(true) }) }) describe('when the project has no more edit collaborator slots', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( false ) }) - it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function () { - await this.call() - this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.projectId, + it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function (ctx) { + await ctx.call() + ctx.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( + ctx.projectId, 'editor-moved-to-pending', null, null, - { userId: this.userId.toString(), role: 'editor' } + { userId: ctx.userId.toString(), role: 'editor' } ) - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, 'readOnly', { pendingEditor: true } ) @@ -574,139 +630,139 @@ describe('CollaboratorsInviteHandler', function () { }) describe('when addUserIdToProject produces an error', function () { - beforeEach(function () { - this.CollaboratorsHandler.promises.addUserIdToProject.callsArgWith( + beforeEach(function (ctx) { + ctx.CollaboratorsHandler.promises.addUserIdToProject.callsArgWith( 4, new Error('woops') ) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject .calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) .should.equal(true) }) - it('should not have called ProjectInvite.deleteOne', async function () { - await expect(this.call()).to.be.rejected - this.ProjectInvite.deleteOne.callCount.should.equal(0) + it('should not have called ProjectInvite.deleteOne', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectInvite.deleteOne.callCount.should.equal(0) }) }) describe('when ProjectInvite.deleteOne produces an error', function () { - beforeEach(function () { - this.ProjectInvite.deleteOne.returns({ + beforeEach(function (ctx) { + ctx.ProjectInvite.deleteOne.returns({ exec: sinon.stub().rejects(new Error('woops')), }) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) }) - it('should have called ProjectInvite.deleteOne', async function () { - await expect(this.call()).to.be.rejected - this.ProjectInvite.deleteOne.callCount.should.equal(1) + it('should have called ProjectInvite.deleteOne', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectInvite.deleteOne.callCount.should.equal(1) }) }) }) describe('_tryCancelInviteNotification', function () { - beforeEach(function () { - this.inviteId = new ObjectId() - this.currentUser = { _id: new ObjectId() } - this.notification = { read: sinon.stub().resolves() } - this.NotificationsBuilder.promises.projectInvite = sinon + beforeEach(function (ctx) { + ctx.inviteId = new ObjectId() + ctx.currentUser = { _id: new ObjectId() } + ctx.notification = { read: sinon.stub().resolves() } + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification( - this.inviteId + .returns(ctx.notification) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification( + ctx.inviteId ) } }) - it('should call notification.read', async function () { - await this.call() - this.notification.read.callCount.should.equal(1) + it('should call notification.read', async function (ctx) { + await ctx.call() + ctx.notification.read.callCount.should.equal(1) }) describe('when notification.read produces an error', function () { - beforeEach(function () { - this.notification = { + beforeEach(function (ctx) { + ctx.notification = { read: sinon.stub().rejects(new Error('woops')), } - this.NotificationsBuilder.promises.projectInvite = sinon + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) + .returns(ctx.notification) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejected + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejected }) }) }) describe('_trySendInviteNotification', function () { - beforeEach(function () { - this.invite = { + beforeEach(function (ctx) { + ctx.invite = { _id: new ObjectId(), token: 'some_token', sendingUserId: new ObjectId(), - projectId: this.project_id, + projectId: ctx.project_id, targetEmail: 'user@example.com', createdAt: new Date(), } - this.sendingUser = { + ctx.sendingUser = { _id: new ObjectId(), first_name: 'jim', } - this.existingUser = { _id: new ObjectId() } - this.UserGetter.promises.getUserByAnyEmail = sinon + ctx.existingUser = { _id: new ObjectId() } + ctx.UserGetter.promises.getUserByAnyEmail = sinon .stub() - .resolves(this.existingUser) - this.fakeProject = { - _id: this.project_id, + .resolves(ctx.existingUser) + ctx.fakeProject = { + _id: ctx.project_id, name: 'some project', } - this.ProjectGetter.promises.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon .stub() - .resolves(this.fakeProject) - this.notification = { create: sinon.stub().resolves() } - this.NotificationsBuilder.promises.projectInvite = sinon + .resolves(ctx.fakeProject) + ctx.notification = { create: sinon.stub().resolves() } + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._trySendInviteNotification( - this.project_id, - this.sendingUser, - this.invite + .returns(ctx.notification) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification( + ctx.project_id, + ctx.sendingUser, + ctx.invite ) } }) @@ -714,119 +770,119 @@ describe('CollaboratorsInviteHandler', function () { describe('when the user exists', function () { beforeEach(function () {}) - it('should call getUser', async function () { - await this.call() - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await ctx.call() + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should call getProject', async function () { - await this.call() - this.ProjectGetter.promises.getProject.callCount.should.equal(1) - this.ProjectGetter.promises.getProject - .calledWith(this.project_id) + it('should call getProject', async function (ctx) { + await ctx.call() + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project_id) .should.equal(true) }) - it('should call NotificationsBuilder.projectInvite.create', async function () { - await this.call() - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should call NotificationsBuilder.projectInvite.create', async function (ctx) { + await ctx.call() + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 1 ) - this.notification.create.callCount.should.equal(1) + ctx.notification.create.callCount.should.equal(1) }) describe('when getProject produces an error', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject.callsArgWith( + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject.callsArgWith( 2, new Error('woops') ) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await expect(this.call()).to.be.rejected - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) describe('when projectInvite.create produces an error', function () { - beforeEach(function () { - this.notification.create.callsArgWith(0, new Error('woops')) + beforeEach(function (ctx) { + ctx.notification.create.callsArgWith(0, new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('when the user does not exist', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByAnyEmail = sinon.stub().resolves(null) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail = sinon.stub().resolves(null) }) - it('should call getUser', async function () { - await this.call() - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await ctx.call() + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should not call getProject', async function () { - await this.call() - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call getProject', async function (ctx) { + await ctx.call() + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await this.call() - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await ctx.call() + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) describe('when the getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByAnyEmail = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should call getUser', async function () { - await expect(this.call()).to.be.rejected - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should not call getProject', async function () { - await expect(this.call()).to.be.rejected - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call getProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await expect(this.call()).to.be.rejected - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) }) diff --git a/services/web/test/unit/src/Contact/ContactController.test.mjs b/services/web/test/unit/src/Contact/ContactController.test.mjs index ea5a1d0220..2defc2c3a7 100644 --- a/services/web/test/unit/src/Contact/ContactController.test.mjs +++ b/services/web/test/unit/src/Contact/ContactController.test.mjs @@ -1,34 +1,47 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Contacts/ContactController.mjs' describe('ContactController', function () { - beforeEach(async function () { - this.SessionManager = { getLoggedInUserId: sinon.stub() } - this.ContactController = await esmock.strict(modulePath, { - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + beforeEach(async function (ctx) { + ctx.SessionManager = { getLoggedInUserId: sinon.stub() } + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: {}, }), - '../../../../app/src/Features/Contacts/ContactManager': - (this.ContactManager = { promises: {} }), - '../../../../app/src/Features/Authentication/SessionManager': - (this.SessionManager = {}), - '../../../../app/src/infrastructure/Modules': (this.Modules = { + })) + + vi.doMock('../../../../app/src/Features/Contacts/ContactManager', () => ({ + default: (ctx.ContactManager = { promises: {} }), + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: (ctx.SessionManager = {}), + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { promises: { hooks: {} }, }), - }) + })) - this.req = {} - this.res = new MockResponse() + ctx.ContactController = (await import(modulePath)).default + + ctx.req = {} + ctx.res = new MockResponse() }) describe('getContacts', function () { - beforeEach(function () { - this.user_id = 'mock-user-id' - this.contact_ids = ['contact-1', 'contact-2', 'contact-3'] - this.contacts = [ + beforeEach(function (ctx) { + ctx.user_id = 'mock-user-id' + ctx.contact_ids = ['contact-1', 'contact-2', 'contact-3'] + ctx.contacts = [ { _id: 'contact-1', email: 'joe@example.com', @@ -52,78 +65,84 @@ describe('ContactController', function () { unsued: 'foo', }, ] - this.SessionManager.getLoggedInUserId = sinon.stub().returns(this.user_id) - this.ContactManager.promises.getContactIds = sinon + ctx.SessionManager.getLoggedInUserId = sinon.stub().returns(ctx.user_id) + ctx.ContactManager.promises.getContactIds = sinon .stub() - .resolves(this.contact_ids) - this.UserGetter.promises.getUsers = sinon.stub().resolves(this.contacts) - this.Modules.promises.hooks.fire = sinon.stub() + .resolves(ctx.contact_ids) + ctx.UserGetter.promises.getUsers = sinon.stub().resolves(ctx.contacts) + ctx.Modules.promises.hooks.fire = sinon.stub() }) - it('should look up the logged in user id', async function () { - this.ContactController.getContacts(this.req, this.res) - this.SessionManager.getLoggedInUserId - .calledWith(this.req.session) + it('should look up the logged in user id', async function (ctx) { + ctx.ContactController.getContacts(ctx.req, ctx.res) + ctx.SessionManager.getLoggedInUserId + .calledWith(ctx.req.session) .should.equal(true) }) - it('should get the users contact ids', async function () { - this.res.callback = () => { + it('should get the users contact ids', async function (ctx) { + ctx.res.callback = () => { expect( - this.ContactManager.promises.getContactIds - ).to.have.been.calledWith(this.user_id, { limit: 50 }) + ctx.ContactManager.promises.getContactIds + ).to.have.been.calledWith(ctx.user_id, { limit: 50 }) } - this.ContactController.getContacts(this.req, this.res) + ctx.ContactController.getContacts(ctx.req, ctx.res) }) - it('should populate the users contacts ids', function (done) { - this.res.callback = () => { - expect(this.UserGetter.promises.getUsers).to.have.been.calledWith( - this.contact_ids, - { - email: 1, - first_name: 1, - last_name: 1, - holdingAccount: 1, - } - ) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should populate the users contacts ids', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.UserGetter.promises.getUsers).to.have.been.calledWith( + ctx.contact_ids, + { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1, + } + ) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) - it('should fire the getContact module hook', function (done) { - this.res.callback = () => { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( - 'getContacts', - this.user_id - ) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should fire the getContact module hook', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( + 'getContacts', + ctx.user_id + ) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) - it('should return a formatted list of contacts in contact list order, without holding accounts', function (done) { - this.res.callback = () => { - this.res.json.args[0][0].contacts.should.deep.equal([ - { - id: 'contact-1', - email: 'joe@example.com', - first_name: 'Joe', - last_name: 'Example', - type: 'user', - }, - { - id: 'contact-3', - email: 'jim@example.com', - first_name: 'Jim', - last_name: 'Example', - type: 'user', - }, - ]) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should return a formatted list of contacts in contact list order, without holding accounts', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.json.args[0][0].contacts.should.deep.equal([ + { + id: 'contact-1', + email: 'joe@example.com', + first_name: 'Joe', + last_name: 'Example', + type: 'user', + }, + { + id: 'contact-3', + email: 'jim@example.com', + first_name: 'Jim', + last_name: 'Example', + type: 'user', + }, + ]) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) }) }) diff --git a/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs b/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs index 22d05fba56..2bb1ed81dd 100644 --- a/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs +++ b/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs @@ -1,15 +1,4 @@ -/* eslint-disable - max-len, - no-return-assign, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = new URL( @@ -18,116 +7,119 @@ const modulePath = new URL( ).pathname describe('CooldownMiddleware', function () { - beforeEach(async function () { - this.CooldownManager = { isProjectOnCooldown: sinon.stub() } - return (this.CooldownMiddleware = await esmock.strict(modulePath, { - '../../../../app/src/Features/Cooldown/CooldownManager.js': - this.CooldownManager, - })) + beforeEach(async function (ctx) { + ctx.CooldownManager = { isProjectOnCooldown: sinon.stub() } + + vi.doMock( + '../../../../app/src/Features/Cooldown/CooldownManager.js', + () => ({ + default: ctx.CooldownManager, + }) + ) + + ctx.CooldownMiddleware = (await import(modulePath)).default }) describe('freezeProject', function () { describe('when project is on cooldown', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, true) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('should not produce an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - return this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + return ctx.next.callCount.should.equal(0) }) - it('should send a 429 status', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.res.sendStatus.callCount.should.equal(1) - return this.res.sendStatus.calledWith(429).should.equal(true) + it('should send a 429 status', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.res.sendStatus.callCount.should.equal(1) + return ctx.res.sendStatus.calledWith(429).should.equal(true) }) }) describe('when project is not on cooldown', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, false) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('call next with no arguments', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args.length).to.equal(0) + it('call next with no arguments', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args.length).to.equal(0) }) }) describe('when isProjectOnCooldown produces an error', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, new Error('woops')) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('call next with an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('call next with an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) }) describe('when projectId is not part of route', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, true) - this.req = { params: { lol: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { lol: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('call next with an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('call next with an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should not call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - return this.CooldownManager.isProjectOnCooldown.callCount.should.equal( - 0 - ) + it('should not call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + return ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(0) }) }) }) diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs index 6a783d452e..095e598d39 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs @@ -1,92 +1,102 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const MODULE_PATH = '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterController.mjs' describe('DocumentUpdaterController', function () { - beforeEach(async function () { - this.DocumentUpdaterHandler = { + beforeEach(async function (ctx) { + ctx.DocumentUpdaterHandler = { promises: { getDocument: sinon.stub(), }, } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: { findElement: sinon.stub(), }, } - this.controller = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Project/ProjectLocator.js': - this.ProjectLocator, - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js': - this.DocumentUpdaterHandler, - }) - this.projectId = '2k3j1lk3j21lk3j' - this.fileId = '12321kklj1lk3jk12' - this.req = { + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator.js', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + ctx.controller = (await import(MODULE_PATH)).default + ctx.projectId = '2k3j1lk3j21lk3j' + ctx.fileId = '12321kklj1lk3jk12' + ctx.req = { params: { - Project_id: this.projectId, - Doc_id: this.docId, + Project_id: ctx.projectId, + Doc_id: ctx.docId, }, get(key) { return undefined }, } - this.lines = ['test', '', 'testing'] - this.res = new MockResponse() - this.next = sinon.stub() - this.doc = { name: 'myfile.tex' } + ctx.lines = ['test', '', 'testing'] + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.doc = { name: 'myfile.tex' } }) describe('getDoc', function () { - beforeEach(function () { - this.DocumentUpdaterHandler.promises.getDocument.resolves({ - lines: this.lines, + beforeEach(function (ctx) { + ctx.DocumentUpdaterHandler.promises.getDocument.resolves({ + lines: ctx.lines, }) - this.ProjectLocator.promises.findElement.resolves({ - element: this.doc, + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.doc, }) - this.res = new MockResponse() + ctx.res = new MockResponse() }) - it('should call the document updater handler with the project_id and doc_id', async function () { - await this.controller.getDoc(this.req, this.res, this.next) + it('should call the document updater handler with the project_id and doc_id', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res, ctx.next) expect( - this.DocumentUpdaterHandler.promises.getDocument + ctx.DocumentUpdaterHandler.promises.getDocument ).to.have.been.calledOnceWith( - this.req.params.Project_id, - this.req.params.Doc_id, + ctx.req.params.Project_id, + ctx.req.params.Doc_id, -1 ) }) - it('should return the content', async function () { - await this.controller.getDoc(this.req, this.res) - expect(this.next).to.not.have.been.called - expect(this.res.statusCode).to.equal(200) - expect(this.res.body).to.equal('test\n\ntesting') + it('should return the content', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) + expect(ctx.next).to.not.have.been.called + expect(ctx.res.statusCode).to.equal(200) + expect(ctx.res.body).to.equal('test\n\ntesting') }) - it('should find the doc in the project', async function () { - await this.controller.getDoc(this.req, this.res) + it('should find the doc in the project', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) expect( - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement ).to.have.been.calledOnceWith({ - project_id: this.projectId, - element_id: this.docId, + project_id: ctx.projectId, + element_id: ctx.docId, type: 'doc', }) }) - it('should set the Content-Disposition header', async function () { - await this.controller.getDoc(this.req, this.res) - expect(this.res.setContentDisposition).to.have.been.calledWith( + it('should set the Content-Disposition header', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) + expect(ctx.res.setContentDisposition).to.have.been.calledWith( 'attachment', - { filename: this.doc.name } + { filename: ctx.doc.name } ) }) }) diff --git a/services/web/test/unit/src/Documents/DocumentController.test.mjs b/services/web/test/unit/src/Documents/DocumentController.test.mjs index 813e8d65f3..06c971be91 100644 --- a/services/web/test/unit/src/Documents/DocumentController.test.mjs +++ b/services/web/test/unit/src/Documents/DocumentController.test.mjs @@ -1,5 +1,5 @@ +import { vi } from 'vitest' import sinon from 'sinon' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import Errors from '../../../../app/src/Features/Errors/Errors.js' @@ -8,14 +8,14 @@ const MODULE_PATH = '../../../../app/src/Features/Documents/DocumentController.mjs' describe('DocumentController', function () { - beforeEach(async function () { - this.res = new MockResponse() - this.req = new MockRequest() - this.next = sinon.stub() - this.doc = { _id: 'doc-id-123' } - this.doc_lines = ['one', 'two', 'three'] - this.version = 42 - this.ranges = { + beforeEach(async function (ctx) { + ctx.res = new MockResponse() + ctx.req = new MockRequest() + ctx.next = sinon.stub() + ctx.doc = { _id: 'doc-id-123' } + ctx.doc_lines = ['one', 'two', 'three'] + ctx.version = 42 + ctx.ranges = { comments: [ { id: 'comment1', @@ -35,11 +35,11 @@ describe('DocumentController', function () { }, ], } - this.pathname = '/a/b/c/file.tex' - this.lastUpdatedAt = new Date().getTime() - this.lastUpdatedBy = 'fake-last-updater-id' - this.rev = 5 - this.project = { + ctx.pathname = '/a/b/c/file.tex' + ctx.lastUpdatedAt = new Date().getTime() + ctx.lastUpdatedBy = 'fake-last-updater-id' + ctx.rev = 5 + ctx.project = { _id: 'project-id-123', overleaf: { history: { @@ -48,81 +48,100 @@ describe('DocumentController', function () { }, }, } - this.resolvedThreadIds = [ + ctx.resolvedThreadIds = [ 'comment2', 'comment4', // Comment in project but not in doc ] - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: { findElement: sinon .stub() - .resolves({ element: this.doc, path: { fileSystem: this.pathname } }), + .resolves({ element: ctx.doc, path: { fileSystem: ctx.pathname } }), }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { promises: { getDoc: sinon.stub().resolves({ - lines: this.doc_lines, - rev: this.rev, - version: this.version, - ranges: this.ranges, + lines: ctx.doc_lines, + rev: ctx.rev, + version: ctx.version, + ranges: ctx.ranges, }), }, } - this.ProjectEntityUpdateHandler = { + ctx.ProjectEntityUpdateHandler = { promises: { updateDocLines: sinon.stub().resolves(), }, } - this.ChatApiHandler = { + ctx.ChatApiHandler = { promises: { - getResolvedThreadIds: sinon.stub().resolves(this.resolvedThreadIds), + getResolvedThreadIds: sinon.stub().resolves(ctx.resolvedThreadIds), }, } - this.DocumentController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/Project/ProjectEntityHandler': - this.ProjectEntityHandler, - '../../../../app/src/Features/Project/ProjectEntityUpdateHandler': - this.ProjectEntityUpdateHandler, - '../../../../app/src/Features/Chat/ChatApiHandler': this.ChatApiHandler, - }) + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', + () => ({ + default: ctx.ProjectEntityUpdateHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({ + default: ctx.ChatApiHandler, + })) + + ctx.DocumentController = (await import(MODULE_PATH)).default }) describe('getDocument', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.project._id, - doc_id: this.doc._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.project._id, + doc_id: ctx.doc._id, } }) describe('when project exists with project history enabled', function () { - beforeEach(function (done) { - this.res.callback = err => { - done(err) - } - this.DocumentController.getDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('should return the history id and display setting to the client as JSON', function () { - this.res.type.should.equal('application/json') - JSON.parse(this.res.body).should.deep.equal({ - lines: this.doc_lines, - version: this.version, - ranges: this.ranges, - pathname: this.pathname, - projectHistoryId: this.project.overleaf.history.id, + it('should return the history id and display setting to the client as JSON', function (ctx) { + ctx.res.type.should.equal('application/json') + JSON.parse(ctx.res.body).should.deep.equal({ + lines: ctx.doc_lines, + version: ctx.version, + ranges: ctx.ranges, + pathname: ctx.pathname, + projectHistoryId: ctx.project.overleaf.history.id, projectHistoryType: 'project-history', resolvedCommentIds: ['comment2'], historyRangesSupport: false, @@ -132,75 +151,81 @@ describe('DocumentController', function () { }) describe('when the project does not exist', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.resolves(null) - this.res.callback = err => { - done(err) - } - this.DocumentController.getDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('returns a 404', function () { - this.res.statusCode.should.equal(404) + it('returns a 404', function (ctx) { + ctx.res.statusCode.should.equal(404) }) }) }) describe('setDocument', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.project._id, - doc_id: this.doc._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.project._id, + doc_id: ctx.doc._id, } }) describe('when the document exists', function () { - beforeEach(function (done) { - this.req.body = { - lines: this.doc_lines, - version: this.version, - ranges: this.ranges, - lastUpdatedAt: this.lastUpdatedAt, - lastUpdatedBy: this.lastUpdatedBy, - } - this.res.callback = err => { - done(err) - } - this.DocumentController.setDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + lines: ctx.doc_lines, + version: ctx.version, + ranges: ctx.ranges, + lastUpdatedAt: ctx.lastUpdatedAt, + lastUpdatedBy: ctx.lastUpdatedBy, + } + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('should update the document in Mongo', function () { + it('should update the document in Mongo', function (ctx) { sinon.assert.calledWith( - this.ProjectEntityUpdateHandler.promises.updateDocLines, - this.project._id, - this.doc._id, - this.doc_lines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.ProjectEntityUpdateHandler.promises.updateDocLines, + ctx.project._id, + ctx.doc._id, + ctx.doc_lines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should return a successful response', function () { - this.res.success.should.equal(true) + it('should return a successful response', function (ctx) { + ctx.res.success.should.equal(true) }) }) describe("when the document doesn't exist", function () { - beforeEach(function (done) { - this.ProjectEntityUpdateHandler.promises.updateDocLines.rejects( - new Errors.NotFoundError('document does not exist') - ) - this.req.body = { lines: this.doc_lines } - this.next.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectEntityUpdateHandler.promises.updateDocLines.rejects( + new Errors.NotFoundError('document does not exist') + ) + ctx.req.body = { lines: ctx.doc_lines } + ctx.next.callsFake(() => { + resolve() + }) + ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next) }) - this.DocumentController.setDocument(this.req, this.res, this.next) }) - it('should call next with the NotFoundError', function () { - this.next + it('should call next with the NotFoundError', function (ctx) { + ctx.next .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) .should.equal(true) }) diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index db9cf19df7..1e339097fa 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -1,3 +1,4 @@ +import { vi } from 'vitest' // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -6,136 +7,150 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import sinon from 'sinon' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Downloads/ProjectDownloadsController.mjs' describe('ProjectDownloadsController', function () { - beforeEach(async function () { - this.project_id = 'project-id-123' - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() - this.DocumentUpdaterHandler = sinon.stub() - return (this.ProjectDownloadsController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs': - (this.ProjectZipStreamManager = {}), - '../../../../app/src/Features/Project/ProjectGetter.js': - (this.ProjectGetter = {}), - '@overleaf/metrics': (this.metrics = {}), - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js': - this.DocumentUpdaterHandler, + beforeEach(async function (ctx) { + ctx.project_id = 'project-id-123' + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.DocumentUpdaterHandler = sinon.stub() + + vi.doMock( + '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs', + () => ({ + default: (ctx.ProjectZipStreamManager = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: (ctx.ProjectGetter = {}), })) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.metrics = {}), + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + ctx.ProjectDownloadsController = (await import(modulePath)).default }) describe('downloadProject', function () { - beforeEach(function () { - this.stream = { pipe: sinon.stub() } - this.ProjectZipStreamManager.createZipStreamForProject = sinon + beforeEach(function (ctx) { + ctx.stream = { pipe: sinon.stub() } + ctx.ProjectZipStreamManager.createZipStreamForProject = sinon .stub() - .callsArgWith(1, null, this.stream) - this.req.params = { Project_id: this.project_id } - this.project_name = 'project name with accênts' - this.ProjectGetter.getProject = sinon + .callsArgWith(1, null, ctx.stream) + ctx.req.params = { Project_id: ctx.project_id } + ctx.project_name = 'project name with accênts' + ctx.ProjectGetter.getProject = sinon .stub() - .callsArgWith(2, null, { name: this.project_name }) - this.DocumentUpdaterHandler.flushProjectToMongo = sinon + .callsArgWith(2, null, { name: ctx.project_name }) + ctx.DocumentUpdaterHandler.flushProjectToMongo = sinon .stub() .callsArgWith(1) - this.metrics.inc = sinon.stub() - return this.ProjectDownloadsController.downloadProject( - this.req, - this.res, - this.next + ctx.metrics.inc = sinon.stub() + return ctx.ProjectDownloadsController.downloadProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should create a zip from the project', function () { - return this.ProjectZipStreamManager.createZipStreamForProject - .calledWith(this.project_id) + it('should create a zip from the project', function (ctx) { + return ctx.ProjectZipStreamManager.createZipStreamForProject + .calledWith(ctx.project_id) .should.equal(true) }) - it('should stream the zip to the request', function () { - return this.stream.pipe.calledWith(this.res).should.equal(true) + it('should stream the zip to the request', function (ctx) { + return ctx.stream.pipe.calledWith(ctx.res).should.equal(true) }) - it('should set the correct content type on the request', function () { - return this.res.contentType + it('should set the correct content type on the request', function (ctx) { + return ctx.res.contentType .calledWith('application/zip') .should.equal(true) }) - it('should flush the project to mongo', function () { - return this.DocumentUpdaterHandler.flushProjectToMongo - .calledWith(this.project_id) + it('should flush the project to mongo', function (ctx) { + return ctx.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(ctx.project_id) .should.equal(true) }) - it("should look up the project's name", function () { - return this.ProjectGetter.getProject - .calledWith(this.project_id, { name: true }) + it("should look up the project's name", function (ctx) { + return ctx.ProjectGetter.getProject + .calledWith(ctx.project_id, { name: true }) .should.equal(true) }) - it('should name the downloaded file after the project', function () { - this.res.headers.should.deep.equal({ - 'Content-Disposition': `attachment; filename="${this.project_name}.zip"`, + it('should name the downloaded file after the project', function (ctx) { + ctx.res.headers.should.deep.equal({ + 'Content-Disposition': `attachment; filename="${ctx.project_name}.zip"`, 'Content-Type': 'application/zip', 'X-Content-Type-Options': 'nosniff', }) }) - it('should record the action via Metrics', function () { - return this.metrics.inc.calledWith('zip-downloads').should.equal(true) + it('should record the action via Metrics', function (ctx) { + return ctx.metrics.inc.calledWith('zip-downloads').should.equal(true) }) }) describe('downloadMultipleProjects', function () { - beforeEach(function () { - this.stream = { pipe: sinon.stub() } - this.ProjectZipStreamManager.createZipStreamForMultipleProjects = sinon + beforeEach(function (ctx) { + ctx.stream = { pipe: sinon.stub() } + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects = sinon .stub() - .callsArgWith(1, null, this.stream) - this.project_ids = ['project-1', 'project-2'] - this.req.query = { project_ids: this.project_ids.join(',') } - this.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon + .callsArgWith(1, null, ctx.stream) + ctx.project_ids = ['project-1', 'project-2'] + ctx.req.query = { project_ids: ctx.project_ids.join(',') } + ctx.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon .stub() .callsArgWith(1) - this.metrics.inc = sinon.stub() - return this.ProjectDownloadsController.downloadMultipleProjects( - this.req, - this.res, - this.next + ctx.metrics.inc = sinon.stub() + return ctx.ProjectDownloadsController.downloadMultipleProjects( + ctx.req, + ctx.res, + ctx.next ) }) - it('should create a zip from the project', function () { - return this.ProjectZipStreamManager.createZipStreamForMultipleProjects - .calledWith(this.project_ids) + it('should create a zip from the project', function (ctx) { + return ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects + .calledWith(ctx.project_ids) .should.equal(true) }) - it('should stream the zip to the request', function () { - return this.stream.pipe.calledWith(this.res).should.equal(true) + it('should stream the zip to the request', function (ctx) { + return ctx.stream.pipe.calledWith(ctx.res).should.equal(true) }) - it('should set the correct content type on the request', function () { - return this.res.contentType + it('should set the correct content type on the request', function (ctx) { + return ctx.res.contentType .calledWith('application/zip') .should.equal(true) }) - it('should flush the projects to mongo', function () { - return this.DocumentUpdaterHandler.flushMultipleProjectsToMongo - .calledWith(this.project_ids) + it('should flush the projects to mongo', function (ctx) { + return ctx.DocumentUpdaterHandler.flushMultipleProjectsToMongo + .calledWith(ctx.project_ids) .should.equal(true) }) - it('should name the downloaded file after the project', function () { - this.res.headers.should.deep.equal({ + it('should name the downloaded file after the project', function (ctx) { + ctx.res.headers.should.deep.equal({ 'Content-Disposition': 'attachment; filename="Overleaf Projects (2 items).zip"', 'Content-Type': 'application/zip', @@ -143,8 +158,8 @@ describe('ProjectDownloadsController', function () { }) }) - it('should record the action via Metrics', function () { - return this.metrics.inc + it('should record the action via Metrics', function (ctx) { + return ctx.metrics.inc .calledWith('zip-downloads-multiple') .should.equal(true) }) diff --git a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs index f86b99bd96..df7486e11d 100644 --- a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs @@ -1,3 +1,4 @@ +import { vi } from 'vitest' // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -9,120 +10,143 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import sinon from 'sinon' -import esmock from 'esmock' import { EventEmitter } from 'events' const modulePath = '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs' describe('ProjectZipStreamManager', function () { - beforeEach(async function () { - this.project_id = 'project-id-123' - this.callback = sinon.stub() - this.archive = { + beforeEach(async function (ctx) { + ctx.project_id = 'project-id-123' + ctx.callback = sinon.stub() + ctx.archive = { on() {}, append: sinon.stub(), } - this.logger = { + ctx.logger = { error: sinon.stub(), info: sinon.stub(), debug: sinon.stub(), } - return (this.ProjectZipStreamManager = await esmock.strict(modulePath, { - archiver: (this.archiver = sinon.stub().returns(this.archive)), - '@overleaf/logger': this.logger, - '../../../../app/src/Features/Project/ProjectEntityHandler': - (this.ProjectEntityHandler = {}), - '../../../../app/src/Features/History/HistoryManager.js': - (this.HistoryManager = {}), - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = {}), - '../../../../app/src/Features/FileStore/FileStoreHandler': - (this.FileStoreHandler = {}), - '../../../../app/src/infrastructure/Features': (this.Features = { + vi.doMock('archiver', () => ({ + default: (ctx.archiver = sinon.stub().returns(ctx.archive)), + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: (ctx.ProjectEntityHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/History/HistoryManager.js', () => ({ + default: (ctx.HistoryManager = {}), + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = {}), + })) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: (ctx.FileStoreHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: (ctx.Features = { hasFeature: sinon .stub() .withArgs('project-history-blobs') .returns(true), }), })) + + ctx.ProjectZipStreamManager = (await import(modulePath)).default }) describe('createZipStreamForMultipleProjects', function () { describe('successfully', function () { - beforeEach(function (done) { - this.project_ids = ['project-1', 'project-2'] - this.zip_streams = { - 'project-1': new EventEmitter(), - 'project-2': new EventEmitter(), - } - - this.project_names = { - 'project-1': 'Project One Name', - 'project-2': 'Project Two Name', - } - - this.ProjectZipStreamManager.createZipStreamForProject = ( - projectId, - callback - ) => { - callback(null, this.zip_streams[projectId]) - setTimeout(() => { - return this.zip_streams[projectId].emit('end') - }) - return 0 - } - sinon.spy(this.ProjectZipStreamManager, 'createZipStreamForProject') - - this.ProjectGetter.getProject = (projectId, fields, callback) => { - return callback(null, { name: this.project_names[projectId] }) - } - sinon.spy(this.ProjectGetter, 'getProject') - - this.ProjectZipStreamManager.createZipStreamForMultipleProjects( - this.project_ids, - (...args) => { - return this.callback(...Array.from(args || [])) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project_ids = ['project-1', 'project-2'] + ctx.zip_streams = { + 'project-1': new EventEmitter(), + 'project-2': new EventEmitter(), } - ) - return (this.archive.finalize = () => done()) + ctx.project_names = { + 'project-1': 'Project One Name', + 'project-2': 'Project Two Name', + } + + ctx.ProjectZipStreamManager.createZipStreamForProject = ( + projectId, + callback + ) => { + callback(null, ctx.zip_streams[projectId]) + setTimeout(() => { + return ctx.zip_streams[projectId].emit('end') + }) + return 0 + } + sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject') + + ctx.ProjectGetter.getProject = (projectId, fields, callback) => { + return callback(null, { name: ctx.project_names[projectId] }) + } + sinon.spy(ctx.ProjectGetter, 'getProject') + + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects( + ctx.project_ids, + (...args) => { + return ctx.callback(...Array.from(args || [])) + } + ) + + return (ctx.archive.finalize = () => resolve()) + }) }) - it('should create a zip archive', function () { - return this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + return ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - return this.callback - .calledBefore(this.ProjectZipStreamManager.createZipStreamForProject) + return ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject) .should.equal(true) }) - it('should get a zip stream for all of the projects', function () { - return Array.from(this.project_ids).map(projectId => - this.ProjectZipStreamManager.createZipStreamForProject + it('should get a zip stream for all of the projects', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith(projectId) .should.equal(true) ) }) - it('should get the names of each project', function () { - return Array.from(this.project_ids).map(projectId => - this.ProjectGetter.getProject + it('should get the names of each project', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.ProjectGetter.getProject .calledWith(projectId, { name: true }) .should.equal(true) ) }) - it('should add all of the projects to the zip', function () { - return Array.from(this.project_ids).map(projectId => - this.archive.append - .calledWith(this.zip_streams[projectId], { - name: this.project_names[projectId] + '.zip', + it('should add all of the projects to the zip', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.archive.append + .calledWith(ctx.zip_streams[projectId], { + name: ctx.project_names[projectId] + '.zip', }) .should.equal(true) ) @@ -130,75 +154,77 @@ describe('ProjectZipStreamManager', function () { }) describe('with a project not existing', function () { - beforeEach(function (done) { - this.project_ids = ['project-1', 'wrong-id'] - this.project_names = { - 'project-1': 'Project One Name', - } - this.zip_streams = { - 'project-1': new EventEmitter(), - } + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project_ids = ['project-1', 'wrong-id'] + ctx.project_names = { + 'project-1': 'Project One Name', + } + ctx.zip_streams = { + 'project-1': new EventEmitter(), + } - this.ProjectZipStreamManager.createZipStreamForProject = ( - projectId, - callback - ) => { - callback(null, this.zip_streams[projectId]) - setTimeout(() => { - this.zip_streams[projectId].emit('end') - }) - } - sinon.spy(this.ProjectZipStreamManager, 'createZipStreamForProject') + ctx.ProjectZipStreamManager.createZipStreamForProject = ( + projectId, + callback + ) => { + callback(null, ctx.zip_streams[projectId]) + setTimeout(() => { + ctx.zip_streams[projectId].emit('end') + }) + } + sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject') - this.ProjectGetter.getProject = (projectId, fields, callback) => { - const name = this.project_names[projectId] - callback(null, name ? { name } : undefined) - } - sinon.spy(this.ProjectGetter, 'getProject') + ctx.ProjectGetter.getProject = (projectId, fields, callback) => { + const name = ctx.project_names[projectId] + callback(null, name ? { name } : undefined) + } + sinon.spy(ctx.ProjectGetter, 'getProject') - this.ProjectZipStreamManager.createZipStreamForMultipleProjects( - this.project_ids, - this.callback - ) + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects( + ctx.project_ids, + ctx.callback + ) - this.archive.finalize = () => done() + ctx.archive.finalize = () => resolve() + }) }) - it('should create a zip archive', function () { - this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - this.callback - .calledBefore(this.ProjectZipStreamManager.createZipStreamForProject) + ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject) .should.equal(true) }) - it('should get the names of each project', function () { - this.project_ids.map(projectId => - this.ProjectGetter.getProject + it('should get the names of each project', function (ctx) { + ctx.project_ids.map(projectId => + ctx.ProjectGetter.getProject .calledWith(projectId, { name: true }) .should.equal(true) ) }) - it('should get a zip stream only for the existing project', function () { - this.ProjectZipStreamManager.createZipStreamForProject + it('should get a zip stream only for the existing project', function (ctx) { + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith('project-1') .should.equal(true) - this.ProjectZipStreamManager.createZipStreamForProject + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith('wrong-id') .should.equal(false) }) - it('should only add the existing project to the zip', function () { - sinon.assert.calledOnce(this.archive.append) - this.archive.append - .calledWith(this.zip_streams['project-1'], { - name: this.project_names['project-1'] + '.zip', + it('should only add the existing project to the zip', function (ctx) { + sinon.assert.calledOnce(ctx.archive.append) + ctx.archive.append + .calledWith(ctx.zip_streams['project-1'], { + name: ctx.project_names['project-1'] + '.zip', }) .should.equal(true) }) @@ -207,160 +233,162 @@ describe('ProjectZipStreamManager', function () { describe('createZipStreamForProject', function () { describe('successfully', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArg(2) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArg(2) - this.archive.finalize = sinon.stub() - return this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + return ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should create a zip archive', function () { - return this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + return ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - this.callback - .calledBefore(this.ProjectZipStreamManager.addAllDocsToArchive) + ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.addAllDocsToArchive) .should.equal(true) - return this.callback - .calledBefore(this.ProjectZipStreamManager.addAllFilesToArchive) + return ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.addAllFilesToArchive) .should.equal(true) }) - it('should add all of the project docs to the zip', function () { - return this.ProjectZipStreamManager.addAllDocsToArchive - .calledWith(this.project_id, this.archive) + it('should add all of the project docs to the zip', function (ctx) { + return ctx.ProjectZipStreamManager.addAllDocsToArchive + .calledWith(ctx.project_id, ctx.archive) .should.equal(true) }) - it('should add all of the project files to the zip', function () { - return this.ProjectZipStreamManager.addAllFilesToArchive - .calledWith(this.project_id, this.archive) + it('should add all of the project files to the zip', function (ctx) { + return ctx.ProjectZipStreamManager.addAllFilesToArchive + .calledWith(ctx.project_id, ctx.archive) .should.equal(true) }) - it('should finalise the stream', function () { - return this.archive.finalize.called.should.equal(true) + it('should finalise the stream', function (ctx) { + return ctx.archive.finalize.called.should.equal(true) }) }) describe('with an error adding docs', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArgWith(2, new Error('something went wrong')) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArg(2) - this.archive.finalize = sinon.stub() - this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should log out an error', function () { - return this.logger.error + it('should log out an error', function (ctx) { + return ctx.logger.error .calledWith(sinon.match.any, 'error adding docs to zip stream') .should.equal(true) }) - it('should continue with the process', function () { - this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + it('should continue with the process', function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( true ) - this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( true ) - return this.archive.finalize.called.should.equal(true) + return ctx.archive.finalize.called.should.equal(true) }) }) describe('with an error adding files', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArg(2) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArgWith(2, new Error('something went wrong')) - this.archive.finalize = sinon.stub() - return this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + return ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should log out an error', function () { - return this.logger.error + it('should log out an error', function (ctx) { + return ctx.logger.error .calledWith(sinon.match.any, 'error adding files to zip stream') .should.equal(true) }) - it('should continue with the process', function () { - this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + it('should continue with the process', function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( true ) - this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( true ) - return this.archive.finalize.called.should.equal(true) + return ctx.archive.finalize.called.should.equal(true) }) }) }) describe('addAllDocsToArchive', function () { - beforeEach(function (done) { - this.docs = { - '/main.tex': { - lines: [ - '\\documentclass{article}', - '\\begin{document}', - 'Hello world', - '\\end{document}', - ], - }, - '/chapters/chapter1.tex': { - lines: ['chapter1', 'content'], - }, - } - this.ProjectEntityHandler.getAllDocs = sinon - .stub() - .callsArgWith(1, null, this.docs) - return this.ProjectZipStreamManager.addAllDocsToArchive( - this.project_id, - this.archive, - error => { - this.callback(error) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.docs = { + '/main.tex': { + lines: [ + '\\documentclass{article}', + '\\begin{document}', + 'Hello world', + '\\end{document}', + ], + }, + '/chapters/chapter1.tex': { + lines: ['chapter1', 'content'], + }, } - ) + ctx.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, ctx.docs) + return ctx.ProjectZipStreamManager.addAllDocsToArchive( + ctx.project_id, + ctx.archive, + error => { + ctx.callback(error) + return resolve() + } + ) + }) }) - it('should get the docs for the project', function () { - return this.ProjectEntityHandler.getAllDocs - .calledWith(this.project_id) + it('should get the docs for the project', function (ctx) { + return ctx.ProjectEntityHandler.getAllDocs + .calledWith(ctx.project_id) .should.equal(true) }) - it('should add each doc to the archive', function () { + it('should add each doc to the archive', function (ctx) { return (() => { const result = [] - for (let path in this.docs) { - const doc = this.docs[path] + for (let path in ctx.docs) { + const doc = ctx.docs[path] path = path.slice(1) // remove "/" result.push( - this.archive.append + ctx.archive.append .calledWith(doc.lines.join('\n'), { name: path }) .should.equal(true) ) @@ -371,8 +399,8 @@ describe('ProjectZipStreamManager', function () { }) describe('addAllFilesToArchive', function () { - beforeEach(function () { - this.files = { + beforeEach(function (ctx) { + ctx.files = { '/image.png': { _id: 'file-id-1', hash: 'abc', @@ -382,93 +410,91 @@ describe('ProjectZipStreamManager', function () { hash: 'def', }, } - this.streams = { + ctx.streams = { 'file-id-1': new EventEmitter(), 'file-id-2': new EventEmitter(), } - this.ProjectEntityHandler.getAllFiles = sinon + ctx.ProjectEntityHandler.getAllFiles = sinon .stub() - .callsArgWith(1, null, this.files) + .callsArgWith(1, null, ctx.files) }) describe('with project-history-blobs feature enabled', function () { - beforeEach(function () { - this.HistoryManager.requestBlobWithFallback = ( + beforeEach(function (ctx) { + ctx.HistoryManager.requestBlobWithFallback = ( projectId, hash, fileId, callback ) => { - return callback(null, { stream: this.streams[fileId] }) + return callback(null, { stream: ctx.streams[fileId] }) } - sinon.spy(this.HistoryManager, 'requestBlobWithFallback') - this.ProjectZipStreamManager.addAllFilesToArchive( - this.project_id, - this.archive, - this.callback + sinon.spy(ctx.HistoryManager, 'requestBlobWithFallback') + ctx.ProjectZipStreamManager.addAllFilesToArchive( + ctx.project_id, + ctx.archive, + ctx.callback ) - for (const path in this.streams) { - const stream = this.streams[path] + for (const path in ctx.streams) { + const stream = ctx.streams[path] stream.emit('end') } }) - it('should get the files for the project', function () { - return this.ProjectEntityHandler.getAllFiles - .calledWith(this.project_id) + it('should get the files for the project', function (ctx) { + return ctx.ProjectEntityHandler.getAllFiles + .calledWith(ctx.project_id) .should.equal(true) }) - it('should get a stream for each file', function () { - for (const path in this.files) { - const file = this.files[path] + it('should get a stream for each file', function (ctx) { + for (const path in ctx.files) { + const file = ctx.files[path] - this.HistoryManager.requestBlobWithFallback - .calledWith(this.project_id, file.hash, file._id) + ctx.HistoryManager.requestBlobWithFallback + .calledWith(ctx.project_id, file.hash, file._id) .should.equal(true) } }) - it('should add each file to the archive', function () { - for (let path in this.files) { - const file = this.files[path] + it('should add each file to the archive', function (ctx) { + for (let path in ctx.files) { + const file = ctx.files[path] path = path.slice(1) // remove "/" - this.archive.append - .calledWith(this.streams[file._id], { name: path }) + ctx.archive.append + .calledWith(ctx.streams[file._id], { name: path }) .should.equal(true) } }) }) describe('with project-history-blobs feature disabled', function () { - beforeEach(function () { - this.FileStoreHandler.getFileStream = ( + beforeEach(function (ctx) { + ctx.FileStoreHandler.getFileStream = ( projectId, fileId, query, callback - ) => callback(null, this.streams[fileId]) + ) => callback(null, ctx.streams[fileId]) - sinon.spy(this.FileStoreHandler, 'getFileStream') - this.Features.hasFeature - .withArgs('project-history-blobs') - .returns(false) - this.ProjectZipStreamManager.addAllFilesToArchive( - this.project_id, - this.archive, - this.callback + sinon.spy(ctx.FileStoreHandler, 'getFileStream') + ctx.Features.hasFeature.withArgs('project-history-blobs').returns(false) + ctx.ProjectZipStreamManager.addAllFilesToArchive( + ctx.project_id, + ctx.archive, + ctx.callback ) - for (const path in this.streams) { - const stream = this.streams[path] + for (const path in ctx.streams) { + const stream = ctx.streams[path] stream.emit('end') } }) - it('should get a stream for each file', function () { - for (const path in this.files) { - const file = this.files[path] + it('should get a stream for each file', function (ctx) { + for (const path in ctx.files) { + const file = ctx.files[path] - this.FileStoreHandler.getFileStream - .calledWith(this.project_id, file._id) + ctx.FileStoreHandler.getFileStream + .calledWith(ctx.project_id, file._id) .should.equal(true) } }) diff --git a/services/web/test/unit/src/Exports/ExportsController.test.mjs b/services/web/test/unit/src/Exports/ExportsController.test.mjs index 65e6e16d27..af9c1483fb 100644 --- a/services/web/test/unit/src/Exports/ExportsController.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsController.test.mjs @@ -1,11 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' const modulePath = new URL( @@ -25,9 +18,9 @@ describe('ExportsController', function () { const license = 'other' const showSource = true - beforeEach(async function () { - this.handler = { getUserNotifications: sinon.stub().callsArgWith(1) } - this.req = { + beforeEach(async function (ctx) { + ctx.handler = { getUserNotifications: sinon.stub().callsArgWith(1) } + ctx.req = { params: { project_id: projectId, brand_variation_id: brandVariationId, @@ -45,152 +38,179 @@ describe('ExportsController', function () { translate() {}, }, } - this.res = { + ctx.res = { json: sinon.stub(), status: sinon.stub(), } - this.res.status.returns(this.res) - this.next = sinon.stub() - this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.req.session.user._id), + ctx.res.status.returns(ctx.res) + ctx.next = sinon.stub() + ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.req.session.user._id), } - return (this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Exports/ExportsHandler.mjs': this.handler, - '../../../../app/src/Features/Authentication/AuthenticationController.js': - this.AuthenticationController, - })) + + vi.doMock( + '../../../../app/src/Features/Exports/ExportsHandler.mjs', + () => ({ + default: ctx.handler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController.js', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + ctx.controller = (await import(modulePath)).default }) describe('without gallery fields', function () { - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ export_v1_id: 897, message: undefined }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: undefined, + }) + return resolve() + }, + }) }) }) }) describe('with a message from v1', function () { - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon.stub().yields(null, { - iAmAnExport: true, - v1_id: 897, - message: 'RESUBMISSION', - }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ - export_v1_id: 897, - message: 'RESUBMISSION', - }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon.stub().yields(null, { + iAmAnExport: true, + v1_id: 897, + message: 'RESUBMISSION', + }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: 'RESUBMISSION', + }) + return resolve() + }, + }) }) }) }) describe('with gallery fields', function () { - beforeEach(function () { - this.req.body.title = title - this.req.body.description = description - this.req.body.author = author - this.req.body.license = license - return (this.req.body.showSource = true) + beforeEach(function (ctx) { + ctx.req.body.title = title + ctx.req.body.description = description + ctx.req.body.author = author + ctx.req.body.license = license + return (ctx.req.body.showSource = true) }) - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - title, - description, - author, - license, - show_source: showSource, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ export_v1_id: 897, message: undefined }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + title, + description, + author, + license, + show_source: showSource, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: undefined, + }) + return resolve() + }, + }) }) }) }) describe('with an error return from v1 to forward to the publish modal', function () { - it('should forward the response onward', function (done) { - this.error_json = { status: 422, message: 'nope' } - this.handler.exportProject = sinon - .stub() - .yields({ forwardResponse: this.error_json }) - this.controller.exportProject(this.req, this.res, this.next) - expect(this.res.json.args[0][0]).to.deep.equal(this.error_json) - expect(this.res.status.args[0][0]).to.equal(this.error_json.status) - return done() + it('should forward the response onward', function (ctx) { + return new Promise(resolve => { + ctx.error_json = { status: 422, message: 'nope' } + ctx.handler.exportProject = sinon + .stub() + .yields({ forwardResponse: ctx.error_json }) + ctx.controller.exportProject(ctx.req, ctx.res, ctx.next) + expect(ctx.res.json.args[0][0]).to.deep.equal(ctx.error_json) + expect(ctx.res.status.args[0][0]).to.equal(ctx.error_json.status) + return resolve() + }) }) }) - it('should ask the handler to return the status of an export', function (done) { - this.handler.fetchExport = sinon.stub().yields( - null, - `{ -"id":897, -"status_summary":"completed", -"status_detail":"all done", -"partner_submission_id":"abc123", -"v2_user_email":"la@tex.com", -"v2_user_first_name":"Arthur", -"v2_user_last_name":"Author", -"title":"my project", -"token":"token" -}` - ) + it('should ask the handler to return the status of an export', function (ctx) { + return new Promise(resolve => { + ctx.handler.fetchExport = sinon.stub().yields( + null, + `{ + "id":897, + "status_summary":"completed", + "status_detail":"all done", + "partner_submission_id":"abc123", + "v2_user_email":"la@tex.com", + "v2_user_first_name":"Arthur", + "v2_user_last_name":"Author", + "title":"my project", + "token":"token" + }` + ) - this.req.params = { project_id: projectId, export_id: 897 } - return this.controller.exportStatus(this.req, { - json: body => { - expect(body).to.deep.equal({ - export_json: { - status_summary: 'completed', - status_detail: 'all done', - partner_submission_id: 'abc123', - v2_user_email: 'la@tex.com', - v2_user_first_name: 'Arthur', - v2_user_last_name: 'Author', - title: 'my project', - token: 'token', - }, - }) - return done() - }, + ctx.req.params = { project_id: projectId, export_id: 897 } + return ctx.controller.exportStatus(ctx.req, { + json: body => { + expect(body).to.deep.equal({ + export_json: { + status_summary: 'completed', + status_detail: 'all done', + partner_submission_id: 'abc123', + v2_user_email: 'la@tex.com', + v2_user_first_name: 'Arthur', + v2_user_last_name: 'Author', + title: 'my project', + token: 'token', + }, + }) + return resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs index 1a7f985250..0eb8a98e26 100644 --- a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs @@ -1,697 +1,736 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ +import { vi } from 'vitest' import sinon from 'sinon' -import esmock from 'esmock' import { expect } from 'chai' const modulePath = '../../../../app/src/Features/Exports/ExportsHandler.mjs' describe('ExportsHandler', function () { - beforeEach(async function () { - this.stubRequest = {} - this.request = { + beforeEach(async function (ctx) { + ctx.stubRequest = {} + ctx.request = { defaults: () => { - return this.stubRequest + return ctx.stubRequest }, } - this.ExportsHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = {}), - '../../../../app/src/Features/Project/ProjectHistoryHandler': - (this.ProjectHistoryHandler = {}), - '../../../../app/src/Features/Project/ProjectLocator': - (this.ProjectLocator = {}), - '../../../../app/src/Features/Project/ProjectRootDocManager': - (this.ProjectRootDocManager = {}), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = {}), - '@overleaf/settings': (this.settings = {}), - request: this.request, - }) - this.project_id = 'project-id-123' - this.project_history_id = 987 - this.user_id = 'user-id-456' - this.brand_variation_id = 789 - this.title = 'title' - this.description = 'description' - this.author = 'author' - this.license = 'other' - this.show_source = true - this.export_params = { - project_id: this.project_id, - brand_variation_id: this.brand_variation_id, - user_id: this.user_id, - title: this.title, - description: this.description, - author: this.author, - license: this.license, - show_source: this.show_source, + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectHistoryHandler', + () => ({ + default: (ctx.ProjectHistoryHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: (ctx.ProjectLocator = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ + default: (ctx.ProjectRootDocManager = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = {}), + })) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = {}), + })) + + vi.doMock('request', () => ({ + default: ctx.request, + })) + + ctx.ExportsHandler = (await import(modulePath)).default + ctx.project_id = 'project-id-123' + ctx.project_history_id = 987 + ctx.user_id = 'user-id-456' + ctx.brand_variation_id = 789 + ctx.title = 'title' + ctx.description = 'description' + ctx.author = 'author' + ctx.license = 'other' + ctx.show_source = true + ctx.export_params = { + project_id: ctx.project_id, + brand_variation_id: ctx.brand_variation_id, + user_id: ctx.user_id, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + show_source: ctx.show_source, } - return (this.callback = sinon.stub()) + ctx.callback = sinon.stub() }) describe('exportProject', function () { - beforeEach(function () { - this.export_data = { iAmAnExport: true } - this.response_body = { iAmAResponseBody: true } - this.ExportsHandler._buildExport = sinon + beforeEach(function (ctx) { + ctx.export_data = { iAmAnExport: true } + ctx.response_body = { iAmAResponseBody: true } + ctx.ExportsHandler._buildExport = sinon .stub() - .yields(null, this.export_data) - return (this.ExportsHandler._requestExport = sinon + .yields(null, ctx.export_data) + ctx.ExportsHandler._requestExport = sinon .stub() - .yields(null, this.response_body)) + .yields(null, ctx.response_body) }) describe('when all goes well', function () { - beforeEach(function (done) { - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should build the export', function () { - return this.ExportsHandler._buildExport - .calledWith(this.export_params) + it('should build the export', function (ctx) { + ctx.ExportsHandler._buildExport + .calledWith(ctx.export_params) .should.equal(true) }) - it('should request the export', function () { - return this.ExportsHandler._requestExport - .calledWith(this.export_data) + it('should request the export', function (ctx) { + ctx.ExportsHandler._requestExport + .calledWith(ctx.export_data) .should.equal(true) }) - it('should return the export', function () { - return this.callback - .calledWith(null, this.export_data) - .should.equal(true) + it('should return the export', function (ctx) { + ctx.callback.calledWith(null, ctx.export_data).should.equal(true) }) }) describe("when request can't be built", function () { - beforeEach(function (done) { - this.ExportsHandler._buildExport = sinon - .stub() - .yields(new Error('cannot export project without root doc')) - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._buildExport = sinon + .stub() + .yields(new Error('cannot export project without root doc')) + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when export request returns an error to forward to the user', function () { - beforeEach(function (done) { - this.error_json = { status: 422, message: 'nope' } - this.ExportsHandler._requestExport = sinon - .stub() - .yields(null, { forwardResponse: this.error_json }) - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.error_json = { status: 422, message: 'nope' } + ctx.ExportsHandler._requestExport = sinon + .stub() + .yields(null, { forwardResponse: ctx.error_json }) + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return success and the response to forward', function () { - ;(this.callback.args[0][0] instanceof Error).should.equal(false) - return this.callback.calledWith(null, { - forwardResponse: this.error_json, + it('should return success and the response to forward', function (ctx) { + expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) + ctx.callback.calledWith(null, { + forwardResponse: ctx.error_json, }) }) }) }) describe('_buildExport', function () { - beforeEach(function (done) { - this.project = { - id: this.project_id, - rootDoc_id: 'doc1_id', - compiler: 'pdflatex', - imageName: 'mock-image-name', - overleaf: { - id: this.project_history_id, // for projects imported from v1 - history: { - id: this.project_history_id, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project = { + id: ctx.project_id, + rootDoc_id: 'doc1_id', + compiler: 'pdflatex', + imageName: 'mock-image-name', + overleaf: { + id: ctx.project_history_id, // for projects imported from v1 + history: { + id: ctx.project_history_id, + }, }, - }, - } - this.user = { - id: this.user_id, - first_name: 'Arthur', - last_name: 'Author', - email: 'arthur.author@arthurauthoring.org', - overleaf: { - id: 876, - }, - } - this.rootDocPath = 'main.tex' - this.historyVersion = 777 - this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) - this.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon - .stub() - .yields(null) - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'main.tex' }]) - this.ProjectRootDocManager.ensureRootDocumentIsValid = sinon - .stub() - .callsArgWith(1, null) - this.UserGetter.getUser = sinon.stub().yields(null, this.user) - this.ExportsHandler._requestVersion = sinon - .stub() - .yields(null, this.historyVersion) - return done() + } + ctx.user = { + id: ctx.user_id, + first_name: 'Arthur', + last_name: 'Author', + email: 'arthur.author@arthurauthoring.org', + overleaf: { + id: 876, + }, + } + ctx.rootDocPath = 'main.tex' + ctx.historyVersion = 777 + ctx.ProjectGetter.getProject = sinon.stub().yields(null, ctx.project) + ctx.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon + .stub() + .yields(null) + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'main.tex' }]) + ctx.ProjectRootDocManager.ensureRootDocumentIsValid = sinon + .stub() + .callsArgWith(1, null) + ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user) + ctx.ExportsHandler._requestVersion = sinon + .stub() + .yields(null, ctx.historyVersion) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should ensure the project has history', function () { - return this.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( + it('should ensure the project has history', function (ctx) { + ctx.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( true ) }) - it('should request the project history version', function () { - return this.ExportsHandler._requestVersion.called.should.equal(true) + it('should request the project history version', function (ctx) { + ctx.ExportsHandler._requestVersion.called.should.equal(true) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, - rootDocPath: this.rootDocPath, - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + id: ctx.project_id, + rootDocPath: ctx.rootDocPath, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when we send replacement user first and last name', function () { - beforeEach(function (done) { - this.custom_first_name = 'FIRST' - this.custom_last_name = 'LAST' - this.export_params.first_name = this.custom_first_name - this.export_params.last_name = this.custom_last_name - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.custom_first_name = 'FIRST' + ctx.custom_last_name = 'LAST' + ctx.export_params.first_name = ctx.custom_first_name + ctx.export_params.last_name = ctx.custom_last_name + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should send the data from the user input', function () { + it('should send the data from the user input', function (ctx) { const expectedExportData = { project: { - id: this.project_id, - rootDocPath: this.rootDocPath, - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + id: ctx.project_id, + rootDocPath: ctx.rootDocPath, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.custom_first_name, - lastName: this.custom_last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.custom_first_name, + lastName: ctx.custom_last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when project is not found', function () { - beforeEach(function (done) { - this.ProjectGetter.getProject = sinon - .stub() - .yields(new Error('project not found')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.getProject = sinon + .stub() + .yields(new Error('project not found')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when project has no root doc', function () { describe('when a root doc can be set automatically', function () { - beforeEach(function (done) { - this.project.rootDoc_id = null - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.rootDoc_id = null + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should set a root doc', function () { - return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + it('should set a root doc', function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( true ) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, + id: ctx.project_id, rootDocPath: 'other.tex', - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) }) describe('when project has an invalid root doc', function () { describe('when a new root doc can be set automatically', function () { - beforeEach(function (done) { - this.fakeDoc_id = '1a2b3c4d5e6f' - this.project.rootDoc_id = this.fakeDoc_id - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.fakeDoc_id = '1a2b3c4d5e6f' + ctx.project.rootDoc_id = ctx.fakeDoc_id + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should set a valid root doc', function () { - return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + it('should set a valid root doc', function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( true ) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, + id: ctx.project_id, rootDocPath: 'other.tex', - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when no root doc can be identified', function () { - beforeEach(function (done) { - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, null]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, null]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) }) describe('when user is not found', function () { - beforeEach(function (done) { - this.UserGetter.getUser = sinon - .stub() - .yields(new Error('user not found')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.getUser = sinon + .stub() + .yields(new Error('user not found')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when project history request fails', function () { - beforeEach(function (done) { - this.ExportsHandler._requestVersion = sinon - .stub() - .yields(new Error('project history call failed')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._requestVersion = sinon + .stub() + .yields(new Error('project history call failed')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) }) describe('_requestExport', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_data = { iAmAnExport: true } - this.export_id = 4096 - this.stubPost = sinon - .stub() - .yields(null, { statusCode: 200 }, { exportId: this.export_id }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_data = { iAmAnExport: true } + ctx.export_id = 4096 + ctx.stubPost = sinon + .stub() + .yields(null, { statusCode: 200 }, { exportId: ctx.export_id }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.post = this.stubPost - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.post = ctx.stubPost + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should issue the request', function () { - return expect(this.stubPost.getCall(0).args[0]).to.deep.equal({ - url: this.settings.apis.v1.url + '/api/v1/overleaf/exports', + it('should issue the request', function (ctx) { + expect(ctx.stubPost.getCall(0).args[0]).to.deep.equal({ + url: ctx.settings.apis.v1.url + '/api/v1/overleaf/exports', auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, - json: this.export_data, + json: ctx.export_data, timeout: 15000, }) }) - it('should return the body with v1 export id', function () { - return this.callback - .calledWith(null, { exportId: this.export_id }) + it('should return the body with v1 export id', function (ctx) { + ctx.callback + .calledWith(null, { exportId: ctx.export_id }) .should.equal(true) }) }) describe('when the request fails', function () { - beforeEach(function (done) { - this.stubRequest.post = sinon - .stub() - .yields(new Error('export request failed')) - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.post = sinon + .stub() + .yields(new Error('export request failed')) + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when the request returns an error response to forward', function () { - beforeEach(function (done) { - this.error_code = 422 - this.error_json = { status: this.error_code, message: 'nope' } - this.stubRequest.post = sinon + beforeEach(function (ctx) { + ctx.error_code = 422 + ctx.error_json = { status: ctx.error_code, message: 'nope' } + ctx.stubRequest.post = sinon .stub() - .yields(null, { statusCode: this.error_code }, this.error_json) - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + .yields(null, { statusCode: ctx.error_code }, ctx.error_json) + return new Promise(resolve => { + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should return success and the response to forward', function () { - ;(this.callback.args[0][0] instanceof Error).should.equal(false) - return this.callback.calledWith(null, { - forwardResponse: this.error_json, + it('should return success and the response to forward', function (ctx) { + expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) + ctx.callback.calledWith(null, { + forwardResponse: ctx.error_json, }) }) }) }) describe('fetchExport', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_id = 897 - this.body = '{"id":897, "status_summary":"completed"}' - this.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: this.body }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = '{"id":897, "status_summary":"completed"}' + ctx.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: ctx.body }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.get = this.stubGet - return this.ExportsHandler.fetchExport( - this.export_id, - (error, body) => { - this.callback(error, body) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.get = ctx.stubGet + ctx.ExportsHandler.fetchExport(ctx.export_id, (error, body) => { + ctx.callback(error, body) + resolve() + }) + }) }) - it('should issue the request', function () { - return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + it('should issue the request', function (ctx) { + expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ url: - this.settings.apis.v1.url + + ctx.settings.apis.v1.url + '/api/v1/overleaf/exports/' + - this.export_id, + ctx.export_id, auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, timeout: 15000, }) }) - it('should return the v1 export id', function () { - return this.callback - .calledWith(null, { body: this.body }) - .should.equal(true) + it('should return the v1 export id', function (ctx) { + ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) }) }) }) describe('fetchDownload', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_id = 897 - this.body = - 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' - this.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: this.body }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = + 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' + ctx.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: ctx.body }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.get = this.stubGet - return this.ExportsHandler.fetchDownload( - this.export_id, - 'zip', - (error, body) => { - this.callback(error, body) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.get = ctx.stubGet + ctx.ExportsHandler.fetchDownload( + ctx.export_id, + 'zip', + (error, body) => { + ctx.callback(error, body) + resolve() + } + ) + }) }) - it('should issue the request', function () { - return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + it('should issue the request', function (ctx) { + expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ url: - this.settings.apis.v1.url + + ctx.settings.apis.v1.url + '/api/v1/overleaf/exports/' + - this.export_id + + ctx.export_id + '/zip_url', auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, timeout: 15000, }) }) - it('should return the v1 export id', function () { - return this.callback - .calledWith(null, { body: this.body }) - .should.equal(true) + it('should return the v1 export id', function (ctx) { + ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) }) }) }) diff --git a/services/web/test/unit/src/FileStore/FileStoreController.test.mjs b/services/web/test/unit/src/FileStore/FileStoreController.test.mjs index 2758068ce3..5c46e516a0 100644 --- a/services/web/test/unit/src/FileStore/FileStoreController.test.mjs +++ b/services/web/test/unit/src/FileStore/FileStoreController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockResponse from '../helpers/MockResponse.js' @@ -12,34 +12,51 @@ const expectedFileHeaders = { 'X-Served-By': 'filestore', } +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('FileStoreController', function () { - beforeEach(async function () { - this.FileStoreHandler = { + beforeEach(async function (ctx) { + ctx.FileStoreHandler = { promises: { getFileStream: sinon.stub(), getFileSize: sinon.stub(), }, } - this.ProjectLocator = { promises: { findElement: sinon.stub() } } - this.Stream = { pipeline: sinon.stub().resolves() } - this.HistoryManager = {} - this.controller = await esmock.strict(MODULE_PATH, { - 'node:stream/promises': this.Stream, - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/FileStore/FileStoreHandler': - this.FileStoreHandler, - '../../../../app/src/Features/History/HistoryManager': - this.HistoryManager, - }) - this.stream = {} - this.projectId = '2k3j1lk3j21lk3j' - this.fileId = '12321kklj1lk3jk12' - this.req = { + ctx.ProjectLocator = { promises: { findElement: sinon.stub() } } + ctx.Stream = { pipeline: sinon.stub().resolves() } + ctx.HistoryManager = {} + + vi.doMock('node:stream/promises', () => ctx.Stream) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: ctx.FileStoreHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({ + default: ctx.HistoryManager, + })) + + ctx.controller = (await import(MODULE_PATH)).default + ctx.stream = {} + ctx.projectId = '2k3j1lk3j21lk3j' + ctx.fileId = '12321kklj1lk3jk12' + ctx.req = { params: { - Project_id: this.projectId, - File_id: this.fileId, + Project_id: ctx.projectId, + File_id: ctx.fileId, }, query: 'query string here', get(key) { @@ -49,61 +66,61 @@ describe('FileStoreController', function () { addFields: sinon.stub(), }, } - this.res = new MockResponse() - this.next = sinon.stub() - this.file = { name: 'myfile.png' } + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.file = { name: 'myfile.png' } }) describe('getFile', function () { - beforeEach(function () { - this.FileStoreHandler.promises.getFileStream.resolves(this.stream) - this.ProjectLocator.promises.findElement.resolves({ element: this.file }) + beforeEach(function (ctx) { + ctx.FileStoreHandler.promises.getFileStream.resolves(ctx.stream) + ctx.ProjectLocator.promises.findElement.resolves({ element: ctx.file }) }) - it('should call the file store handler with the project_id file_id and any query string', async function () { - await this.controller.getFile(this.req, this.res) - this.FileStoreHandler.promises.getFileStream.should.have.been.calledWith( - this.req.params.Project_id, - this.req.params.File_id, - this.req.query + it('should call the file store handler with the project_id file_id and any query string', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.FileStoreHandler.promises.getFileStream.should.have.been.calledWith( + ctx.req.params.Project_id, + ctx.req.params.File_id, + ctx.req.query ) }) - it('should pipe to res', async function () { - await this.controller.getFile(this.req, this.res) - this.Stream.pipeline.should.have.been.calledWith(this.stream, this.res) + it('should pipe to res', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.Stream.pipeline.should.have.been.calledWith(ctx.stream, ctx.res) }) - it('should get the file from the db', async function () { - await this.controller.getFile(this.req, this.res) - this.ProjectLocator.promises.findElement.should.have.been.calledWith({ - project_id: this.projectId, - element_id: this.fileId, + it('should get the file from the db', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.ProjectLocator.promises.findElement.should.have.been.calledWith({ + project_id: ctx.projectId, + element_id: ctx.fileId, type: 'file', }) }) - it('should set the Content-Disposition header', async function () { - await this.controller.getFile(this.req, this.res) - this.res.setContentDisposition.should.be.calledWith('attachment', { - filename: this.file.name, + it('should set the Content-Disposition header', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.setContentDisposition.should.be.calledWith('attachment', { + filename: ctx.file.name, }) }) - it('should return a 404 when not found', async function () { - this.ProjectLocator.promises.findElement.rejects( + it('should return a 404 when not found', async function (ctx) { + ctx.ProjectLocator.promises.findElement.rejects( new Errors.NotFoundError() ) - await this.controller.getFile(this.req, this.res) - expect(this.res.statusCode).to.equal(404) + await ctx.controller.getFile(ctx.req, ctx.res) + expect(ctx.res.statusCode).to.equal(404) }) // Test behaviour around handling html files ;['.html', '.htm', '.xhtml'].forEach(extension => { describe(`with a '${extension}' file extension`, function () { - beforeEach(function () { - this.file.name = `bad${extension}` - this.req.get = key => { + beforeEach(function (ctx) { + ctx.file.name = `bad${extension}` + ctx.req.get = key => { if (key === 'User-Agent') { return 'A generic browser' } @@ -111,26 +128,26 @@ describe('FileStoreController', function () { }) describe('from a non-ios browser', function () { - it('should not set Content-Type', async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it('should not set Content-Type', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, }) }) }) describe('from an iPhone', function () { - beforeEach(function () { - this.req.get = key => { + beforeEach(function (ctx) { + ctx.req.get = key => { if (key === 'User-Agent') { return 'An iPhone browser' } } }) - it("should set Content-Type to 'text/plain'", async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it("should set Content-Type to 'text/plain'", async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', @@ -139,17 +156,17 @@ describe('FileStoreController', function () { }) describe('from an iPad', function () { - beforeEach(function () { - this.req.get = key => { + beforeEach(function (ctx) { + ctx.req.get = key => { if (key === 'User-Agent') { return 'An iPad browser' } } }) - it("should set Content-Type to 'text/plain'", async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it("should set Content-Type to 'text/plain'", async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', @@ -166,24 +183,24 @@ describe('FileStoreController', function () { 'somefile', ].forEach(filename => { describe(`with filename as '${filename}'`, function () { - beforeEach(function () { - this.user_agent = 'A generic browser' - this.file.name = filename - this.req.get = key => { + beforeEach(function (ctx) { + ctx.user_agent = 'A generic browser' + ctx.file.name = filename + ctx.req.get = key => { if (key === 'User-Agent') { - return this.user_agent + return ctx.user_agent } } }) ;['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach(browser => { describe(`downloaded from ${browser}`, function () { - beforeEach(function () { - this.user_agent = `Some ${browser} thing` + beforeEach(function (ctx) { + ctx.user_agent = `Some ${browser} thing` }) - it('Should not set the Content-type', async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it('Should not set the Content-type', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, }) }) @@ -194,42 +211,46 @@ describe('FileStoreController', function () { }) describe('getFileHead', function () { - beforeEach(function () { - this.ProjectLocator.promises.findElement.resolves({ element: this.file }) + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findElement.resolves({ element: ctx.file }) }) - it('reports the file size', function (done) { - const expectedFileSize = 99393 - this.FileStoreHandler.promises.getFileSize.rejects( - new Error('getFileSize: unexpected arguments') - ) - this.FileStoreHandler.promises.getFileSize - .withArgs(this.projectId, this.fileId) - .resolves(expectedFileSize) + it('reports the file size', function (ctx) { + return new Promise(resolve => { + const expectedFileSize = 99393 + ctx.FileStoreHandler.promises.getFileSize.rejects( + new Error('getFileSize: unexpected arguments') + ) + ctx.FileStoreHandler.promises.getFileSize + .withArgs(ctx.projectId, ctx.fileId) + .resolves(expectedFileSize) - this.res.end = () => { - expect(this.res.status.lastCall.args).to.deep.equal([200]) - expect(this.res.header.lastCall.args).to.deep.equal([ - 'Content-Length', - expectedFileSize, - ]) - done() - } + ctx.res.end = () => { + expect(ctx.res.status.lastCall.args).to.deep.equal([200]) + expect(ctx.res.header.lastCall.args).to.deep.equal([ + 'Content-Length', + expectedFileSize, + ]) + resolve() + } - this.controller.getFileHead(this.req, this.res) + ctx.controller.getFileHead(ctx.req, ctx.res) + }) }) - it('returns 404 on NotFoundError', function (done) { - this.FileStoreHandler.promises.getFileSize.rejects( - new Errors.NotFoundError() - ) + it('returns 404 on NotFoundError', function (ctx) { + return new Promise(resolve => { + ctx.FileStoreHandler.promises.getFileSize.rejects( + new Errors.NotFoundError() + ) - this.res.end = () => { - expect(this.res.status.lastCall.args).to.deep.equal([404]) - done() - } + ctx.res.end = () => { + expect(ctx.res.status.lastCall.args).to.deep.equal([404]) + resolve() + } - this.controller.getFileHead(this.req, this.res) + ctx.controller.getFileHead(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs b/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs index f1b7b58c10..b29d10bba4 100644 --- a/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs +++ b/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs @@ -1,155 +1,205 @@ +import { vi } from 'vitest' import { expect } from 'chai' -import esmock from 'esmock' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/LinkedFiles/LinkedFilesController.mjs' describe('LinkedFilesController', function () { - beforeEach(function () { - this.fakeTime = new Date() - this.clock = sinon.useFakeTimers(this.fakeTime.getTime()) + beforeEach(function (ctx) { + ctx.fakeTime = new Date() + ctx.clock = sinon.useFakeTimers(ctx.fakeTime.getTime()) }) - afterEach(function () { - this.clock.restore() + afterEach(function (ctx) { + ctx.clock.restore() }) - beforeEach(async function () { - this.userId = 'user-id' - this.Agent = { + beforeEach(async function (ctx) { + ctx.userId = 'user-id' + ctx.Agent = { promises: { createLinkedFile: sinon.stub().resolves(), refreshLinkedFile: sinon.stub().resolves(), }, } - this.projectId = 'projectId' - this.provider = 'provider' - this.name = 'linked-file-name' - this.data = { customAgentData: 'foo' } - this.LinkedFilesHandler = { + ctx.projectId = 'projectId' + ctx.provider = 'provider' + ctx.fileName = 'linked-file-name' + ctx.data = { customAgentData: 'foo' } + ctx.LinkedFilesHandler = { promises: { getFileById: sinon.stub(), }, } - this.AnalyticsManager = {} - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.userId), + ctx.AnalyticsManager = {} + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.userId), } - this.EditorRealTimeController = {} - this.ReferencesHandler = {} - this.UrlAgent = {} - this.ProjectFileAgent = {} - this.ProjectOutputFileAgent = {} - this.EditorController = {} - this.ProjectLocator = {} - this.logger = { + ctx.EditorRealTimeController = {} + ctx.ReferencesHandler = {} + ctx.UrlAgent = {} + ctx.ProjectFileAgent = {} + ctx.ProjectOutputFileAgent = {} + ctx.EditorController = {} + ctx.ProjectLocator = {} + ctx.logger = { error: sinon.stub(), } - this.settings = { enabledLinkedFileTypes: [] } - this.LinkedFilesController = await esmock.strict(modulePath, { - '.../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Analytics/AnalyticsManager': - this.AnalyticsManager, - '../../../../app/src/Features/LinkedFiles/LinkedFilesHandler': - this.LinkedFilesHandler, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/References/ReferencesHandler': - this.ReferencesHandler, - '../../../../app/src/Features/LinkedFiles/UrlAgent': this.UrlAgent, - '../../../../app/src/Features/LinkedFiles/ProjectFileAgent': - this.ProjectFileAgent, - '../../../../app/src/Features/LinkedFiles/ProjectOutputFileAgent': - this.ProjectOutputFileAgent, - '../../../../app/src/Features/Editor/EditorController': - this.EditorController, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '@overleaf/logger': this.logger, - '@overleaf/settings': this.settings, - }) - this.LinkedFilesController._getAgent = sinon.stub().resolves(this.Agent) + ctx.settings = { enabledLinkedFileTypes: [] } + + vi.doMock( + '.../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/LinkedFilesHandler', + () => ({ + default: ctx.LinkedFilesHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/References/ReferencesHandler', + () => ({ + default: ctx.ReferencesHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/LinkedFiles/UrlAgent', () => ({ + default: ctx.UrlAgent, + })) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/ProjectFileAgent', + () => ({ + default: ctx.ProjectFileAgent, + }) + ) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/ProjectOutputFileAgent', + () => ({ + default: ctx.ProjectOutputFileAgent, + }) + ) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + ctx.LinkedFilesController = (await import(modulePath)).default + ctx.LinkedFilesController._getAgent = sinon.stub().resolves(ctx.Agent) }) describe('createLinkedFile', function () { - beforeEach(function () { - this.req = { - params: { project_id: this.projectId }, + beforeEach(function (ctx) { + ctx.req = { + params: { project_id: ctx.projectId }, body: { - name: this.name, - provider: this.provider, - data: this.data, + name: ctx.fileName, + provider: ctx.provider, + data: ctx.data, }, } - this.next = sinon.stub() + ctx.next = sinon.stub() }) - it('sets importedAt timestamp on linkedFileData', function (done) { - this.next = sinon.stub().callsFake(() => done('unexpected error')) - this.res = { - json: () => { - expect(this.Agent.promises.createLinkedFile).to.have.been.calledWith( - this.projectId, - { ...this.data, importedAt: this.fakeTime.toISOString() }, - this.name, - undefined, - this.userId - ) - done() - }, - } - this.LinkedFilesController.createLinkedFile(this.req, this.res, this.next) + it('sets importedAt timestamp on linkedFileData', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(() => resolve('unexpected error')) + ctx.res = { + json: () => { + expect(ctx.Agent.promises.createLinkedFile).to.have.been.calledWith( + ctx.projectId, + { ...ctx.data, importedAt: ctx.fakeTime.toISOString() }, + ctx.fileName, + undefined, + ctx.userId + ) + resolve() + }, + } + ctx.LinkedFilesController.createLinkedFile(ctx.req, ctx.res, ctx.next) + }) }) }) describe('refreshLinkedFiles', function () { - beforeEach(function () { - this.data.provider = this.provider - this.file = { - name: this.name, + beforeEach(function (ctx) { + ctx.data.provider = ctx.provider + ctx.file = { + name: ctx.fileName, linkedFileData: { - ...this.data, + ...ctx.data, importedAt: new Date(2020, 1, 1).toISOString(), }, } - this.LinkedFilesHandler.promises.getFileById - .withArgs(this.projectId, 'file-id') + ctx.LinkedFilesHandler.promises.getFileById + .withArgs(ctx.projectId, 'file-id') .resolves({ - file: this.file, + file: ctx.file, path: 'fake-path', parentFolder: { _id: 'parent-folder-id', }, }) - this.req = { - params: { project_id: this.projectId, file_id: 'file-id' }, + ctx.req = { + params: { project_id: ctx.projectId, file_id: 'file-id' }, body: {}, } - this.next = sinon.stub() + ctx.next = sinon.stub() }) - it('resets importedAt timestamp on linkedFileData', function (done) { - this.next = sinon.stub().callsFake(() => done('unexpected error')) - this.res = { - json: () => { - expect(this.Agent.promises.refreshLinkedFile).to.have.been.calledWith( - this.projectId, - { - ...this.data, - importedAt: this.fakeTime.toISOString(), - }, - this.name, - 'parent-folder-id', - this.userId - ) - done() - }, - } - this.LinkedFilesController.refreshLinkedFile( - this.req, - this.res, - this.next - ) + it('resets importedAt timestamp on linkedFileData', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(() => resolve('unexpected error')) + ctx.res = { + json: () => { + expect( + ctx.Agent.promises.refreshLinkedFile + ).to.have.been.calledWith( + ctx.projectId, + { + ...ctx.data, + importedAt: ctx.fakeTime.toISOString(), + }, + ctx.name, + 'parent-folder-id', + ctx.userId + ) + resolve() + }, + } + ctx.LinkedFilesController.refreshLinkedFile(ctx.req, ctx.res, ctx.next) + }) }) }) }) diff --git a/services/web/test/unit/src/Metadata/MetaController.test.mjs b/services/web/test/unit/src/Metadata/MetaController.test.mjs index 5695d289f7..00b3568ae2 100644 --- a/services/web/test/unit/src/Metadata/MetaController.test.mjs +++ b/services/web/test/unit/src/Metadata/MetaController.test.mjs @@ -1,31 +1,38 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Metadata/MetaController.mjs' describe('MetaController', function () { - beforeEach(async function () { - this.EditorRealTimeController = { + beforeEach(async function (ctx) { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.MetaHandler = { + ctx.MetaHandler = { promises: { getAllMetaForProject: sinon.stub(), getMetaForDoc: sinon.stub(), }, } - this.MetadataController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Metadata/MetaHandler': this.MetaHandler, - }) + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/Features/Metadata/MetaHandler', () => ({ + default: ctx.MetaHandler, + })) + + ctx.MetadataController = (await import(modulePath)).default }) describe('getMetadata', function () { - it('should respond with json', async function () { + it('should respond with json', async function (ctx) { const projectMeta = { 'doc-id': { labels: ['foo'], @@ -34,7 +41,7 @@ describe('MetaController', function () { }, } - this.MetaHandler.promises.getAllMetaForProject = sinon + ctx.MetaHandler.promises.getAllMetaForProject = sinon .stub() .resolves(projectMeta) @@ -42,9 +49,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.getMetadata(req, res, next) + await ctx.MetadataController.getMetadata(req, res, next) - this.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( + ctx.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( 'project-id' ) res.json.should.have.been.calledOnceWith({ @@ -54,8 +61,8 @@ describe('MetaController', function () { next.should.not.have.been.called }) - it('should handle an error', async function () { - this.MetaHandler.promises.getAllMetaForProject = sinon + it('should handle an error', async function (ctx) { + ctx.MetaHandler.promises.getAllMetaForProject = sinon .stub() .throws(new Error('woops')) @@ -63,9 +70,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.getMetadata(req, res, next) + await ctx.MetadataController.getMetadata(req, res, next) - this.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( + ctx.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called @@ -74,14 +81,14 @@ describe('MetaController', function () { }) describe('broadcastMetadataForDoc', function () { - it('should broadcast on broadcast:true ', async function () { - this.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves({ + it('should broadcast on broadcast:true ', async function (ctx) { + ctx.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves({ labels: ['foo'], packages: { a: { commands: [] } }, packageNames: ['a'], }) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -90,32 +97,32 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called res.sendStatus.should.have.been.calledOnceWith(200) next.should.not.have.been.called - this.EditorRealTimeController.emitToRoom.should.have.been.calledOnce - const { lastCall } = this.EditorRealTimeController.emitToRoom + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledOnce + const { lastCall } = ctx.EditorRealTimeController.emitToRoom expect(lastCall.args[0]).to.equal('project-id') expect(lastCall.args[1]).to.equal('broadcastDocMeta') expect(lastCall.args[2]).to.have.all.keys(['docId', 'meta']) }) - it('should return json on broadcast:false ', async function () { + it('should return json on broadcast:false ', async function (ctx) { const docMeta = { labels: ['foo'], packages: { a: [] }, packageNames: ['a'], } - this.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves(docMeta) + ctx.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves(docMeta) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -124,12 +131,12 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) - this.EditorRealTimeController.emitToRoom.should.not.have.been.called + ctx.EditorRealTimeController.emitToRoom.should.not.have.been.called res.json.should.have.been.calledOnceWith({ docId: 'doc-id', meta: docMeta, @@ -137,12 +144,12 @@ describe('MetaController', function () { next.should.not.have.been.called }) - it('should handle an error', async function () { - this.MetaHandler.promises.getMetaForDoc = sinon + it('should handle an error', async function (ctx) { + ctx.MetaHandler.promises.getMetaForDoc = sinon .stub() .throws(new Error('woops')) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -151,9 +158,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called diff --git a/services/web/test/unit/src/Metadata/MetaHandler.test.mjs b/services/web/test/unit/src/Metadata/MetaHandler.test.mjs index 289fd0b164..c6009a2dd6 100644 --- a/services/web/test/unit/src/Metadata/MetaHandler.test.mjs +++ b/services/web/test/unit/src/Metadata/MetaHandler.test.mjs @@ -1,15 +1,15 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' const modulePath = '../../../../app/src/Features/Metadata/MetaHandler.mjs' describe('MetaHandler', function () { - beforeEach(async function () { - this.projectId = 'someprojectid' - this.docId = 'somedocid' + beforeEach(async function (ctx) { + ctx.projectId = 'someprojectid' + ctx.docId = 'somedocid' - this.lines = [ + ctx.lines = [ '\\usepackage{ foo, bar }', '\\usepackage{baz}', 'one', @@ -23,28 +23,28 @@ describe('MetaHandler', function () { '\\begin{lstlisting}[label={lst:foo},caption={Test}]', // lst:foo should be in the returned labels ] - this.docs = { - [this.docId]: { - _id: this.docId, - lines: this.lines, + ctx.docs = { + [ctx.docId]: { + _id: ctx.docId, + lines: ctx.lines, }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { promises: { - getAllDocs: sinon.stub().resolves(this.docs), - getDoc: sinon.stub().resolves(this.docs[this.docId]), + getAllDocs: sinon.stub().resolves(ctx.docs), + getDoc: sinon.stub().resolves(ctx.docs[ctx.docId]), }, } - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { flushDocToMongo: sinon.stub().resolves(), flushProjectToMongo: sinon.stub().resolves(), }, } - this.packageMapping = { + ctx.packageMapping = { foo: [ { caption: '\\bar', @@ -69,47 +69,58 @@ describe('MetaHandler', function () { ], } - this.MetaHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectEntityHandler': - this.ProjectEntityHandler, - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../../../../app/src/Features/Metadata/packageMapping': - this.packageMapping, - }) + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Metadata/packageMapping', () => ({ + default: ctx.packageMapping, + })) + + ctx.MetaHandler = (await import(modulePath)).default }) describe('getMetaForDoc', function () { - it('should extract all the labels and packages', async function () { - const result = await this.MetaHandler.promises.getMetaForDoc( - this.projectId, - this.docId + it('should extract all the labels and packages', async function (ctx) { + const result = await ctx.MetaHandler.promises.getMetaForDoc( + ctx.projectId, + ctx.docId ) expect(result).to.deep.equal({ labels: ['aaa', 'ccc', 'ddd', 'e,f,g', 'foo', 'lst:foo'], packages: { - foo: this.packageMapping.foo, - baz: this.packageMapping.baz, + foo: ctx.packageMapping.foo, + baz: ctx.packageMapping.baz, }, packageNames: ['foo', 'bar', 'baz'], }) - this.DocumentUpdaterHandler.promises.flushDocToMongo.should.be.calledWith( - this.projectId, - this.docId + ctx.DocumentUpdaterHandler.promises.flushDocToMongo.should.be.calledWith( + ctx.projectId, + ctx.docId ) - this.ProjectEntityHandler.promises.getDoc.should.be.calledWith( - this.projectId, - this.docId + ctx.ProjectEntityHandler.promises.getDoc.should.be.calledWith( + ctx.projectId, + ctx.docId ) }) }) describe('getAllMetaForProject', function () { - it('should extract all metadata', async function () { - this.ProjectEntityHandler.promises.getAllDocs = sinon.stub().resolves({ + it('should extract all metadata', async function (ctx) { + ctx.ProjectEntityHandler.promises.getAllDocs = sinon.stub().resolves({ doc_one: { _id: 'id_one', lines: ['one', '\\label{aaa} two', 'three'], @@ -142,8 +153,8 @@ describe('MetaHandler', function () { }, }) - const result = await this.MetaHandler.promises.getAllMetaForProject( - this.projectId + const result = await ctx.MetaHandler.promises.getAllMetaForProject( + ctx.projectId ) expect(result).to.deep.equal({ @@ -206,12 +217,12 @@ describe('MetaHandler', function () { }, }) - this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.be.calledWith( - this.projectId + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo.should.be.calledWith( + ctx.projectId ) - this.ProjectEntityHandler.promises.getAllDocs.should.be.calledWith( - this.projectId + ctx.ProjectEntityHandler.promises.getAllDocs.should.be.calledWith( + ctx.projectId ) }) }) diff --git a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs index 0e22b228c5..6e1f9177c0 100644 --- a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs +++ b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = new URL( @@ -10,12 +10,12 @@ describe('NotificationsController', function () { const userId = '123nd3ijdks' const notificationId = '123njdskj9jlk' - beforeEach(async function () { - this.handler = { + beforeEach(async function (ctx) { + ctx.handler = { getUserNotifications: sinon.stub().callsArgWith(1), markAsRead: sinon.stub().callsArgWith(2), } - this.req = { + ctx.req = { params: { notificationId, }, @@ -28,39 +28,53 @@ describe('NotificationsController', function () { translate() {}, }, } - this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.req.session.user._id), + ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.req.session.user._id), } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Notifications/NotificationsHandler': - this.handler, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsHandler', + () => ({ + default: ctx.handler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + ctx.controller = (await import(modulePath)).default + }) + + it('should ask the handler for all unread notifications', function (ctx) { + return new Promise(resolve => { + const allNotifications = [{ _id: notificationId, user_id: userId }] + ctx.handler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, allNotifications) + ctx.controller.getAllUnreadNotifications(ctx.req, { + json: body => { + body.should.deep.equal(allNotifications) + ctx.handler.getUserNotifications.calledWith(userId).should.equal(true) + resolve() + }, + }) }) }) - it('should ask the handler for all unread notifications', function (done) { - const allNotifications = [{ _id: notificationId, user_id: userId }] - this.handler.getUserNotifications = sinon - .stub() - .callsArgWith(1, null, allNotifications) - this.controller.getAllUnreadNotifications(this.req, { - json: body => { - body.should.deep.equal(allNotifications) - this.handler.getUserNotifications.calledWith(userId).should.equal(true) - done() - }, - }) - }) - - it('should send a delete request when a delete has been received to mark a notification', function (done) { - this.controller.markNotificationAsRead(this.req, { - sendStatus: () => { - this.handler.markAsRead - .calledWith(userId, notificationId) - .should.equal(true) - done() - }, + it('should send a delete request when a delete has been received to mark a notification', function (ctx) { + return new Promise(resolve => { + ctx.controller.markNotificationAsRead(ctx.req, { + sendStatus: () => { + ctx.handler.markAsRead + .calledWith(userId, notificationId) + .should.equal(true) + resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs index 6df3c765b1..e4cf6e569f 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs +++ b/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import MockResponse from '../helpers/MockResponse.js' @@ -9,16 +9,16 @@ const MODULE_PATH = new URL( ).pathname describe('PasswordResetController', function () { - beforeEach(async function () { - this.email = 'bob@bob.com' - this.user_id = 'mock-user-id' - this.token = 'my security token that was emailed to me' - this.password = 'my new password' - this.req = { + beforeEach(async function (ctx) { + ctx.email = 'bob@bob.com' + ctx.user_id = 'mock-user-id' + ctx.token = 'my security token that was emailed to me' + ctx.password = 'my new password' + ctx.req = { body: { - email: this.email, - passwordResetToken: this.token, - password: this.password, + email: ctx.email, + passwordResetToken: ctx.token, + password: ctx.password, }, i18n: { translate() { @@ -28,456 +28,540 @@ describe('PasswordResetController', function () { session: {}, query: {}, } - this.res = new MockResponse() + ctx.res = new MockResponse() - this.settings = {} - this.PasswordResetHandler = { + ctx.settings = {} + ctx.PasswordResetHandler = { generateAndEmailResetToken: sinon.stub(), promises: { generateAndEmailResetToken: sinon.stub(), setNewUserPassword: sinon.stub().resolves({ found: true, reset: true, - userID: this.user_id, + userID: ctx.user_id, mustReconfirm: true, }), getUserForPasswordResetToken: sinon .stub() - .withArgs(this.token) + .withArgs(ctx.token) .resolves({ - user: { _id: this.user_id }, + user: { _id: ctx.user_id }, remainingPeeks: 1, }), }, } - this.UserSessionsManager = { + ctx.UserSessionsManager = { promises: { removeSessionsFromRedis: sinon.stub().resolves(), }, } - this.UserUpdater = { + ctx.UserUpdater = { promises: { removeReconfirmFlag: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves('default'), }, } - this.PasswordResetController = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/PasswordReset/PasswordResetHandler': - this.PasswordResetHandler, - '../../../../app/src/Features/Authentication/AuthenticationManager': { - validatePassword: sinon.stub().returns(null), - }, - '../../../../app/src/Features/Authentication/AuthenticationController': - (this.AuthenticationController = { + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/Features/PasswordReset/PasswordResetHandler', + () => ({ + default: ctx.PasswordResetHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: { + validatePassword: sinon.stub().returns(null), + }, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: (ctx.AuthenticationController = { getLoggedInUserId: sinon.stub(), finishLogin: sinon.stub(), setAuditInfo: sinon.stub(), }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: { getUser: sinon.stub(), }, }), - '../../../../app/src/Features/User/UserSessionsManager': - this.UserSessionsManager, - '../../../../app/src/Features/User/UserUpdater': this.UserUpdater, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: ctx.UserUpdater, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.PasswordResetController = (await import(MODULE_PATH)).default }) describe('requestReset', function () { - it('should tell the handler to process that email', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'primary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(200) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - expect( - this.PasswordResetHandler.promises.generateAndEmailResetToken.lastCall - .args[0] - ).equal(this.email) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) - }) - - it('should send a 500 if there is an error', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.rejects( - new Error('error') - ) - this.PasswordResetController.requestReset(this.req, this.res, error => { - expect(error).to.exist - done() + it('should tell the handler to process that email', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'primary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(200) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + expect( + ctx.PasswordResetHandler.promises.generateAndEmailResetToken + .lastCall.args[0] + ).equal(ctx.email) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) }) }) - it("should send a 404 if the email doesn't exist", function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - null - ) - this.res.callback = () => { - this.res.statusCode.should.equal(404) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it('should send a 500 if there is an error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.rejects( + new Error('error') + ) + ctx.PasswordResetController.requestReset(ctx.req, ctx.res, error => { + expect(error).to.exist + resolve() + }) + }) }) - it('should send a 404 if the email is registered as a secondard email', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'secondary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(404) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it("should send a 404 if the email doesn't exist", function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + null + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(404) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) }) - it('should normalize the email address', function (done) { - this.email = ' UPperCaseEMAILWithSpacesAround@example.Com ' - this.req.body.email = this.email - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'primary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(200) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it('should send a 404 if the email is registered as a secondard email', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'secondary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(404) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) + }) + + it('should normalize the email address', function (ctx) { + return new Promise(resolve => { + ctx.email = ' UPperCaseEMAILWithSpacesAround@example.Com ' + ctx.req.body.email = ctx.email + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'primary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(200) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) }) }) describe('setNewUserPassword', function () { - beforeEach(function () { - this.req.session.resetToken = this.token + beforeEach(function (ctx) { + ctx.req.session.resetToken = ctx.token }) - it('should tell the user handler to reset the password', function (done) { - this.res.sendStatus = code => { - code.should.equal(200) - this.PasswordResetHandler.promises.setNewUserPassword - .calledWith(this.token, this.password) - .should.equal(true) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) - }) - - it('should preserve spaces in the password', function (done) { - this.password = this.req.body.password = ' oh! clever! spaces around! ' - this.res.sendStatus = code => { - code.should.equal(200) - this.PasswordResetHandler.promises.setNewUserPassword.should.have.been.calledWith( - this.token, - this.password - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) - }) - - it('should send 404 if the token was not found', function (done) { - this.PasswordResetHandler.promises.setNewUserPassword.resolves({ - found: false, - reset: false, - userId: this.user_id, + it('should tell the user handler to reset the password', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.PasswordResetHandler.promises.setNewUserPassword + .calledWith(ctx.token, ctx.password) + .should.equal(true) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.res.status = code => { - code.should.equal(404) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('token-expired') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) - it('should return 500 if not reset', function (done) { - this.PasswordResetHandler.promises.setNewUserPassword.resolves({ - found: true, - reset: false, - userId: this.user_id, + it('should preserve spaces in the password', function (ctx) { + return new Promise(resolve => { + ctx.password = ctx.req.body.password = ' oh! clever! spaces around! ' + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.PasswordResetHandler.promises.setNewUserPassword.should.have.been.calledWith( + ctx.token, + ctx.password + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.res.status = code => { - code.should.equal(500) - return this.res - } - this.res.json = data => { - expect(data.message).to.exist - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) - it('should return 400 (Bad Request) if there is no password', function (done) { - this.req.body.password = '' - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - false - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should send 404 if the token was not found', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({ + found: false, + reset: false, + userId: ctx.user_id, + }) + ctx.res.status = code => { + code.should.equal(404) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('token-expired') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 (Bad Request) if there is no passwordResetToken', function (done) { - this.req.body.passwordResetToken = '' - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - false - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 500 if not reset', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({ + found: true, + reset: false, + userId: ctx.user_id, + }) + ctx.res.status = code => { + code.should.equal(500) + return ctx.res + } + ctx.res.json = data => { + expect(data.message).to.exist + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 (Bad Request) if the password is invalid', function (done) { - this.req.body.password = 'correct horse battery staple' - const err = new Error('bad') - err.name = 'InvalidPasswordError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(err) - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - true - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if there is no password', function (ctx) { + return new Promise(resolve => { + ctx.req.body.password = '' + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + false + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should clear sessions', function (done) { - this.res.sendStatus = code => { - this.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal( - 1 - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if there is no passwordResetToken', function (ctx) { + return new Promise(resolve => { + ctx.req.body.passwordResetToken = '' + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + false + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should call removeReconfirmFlag if user.must_reconfirm', function (done) { - this.res.sendStatus = code => { - this.UserUpdater.promises.removeReconfirmFlag.callCount.should.equal(1) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if the password is invalid', function (ctx) { + return new Promise(resolve => { + ctx.req.body.password = 'correct horse battery staple' + const err = new Error('bad') + err.name = 'InvalidPasswordError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(err) + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + true + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) + }) + + it('should clear sessions', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal( + 1 + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) + }) + + it('should call removeReconfirmFlag if user.must_reconfirm', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.UserUpdater.promises.removeReconfirmFlag.callCount.should.equal(1) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) describe('catch errors', function () { - it('should return 404 for NotFoundError', function (done) { - const anError = new Error('oops') - anError.name = 'NotFoundError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(404) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('token-expired') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 404 for NotFoundError', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + anError.name = 'NotFoundError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(404) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('token-expired') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 for InvalidPasswordError', function (done) { - const anError = new Error('oops') - anError.name = 'InvalidPasswordError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 for InvalidPasswordError', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + anError.name = 'InvalidPasswordError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 500 for other errors', function (done) { - const anError = new Error('oops') - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(500) - return this.res - } - this.res.json = data => { - expect(data.message).to.exist - done() - } - this.res.sendStatus = code => { - code.should.equal(500) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 500 for other errors', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(500) + return ctx.res + } + ctx.res.json = data => { + expect(data.message).to.exist + resolve() + } + ctx.res.sendStatus = code => { + code.should.equal(500) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) }) describe('when doLoginAfterPasswordReset is set', function () { - beforeEach(function () { - this.user = { - _id: this.userId, + beforeEach(function (ctx) { + ctx.user = { + _id: ctx.userId, email: 'joe@example.com', } - this.UserGetter.promises.getUser.resolves(this.user) - this.req.session.doLoginAfterPasswordReset = 'true' + ctx.UserGetter.promises.getUser.resolves(ctx.user) + ctx.req.session.doLoginAfterPasswordReset = 'true' }) - it('should login user', function (done) { - this.AuthenticationController.finishLogin.callsFake((...args) => { - expect(args[0]).to.equal(this.user) - done() + it('should login user', function (ctx) { + return new Promise(resolve => { + ctx.AuthenticationController.finishLogin.callsFake((...args) => { + expect(args[0]).to.equal(ctx.user) + resolve() + }) + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) }) }) describe('renderSetPasswordForm', function () { describe('with token in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token }) - it('should set session.resetToken and redirect', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with expired token in query', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.PasswordResetHandler.promises.getUserForPasswordResetToken = sinon + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = sinon .stub() - .withArgs(this.token) - .resolves({ user: { _id: this.user_id }, remainingPeeks: 0 }) + .withArgs(ctx.token) + .resolves({ user: { _id: ctx.user_id }, remainingPeeks: 0 }) }) - it('should redirect to the reset request page with an error message', function (done) { - this.res.redirect = path => { - path.should.equal('/user/password/reset?error=token_expired') - this.req.session.should.not.have.property('resetToken') - done() - } - this.res.render = (templatePath, options) => { - done('should not render') - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should redirect to the reset request page with an error message', function (ctx) { + return new Promise(resolve => { + ctx.res.redirect = path => { + path.should.equal('/user/password/reset?error=token_expired') + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.res.render = (templatePath, options) => { + resolve('should not render') + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = 'foo@bar.com' + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = 'foo@bar.com' }) - it('should set session.resetToken and redirect with email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set?email=foo%40bar.com') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect with email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set?email=foo%40bar.com') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and invalid email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = 'not-an-email' + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = 'not-an-email' }) - it('should set session.resetToken and redirect without email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect without email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and non-string email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = { foo: 'bar' } + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = { foo: 'bar' } }) - it('should set session.resetToken and redirect without email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect without email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('without a token in query-string', function () { describe('with token in session', function () { - beforeEach(function () { - this.req.session.resetToken = this.token + beforeEach(function (ctx) { + ctx.req.session.resetToken = ctx.token }) - it('should render the page, passing the reset token', function (done) { - this.res.render = (templatePath, options) => { - options.passwordResetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should render the page, passing the reset token', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (templatePath, options) => { + options.passwordResetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) - it('should clear the req.session.resetToken', function (done) { - this.res.render = (templatePath, options) => { - this.req.session.should.not.have.property('resetToken') - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should clear the req.session.resetToken', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (templatePath, options) => { + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('without a token in session', function () { - it('should redirect to the reset request page', function (done) { - this.res.redirect = path => { - path.should.equal('/user/password/reset') - this.req.session.should.not.have.property('resetToken') - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should redirect to the reset request page', function (ctx) { + return new Promise(resolve => { + ctx.res.redirect = path => { + path.should.equal('/user/password/reset') + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs index b99cc527e2..25d664b795 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs +++ b/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = new URL( @@ -7,9 +7,9 @@ const modulePath = new URL( ).pathname describe('PasswordResetHandler', function () { - beforeEach(async function () { - this.settings = { siteUrl: 'https://www.overleaf.com' } - this.OneTimeTokenHandler = { + beforeEach(async function (ctx) { + ctx.settings = { siteUrl: 'https://www.overleaf.com' } + ctx.OneTimeTokenHandler = { promises: { getNewToken: sinon.stub(), peekValueFromToken: sinon.stub(), @@ -17,7 +17,7 @@ describe('PasswordResetHandler', function () { peekValueFromToken: sinon.stub(), expireToken: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { getUserByMainEmail: sinon.stub(), getUser: sinon.stub(), promises: { @@ -25,123 +25,153 @@ describe('PasswordResetHandler', function () { getUserByMainEmail: sinon.stub(), }, } - this.EmailHandler = { promises: { sendEmail: sinon.stub() } } - this.AuthenticationManager = { + ctx.EmailHandler = { promises: { sendEmail: sinon.stub() } } + ctx.AuthenticationManager = { setUserPasswordInV2: sinon.stub(), promises: { setUserPassword: sinon.stub().resolves(), }, } - this.PasswordResetHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/User/UserAuditLogHandler': - (this.UserAuditLogHandler = { - promises: { - addEntry: sinon.stub().resolves(), - }, - }), - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Security/OneTimeTokenHandler': - this.OneTimeTokenHandler, - '../../../../app/src/Features/Email/EmailHandler': this.EmailHandler, - '../../../../app/src/Features/Authentication/AuthenticationManager': - this.AuthenticationManager, - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Authorization/PermissionsManager': - (this.PermissionsManager = { + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: (ctx.UserAuditLogHandler = { + promises: { + addEntry: sinon.stub().resolves(), + }, + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Security/OneTimeTokenHandler', + () => ({ + default: ctx.OneTimeTokenHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: ctx.AuthenticationManager, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/Features/Authorization/PermissionsManager', + () => ({ + default: (ctx.PermissionsManager = { promises: { assertUserPermissions: sinon.stub(), }, }), - }) - this.token = '12312321i' - this.user_id = 'user_id_here' - this.user = { email: (this.email = 'bob@bob.com'), _id: this.user_id } - this.password = 'my great secret password' - this.callback = sinon.stub() + }) + ) + + ctx.PasswordResetHandler = (await import(modulePath)).default + ctx.token = '12312321i' + ctx.user_id = 'user_id_here' + ctx.user = { email: (ctx.email = 'bob@bob.com'), _id: ctx.user_id } + ctx.password = 'my great secret password' + ctx.callback = sinon.stub() // this should not have any effect now - this.settings.overleaf = true + ctx.settings.overleaf = true }) - afterEach(function () { - this.settings.overleaf = false + afterEach(function (ctx) { + ctx.settings.overleaf = false }) describe('generateAndEmailResetToken', function () { - it('should check the user exists', function () { - this.UserGetter.promises.getUserByAnyEmail.resolves() - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - this.callback + it('should check the user exists', function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves() + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + ctx.callback ) - this.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith( - this.user.email + ctx.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith( + ctx.user.email ) }) - it('should send the email with the token', function (done) { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) - this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token) - this.EmailHandler.promises.sendEmail.resolves() - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - (err, status) => { - expect(err).to.not.exist - this.EmailHandler.promises.sendEmail.called.should.equal(true) - status.should.equal('primary') - const args = this.EmailHandler.promises.sendEmail.args[0] - args[0].should.equal('passwordResetRequested') - args[1].setNewPasswordUrl.should.equal( - `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ - this.token - }&email=${encodeURIComponent(this.user.email)}` - ) - done() - } - ) + it('should send the email with the token', function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token) + ctx.EmailHandler.promises.sendEmail.resolves() + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + (err, status) => { + expect(err).to.not.exist + ctx.EmailHandler.promises.sendEmail.called.should.equal(true) + status.should.equal('primary') + const args = ctx.EmailHandler.promises.sendEmail.args[0] + args[0].should.equal('passwordResetRequested') + args[1].setNewPasswordUrl.should.equal( + `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${ + ctx.token + }&email=${encodeURIComponent(ctx.user.email)}` + ) + resolve() + } + ) + }) }) - it('should return errors from getUserByAnyEmail', function (done) { - const err = new Error('oops') - this.UserGetter.promises.getUserByAnyEmail.rejects(err) - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - err => { - expect(err).to.equal(err) - done() - } - ) + it('should return errors from getUserByAnyEmail', function (ctx) { + return new Promise(resolve => { + const err = new Error('oops') + ctx.UserGetter.promises.getUserByAnyEmail.rejects(err) + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + err => { + expect(err).to.equal(err) + resolve() + } + ) + }) }) describe('when the email exists', function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) - this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token) - this.EmailHandler.promises.sendEmail.resolves() + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token) + ctx.EmailHandler.promises.sendEmail.resolves() result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( - this.email + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( + ctx.email ) }) - it('should set the password token data to the user id and email', function () { - this.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith( + it('should set the password token data to the user id and email', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith( 'password', { - email: this.email, - user_id: this.user._id, + email: ctx.email, + user_id: ctx.user._id, } ) }) - it('should send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(true) - const args = this.EmailHandler.promises.sendEmail.args[0] + it('should send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(true) + const args = ctx.EmailHandler.promises.sendEmail.args[0] args[0].should.equal('passwordResetRequested') args[1].setNewPasswordUrl.should.equal( - `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ - this.token - }&email=${encodeURIComponent(this.user.email)}` + `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${ + ctx.token + }&email=${encodeURIComponent(ctx.user.email)}` ) }) @@ -152,20 +182,20 @@ describe('PasswordResetHandler', function () { describe("when the email doesn't exist", function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(null) + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(null) result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( - this.email + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( + ctx.email ) }) - it('should not set the password token data', function () { - this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) + it('should not set the password token data', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) }) - it('should send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(false) + it('should send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(false) }) it('should return status == null', function () { @@ -175,20 +205,20 @@ describe('PasswordResetHandler', function () { describe('when the email is a secondary email', function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( 'secondary@email.com' ) }) - it('should not set the password token data', function () { - this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) + it('should not set the password token data', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) }) - it('should not send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(false) + it('should not send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(false) }) it('should return status == secondary', function () { @@ -198,19 +228,19 @@ describe('PasswordResetHandler', function () { }) describe('setNewUserPassword', function () { - beforeEach(function () { - this.auditLog = { ip: '0:0:0:0' } + beforeEach(function (ctx) { + ctx.auditLog = { ip: '0:0:0:0' } }) describe('when no data is found', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null) + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null) }) - it('should return found == false and reset == false', function () { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, + it('should return found == false and reset == false', function (ctx) { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, (error, result) => { expect(error).to.not.exist expect(result).to.deep.equal({ @@ -224,202 +254,220 @@ describe('PasswordResetHandler', function () { }) describe('when the token has a user_id and email', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - user_id: this.user._id, - email: this.email, + user_id: ctx.user._id, + email: ctx.email, }, }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .resolves(true) - this.OneTimeTokenHandler.expireToken = sinon - .stub() - .callsArgWith(2, null) + ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null) }) describe('when no user is found with this email', function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) .yields(null, null) }) - it('should return found == false and reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { found, reset } = result - expect(err).to.not.exist - expect(found).to.be.false - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0) - done() - } - ) + it('should return found == false and reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { found, reset } = result + expect(err).to.not.exist + expect(found).to.be.false + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal( + 0 + ) + resolve() + } + ) + }) }) }) describe("when the email and user don't match", function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) - .yields(null, { _id: 'not-the-same', email: this.email }) - this.OneTimeTokenHandler.expireToken.callsArgWith(2, null) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) + .yields(null, { _id: 'not-the-same', email: ctx.email }) + ctx.OneTimeTokenHandler.expireToken.callsArgWith(2, null) }) - it('should return found == false and reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { found, reset } = result - expect(err).to.not.exist - expect(found).to.be.false - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0) - done() - } - ) + it('should return found == false and reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { found, reset } = result + expect(err).to.not.exist + expect(found).to.be.false + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal( + 0 + ) + resolve() + } + ) + }) }) }) describe('when the email and user match', function () { describe('success', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByMainEmail.resolves(this.user) - this.OneTimeTokenHandler.expireToken = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.expireToken = sinon .stub() .callsArgWith(2, null) }) - it('should update the user audit log', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, result) => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user_id, - 'reset-password', - undefined, - this.auditLog.ip, - { token: this.token.substring(0, 10) } - ) - expect(error).to.not.exist - done() - } - ) + it('should update the user audit log', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, result) => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user_id, + 'reset-password', + undefined, + ctx.auditLog.ip, + { token: ctx.token.substring(0, 10) } + ) + expect(error).to.not.exist + resolve() + } + ) + }) }) - it('should return reset == true and the user id', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset, userId } = result - expect(err).to.not.exist - expect(reset).to.be.true - expect(userId).to.equal(this.user._id) - done() - } - ) + it('should return reset == true and the user id', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset, userId } = result + expect(err).to.not.exist + expect(reset).to.be.true + expect(userId).to.equal(ctx.user._id) + resolve() + } + ) + }) }) - it('should expire the token', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (_err, _result) => { - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - true - ) - done() - } - ) + it('should expire the token', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (_err, _result) => { + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + true + ) + resolve() + } + ) + }) }) describe('when logged in', function () { - beforeEach(function () { - this.auditLog.initiatorId = this.user_id + beforeEach(function (ctx) { + ctx.auditLog.initiatorId = ctx.user_id }) - it('should update the user audit log with initiatorId', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, result) => { - expect(error).to.not.exist - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user_id, - 'reset-password', - this.user_id, - this.auditLog.ip, - { token: this.token.substring(0, 10) } - ) - done() - } - ) + it('should update the user audit log with initiatorId', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, result) => { + expect(error).to.not.exist + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user_id, + 'reset-password', + ctx.user_id, + ctx.auditLog.ip, + { token: ctx.token.substring(0, 10) } + ) + resolve() + } + ) + }) }) }) }) describe('errors', function () { describe('via setUserPassword', function () { - beforeEach(function () { - this.PasswordResetHandler.promises.getUserForPasswordResetToken = - sinon.stub().withArgs(this.token).resolves({ user: this.user }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + beforeEach(function (ctx) { + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = + sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user }) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .rejects() }) - it('should return the error', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, _result) => { - expect(error).to.exist - expect( - this.UserAuditLogHandler.promises.addEntry.callCount - ).to.equal(1) - done() - } - ) + it('should return the error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, _result) => { + expect(error).to.exist + expect( + ctx.UserAuditLogHandler.promises.addEntry.callCount + ).to.equal(1) + resolve() + } + ) + }) }) }) describe('via UserAuditLogHandler', function () { - beforeEach(function () { - this.PasswordResetHandler.promises.getUserForPasswordResetToken = - sinon.stub().withArgs(this.token).resolves({ user: this.user }) - this.UserAuditLogHandler.promises.addEntry.rejects( + beforeEach(function (ctx) { + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = + sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user }) + ctx.UserAuditLogHandler.promises.addEntry.rejects( new Error('oops') ) }) - it('should return the error', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, _result) => { - expect(error).to.exist - expect( - this.UserAuditLogHandler.promises.addEntry.callCount - ).to.equal(1) - expect(this.AuthenticationManager.promises.setUserPassword).to - .not.have.been.called - done() - } - ) + it('should return the error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, _result) => { + expect(error).to.exist + expect( + ctx.UserAuditLogHandler.promises.addEntry.callCount + ).to.equal(1) + expect(ctx.AuthenticationManager.promises.setUserPassword) + .to.not.have.been.called + resolve() + } + ) + }) }) }) }) @@ -427,120 +475,126 @@ describe('PasswordResetHandler', function () { }) describe('when the token has a v1_user_id and email', function () { - beforeEach(function () { - this.user.overleaf = { id: 184 } - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.user.overleaf = { id: 184 } + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - v1_user_id: this.user.overleaf.id, - email: this.email, + v1_user_id: ctx.user.overleaf.id, + email: ctx.email, }, }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .resolves(true) - this.OneTimeTokenHandler.expireToken = sinon - .stub() - .callsArgWith(2, null) + ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null) }) describe('when no user is reset with this email', function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) .yields(null, null) }) - it('should return reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset } = result - expect(err).to.not.exist - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - false - ) - done() - } - ) + it('should return reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset } = result + expect(err).to.not.exist + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + false + ) + resolve() + } + ) + }) }) }) describe("when the email and user don't match", function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail.withArgs(this.email).yields(null, { - _id: this.user._id, - email: this.email, + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail.withArgs(ctx.email).yields(null, { + _id: ctx.user._id, + email: ctx.email, overleaf: { id: 'not-the-same' }, }) }) - it('should return reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset } = result - expect(err).to.not.exist - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - false - ) - done() - } - ) + it('should return reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset } = result + expect(err).to.not.exist + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + false + ) + resolve() + } + ) + }) }) }) describe('when the email and user match', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByMainEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user) }) - it('should return reset == true and the user id', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset, userId } = result - expect(err).to.not.exist - expect(reset).to.be.true - expect(userId).to.equal(this.user._id) - expect(this.OneTimeTokenHandler.expireToken.called).to.equal(true) - done() - } - ) + it('should return reset == true and the user id', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset, userId } = result + expect(err).to.not.exist + expect(reset).to.be.true + expect(userId).to.equal(ctx.user._id) + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + true + ) + resolve() + } + ) + }) }) }) }) }) describe('getUserForPasswordResetToken', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - user_id: this.user._id, - email: this.email, + user_id: ctx.user._id, + email: ctx.email, }, remainingPeeks: 1, }) - this.UserGetter.promises.getUserByMainEmail.resolves({ - _id: this.user._id, - email: this.email, + ctx.UserGetter.promises.getUserByMainEmail.resolves({ + _id: ctx.user._id, + email: ctx.email, }) }) - it('should returns errors from user permissions', async function () { + it('should returns errors from user permissions', async function (ctx) { let error const err = new Error('nope') - this.PermissionsManager.promises.assertUserPermissions.rejects(err) + ctx.PermissionsManager.promises.assertUserPermissions.rejects(err) try { - await this.PasswordResetHandler.promises.getUserForPasswordResetToken( + await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken( 'abc123' ) } catch (e) { @@ -549,13 +603,13 @@ describe('PasswordResetHandler', function () { expect(error).to.deep.equal(error) }) - it('returns user when user has permissions and remaining peaks', async function () { + it('returns user when user has permissions and remaining peaks', async function (ctx) { const result = - await this.PasswordResetHandler.promises.getUserForPasswordResetToken( + await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken( 'abc123' ) expect(result).to.deep.equal({ - user: { _id: this.user._id, email: this.email }, + user: { _id: ctx.user._id, email: ctx.email }, remainingPeeks: 1, }) }) diff --git a/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs b/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs index 4f1f3b4f5f..55c4187f83 100644 --- a/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs +++ b/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs @@ -1,16 +1,14 @@ -import esmock from 'esmock' - const modulePath = '../../../../app/src/Features/Project/DocLinesComparitor.mjs' describe('doc lines comparitor', function () { - beforeEach(async function () { - this.comparitor = await esmock.strict(modulePath, {}) + beforeEach(async function (ctx) { + ctx.comparitor = (await import(modulePath)).default }) - it('should return true when the lines are the same', function () { + it('should return true when the lines are the same', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) ;[ @@ -23,58 +21,58 @@ describe('doc lines comparitor', function () { lines2: ['hello', 'wrld'], }, ].forEach(({ lines1, lines2 }) => { - it('should return false when the lines are different', function () { - const result = this.comparitor.areSame(lines1, lines2) + it('should return false when the lines are different', function (ctx) { + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) }) - it('should return true when the lines are same', function () { + it('should return true when the lines are same', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) - it('should return false if the doc lines are different in length', function () { + it('should return false if the doc lines are different in length', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world', 'please'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the first array is undefined', function () { + it('should return false if the first array is undefined', function (ctx) { const lines1 = undefined const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the second array is undefined', function () { + it('should return false if the second array is undefined', function (ctx) { const lines1 = ['hello'] const lines2 = undefined - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the second array is not an array', function () { + it('should return false if the second array is not an array', function (ctx) { const lines1 = ['hello'] const lines2 = '' - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return true when comparing equal orchard docs', function () { + it('should return true when comparing equal orchard docs', function (ctx) { const lines1 = [{ text: 'hello world' }] const lines2 = [{ text: 'hello world' }] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) - it('should return false when comparing different orchard docs', function () { + it('should return false when comparing different orchard docs', function (ctx) { const lines1 = [{ text: 'goodbye world' }] const lines2 = [{ text: 'hello world' }] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) }) diff --git a/services/web/test/unit/src/Project/ProjectApiController.test.mjs b/services/web/test/unit/src/Project/ProjectApiController.test.mjs index bda54a932c..c73f327cd2 100644 --- a/services/web/test/unit/src/Project/ProjectApiController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectApiController.test.mjs @@ -1,57 +1,57 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/Project/ProjectApiController' describe('Project api controller', function () { - beforeEach(async function () { - this.ProjectDetailsHandler = { getDetails: sinon.stub() } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectDetailsHandler': - this.ProjectDetailsHandler, - }) - this.project_id = '321l3j1kjkjl' - this.req = { + beforeEach(async function (ctx) { + ctx.ProjectDetailsHandler = { getDetails: sinon.stub() } + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + ctx.controller = (await import(modulePath)).default + ctx.project_id = '321l3j1kjkjl' + ctx.req = { params: { - project_id: this.project_id, + project_id: ctx.project_id, }, session: { destroy: sinon.stub(), }, } - this.res = {} - this.next = sinon.stub() - return (this.projDetails = { name: 'something' }) + ctx.res = {} + ctx.next = sinon.stub() + return (ctx.projDetails = { name: 'something' }) }) describe('getProjectDetails', function () { - it('should ask the project details handler for proj details', function (done) { - this.ProjectDetailsHandler.getDetails.callsArgWith( - 1, - null, - this.projDetails - ) - this.res.json = data => { - this.ProjectDetailsHandler.getDetails - .calledWith(this.project_id) - .should.equal(true) - data.should.deep.equal(this.projDetails) - return done() - } - return this.controller.getProjectDetails(this.req, this.res) + it('should ask the project details handler for proj details', function (ctx) { + return new Promise(resolve => { + ctx.ProjectDetailsHandler.getDetails.callsArgWith( + 1, + null, + ctx.projDetails + ) + ctx.res.json = data => { + ctx.ProjectDetailsHandler.getDetails + .calledWith(ctx.project_id) + .should.equal(true) + data.should.deep.equal(ctx.projDetails) + return resolve() + } + return ctx.controller.getProjectDetails(ctx.req, ctx.res) + }) }) - it('should send a 500 if there is an error', function () { - this.ProjectDetailsHandler.getDetails.callsArgWith(1, 'error') - this.controller.getProjectDetails(this.req, this.res, this.next) - return this.next.calledWith('error').should.equal(true) + it('should send a 500 if there is an error', function (ctx) { + ctx.ProjectDetailsHandler.getDetails.callsArgWith(1, 'error') + ctx.controller.getProjectDetails(ctx.req, ctx.res, ctx.next) + return ctx.next.calledWith('error').should.equal(true) }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index 827d16b737..a051382279 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -12,10 +12,10 @@ const MODULE_PATH = new URL( ).pathname describe('ProjectListController', function () { - beforeEach(async function () { - this.project_id = new ObjectId('abcdefabcdefabcdefabcdef') + beforeEach(async function (ctx) { + ctx.project_id = new ObjectId('abcdefabcdefabcdefabcdef') - this.user = { + ctx.user = { _id: new ObjectId('123456123456123456123456'), email: 'test@overleaf.com', first_name: 'bjkdsjfk', @@ -23,7 +23,7 @@ describe('ProjectListController', function () { emails: [{ email: 'test@overleaf.com' }], lastLoginIp: '111.111.111.112', } - this.users = { + ctx.users = { 'user-1': { first_name: 'James', }, @@ -31,17 +31,17 @@ describe('ProjectListController', function () { first_name: 'Henry', }, } - this.users[this.user._id] = this.user // Owner - this.usersArr = Object.entries(this.users).map(([key, value]) => ({ + ctx.users[ctx.user._id] = ctx.user // Owner + ctx.usersArr = Object.entries(ctx.users).map(([key, value]) => ({ _id: key, ...value, })) - this.tags = [ + ctx.tags = [ { name: 1, project_ids: ['1', '2', '3'] }, { name: 2, project_ids: ['a', '1'] }, { name: 3, project_ids: ['a', 'b', 'c', 'd'] }, ] - this.notifications = [ + ctx.notifications = [ { _id: '1', user_id: '2', @@ -50,63 +50,63 @@ describe('ProjectListController', function () { key: '5', }, ] - this.settings = { + ctx.settings = { siteUrl: 'https://overleaf.com', } - this.TagsHandler = { + ctx.TagsHandler = { promises: { - getAllTags: sinon.stub().resolves(this.tags), + getAllTags: sinon.stub().resolves(ctx.tags), }, } - this.NotificationsHandler = { + ctx.NotificationsHandler = { promises: { - getUserNotifications: sinon.stub().resolves(this.notifications), + getUserNotifications: sinon.stub().resolves(ctx.notifications), }, } - this.UserModel = { - findById: sinon.stub().resolves(this.user), + ctx.UserModel = { + findById: sinon.stub().resolves(ctx.user), } - this.UserPrimaryEmailCheckHandler = { + ctx.UserPrimaryEmailCheckHandler = { requiresPrimaryEmailCheck: sinon.stub().returns(false), } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { findAllUsersProjects: sinon.stub(), }, } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchived: sinon.stub(), isTrashed: sinon.stub(), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.UserController = { + ctx.UserController = { logout: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUsers: sinon.stub().resolves(this.usersArr), + getUsers: sinon.stub().resolves(ctx.usersArr), getUserFullEmails: sinon.stub().resolves([]), }, } - this.Features = { + ctx.Features = { hasFeature: sinon.stub(), } - this.Metrics = { + ctx.Metrics = { inc: sinon.stub(), } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, } - this.SplitTestSessionHandler = { + ctx.SplitTestSessionHandler = { promises: { sessionMaintenance: sinon.stub().resolves(), }, } - this.SubscriptionViewModelBuilder = { + ctx.SubscriptionViewModelBuilder = { promises: { getUsersSubscriptionDetails: sinon.stub().resolves({ bestSubscription: { type: 'free' }, @@ -115,17 +115,17 @@ describe('ProjectListController', function () { }), }, } - this.SurveyHandler = { + ctx.SurveyHandler = { promises: { getSurvey: sinon.stub().resolves({}), }, } - this.NotificationBuilder = { + ctx.NotificationBuilder = { promises: { ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }), }, } - this.GeoIpLookup = { + ctx.GeoIpLookup = { promises: { getCurrencyCode: sinon.stub().resolves({ countryCode: 'US', @@ -133,11 +133,11 @@ describe('ProjectListController', function () { }), }, } - this.TutorialHandler = { + ctx.TutorialHandler = { getInactiveTutorials: sinon.stub().returns([]), } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves([]), @@ -145,58 +145,133 @@ describe('ProjectListController', function () { }, } - this.ProjectListController = await esmock.strict(MODULE_PATH, { - 'mongodb-legacy': { ObjectId }, - '@overleaf/settings': this.settings, - '@overleaf/metrics': this.Metrics, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/SplitTests/SplitTestSessionHandler': - this.SplitTestSessionHandler, - '../../../../app/src/Features/User/UserController': this.UserController, - '../../../../app/src/Features/Project/ProjectHelper': this.ProjectHelper, - '../../../../app/src/Features/Tags/TagsHandler': this.TagsHandler, - '../../../../app/src/Features/Notifications/NotificationsHandler': - this.NotificationsHandler, - '../../../../app/src/models/User': { User: this.UserModel }, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/infrastructure/Features': this.Features, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder': - this.SubscriptionViewModelBuilder, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/Survey/SurveyHandler': this.SurveyHandler, - '../../../../app/src/Features/User/UserPrimaryEmailCheckHandler': - this.UserPrimaryEmailCheckHandler, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationBuilder, - '../../../../app/src/infrastructure/GeoIpLookup': this.GeoIpLookup, - '../../../../app/src/Features/Tutorial/TutorialHandler': - this.TutorialHandler, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) - this.req = { + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.Metrics, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestSessionHandler', + () => ({ + default: ctx.SplitTestSessionHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserController', () => ({ + default: ctx.UserController, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsHandler', + () => ({ + default: ctx.NotificationsHandler, + }) + ) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.UserModel, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder', + () => ({ + default: ctx.SubscriptionViewModelBuilder, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock('../../../../app/src/Features/Survey/SurveyHandler', () => ({ + default: ctx.SurveyHandler, + })) + + vi.doMock( + '../../../../app/src/Features/User/UserPrimaryEmailCheckHandler', + () => ({ + default: ctx.UserPrimaryEmailCheckHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationBuilder, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/GeoIpLookup', () => ({ + default: ctx.GeoIpLookup, + })) + + vi.doMock('../../../../app/src/Features/Tutorial/TutorialHandler', () => ({ + default: ctx.TutorialHandler, + })) + + ctx.ProjectListController = (await import(MODULE_PATH)).default + + ctx.req = { query: {}, params: { - Project_id: this.project_id, + Project_id: ctx.project_id, }, headers: {}, session: { - user: this.user, + user: ctx.user, }, body: {}, i18n: { translate() {}, }, } - this.res = {} + ctx.res = {} }) describe('projectListPage', function () { - beforeEach(function () { - this.projects = [ + beforeEach(function (ctx) { + ctx.projects = [ { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, { _id: 2, @@ -205,184 +280,206 @@ describe('ProjectListController', function () { lastUpdatedBy: 'user-1', }, ] - this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] - this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] - this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] - this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }] - this.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }] - this.allProjects = { - owned: this.projects, - readAndWrite: this.readAndWrite, - readOnly: this.readOnly, - tokenReadAndWrite: this.tokenReadAndWrite, - tokenReadOnly: this.tokenReadOnly, - review: this.review, + ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + ctx.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }] + ctx.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }] + ctx.allProjects = { + owned: ctx.projects, + readAndWrite: ctx.readAndWrite, + readOnly: ctx.readOnly, + tokenReadAndWrite: ctx.tokenReadAndWrite, + tokenReadOnly: ctx.tokenReadOnly, + review: ctx.review, } - this.ProjectGetter.promises.findAllUsersProjects.resolves( - this.allProjects - ) + ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects) }) - it('should render the project/list-react page', function (done) { - this.res.render = (pageName, opts) => { - pageName.should.equal('project/list-react') - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should invoke the session maintenance', function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.res.render = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req, - this.user - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the tags', function (done) { - this.res.render = (pageName, opts) => { - opts.tags.length.should.equal(this.tags.length) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should create trigger ip matcher notifications', function (done) { - this.settings.overleaf = true - this.req.ip = '111.111.111.111' - this.res.render = (pageName, opts) => { - this.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal( - true - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the projects', function (done) { - this.res.render = (pageName, opts) => { - opts.prefetchedProjectsBlob.projects.length.should.equal( - this.projects.length + - this.readAndWrite.length + - this.readOnly.length + - this.tokenReadAndWrite.length + - this.tokenReadOnly.length + - this.review.length - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the user', function (done) { - this.res.render = (pageName, opts) => { - opts.user.should.deep.equal(this.user) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should inject the users', function (done) { - this.res.render = (pageName, opts) => { - const projects = opts.prefetchedProjectsBlob.projects - - projects - .filter(p => p.id === '1')[0] - .owner.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 1)[0].owner_ref] - .first_name - ) - projects - .filter(p => p.id === '2')[0] - .owner.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 2)[0].owner_ref] - .first_name - ) - projects - .filter(p => p.id === '2')[0] - .lastUpdatedBy.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 2)[0].lastUpdatedBy] - .first_name - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it("should send the user's best subscription when saas feature present", function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.res.render = (pageName, opts) => { - expect(opts.usersBestSubscription).to.deep.include({ type: 'free' }) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should not return a best subscription without saas feature', function (done) { - this.Features.hasFeature.withArgs('saas').returns(false) - this.res.render = (pageName, opts) => { - expect(opts.usersBestSubscription).to.be.undefined - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should show INR Banner for Indian users with free account', function (done) { - // usersBestSubscription is only available when saas feature is present - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( - { - bestSubscription: { - type: 'free', - }, + it('should render the project/list-react page', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + pageName.should.equal('project/list-react') + resolve() } - ) - this.GeoIpLookup.promises.getCurrencyCode.resolves({ - countryCode: 'IN', + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - this.res.render = (pageName, opts) => { - expect(opts.showInrGeoBanner).to.be.true - done() - } - this.ProjectListController.projectListPage(this.req, this.res) }) - it('should not show INR Banner for Indian users with premium account', function (done) { - // usersBestSubscription is only available when saas feature is present - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( - { - bestSubscription: { - type: 'individual', - }, + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.res.render = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req, + ctx.user + ) + resolve() } - ) - this.GeoIpLookup.promises.getCurrencyCode.resolves({ - countryCode: 'IN', + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the tags', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.tags.length.should.equal(ctx.tags.length) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should create trigger ip matcher notifications', function (ctx) { + return new Promise(resolve => { + ctx.settings.overleaf = true + ctx.req.ip = '111.111.111.111' + ctx.res.render = (pageName, opts) => { + ctx.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal( + true + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the projects', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.prefetchedProjectsBlob.projects.length.should.equal( + ctx.projects.length + + ctx.readAndWrite.length + + ctx.readOnly.length + + ctx.tokenReadAndWrite.length + + ctx.tokenReadOnly.length + + ctx.review.length + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the user', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.user.should.deep.equal(ctx.user) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should inject the users', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + const projects = opts.prefetchedProjectsBlob.projects + + projects + .filter(p => p.id === '1')[0] + .owner.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 1)[0].owner_ref] + .first_name + ) + projects + .filter(p => p.id === '2')[0] + .owner.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 2)[0].owner_ref] + .first_name + ) + projects + .filter(p => p.id === '2')[0] + .lastUpdatedBy.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 2)[0].lastUpdatedBy] + .first_name + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it("should send the user's best subscription when saas feature present", function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.res.render = (pageName, opts) => { + expect(opts.usersBestSubscription).to.deep.include({ type: 'free' }) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should not return a best subscription without saas feature', function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(false) + ctx.res.render = (pageName, opts) => { + expect(opts.usersBestSubscription).to.be.undefined + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should show INR Banner for Indian users with free account', function (ctx) { + return new Promise(resolve => { + // usersBestSubscription is only available when saas feature is present + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + { + bestSubscription: { + type: 'free', + }, + } + ) + ctx.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'IN', + }) + ctx.res.render = (pageName, opts) => { + expect(opts.showInrGeoBanner).to.be.true + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should not show INR Banner for Indian users with premium account', function (ctx) { + return new Promise(resolve => { + // usersBestSubscription is only available when saas feature is present + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + { + bestSubscription: { + type: 'individual', + }, + } + ) + ctx.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'IN', + }) + ctx.res.render = (pageName, opts) => { + expect(opts.showInrGeoBanner).to.be.false + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - this.res.render = (pageName, opts) => { - expect(opts.showInrGeoBanner).to.be.false - done() - } - this.ProjectListController.projectListPage(this.req, this.res) }) describe('With Institution SSO feature', function () { - beforeEach(function (done) { - this.institutionEmail = 'test@overleaf.com' - this.institutionName = 'Overleaf' - this.Features.hasFeature.withArgs('saml').returns(true) - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.Features.hasFeature.withArgs('saas').returns(true) - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.institutionEmail = 'test@overleaf.com' + ctx.institutionName = 'Overleaf' + ctx.Features.hasFeature.withArgs('saml').returns(true) + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.Features.hasFeature.withArgs('saas').returns(true) + resolve() + }) }) - it('should show institution SSO available notification for confirmed domains', function () { - this.UserGetter.promises.getUserFullEmails.resolves([ + it('should show institution SSO available notification for confirmed domains', function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@overleaf.com', affiliation: { @@ -396,64 +493,64 @@ describe('ProjectListController', function () { }, }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, + email: ctx.institutionEmail, institutionId: 1, - institutionName: this.institutionName, + institutionName: ctx.institutionName, templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a linked notification', function () { - this.req.session.saml = { - institutionEmail: this.institutionEmail, + it('should show a linked notification', function (ctx) { + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, - institutionName: this.institutionName, + email: ctx.institutionEmail, + institutionName: ctx.institutionName, templateKey: 'notification_institution_sso_linked', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a linked another email notification', function () { + it('should show a linked another email notification', function (ctx) { // when they request to link an email but the institution returns // a different email - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - institutionEmail: this.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: 'requested@overleaf.com', templateKey: 'notification_institution_sso_non_canonical', }) } - this.req.session.saml = { - emailNonCanonical: this.institutionEmail, - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + emailNonCanonical: ctx.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: 'requested@overleaf.com', linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a notification when intent was to register via SSO but account existed', function () { - this.res.render = (pageName, opts) => { + it('should show a notification when intent was to register via SSO but account existed', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, + email: ctx.institutionEmail, templateKey: 'notification_institution_sso_already_registered', }) } - this.req.session.saml = { - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, universityName: 'Overleaf', @@ -463,29 +560,29 @@ describe('ProjectListController', function () { name: 'Example University', }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should not show a register notification if the flow was abandoned', function () { + it('should not show a register notification if the flow was abandoned', function (ctx) { // could initially start to register with an SSO email and then // abandon flow and login with an existing non-institution SSO email - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', templateKey: 'notification_institution_sso_already_registered', }) } - this.req.session.saml = { + ctx.req.session.saml = { registerIntercept: { id: 1, name: 'Example University', }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show error notification', function () { - this.res.render = (pageName, opts) => { + it('should show error notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution.length).to.equal(1) expect(opts.notificationsInstitution[0].templateKey).to.equal( 'notification_institution_sso_error' @@ -494,81 +591,85 @@ describe('ProjectListController', function () { Errors.SAMLAlreadyLinkedError ) } - this.req.session.saml = { - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, error: new Errors.SAMLAlreadyLinkedError(), } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) describe('for an unconfirmed domain for an SSO institution', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUserFullEmails.resolves([ - { - email: 'test@overleaf-uncofirmed.com', - affiliation: { - institution: { - id: 1, - confirmed: false, - name: 'Overleaf', - ssoBeta: false, - ssoEnabled: true, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserFullEmails.resolves([ + { + email: 'test@overleaf-uncofirmed.com', + affiliation: { + institution: { + id: 1, + confirmed: false, + name: 'Overleaf', + ssoBeta: false, + ssoEnabled: true, + }, }, }, - }, - ]) - done() + ]) + resolve() + }) }) - it('should not show institution SSO available notification', function () { - this.res.render = (pageName, opts) => { + it('should not show institution SSO available notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution.length).to.equal(0) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('when linking/logging in initiated on institution side', function () { - it('should not show a linked another email notification', function () { + it('should not show a linked another email notification', function (ctx) { // this is only used when initated on Overleaf, // because we keep track of the requested email they tried to link - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.not.deep.include({ - institutionEmail: this.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: undefined, templateKey: 'notification_institution_sso_non_canonical', }) } - this.req.session.saml = { - emailNonCanonical: this.institutionEmail, - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + emailNonCanonical: ctx.institutionEmail, + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('Institution with SSO beta testable', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUserFullEmails.resolves([ - { - email: 'beta@beta.com', - affiliation: { - institution: { - id: 2, - confirmed: true, - name: 'Beta University', - ssoBeta: true, - ssoEnabled: false, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserFullEmails.resolves([ + { + email: 'beta@beta.com', + affiliation: { + institution: { + id: 2, + confirmed: true, + name: 'Beta University', + ssoBeta: true, + ssoEnabled: false, + }, }, }, - }, - ]) - done() + ]) + resolve() + }) }) - it('should show institution SSO available notification when on a beta testing session', function () { - this.req.session.samlBeta = true - this.res.render = (pageName, opts) => { + it('should show institution SSO available notification when on a beta testing session', function (ctx) { + ctx.req.session.samlBeta = true + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ email: 'beta@beta.com', institutionId: 2, @@ -576,11 +677,11 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should not show institution SSO available notification when not on a beta testing session', function () { - this.req.session.samlBeta = false - this.res.render = (pageName, opts) => { + it('should not show institution SSO available notification when not on a beta testing session', function (ctx) { + ctx.req.session.samlBeta = false + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', institutionId: 1, @@ -588,18 +689,20 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) }) describe('Without Institution SSO feature', function () { - beforeEach(function (done) { - this.Features.hasFeature.withArgs('saml').returns(false) - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saml').returns(false) + resolve() + }) }) - it('should not show institution sso available notification', function () { - this.res.render = (pageName, opts) => { + it('should not show institution sso available notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', institutionId: 1, @@ -607,35 +710,33 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('enterprise banner', function () { - beforeEach(function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + beforeEach(function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( { memberGroupSubscriptions: [] } ) - this.UserGetter.promises.getUserFullEmails.resolves([ + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@test-domain.com', }, ]) - - done() }) describe('normal enterprise banner', function () { - it('shows banner', function () { - this.res.render = (pageName, opts) => { + it('shows banner', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.true } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('does not show banner if user is part of any affiliation', function () { - this.UserGetter.promises.getUserFullEmails.resolves([ + it('does not show banner if user is part of any affiliation', function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@overleaf.com', affiliation: { @@ -651,36 +752,36 @@ describe('ProjectListController', function () { }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('does not show banner if user is part of any group subscription', function () { - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + it('does not show banner if user is part of any group subscription', function (ctx) { + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( { memberGroupSubscriptions: [{}] } ) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('have a banner variant of "FOMO" or "on-premise"', function () { - this.res.render = (pageName, opts) => { + it('have a banner variant of "FOMO" or "on-premise"', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.groupsAndEnterpriseBannerVariant).to.be.oneOf([ 'FOMO', 'on-premise', ]) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('US government enterprise banner', function () { - it('does not show enterprise banner if US government enterprise banner is shown', function () { + it('does not show enterprise banner if US government enterprise banner is shown', function (ctx) { const emails = [ { email: 'test@test.mil', @@ -688,8 +789,8 @@ describe('ProjectListController', function () { }, ] - this.UserGetter.promises.getUserFullEmails.resolves(emails) - this.Modules.promises.hooks.fire + ctx.UserGetter.promises.getUserFullEmails.resolves(emails) + ctx.Modules.promises.hooks.fire .withArgs('getUSGovBanner', emails, false, []) .resolves([ { @@ -697,66 +798,68 @@ describe('ProjectListController', function () { usGovBannerVariant: 'variant', }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false expect(opts.showUSGovBanner).to.be.true } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) }) }) describe('projectListReactPage with duplicate projects', function () { - beforeEach(function () { - this.projects = [ + beforeEach(function (ctx) { + ctx.projects = [ { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, { _id: 2, lastUpdated: 2, owner_ref: 'user-2' }, ] - this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] - this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] - this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] - this.tokenReadOnly = [ + ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + ctx.tokenReadOnly = [ { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite { _id: 7, lastUpdated: 4, owner_ref: 'user-5' }, ] - this.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }] - this.allProjects = { - owned: this.projects, - readAndWrite: this.readAndWrite, - readOnly: this.readOnly, - tokenReadAndWrite: this.tokenReadAndWrite, - tokenReadOnly: this.tokenReadOnly, - review: this.review, + ctx.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }] + ctx.allProjects = { + owned: ctx.projects, + readAndWrite: ctx.readAndWrite, + readOnly: ctx.readOnly, + tokenReadAndWrite: ctx.tokenReadAndWrite, + tokenReadOnly: ctx.tokenReadOnly, + review: ctx.review, } - this.ProjectGetter.promises.findAllUsersProjects.resolves( - this.allProjects - ) + ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects) }) - it('should render the project/list-react page', function (done) { - this.res.render = (pageName, opts) => { - pageName.should.equal('project/list-react') - done() - } - this.ProjectListController.projectListPage(this.req, this.res) + it('should render the project/list-react page', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + pageName.should.equal('project/list-react') + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) }) - it('should omit one of the projects', function (done) { - this.res.render = (pageName, opts) => { - opts.prefetchedProjectsBlob.projects.length.should.equal( - this.projects.length + - this.readAndWrite.length + - this.readOnly.length + - this.tokenReadAndWrite.length + - this.tokenReadOnly.length + - this.review.length - - 1 - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) + it('should omit one of the projects', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.prefetchedProjectsBlob.projects.length.should.equal( + ctx.projects.length + + ctx.readAndWrite.length + + ctx.readOnly.length + + ctx.tokenReadAndWrite.length + + ctx.tokenReadOnly.length + + ctx.review.length - + 1 + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/Referal/ReferalConnect.test.mjs b/services/web/test/unit/src/Referal/ReferalConnect.test.mjs index c6e56c3c6a..33e6c6816e 100644 --- a/services/web/test/unit/src/Referal/ReferalConnect.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalConnect.test.mjs @@ -1,132 +1,153 @@ -import esmock from 'esmock' const modulePath = new URL( '../../../../app/src/Features/Referal/ReferalConnect.mjs', import.meta.url ).pathname describe('Referal connect middle wear', function () { - beforeEach(async function () { - this.connect = await esmock.strict(modulePath, {}) + beforeEach(async function (ctx) { + ctx.connect = (await import(modulePath)).default }) - it('should take a referal query string and put it on the session if it exists', function (done) { - const req = { - query: { referal: '12345' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal(req.query.referal) - done() + it('should take a referal query string and put it on the session if it exists', function (ctx) { + return new Promise(resolve => { + const req = { + query: { referal: '12345' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal(req.query.referal) + resolve() + }) }) }) - it('should not change the referal_id on the session if not in query', function (done) { - const req = { - query: {}, - session: { referal_id: 'same' }, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal('same') - done() + it('should not change the referal_id on the session if not in query', function (ctx) { + return new Promise(resolve => { + const req = { + query: {}, + session: { referal_id: 'same' }, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal('same') + resolve() + }) }) }) - it('should take a facebook referal query string and put it on the session if it exists', function (done) { - const req = { - query: { fb_ref: '12345' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal(req.query.fb_ref) - done() + it('should take a facebook referal query string and put it on the session if it exists', function (ctx) { + return new Promise(resolve => { + const req = { + query: { fb_ref: '12345' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal(req.query.fb_ref) + resolve() + }) }) }) - it('should map the facebook medium into the session', function (done) { - const req = { - query: { rm: 'fb' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('facebook') - done() + it('should map the facebook medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'fb' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('facebook') + resolve() + }) }) }) - it('should map the twitter medium into the session', function (done) { - const req = { - query: { rm: 't' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('twitter') - done() + it('should map the twitter medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 't' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('twitter') + resolve() + }) }) }) - it('should map the google plus medium into the session', function (done) { - const req = { - query: { rm: 'gp' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('google_plus') - done() + it('should map the google plus medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'gp' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('google_plus') + resolve() + }) }) }) - it('should map the email medium into the session', function (done) { - const req = { - query: { rm: 'e' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('email') - done() + it('should map the email medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'e' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('email') + resolve() + }) }) }) - it('should map the direct medium into the session', function (done) { - const req = { - query: { rm: 'd' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('direct') - done() + it('should map the direct medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'd' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('direct') + resolve() + }) }) }) - it('should map the bonus source into the session', function (done) { - const req = { - query: { rs: 'b' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('bonus') - done() + it('should map the bonus source into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'b' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('bonus') + resolve() + }) }) }) - it('should map the public share source into the session', function (done) { - const req = { - query: { rs: 'ps' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('public_share') - done() + it('should map the public share source into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'ps' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('public_share') + resolve() + }) }) }) - it('should map the collaborator invite into the session', function (done) { - const req = { - query: { rs: 'ci' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('collaborator_invite') - done() + it('should map the collaborator invite into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'ci' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('collaborator_invite') + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Referal/ReferalController.test.mjs b/services/web/test/unit/src/Referal/ReferalController.test.mjs index 523fd23728..0a7b8aa87d 100644 --- a/services/web/test/unit/src/Referal/ReferalController.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalController.test.mjs @@ -1,11 +1,7 @@ -import esmock from 'esmock' -const modulePath = new URL( - '../../../../app/src/Features/Referal/ReferalController.js', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Referal/ReferalController.js' -describe('Referal controller', function () { - beforeEach(async function () { - this.controller = await esmock.strict(modulePath, {}) +describe.skip('Referal controller', function () { + beforeEach(async function (ctx) { + ctx.controller = (await import(modulePath)).default }) }) diff --git a/services/web/test/unit/src/Referal/ReferalHandler.test.mjs b/services/web/test/unit/src/Referal/ReferalHandler.test.mjs index 6fd58a6569..5174918bd7 100644 --- a/services/web/test/unit/src/Referal/ReferalHandler.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalHandler.test.mjs @@ -1,88 +1,86 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -const modulePath = new URL( - '../../../../app/src/Features/Referal/ReferalHandler.mjs', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Referal/ReferalHandler.mjs' describe('Referal handler', function () { - beforeEach(async function () { - this.User = { + beforeEach(async function (ctx) { + ctx.User = { findById: sinon.stub().returns({ exec: sinon.stub(), }), } - this.handler = await esmock.strict(modulePath, { - '../../../../app/src/models/User': { - User: this.User, - }, - }) - this.user_id = '12313' + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + ctx.handler = (await import(modulePath)).default + ctx.user_id = '12313' }) describe('getting refered user_ids', function () { - it('should get the user from mongo and return the refered users array', async function () { + it('should get the user from mongo and return the refered users array', async function (ctx) { const user = { refered_users: ['1234', '312312', '3213129'], refered_user_count: 3, } - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUsers: passedReferedUserIds, referedUserCount: passedReferedUserCount, - } = await this.handler.promises.getReferedUsers(this.user_id) + } = await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserIds.should.deep.equal(user.refered_users) passedReferedUserCount.should.equal(3) }) - it('should return an empty array if it is not set', async function () { + it('should return an empty array if it is not set', async function (ctx) { const user = {} - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUsers: passedReferedUserIds } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserIds.length.should.equal(0) }) - it('should return a zero count if neither it or the array are set', async function () { + it('should return a zero count if neither it or the array are set', async function (ctx) { const user = {} - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUserCount: passedReferedUserCount } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserCount.should.equal(0) }) - it('should return the array length if count is not set', async function () { + it('should return the array length if count is not set', async function (ctx) { const user = { refered_users: ['1234', '312312', '3213129'] } - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUserCount: passedReferedUserCount } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserCount.should.equal(3) }) - it('should error if finding the user fails', async function () { - this.User.findById.returns({ + it('should error if finding the user fails', async function (ctx) { + ctx.User.findById.returns({ exec: sinon.stub().rejects(new Error('user not found')), }) expect( - this.handler.promises.getReferedUsers(this.user_id) + ctx.handler.promises.getReferedUsers(ctx.user_id) ).to.be.rejectedWith('user not found') }) }) diff --git a/services/web/test/unit/src/References/ReferencesController.test.mjs b/services/web/test/unit/src/References/ReferencesController.test.mjs index fca2acea12..679e835840 100644 --- a/services/web/test/unit/src/References/ReferencesController.test.mjs +++ b/services/web/test/unit/src/References/ReferencesController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' @@ -6,182 +6,207 @@ const modulePath = '../../../../app/src/Features/References/ReferencesController' describe('ReferencesController', function () { - beforeEach(async function () { - this.projectId = '2222' - this.controller = await esmock.strict(modulePath, { - '@overleaf/settings': (this.settings = { + beforeEach(async function (ctx) { + ctx.projectId = '2222' + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { apis: { web: { url: 'http://some.url' } }, }), - '../../../../app/src/Features/References/ReferencesHandler': - (this.ReferencesHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/References/ReferencesHandler', + () => ({ + default: (ctx.ReferencesHandler = { index: sinon.stub(), indexAll: sinon.stub(), }), - '../../../../app/src/Features/Editor/EditorRealTimeController': - (this.EditorRealTimeController = { + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: (ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), }), - }) - this.req = new MockRequest() - this.req.params.Project_id = this.projectId - this.req.body = { - docIds: (this.docIds = ['aaa', 'bbb']), + }) + ) + + ctx.controller = (await import(modulePath)).default + ctx.req = new MockRequest() + ctx.req.params.Project_id = ctx.projectId + ctx.req.body = { + docIds: (ctx.docIds = ['aaa', 'bbb']), shouldBroadcast: false, } - this.res = new MockResponse() - this.res.json = sinon.stub() - this.res.sendStatus = sinon.stub() - this.next = sinon.stub() - this.fakeResponseData = { - projectId: this.projectId, + ctx.res = new MockResponse() + ctx.res.json = sinon.stub() + ctx.res.sendStatus = sinon.stub() + ctx.next = sinon.stub() + ctx.fakeResponseData = { + projectId: ctx.projectId, keys: ['one', 'two', 'three'], } }) describe('indexAll', function () { - beforeEach(function () { - this.req.body = { shouldBroadcast: false } - this.ReferencesHandler.indexAll.callsArgWith( - 1, - null, - this.fakeResponseData - ) - this.call = callback => { - this.controller.indexAll(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.body = { shouldBroadcast: false } + ctx.ReferencesHandler.indexAll.callsArgWith(1, null, ctx.fakeResponseData) + ctx.call = callback => { + ctx.controller.indexAll(ctx.req, ctx.res, ctx.next) return callback() } }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) - it('should call ReferencesHandler.indexAll', function (done) { - this.call(() => { - this.ReferencesHandler.indexAll.callCount.should.equal(1) - this.ReferencesHandler.indexAll - .calledWith(this.projectId) - .should.equal(true) - done() + it('should call ReferencesHandler.indexAll', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.ReferencesHandler.indexAll.callCount.should.equal(1) + ctx.ReferencesHandler.indexAll + .calledWith(ctx.projectId) + .should.equal(true) + resolve() + }) }) }) describe('when shouldBroadcast is true', function () { - beforeEach(function () { - this.ReferencesHandler.index.callsArgWith( - 2, - null, - this.fakeResponseData - ) - this.req.body.shouldBroadcast = true + beforeEach(function (ctx) { + ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData) + ctx.req.body.shouldBroadcast = true }) - it('should call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - done() + it('should call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should still return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should still return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) }) describe('when shouldBroadcast is false', function () { - beforeEach(function () { - this.ReferencesHandler.index.callsArgWith( - 2, - null, - this.fakeResponseData - ) - this.req.body.shouldBroadcast = false + beforeEach(function (ctx) { + ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData) + ctx.req.body.shouldBroadcast = false }) - it('should not call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) - done() + it('should not call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should still return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should still return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) }) }) describe('there is no data', function () { - beforeEach(function () { - this.ReferencesHandler.indexAll.callsArgWith(1) - this.call = callback => { - this.controller.indexAll(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.ReferencesHandler.indexAll.callsArgWith(1) + ctx.call = callback => { + ctx.controller.indexAll(ctx.req, ctx.res, ctx.next) callback() } }) - it('should not call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) - done() + it('should not call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should send a response with an empty keys list', function (done) { - this.call(() => { - this.res.json.called.should.equal(true) - this.res.json - .calledWith({ projectId: this.projectId, keys: [] }) - .should.equal(true) - done() + it('should send a response with an empty keys list', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.called.should.equal(true) + ctx.res.json + .calledWith({ projectId: ctx.projectId, keys: [] }) + .should.equal(true) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/References/ReferencesHandler.test.mjs b/services/web/test/unit/src/References/ReferencesHandler.test.mjs index 57570dcf12..ae7b86822a 100644 --- a/services/web/test/unit/src/References/ReferencesHandler.test.mjs +++ b/services/web/test/unit/src/References/ReferencesHandler.test.mjs @@ -1,11 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' @@ -13,13 +6,17 @@ import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/References/ReferencesHandler.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('ReferencesHandler', function () { - beforeEach(async function () { - this.projectId = '222' - this.historyId = 42 - this.fakeProject = { - _id: this.projectId, - owner_ref: (this.fakeOwner = { + beforeEach(async function (ctx) { + ctx.projectId = '222' + ctx.historyId = 42 + ctx.fakeProject = { + _id: ctx.projectId, + owner_ref: (ctx.fakeOwner = { _id: 'some_owner', features: { references: false, @@ -43,11 +40,12 @@ describe('ReferencesHandler', function () { ], }, ], - overleaf: { history: { id: this.historyId } }, + overleaf: { history: { id: ctx.historyId } }, } - this.docIds = ['aaa', 'ccc'] - this.handler = await esmock.strict(modulePath, { - '@overleaf/settings': (this.settings = { + ctx.docIds = ['aaa', 'ccc'] + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { apis: { references: { url: 'http://some.url/references' }, docstore: { url: 'http://some.url/docstore' }, @@ -56,228 +54,278 @@ describe('ReferencesHandler', function () { }, enableProjectHistoryBlobs: true, }), - request: (this.request = { + })) + + vi.doMock('request', () => ({ + default: (ctx.request = { get: sinon.stub(), post: sinon.stub(), }), - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = { - getProject: sinon.stub().callsArgWith(2, null, this.fakeProject), - }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, ctx.fakeProject), + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { getUser: sinon.stub(), }), - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler': - (this.DocumentUpdaterHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: (ctx.DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null), }), - '../../../../app/src/infrastructure/Features': (this.Features = { + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: (ctx.Features = { hasFeature: sinon.stub().returns(true), }), - }) - this.fakeResponseData = { - projectId: this.projectId, + })) + + ctx.handler = (await import(modulePath)).default + ctx.fakeResponseData = { + projectId: ctx.projectId, keys: ['k1', 'k2'], } }) describe('indexAll', function () { - beforeEach(function () { - sinon.stub(this.handler, '_findBibDocIds').returns(['aaa', 'ccc']) + beforeEach(function (ctx) { + sinon.stub(ctx.handler, '_findBibDocIds').returns(['aaa', 'ccc']) sinon - .stub(this.handler, '_findBibFileRefs') + .stub(ctx.handler, '_findBibFileRefs') .returns([{ _id: 'fff' }, { _id: 'ggg', hash: 'hash' }]) - sinon.stub(this.handler, '_isFullIndex').callsArgWith(1, null, true) - this.request.post.callsArgWith( + sinon.stub(ctx.handler, '_isFullIndex').callsArgWith(1, null, true) + ctx.request.post.callsArgWith( 1, null, { statusCode: 200 }, - this.fakeResponseData + ctx.fakeResponseData ) - return (this.call = callback => { - return this.handler.indexAll(this.projectId, callback) + return (ctx.call = callback => { + return ctx.handler.indexAll(ctx.projectId, callback) }) }) - it('should call _findBibDocIds', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.handler._findBibDocIds.callCount.should.equal(1) - this.handler._findBibDocIds - .calledWith(this.fakeProject) - .should.equal(true) - return done() + it('should call _findBibDocIds', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.handler._findBibDocIds.callCount.should.equal(1) + ctx.handler._findBibDocIds + .calledWith(ctx.fakeProject) + .should.equal(true) + return resolve() + }) }) }) - it('should call _findBibFileRefs', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.handler._findBibDocIds.callCount.should.equal(1) - this.handler._findBibDocIds - .calledWith(this.fakeProject) - .should.equal(true) - return done() + it('should call _findBibFileRefs', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.handler._findBibDocIds.callCount.should.equal(1) + ctx.handler._findBibDocIds + .calledWith(ctx.fakeProject) + .should.equal(true) + return resolve() + }) }) }) - it('should call DocumentUpdaterHandler.flushDocToMongo', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) - return done() + it('should call DocumentUpdaterHandler.flushDocToMongo', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) + return resolve() + }) }) }) - it('should make a request to references service', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.request.post.callCount.should.equal(1) - const arg = this.request.post.firstCall.args[0] - expect(arg.json).to.have.all.keys('docUrls', 'sourceURLs', 'fullIndex') - expect(arg.json.docUrls.length).to.equal(4) - expect(arg.json.docUrls).to.deep.equal([ - `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/aaa/raw`, - `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/ccc/raw`, - `${this.settings.apis.filestore.url}/project/${this.projectId}/file/fff?from=bibFileUrls`, - `${this.settings.apis.filestore.url}/project/${this.projectId}/file/ggg?from=bibFileUrls`, - ]) - expect(arg.json.sourceURLs.length).to.equal(4) - expect(arg.json.sourceURLs).to.deep.equal([ - { - url: `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/aaa/raw`, - }, - { - url: `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/ccc/raw`, - }, - { - url: `${this.settings.apis.filestore.url}/project/${this.projectId}/file/fff?from=bibFileUrls`, - }, - { - url: `${this.settings.apis.project_history.url}/project/${this.historyId}/blob/hash`, - fallbackURL: `${this.settings.apis.filestore.url}/project/${this.projectId}/file/ggg?from=bibFileUrls`, - }, - ]) - expect(arg.json.fullIndex).to.equal(true) - return done() + it('should make a request to references service', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.request.post.callCount.should.equal(1) + const arg = ctx.request.post.firstCall.args[0] + expect(arg.json).to.have.all.keys( + 'docUrls', + 'sourceURLs', + 'fullIndex' + ) + expect(arg.json.docUrls.length).to.equal(4) + expect(arg.json.docUrls).to.deep.equal([ + `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`, + `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`, + `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/fff?from=bibFileUrls`, + `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/ggg?from=bibFileUrls`, + ]) + expect(arg.json.sourceURLs.length).to.equal(4) + expect(arg.json.sourceURLs).to.deep.equal([ + { + url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`, + }, + { + url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`, + }, + { + url: `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/fff?from=bibFileUrls`, + }, + { + url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/hash`, + fallbackURL: `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/ggg?from=bibFileUrls`, + }, + ]) + expect(arg.json.fullIndex).to.equal(true) + return resolve() + }) }) }) - it('should not produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.equal(null) - return done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.equal(null) + return resolve() + }) }) }) - it('should return data', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - expect(data).to.not.equal(null) - expect(data).to.not.equal(undefined) - expect(data).to.equal(this.fakeResponseData) - return done() + it('should return data', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + expect(data).to.not.equal(null) + expect(data).to.not.equal(undefined) + expect(data).to.equal(ctx.fakeResponseData) + return resolve() + }) }) }) describe('when ProjectGetter.getProject produces an error', function () { - beforeEach(function () { - return this.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when ProjectGetter.getProject returns null', function () { - beforeEach(function () { - return this.ProjectGetter.getProject.callsArgWith(2, null) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Errors.NotFoundError) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Errors.NotFoundError) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when _isFullIndex produces an error', function () { - beforeEach(function () { - this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) - return this.handler._isFullIndex.callsArgWith(1, new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject) + ctx.handler._isFullIndex.callsArgWith(1, new Error('woops')) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when flushDocToMongo produces an error', function () { - beforeEach(function () { - this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) - this.handler._isFullIndex.callsArgWith(1, false) - return this.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject) + ctx.handler._isFullIndex.callsArgWith(1, false) + ctx.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( 2, new Error('woops') ) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) }) describe('_findBibDocIds', function () { - beforeEach(function () { - this.fakeProject = { + beforeEach(function (ctx) { + ctx.fakeProject = { rootFolder: [ { docs: [ @@ -290,24 +338,24 @@ describe('ReferencesHandler', function () { }, ], } - return (this.expectedIds = ['aaa', 'ccc']) + ctx.expectedIds = ['aaa', 'ccc'] }) - it('should select the correct docIds', function () { - const result = this.handler._findBibDocIds(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should select the correct docIds', function (ctx) { + const result = ctx.handler._findBibDocIds(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) - it('should not error with a non array of folders from dirty data', function () { - this.fakeProject.rootFolder[0].folders[0].folders = {} - const result = this.handler._findBibDocIds(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should not error with a non array of folders from dirty data', function (ctx) { + ctx.fakeProject.rootFolder[0].folders[0].folders = {} + const result = ctx.handler._findBibDocIds(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) }) describe('_findBibFileRefs', function () { - beforeEach(function () { - this.fakeProject = { + beforeEach(function (ctx) { + ctx.fakeProject = { rootFolder: [ { docs: [ @@ -325,73 +373,73 @@ describe('ReferencesHandler', function () { }, ], } - this.expectedIds = [ - this.fakeProject.rootFolder[0].fileRefs[0], - this.fakeProject.rootFolder[0].folders[0].fileRefs[0], + ctx.expectedIds = [ + ctx.fakeProject.rootFolder[0].fileRefs[0], + ctx.fakeProject.rootFolder[0].folders[0].fileRefs[0], ] }) - it('should select the correct docIds', function () { - const result = this.handler._findBibFileRefs(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should select the correct docIds', function (ctx) { + const result = ctx.handler._findBibFileRefs(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) }) describe('_isFullIndex', function () { - beforeEach(function () { - this.fakeProject = { owner_ref: (this.owner_ref = 'owner-ref-123') } - this.owner = { + beforeEach(function (ctx) { + ctx.fakeProject = { owner_ref: (ctx.owner_ref = 'owner-ref-123') } + ctx.owner = { features: { references: false, }, } - this.UserGetter.getUser = sinon.stub() - this.UserGetter.getUser - .withArgs(this.owner_ref, { features: true }) - .yields(null, this.owner) - return (this.call = callback => { - return this.handler._isFullIndex(this.fakeProject, callback) - }) + ctx.UserGetter.getUser = sinon.stub() + ctx.UserGetter.getUser + .withArgs(ctx.owner_ref, { features: true }) + .yields(null, ctx.owner) + ctx.call = callback => { + ctx.handler._isFullIndex(ctx.fakeProject, callback) + } }) describe('with references feature on', function () { - beforeEach(function () { - return (this.owner.features.references = true) + beforeEach(function (ctx) { + ctx.owner.features.references = true }) - it('should return true', function () { - return this.call((err, isFullIndex) => { + it('should return true', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(true) + expect(isFullIndex).to.equal(true) }) }) }) describe('with references feature off', function () { - beforeEach(function () { - return (this.owner.features.references = false) + beforeEach(function (ctx) { + ctx.owner.features.references = false }) - it('should return false', function () { - return this.call((err, isFullIndex) => { + it('should return false', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(false) + expect(isFullIndex).to.equal(false) }) }) }) describe('with referencesSearch', function () { - beforeEach(function () { - return (this.owner.features = { + beforeEach(function (ctx) { + ctx.owner.features = { referencesSearch: true, references: false, - }) + } }) - it('should return true', function () { - return this.call((err, isFullIndex) => { + it('should return true', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(true) + expect(isFullIndex).to.equal(true) }) }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs index c1ce6733ca..30301ec8cc 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs @@ -1,68 +1,68 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/Subscription/SubscriptionGroupController' describe('SubscriptionGroupController', function () { - beforeEach(async function () { - this.user = { _id: '!@312431', email: 'user@email.com' } - this.adminUserId = '123jlkj' - this.subscriptionId = '123434325412' - this.user_email = 'bob@gmail.com' - this.req = { + beforeEach(async function (ctx) { + ctx.user = { _id: '!@312431', email: 'user@email.com' } + ctx.adminUserId = '123jlkj' + ctx.subscriptionId = '123434325412' + ctx.user_email = 'bob@gmail.com' + ctx.req = { session: { user: { - _id: this.adminUserId, - email: this.user_email, + _id: ctx.adminUserId, + email: ctx.user_email, }, }, params: { - subscriptionId: this.subscriptionId, + subscriptionId: ctx.subscriptionId, }, query: {}, } - this.subscription = { - _id: this.subscriptionId, + ctx.subscription = { + _id: ctx.subscriptionId, teamName: 'Cool group', groupPlan: true, membersLimit: 5, } - this.plan = { + ctx.plan = { canUseFlexibleLicensing: true, } - this.recurlySubscription = { + ctx.recurlySubscription = { get isCollectionMethodManual() { return true }, } - this.previewSubscriptionChangeData = { + ctx.previewSubscriptionChangeData = { change: {}, currency: 'USD', } - this.createSubscriptionChangeData = { adding: 1 } + ctx.createSubscriptionChangeData = { adding: 1 } - this.paymentMethod = { cardType: 'Visa', lastFour: '1111' } + ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' } - this.SubscriptionGroupHandler = { + ctx.SubscriptionGroupHandler = { promises: { removeUserFromGroup: sinon.stub().resolves(), getUsersGroupSubscriptionDetails: sinon.stub().resolves({ - subscription: this.subscription, - plan: this.plan, - recurlySubscription: this.recurlySubscription, + subscription: ctx.subscription, + plan: ctx.plan, + recurlySubscription: ctx.recurlySubscription, }), previewAddSeatsSubscriptionChange: sinon .stub() - .resolves(this.previewSubscriptionChangeData), + .resolves(ctx.previewSubscriptionChangeData), createAddSeatsSubscriptionChange: sinon .stub() - .resolves(this.createSubscriptionChangeData), + .resolves(ctx.createSubscriptionChangeData), ensureFlexibleLicensingEnabled: sinon.stub().resolves(), ensureSubscriptionIsActive: sinon.stub().resolves(), ensureSubscriptionCollectionMethodIsNotManual: sinon.stub().resolves(), @@ -70,19 +70,19 @@ describe('SubscriptionGroupController', function () { ensureSubscriptionHasNoPastDueInvoice: sinon.stub().resolves(), getGroupPlanUpgradePreview: sinon .stub() - .resolves(this.previewSubscriptionChangeData), - checkBillingInfoExistence: sinon.stub().resolves(this.paymentMethod), + .resolves(ctx.previewSubscriptionChangeData), + checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod), updateSubscriptionPaymentTerms: sinon.stub().resolves(), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { - getSubscription: sinon.stub().resolves(this.subscription), + getSubscription: sinon.stub().resolves(ctx.subscription), }, } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId(session) { return session.user._id }, @@ -91,13 +91,13 @@ describe('SubscriptionGroupController', function () { }, } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves(), @@ -105,35 +105,35 @@ describe('SubscriptionGroupController', function () { }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'enabled' }), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUserEmail: sinon.stub().resolves(this.user.email), + getUserEmail: sinon.stub().resolves(ctx.user.email), }, } - this.paymentMethod = { cardType: 'Visa', lastFour: '1111' } + ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' } - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { - getPaymentMethod: sinon.stub().resolves(this.paymentMethod), + getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod), }, } - this.SubscriptionController = {} + ctx.SubscriptionController = {} - this.SubscriptionModel = { Subscription: {} } + ctx.SubscriptionModel = { Subscription: {} } - this.PlansHelper = { + ctx.PlansHelper = { isProfessionalGroupPlan: sinon.stub().returns(false), } - this.Errors = { + ctx.Errors = { MissingBillingInfoError: class extends Error {}, ManuallyCollectedError: class extends Error {}, PendingChangeError: class extends Error {}, @@ -142,632 +142,743 @@ describe('SubscriptionGroupController', function () { HasPastDueInvoiceError: class extends Error {}, } - this.Controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Subscription/SubscriptionGroupHandler': - this.SubscriptionGroupHandler, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/User/UserAuditLogHandler': - this.UserAuditLogHandler, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Errors/ErrorController': - (this.ErrorController = { - notFound: sinon.stub(), - }), - '../../../../app/src/Features/Subscription/SubscriptionController': - this.SubscriptionController, - '../../../../app/src/Features/Subscription/RecurlyClient': - this.RecurlyClient, - '../../../../app/src/Features/Subscription/PlansHelper': this.PlansHelper, - '../../../../app/src/Features/Subscription/Errors': this.Errors, - '../../../../app/src/models/Subscription': this.SubscriptionModel, - '@overleaf/logger': { + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionGroupHandler', + () => ({ + default: ctx.SubscriptionGroupHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: (ctx.ErrorController = { + notFound: sinon.stub(), + }), + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionController', + () => ({ + default: ctx.SubscriptionController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/PlansHelper', + () => ctx.PlansHelper + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/Errors', + () => ctx.Errors + ) + + vi.doMock( + '../../../../app/src/models/Subscription', + () => ctx.SubscriptionModel + ) + + vi.doMock('@overleaf/logger', () => ({ + default: { err: sinon.stub(), error: sinon.stub(), warn: sinon.stub(), log: sinon.stub(), debug: sinon.stub(), }, - }) + })) + + ctx.Controller = (await import(modulePath)).default }) describe('removeUserFromGroup', function () { - it('should use the subscription id for the logged in user and take the user id from the params', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription + it('should use the subscription id for the logged in user and take the user id from the params', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription - const res = { - sendStatus: () => { - this.SubscriptionGroupHandler.promises.removeUserFromGroup - .calledWith(this.subscriptionId, userIdToRemove, { - initiatorId: this.req.session.user._id, - ipAddress: this.req.ip, - }) - .should.equal(true) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + ctx.SubscriptionGroupHandler.promises.removeUserFromGroup + .calledWith(ctx.subscriptionId, userIdToRemove, { + initiatorId: ctx.req.session.user._id, + ipAddress: ctx.req.ip, + }) + .should.equal(true) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) - it('should log that the user has been removed', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription + it('should log that the user has been removed', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - userIdToRemove, - 'remove-from-group-subscription', - this.adminUserId, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) - }) - - it('should call the group SSO hooks with group SSO enabled', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([true]) - - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith( - 'unlinkUserFromGroupSSO', + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, userIdToRemove, - this.subscriptionId + 'remove-from-group-subscription', + ctx.adminUserId, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } ) - .should.equal(true) - sinon.assert.calledTwice(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) - it('should call the group SSO hooks with group SSO disabled', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([false]) + it('should call the group SSO hooks with group SSO enabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([true]) - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + ctx.Modules.promises.hooks.fire + .calledWith( + 'unlinkUserFromGroupSSO', + userIdToRemove, + ctx.subscriptionId + ) + .should.equal(true) + sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO disabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([false]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) }) describe('removeSelfFromGroup', function () { - it('gets subscription and remove user', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = 123456789 - this.req.session.user._id = memberUserIdToremove + it('gets subscription and remove user', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = 123456789 + ctx.req.session.user._id = memberUserIdToremove - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.SubscriptionLocator.promises.getSubscription, - this.subscriptionId - ) - sinon.assert.calledWith( - this.SubscriptionGroupHandler.promises.removeUserFromGroup, - this.subscriptionId, - memberUserIdToremove, - { - initiatorId: this.req.session.user._id, - ipAddress: this.req.ip, - } - ) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) - }) - - it('should log that the user has left the subscription', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = '123456789' - this.req.session.user._id = memberUserIdToremove - - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - memberUserIdToremove, - 'remove-from-group-subscription', - memberUserIdToremove, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) - }) - - it('should call the group SSO hooks with group SSO enabled', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = '123456789' - this.req.session.user._id = memberUserIdToremove - - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([true]) - - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith( - 'unlinkUserFromGroupSSO', - memberUserIdToremove, - this.subscriptionId + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.SubscriptionLocator.promises.getSubscription, + ctx.subscriptionId ) - .should.equal(true) - sinon.assert.calledTwice(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) + sinon.assert.calledWith( + ctx.SubscriptionGroupHandler.promises.removeUserFromGroup, + ctx.subscriptionId, + memberUserIdToremove, + { + initiatorId: ctx.req.session.user._id, + ipAddress: ctx.req.ip, + } + ) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) }) - it('should call the group SSO hooks with group SSO disabled', function (done) { - const userIdToRemove = '31231' - this.req.session.user._id = userIdToRemove - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([false]) + it('should log that the user has left the subscription', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = '123456789' + ctx.req.session.user._id = memberUserIdToremove - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + memberUserIdToremove, + 'remove-from-group-subscription', + memberUserIdToremove, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } + ) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = '123456789' + ctx.req.session.user._id = memberUserIdToremove + + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([true]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + ctx.Modules.promises.hooks.fire + .calledWith( + 'unlinkUserFromGroupSSO', + memberUserIdToremove, + ctx.subscriptionId + ) + .should.equal(true) + sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO disabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.session.user._id = userIdToRemove + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([false]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) }) }) describe('addSeatsToGroupSubscription', function () { - it('should render the "add seats" page', function (done) { - const res = { - render: (page, props) => { - this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails - .calledWith(this.req.session.user._id) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled - .calledWith(this.plan) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges - .calledWith(this.recurlySubscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive - .calledWith(this.subscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice - .calledWith(this.subscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.checkBillingInfoExistence - .calledWith(this.recurlySubscription, this.adminUserId) - .should.equal(true) - page.should.equal('subscriptions/add-seats') - props.subscriptionId.should.equal(this.subscriptionId) - props.groupName.should.equal(this.subscription.teamName) - props.totalLicenses.should.equal(this.subscription.membersLimit) - props.isProfessional.should.equal(false) - props.isCollectionMethodManual.should.equal(true) - done() - }, - } + it('should render the "add seats" page', function (ctx) { + return new Promise((resolve, reject) => { + const res = { + render: (page, props) => { + ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails + .calledWith(ctx.req.session.user._id) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled + .calledWith(ctx.plan) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges + .calledWith(ctx.recurlySubscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive + .calledWith(ctx.subscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice + .calledWith(ctx.subscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence + .calledWith(ctx.recurlySubscription, ctx.adminUserId) + .should.equal(true) + page.should.equal('subscriptions/add-seats') + props.subscriptionId.should.equal(ctx.subscriptionId) + props.groupName.should.equal(ctx.subscription.teamName) + props.totalLicenses.should.equal(ctx.subscription.membersLimit) + props.isProfessional.should.equal(false) + props.isCollectionMethodManual.should.equal(true) + resolve() + }, + } - this.Controller.addSeatsToGroupSubscription(this.req, res) + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) }) - it('should redirect to subscription page when getting subscription details fails', function (done) { - this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails = + it('should redirect to subscription page when getting subscription details fails', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when flexible licensing is not enabled', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to missing billing information page when billing information is missing', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon + .stub() + .throws(new ctx.Errors.MissingBillingInfoError()) + + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/missing-billing-information' + ) + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when there is a pending change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges = + sinon.stub().throws(new ctx.Errors.PendingChangeError()) + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when subscription is not active', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon + .stub() + .rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when subscription has pending invoice', function (ctx) { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice = sinon.stub().rejects() + return new Promise(resolve => { + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when flexible licensing is not enabled', function (done) { - this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled = - sinon.stub().rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to missing billing information page when billing information is missing', function (done) { - this.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon - .stub() - .throws(new this.Errors.MissingBillingInfoError()) - - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/missing-billing-information' - ) - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when there is a pending change', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges = - sinon.stub().throws(new this.Errors.PendingChangeError()) - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when subscription is not active', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon - .stub() - .rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when subscription has pending invoice', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice = - sinon.stub().rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) }) }) describe('previewAddSeatsSubscriptionChange', function () { - it('should preview "add seats" change', function (done) { - this.req.body = { adding: 2 } + it('should preview "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } - const res = { - json: data => { - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange - .calledWith(this.req.session.user._id, this.req.body.adding) - .should.equal(true) - data.should.deep.equal(this.previewSubscriptionChangeData) - done() - }, - } + const res = { + json: data => { + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange + .calledWith(ctx.req.session.user._id, ctx.req.body.adding) + .should.equal(true) + data.should.deep.equal(ctx.previewSubscriptionChangeData) + resolve() + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail previewing "add seats" change', function (done) { - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = - sinon.stub().rejects() + it('should fail previewing "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = + sinon.stub().rejects() - const res = { - status: statusCode => { - statusCode.should.equal(500) + const res = { + status: statusCode => { + statusCode.should.equal(500) - return { - end: () => { - done() - }, - } - }, - } + return { + end: () => { + resolve() + }, + } + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail previewing "add seats" change with SubtotalLimitExceededError', function (done) { - this.req.body = { adding: 2 } - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = - sinon.stub().throws(new this.Errors.SubtotalLimitExceededError()) + it('should fail previewing "add seats" change with SubtotalLimitExceededError', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = + sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - status: statusCode => { - statusCode.should.equal(422) + const res = { + status: statusCode => { + statusCode.should.equal(422) - return { - json: data => { - data.should.deep.equal({ - code: 'subtotal_limit_exceeded', - adding: this.req.body.adding, - }) - done() - }, - } - }, - } + return { + json: data => { + data.should.deep.equal({ + code: 'subtotal_limit_exceeded', + adding: ctx.req.body.adding, + }) + resolve() + }, + } + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) }) describe('createAddSeatsSubscriptionChange', function () { - it('should apply "add seats" change', function (done) { - this.req.body = { adding: 2 } + it('should apply "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } - const res = { - json: data => { - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange - .calledWith(this.req.session.user._id, this.req.body.adding) - .should.equal(true) - data.should.deep.equal(this.createSubscriptionChangeData) - done() - }, - } + const res = { + json: data => { + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange + .calledWith(ctx.req.session.user._id, ctx.req.body.adding) + .should.equal(true) + data.should.deep.equal(ctx.createSubscriptionChangeData) + resolve() + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail applying "add seats" change', function (done) { - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = - sinon.stub().rejects() + it('should fail applying "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = + sinon.stub().rejects() - const res = { - status: statusCode => { - statusCode.should.equal(500) + const res = { + status: statusCode => { + statusCode.should.equal(500) - return { - end: () => { - done() - }, - } - }, - } + return { + end: () => { + resolve() + }, + } + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail applying "add seats" change with SubtotalLimitExceededError', function (done) { - this.req.body = { adding: 2 } - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = - sinon.stub().throws(new this.Errors.SubtotalLimitExceededError()) + it('should fail applying "add seats" change with SubtotalLimitExceededError', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = + sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - status: statusCode => { - statusCode.should.equal(422) + const res = { + status: statusCode => { + statusCode.should.equal(422) - return { - json: data => { - data.should.deep.equal({ - code: 'subtotal_limit_exceeded', - adding: this.req.body.adding, - }) - done() - }, - } - }, - } + return { + json: data => { + data.should.deep.equal({ + code: 'subtotal_limit_exceeded', + adding: ctx.req.body.adding, + }) + resolve() + }, + } + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) }) describe('submitForm', function () { - it('should build and pass the request body to the sales submit handler', function (done) { - const adding = 100 - const poNumber = 'PO123456' - this.req.body = { adding, poNumber } + it('should build and pass the request body to the sales submit handler', function (ctx) { + return new Promise(resolve => { + const adding = 100 + const poNumber = 'PO123456' + ctx.req.body = { adding, poNumber } - const res = { - sendStatus: code => { - this.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms( - this.adminUserId, - this.recurlySubscription, - poNumber - ) - this.Modules.promises.hooks.fire - .calledWith('sendSupportRequest', { - email: this.user.email, - subject: 'Sales Contact Form', - message: - '\n' + - '**Overleaf Sales Contact Form:**\n' + - '\n' + - '**Subject:** Self-Serve Group User Increase Request\n' + - '\n' + - `**Estimated Number of Users:** ${adding}\n` + - '\n' + - `**PO Number:** ${poNumber}\n` + - '\n' + - `**Message:** This email has been generated on behalf of user with email **${this.user.email}** to request an increase in the total number of users for their subscription.`, - inbox: 'sales', - }) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - code.should.equal(204) - done() - }, - } - this.Controller.submitForm(this.req, res, done) + const res = { + sendStatus: code => { + ctx.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms( + ctx.adminUserId, + ctx.recurlySubscription, + poNumber + ) + ctx.Modules.promises.hooks.fire + .calledWith('sendSupportRequest', { + email: ctx.user.email, + subject: 'Sales Contact Form', + message: + '\n' + + '**Overleaf Sales Contact Form:**\n' + + '\n' + + '**Subject:** Self-Serve Group User Increase Request\n' + + '\n' + + `**Estimated Number of Users:** ${adding}\n` + + '\n' + + `**PO Number:** ${poNumber}\n` + + '\n' + + `**Message:** This email has been generated on behalf of user with email **${ctx.user.email}** to request an increase in the total number of users for their subscription.`, + inbox: 'sales', + }) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + code.should.equal(204) + resolve() + }, + } + ctx.Controller.submitForm(ctx.req, res, resolve) + }) }) }) describe('subscriptionUpgradePage', function () { - it('should render "subscription upgrade" page', function (done) { - const olSubscription = { membersLimit: 1, teamName: 'test team' } - this.SubscriptionModel.Subscription.findOne = () => { - return { - exec: () => olSubscription, + it('should render "subscription upgrade" page', function (ctx) { + return new Promise(resolve => { + const olSubscription = { membersLimit: 1, teamName: 'test team' } + ctx.SubscriptionModel.Subscription.findOne = () => { + return { + exec: () => olSubscription, + } } - } - const res = { - render: (page, data) => { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview - .calledWith(this.req.session.user._id) - .should.equal(true) - page.should.equal('subscriptions/upgrade-group-subscription-react') - data.totalLicenses.should.equal(olSubscription.membersLimit) - data.groupName.should.equal(olSubscription.teamName) - data.changePreview.should.equal(this.previewSubscriptionChangeData) - done() - }, - } + const res = { + render: (page, data) => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview + .calledWith(ctx.req.session.user._id) + .should.equal(true) + page.should.equal('subscriptions/upgrade-group-subscription-react') + data.totalLicenses.should.equal(olSubscription.membersLimit) + data.groupName.should.equal(olSubscription.teamName) + data.changePreview.should.equal(ctx.previewSubscriptionChangeData) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect if failed to generate preview', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .rejects() + it('should redirect if failed to generate preview', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .rejects() - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to missing billing information page when billing information is missing', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.MissingBillingInfoError()) + it('should redirect to missing billing information page when billing information is missing', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.MissingBillingInfoError()) - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/missing-billing-information' - ) - done() - }, - } + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/missing-billing-information' + ) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to manually collected subscription error page when collection method is manual', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.ManuallyCollectedError()) + it('should redirect to manually collected subscription error page when collection method is manual', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.ManuallyCollectedError()) - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/manually-collected-subscription' - ) - done() - }, - } + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/manually-collected-subscription' + ) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to subtotal limit exceeded page', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.SubtotalLimitExceededError()) + it('should redirect to subtotal limit exceeded page', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - redirect: url => { - url.should.equal('/user/subscription/group/subtotal-limit-exceeded') - done() - }, - } + const res = { + redirect: url => { + url.should.equal('/user/subscription/group/subtotal-limit-exceeded') + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) }) describe('upgradeSubscription', function () { - it('should send 200 response', function (done) { - this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon - .stub() - .resolves() + it('should send 200 response', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon + .stub() + .resolves() - const res = { - sendStatus: code => { - code.should.equal(200) - done() - }, - } + const res = { + sendStatus: code => { + code.should.equal(200) + resolve() + }, + } - this.Controller.upgradeSubscription(this.req, res) + ctx.Controller.upgradeSubscription(ctx.req, res) + }) }) - it('should send 500 response', function (done) { - this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon - .stub() - .rejects() + it('should send 500 response', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon + .stub() + .rejects() - const res = { - sendStatus: code => { - code.should.equal(500) - done() - }, - } + const res = { + sendStatus: code => { + code.should.equal(500) + resolve() + }, + } - this.Controller.upgradeSubscription(this.req, res) + ctx.Controller.upgradeSubscription(ctx.req, res) + }) }) }) }) diff --git a/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs b/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs index 3a1e8c3462..b72a406ac0 100644 --- a/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs +++ b/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs @@ -1,20 +1,20 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = '../../../../app/src/Features/Subscription/TeamInvitesController' describe('TeamInvitesController', function () { - beforeEach(async function () { - this.user = { _id: '!@312431', email: 'user@email.com' } - this.adminUserId = '123jlkj' - this.subscriptionId = '123434325412' - this.user_email = 'bob@gmail.com' - this.req = { + beforeEach(async function (ctx) { + ctx.user = { _id: '!@312431', email: 'user@email.com' } + ctx.adminUserId = '123jlkj' + ctx.subscriptionId = '123434325412' + ctx.user_email = 'bob@gmail.com' + ctx.req = { session: { user: { - _id: this.adminUserId, - email: this.user_email, + _id: ctx.adminUserId, + email: ctx.user_email, }, }, params: {}, @@ -22,33 +22,33 @@ describe('TeamInvitesController', function () { ip: '0.0.0.0', } - this.subscription = { - _id: this.subscriptionId, + ctx.subscription = { + _id: ctx.subscriptionId, } - this.TeamInvitesHandler = { + ctx.TeamInvitesHandler = { promises: { - acceptInvite: sinon.stub().resolves(this.subscription), + acceptInvite: sinon.stub().resolves(ctx.subscription), getInvite: sinon.stub().resolves({ invite: { - email: this.user.email, + email: ctx.user.email, token: 'token123', - inviterName: this.user_email, + inviterName: ctx.user_email, }, - subscription: this.subscription, + subscription: ctx.subscription, }), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { hasSSOEnabled: sinon.stub().resolves(true), getUsersSubscription: sinon.stub().resolves(), }, } - this.ErrorController = { notFound: sinon.stub() } + ctx.ErrorController = { notFound: sinon.stub() } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId(session) { return session.user?._id }, @@ -57,74 +57,112 @@ describe('TeamInvitesController', function () { }, } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUser: sinon.stub().resolves(this.user), - getUserByMainEmail: sinon.stub().resolves(this.user), - getUserByAnyEmail: sinon.stub().resolves(this.user), + getUser: sinon.stub().resolves(ctx.user), + getUserByMainEmail: sinon.stub().resolves(ctx.user), + getUserByAnyEmail: sinon.stub().resolves(ctx.user), }, } - this.EmailHandler = { + ctx.EmailHandler = { sendDeferredEmail: sinon.stub().resolves(), } - this.RateLimiter = { + ctx.RateLimiter = { RateLimiter: class {}, } - this.Controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Subscription/TeamInvitesHandler': - this.TeamInvitesHandler, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/Features/User/UserAuditLogHandler': - this.UserAuditLogHandler, - '../../../../app/src/Features/Errors/ErrorController': - this.ErrorController, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Email/EmailHandler': this.EmailHandler, - '../../../../app/src/infrastructure/RateLimiter': this.RateLimiter, - '../../../../app/src/infrastructure/Modules': (this.Modules = { + vi.doMock( + '../../../../app/src/Features/Subscription/TeamInvitesHandler', + () => ({ + default: ctx.TeamInvitesHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: ctx.ErrorController, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/infrastructure/RateLimiter', + () => ctx.RateLimiter + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves([]), }, }, }), - '../../../../app/src/Features/SplitTests/SplitTestHandler': - (this.SplitTestHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: (ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({}), }, }), - }) + }) + ) + + ctx.Controller = (await import(modulePath)).default }) describe('acceptInvite', function () { - it('should add an audit log entry', function (done) { - this.req.params.token = 'foo' - this.req.session.user = this.user - const res = { - json: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user._id, - 'accept-group-invitation', - this.user._id, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.acceptInvite(this.req, res) + it('should add an audit log entry', function (ctx) { + return new Promise(resolve => { + ctx.req.params.token = 'foo' + ctx.req.session.user = ctx.user + const res = { + json: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user._id, + 'accept-group-invitation', + ctx.user._id, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } + ) + resolve() + }, + } + ctx.Controller.acceptInvite(ctx.req, res) + }) }) }) @@ -138,90 +176,102 @@ describe('TeamInvitesController', function () { } describe('hasIndividualRecurlySubscription', function () { - it('is true for personal subscription', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: false, + it('is true for personal subscription', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: false, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.true + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.true - done() - }, - } - this.Controller.viewInvite(req, res) }) - it('is true for group subscriptions', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: true, + it('is true for group subscriptions', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: true, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.false + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.false - done() - }, - } - this.Controller.viewInvite(req, res) }) - it('is false for canceled subscriptions', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: false, - recurlyStatus: { - state: 'canceled', - }, + it('is false for canceled subscriptions', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: false, + recurlyStatus: { + state: 'canceled', + }, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.false + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.false - done() - }, - } - this.Controller.viewInvite(req, res) }) }) describe('when user is logged out', function () { - it('renders logged out invite page', function (done) { - const res = { - render: (template, data) => { - expect(template).to.equal('subscriptions/team/invite_logged_out') - expect(data.groupSSOActive).to.be.undefined - done() - }, - } - this.Controller.viewInvite( - { params: { token: 'token123' }, session: {} }, - res - ) + it('renders logged out invite page', function (ctx) { + return new Promise(resolve => { + const res = { + render: (template, data) => { + expect(template).to.equal('subscriptions/team/invite_logged_out') + expect(data.groupSSOActive).to.be.undefined + resolve() + }, + } + ctx.Controller.viewInvite( + { params: { token: 'token123' }, session: {} }, + res + ) + }) }) - it('includes groupSSOActive flag when the group has SSO enabled', function (done) { - this.Modules.promises.hooks.fire = sinon.stub().resolves([true]) - const res = { - render: (template, data) => { - expect(data.groupSSOActive).to.be.true - done() - }, - } - this.Controller.viewInvite( - { params: { token: 'token123' }, session: {} }, - res - ) + it('includes groupSSOActive flag when the group has SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.Modules.promises.hooks.fire = sinon.stub().resolves([true]) + const res = { + render: (template, data) => { + expect(data.groupSSOActive).to.be.true + resolve() + }, + } + ctx.Controller.viewInvite( + { params: { token: 'token123' }, session: {} }, + res + ) + }) }) }) - it('renders the view', function (done) { - const res = { - render: template => { - expect(template).to.equal('subscriptions/team/invite') - done() - }, - } - this.Controller.viewInvite(req, res) + it('renders the view', function (ctx) { + return new Promise(resolve => { + const res = { + render: template => { + expect(template).to.equal('subscriptions/team/invite') + resolve() + }, + } + ctx.Controller.viewInvite(req, res) + }) }) }) }) diff --git a/services/web/test/unit/src/Tags/TagsController.test.mjs b/services/web/test/unit/src/Tags/TagsController.test.mjs index 4474ba0d38..927c6283a5 100644 --- a/services/web/test/unit/src/Tags/TagsController.test.mjs +++ b/services/web/test/unit/src/Tags/TagsController.test.mjs @@ -1,17 +1,14 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { assert } from 'chai' -const modulePath = new URL( - '../../../../app/src/Features/Tags/TagsController.mjs', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Tags/TagsController.mjs' describe('TagsController', function () { const userId = '123nd3ijdks' const projectId = '123njdskj9jlk' - beforeEach(async function () { - this.TagsHandler = { + beforeEach(async function (ctx) { + ctx.TagsHandler = { promises: { addProjectToTag: sinon.stub().resolves(), addProjectsToTag: sinon.stub().resolves(), @@ -23,17 +20,25 @@ describe('TagsController', function () { createTag: sinon.stub().resolves(), }, } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId: session => { return session.user._id }, } - this.TagsController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Tags/TagsHandler': this.TagsHandler, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - }) - this.req = { + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + ctx.TagsController = (await import(modulePath)).default + ctx.req = { params: { projectId, }, @@ -45,149 +50,235 @@ describe('TagsController', function () { body: {}, } - this.res = {} - this.res.status = sinon.stub().returns(this.res) - this.res.end = sinon.stub() - this.res.json = sinon.stub() + ctx.res = {} + ctx.res.status = sinon.stub().returns(ctx.res) + ctx.res.end = sinon.stub() + ctx.res.json = sinon.stub() }) - it('get all tags', function (done) { - const allTags = [{ name: 'tag', projects: ['123423', '423423'] }] - this.TagsHandler.promises.getAllTags = sinon.stub().resolves(allTags) - this.TagsController.getAllTags(this.req, { - json: body => { - body.should.equal(allTags) - sinon.assert.calledWith(this.TagsHandler.promises.getAllTags, userId) - done() - return { - end: () => {}, - } - }, + it('get all tags', function (ctx) { + return new Promise(resolve => { + const allTags = [{ name: 'tag', projects: ['123423', '423423'] }] + ctx.TagsHandler.promises.getAllTags = sinon.stub().resolves(allTags) + ctx.TagsController.getAllTags(ctx.req, { + json: body => { + body.should.equal(allTags) + sinon.assert.calledWith(ctx.TagsHandler.promises.getAllTags, userId) + resolve() + return { + end: () => {}, + } + }, + }) }) }) describe('create a tag', function (done) { - it('without a color', function (done) { - this.tag = { mock: 'tag' } - this.TagsHandler.promises.createTag = sinon.stub().resolves(this.tag) - this.req.session.user._id = this.userId = 'user-id-123' - this.req.body = { name: (this.name = 'tag-name') } - this.TagsController.createTag(this.req, { - json: () => { - sinon.assert.calledWith( - this.TagsHandler.promises.createTag, - this.userId, - this.name - ) - done() - return { - end: () => {}, - } - }, + it('without a color', function (ctx) { + return new Promise(resolve => { + ctx.tag = { mock: 'tag' } + ctx.TagsHandler.promises.createTag = sinon.stub().resolves(ctx.tag) + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.req.body = { name: (ctx.tagName = 'tag-name') } + ctx.TagsController.createTag(ctx.req, { + json: () => { + sinon.assert.calledWith( + ctx.TagsHandler.promises.createTag, + ctx.userId, + ctx.tagName + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) - it('with a color', function (done) { - this.tag = { mock: 'tag' } - this.TagsHandler.promises.createTag = sinon.stub().resolves(this.tag) - this.req.session.user._id = this.userId = 'user-id-123' - this.req.body = { - name: (this.name = 'tag-name'), - color: (this.color = '#123456'), - } - this.TagsController.createTag(this.req, { - json: () => { - sinon.assert.calledWith( - this.TagsHandler.promises.createTag, - this.userId, - this.name, - this.color - ) - done() - return { - end: () => {}, - } - }, + it('with a color', function (ctx) { + return new Promise(resolve => { + ctx.tag = { mock: 'tag' } + ctx.TagsHandler.promises.createTag = sinon.stub().resolves(ctx.tag) + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.req.body = { + name: (ctx.tagName = 'tag-name'), + color: (ctx.color = '#123456'), + } + ctx.TagsController.createTag(ctx.req, { + json: () => { + sinon.assert.calledWith( + ctx.TagsHandler.promises.createTag, + ctx.userId, + ctx.tagName, + ctx.color + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) }) - it('delete a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.deleteTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.deleteTag, - this.userId, - this.tagId - ) - done() - return { - end: () => {}, - } - }, + it('delete a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.deleteTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.deleteTag, + ctx.userId, + ctx.tagId + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) describe('edit a tag', function () { - beforeEach(function () { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.session.user._id = this.userId = 'user-id-123' + beforeEach(function (ctx) { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' }) - it('with a name and no color', function (done) { - this.req.body = { - name: (this.name = 'new-name'), - } - this.TagsController.editTag(this.req, { + it('with a name and no color', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + name: (ctx.tagName = 'new-name'), + } + ctx.TagsController.editTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.editTag, + ctx.userId, + ctx.tagId, + ctx.tagName + ) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + + it('with a name and color', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + name: (ctx.tagName = 'new-name'), + color: (ctx.color = '#FF0011'), + } + ctx.TagsController.editTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.editTag, + ctx.userId, + ctx.tagId, + ctx.tagName, + ctx.color + ) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + + it('without a name', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { name: undefined } + ctx.TagsController.renameTag(ctx.req, { + status: code => { + assert.equal(code, 400) + sinon.assert.notCalled(ctx.TagsHandler.promises.renameTag) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + }) + + it('add a project to a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.params.projectId = ctx.projectId = 'project-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.addProjectToTag(ctx.req, { status: code => { assert.equal(code, 204) sinon.assert.calledWith( - this.TagsHandler.promises.editTag, - this.userId, - this.tagId, - this.name + ctx.TagsHandler.promises.addProjectToTag, + ctx.userId, + ctx.tagId, + ctx.projectId ) - done() + resolve() return { end: () => {}, } }, }) }) + }) - it('with a name and color', function (done) { - this.req.body = { - name: (this.name = 'new-name'), - color: (this.color = '#FF0011'), - } - this.TagsController.editTag(this.req, { + it('add projects to a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.body.projectIds = ctx.projectIds = [ + 'project-id-123', + 'project-id-234', + ] + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.addProjectsToTag(ctx.req, { status: code => { assert.equal(code, 204) sinon.assert.calledWith( - this.TagsHandler.promises.editTag, - this.userId, - this.tagId, - this.name, - this.color + ctx.TagsHandler.promises.addProjectsToTag, + ctx.userId, + ctx.tagId, + ctx.projectIds ) - done() + resolve() return { end: () => {}, } }, }) }) + }) - it('without a name', function (done) { - this.req.body = { name: undefined } - this.TagsController.renameTag(this.req, { + it('remove a project from a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.params.projectId = ctx.projectId = 'project-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.removeProjectFromTag(ctx.req, { status: code => { - assert.equal(code, 400) - sinon.assert.notCalled(this.TagsHandler.promises.renameTag) - done() + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.removeProjectFromTag, + ctx.userId, + ctx.tagId, + ctx.projectId + ) + resolve() return { end: () => {}, } @@ -196,93 +287,29 @@ describe('TagsController', function () { }) }) - it('add a project to a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.params.projectId = this.projectId = 'project-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.addProjectToTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.addProjectToTag, - this.userId, - this.tagId, - this.projectId - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('add projects to a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.body.projectIds = this.projectIds = [ - 'project-id-123', - 'project-id-234', - ] - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.addProjectsToTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.addProjectsToTag, - this.userId, - this.tagId, - this.projectIds - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('remove a project from a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.params.projectId = this.projectId = 'project-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.removeProjectFromTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectFromTag, - this.userId, - this.tagId, - this.projectId - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('remove projects from a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.body.projectIds = this.projectIds = [ - 'project-id-123', - 'project-id-234', - ] - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.removeProjectsFromTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectsFromTag, - this.userId, - this.tagId, - this.projectIds - ) - done() - return { - end: () => {}, - } - }, + it('remove projects from a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.body.projectIds = ctx.projectIds = [ + 'project-id-123', + 'project-id-234', + ] + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.removeProjectsFromTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.removeProjectsFromTag, + ctx.userId, + ctx.tagId, + ctx.projectIds + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs index 4dd72b117f..313f2d2456 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import mongodb from 'mongodb-legacy' import { expect } from 'chai' -import esmock from 'esmock' import sinon from 'sinon' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockResponse from '../helpers/MockResponse.js' @@ -12,498 +12,557 @@ const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsController.mjs' describe('TpdsController', function () { - beforeEach(async function () { - this.metadata = { + beforeEach(async function (ctx) { + ctx.metadata = { projectId: new ObjectId(), entityId: new ObjectId(), folderId: new ObjectId(), entityType: 'doc', rev: 2, } - this.TpdsUpdateHandler = { + ctx.TpdsUpdateHandler = { promises: { - newUpdate: sinon.stub().resolves(this.metadata), - deleteUpdate: sinon.stub().resolves(this.metadata.entityId), + newUpdate: sinon.stub().resolves(ctx.metadata), + deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId), createFolder: sinon.stub().resolves(), }, } - this.UpdateMerger = { + ctx.UpdateMerger = { promises: { - mergeUpdate: sinon.stub().resolves(this.metadata), - deleteUpdate: sinon.stub().resolves(this.metadata.entityId), + mergeUpdate: sinon.stub().resolves(ctx.metadata), + deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId), }, } - this.NotificationsBuilder = { + ctx.NotificationsBuilder = { tpdsFileLimit: sinon.stub().returns({ create: sinon.stub() }), } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId: sinon.stub().returns('user-id'), } - this.TpdsQueueManager = { + ctx.TpdsQueueManager = { promises: { getQueues: sinon.stub().resolves('queues'), }, } - this.HttpErrorHandler = { + ctx.HttpErrorHandler = { conflict: sinon.stub(), } - this.newProject = { _id: new ObjectId() } - this.ProjectCreationHandler = { - promises: { createBlankProject: sinon.stub().resolves(this.newProject) }, + ctx.newProject = { _id: new ObjectId() } + ctx.ProjectCreationHandler = { + promises: { createBlankProject: sinon.stub().resolves(ctx.newProject) }, } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { promises: { generateUniqueName: sinon.stub().resolves('unique'), }, } - this.TpdsController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler': - this.TpdsUpdateHandler, - '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger': - this.UpdateMerger, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationsBuilder, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Errors/HttpErrorHandler': - this.HttpErrorHandler, - '../../../../app/src/Features/ThirdPartyDataStore/TpdsQueueManager': - this.TpdsQueueManager, - '../../../../app/src/Features/Project/ProjectCreationHandler': - this.ProjectCreationHandler, - '../../../../app/src/Features/Project/ProjectDetailsHandler': - this.ProjectDetailsHandler, - }) - this.user_id = 'dsad29jlkjas' + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler', + () => ({ + default: ctx.TpdsUpdateHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger', + () => ({ + default: ctx.UpdateMerger, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({ + default: ctx.HttpErrorHandler, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsQueueManager', + () => ({ + default: ctx.TpdsQueueManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + ctx.TpdsController = (await import(MODULE_PATH)).default + + ctx.user_id = 'dsad29jlkjas' }) describe('creating a project', function () { - it('should yield the new projects id', function (done) { - const res = new MockResponse() - const req = new MockRequest() - req.params.user_id = this.user_id - req.body = { projectName: 'foo' } - res.callback = err => { - if (err) done(err) - expect(res.body).to.equal( - JSON.stringify({ projectId: this.newProject._id.toString() }) - ) - expect( - this.ProjectDetailsHandler.promises.generateUniqueName - ).to.have.been.calledWith(this.user_id, 'foo') - expect( - this.ProjectCreationHandler.promises.createBlankProject - ).to.have.been.calledWith( - this.user_id, - 'unique', - {}, - { skipCreatingInTPDS: true } - ) - done() - } - this.TpdsController.createProject(req, res) + it('should yield the new projects id', function (ctx) { + return new Promise(resolve => { + const res = new MockResponse() + const req = new MockRequest() + req.params.user_id = ctx.user_id + req.body = { projectName: 'foo' } + res.callback = err => { + if (err) resolve(err) + expect(res.body).to.equal( + JSON.stringify({ projectId: ctx.newProject._id.toString() }) + ) + expect( + ctx.ProjectDetailsHandler.promises.generateUniqueName + ).to.have.been.calledWith(ctx.user_id, 'foo') + expect( + ctx.ProjectCreationHandler.promises.createBlankProject + ).to.have.been.calledWith( + ctx.user_id, + 'unique', + {}, + { skipCreatingInTPDS: true } + ) + resolve() + } + ctx.TpdsController.createProject(req, res) + }) }) }) describe('getting an update', function () { - beforeEach(function () { - this.projectName = 'projectName' - this.path = '/here.txt' - this.req = { + beforeEach(function (ctx) { + ctx.projectName = 'projectName' + ctx.path = '/here.txt' + ctx.req = { params: { - 0: `${this.projectName}${this.path}`, - user_id: this.user_id, + 0: `${ctx.projectName}${ctx.path}`, + user_id: ctx.user_id, project_id: '', }, headers: { - 'x-update-source': (this.source = 'dropbox'), + 'x-update-source': (ctx.source = 'dropbox'), }, } }) - it('should process the update with the update receiver by name', function (done) { - const res = { - json: payload => { - expect(payload).to.deep.equal({ - status: 'applied', - projectId: this.metadata.projectId.toString(), - entityId: this.metadata.entityId.toString(), - folderId: this.metadata.folderId.toString(), - entityType: this.metadata.entityType, - rev: this.metadata.rev.toString(), - }) - this.TpdsUpdateHandler.promises.newUpdate - .calledWith( - this.user_id, - '', // projectId - this.projectName, - this.path, - this.req, - this.source + it('should process the update with the update receiver by name', function (ctx) { + return new Promise(resolve => { + const res = { + json: payload => { + expect(payload).to.deep.equal({ + status: 'applied', + projectId: ctx.metadata.projectId.toString(), + entityId: ctx.metadata.entityId.toString(), + folderId: ctx.metadata.folderId.toString(), + entityType: ctx.metadata.entityType, + rev: ctx.metadata.rev.toString(), + }) + ctx.TpdsUpdateHandler.promises.newUpdate + .calledWith( + ctx.user_id, + '', // projectId + ctx.projectName, + ctx.path, + ctx.req, + ctx.source + ) + .should.equal(true) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should indicate in the response when the update was rejected', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.resolves(null) + const res = { + json: payload => { + expect(payload).to.deep.equal({ status: 'rejected' }) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should process the update with the update receiver by id', function (ctx) { + return new Promise(resolve => { + const path = '/here.txt' + const req = { + pause() {}, + params: { 0: path, user_id: ctx.user_id, project_id: '123' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + json: () => { + ctx.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith( + ctx.user_id, + '123', + '', // projectName + '/here.txt', + req, + ctx.source ) - .should.equal(true) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) - }) - - it('should indicate in the response when the update was rejected', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.resolves(null) - const res = { - json: payload => { - expect(payload).to.deep.equal({ status: 'rejected' }) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) - }) - - it('should process the update with the update receiver by id', function (done) { - const path = '/here.txt' - const req = { - pause() {}, - params: { 0: path, user_id: this.user_id, project_id: '123' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - json: () => { - this.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith( - this.user_id, - '123', - '', // projectName - '/here.txt', - req, - this.source - ) - done() - }, - } - this.TpdsController.mergeUpdate(req, res) - }) - - it('should return a 500 error when the update receiver fails', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects(new Error()) - const res = { - json: sinon.stub(), - } - this.TpdsController.mergeUpdate(this.req, res, err => { - expect(err).to.exist - expect(res.json).not.to.have.been.called - done() + resolve() + }, + } + ctx.TpdsController.mergeUpdate(req, res) }) }) - it('should return a 400 error when the project is too big', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects({ - message: 'project_has_too_many_files', + it('should return a 500 error when the update receiver fails', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects(new Error()) + const res = { + json: sinon.stub(), + } + ctx.TpdsController.mergeUpdate(ctx.req, res, err => { + expect(err).to.exist + expect(res.json).not.to.have.been.called + resolve() + }) }) - const res = { - sendStatus: status => { - expect(status).to.equal(400) - this.NotificationsBuilder.tpdsFileLimit.should.have.been.calledWith( - this.user_id - ) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) }) - it('should return a 429 error when the update receiver fails due to too many requests error', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects( - new Errors.TooManyRequestsError('project on cooldown') - ) - const res = { - sendStatus: status => { - expect(status).to.equal(429) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) + it('should return a 400 error when the project is too big', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects({ + message: 'project_has_too_many_files', + }) + const res = { + sendStatus: status => { + expect(status).to.equal(400) + ctx.NotificationsBuilder.tpdsFileLimit.should.have.been.calledWith( + ctx.user_id + ) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should return a 429 error when the update receiver fails due to too many requests error', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects( + new Errors.TooManyRequestsError('project on cooldown') + ) + const res = { + sendStatus: status => { + expect(status).to.equal(429) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) }) }) describe('getting a delete update', function () { - it('should process the delete with the update receiver by name', function (done) { - const path = '/projectName/here.txt' - const req = { - params: { 0: path, user_id: this.user_id, project_id: '' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - sendStatus: () => { - this.TpdsUpdateHandler.promises.deleteUpdate - .calledWith( - this.user_id, - '', - 'projectName', - '/here.txt', - this.source - ) - .should.equal(true) - done() - }, - } - this.TpdsController.deleteUpdate(req, res) + it('should process the delete with the update receiver by name', function (ctx) { + return new Promise(resolve => { + const path = '/projectName/here.txt' + const req = { + params: { 0: path, user_id: ctx.user_id, project_id: '' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + sendStatus: () => { + ctx.TpdsUpdateHandler.promises.deleteUpdate + .calledWith( + ctx.user_id, + '', + 'projectName', + '/here.txt', + ctx.source + ) + .should.equal(true) + resolve() + }, + } + ctx.TpdsController.deleteUpdate(req, res) + }) }) - it('should process the delete with the update receiver by id', function (done) { - const path = '/here.txt' - const req = { - params: { 0: path, user_id: this.user_id, project_id: '123' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - sendStatus: () => { - this.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith( - this.user_id, - '123', - '', // projectName - '/here.txt', - this.source - ) - done() - }, - } - this.TpdsController.deleteUpdate(req, res) + it('should process the delete with the update receiver by id', function (ctx) { + return new Promise(resolve => { + const path = '/here.txt' + const req = { + params: { 0: path, user_id: ctx.user_id, project_id: '123' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + sendStatus: () => { + ctx.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith( + ctx.user_id, + '123', + '', // projectName + '/here.txt', + ctx.source + ) + resolve() + }, + } + ctx.TpdsController.deleteUpdate(req, res) + }) }) }) describe('updateFolder', function () { - beforeEach(function () { - this.req = { - body: { userId: this.user_id, path: '/abc/def/ghi.txt' }, + beforeEach(function (ctx) { + ctx.req = { + body: { userId: ctx.user_id, path: '/abc/def/ghi.txt' }, } - this.res = { + ctx.res = { json: sinon.stub(), } }) - it("creates a folder if it doesn't exist", function (done) { - const metadata = { - folderId: new ObjectId(), - projectId: new ObjectId(), - path: '/def/ghi.txt', - parentFolderId: new ObjectId(), - } - this.TpdsUpdateHandler.promises.createFolder.resolves(metadata) - this.res.json.callsFake(body => { - expect(body).to.deep.equal({ - entityId: metadata.folderId.toString(), - projectId: metadata.projectId.toString(), - path: metadata.path, - folderId: metadata.parentFolderId.toString(), + it("creates a folder if it doesn't exist", function (ctx) { + return new Promise(resolve => { + const metadata = { + folderId: new ObjectId(), + projectId: new ObjectId(), + path: '/def/ghi.txt', + parentFolderId: new ObjectId(), + } + ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata) + ctx.res.json.callsFake(body => { + expect(body).to.deep.equal({ + entityId: metadata.folderId.toString(), + projectId: metadata.projectId.toString(), + path: metadata.path, + folderId: metadata.parentFolderId.toString(), + }) + resolve() }) - done() + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) - it('supports top level folders', function (done) { - const metadata = { - folderId: new ObjectId(), - projectId: new ObjectId(), - path: '/', - parentFolderId: null, - } - this.TpdsUpdateHandler.promises.createFolder.resolves(metadata) - this.res.json.callsFake(body => { - expect(body).to.deep.equal({ - entityId: metadata.folderId.toString(), - projectId: metadata.projectId.toString(), - path: metadata.path, - folderId: null, + it('supports top level folders', function (ctx) { + return new Promise(resolve => { + const metadata = { + folderId: new ObjectId(), + projectId: new ObjectId(), + path: '/', + parentFolderId: null, + } + ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata) + ctx.res.json.callsFake(body => { + expect(body).to.deep.equal({ + entityId: metadata.folderId.toString(), + projectId: metadata.projectId.toString(), + path: metadata.path, + folderId: null, + }) + resolve() }) - done() + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) - it("returns a 409 if the folder couldn't be created", function (done) { - this.TpdsUpdateHandler.promises.createFolder.resolves(null) - this.HttpErrorHandler.conflict.callsFake((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - done() + it("returns a 409 if the folder couldn't be created", function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.createFolder.resolves(null) + ctx.HttpErrorHandler.conflict.callsFake((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + resolve() + }) + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) }) describe('parseParams', function () { - it('should take the project name off the start and replace with slash', function () { + it('should take the project name off the start and replace with slash', function (ctx) { const path = 'noSlashHere' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) - result.userId.should.equal(this.user_id) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) + result.userId.should.equal(ctx.user_id) result.filePath.should.equal('/') result.projectName.should.equal(path) }) - it('should take the project name off the start and it with no slashes in', function () { + it('should take the project name off the start and it with no slashes in', function (ctx) { const path = '/project/file.tex' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) - result.userId.should.equal(this.user_id) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) + result.userId.should.equal(ctx.user_id) result.filePath.should.equal('/file.tex') result.projectName.should.equal('project') }) - it('should take the project name of and return a slash for the file path', function () { + it('should take the project name of and return a slash for the file path', function (ctx) { const path = '/project_name' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) result.projectName.should.equal('project_name') result.filePath.should.equal('/') }) }) describe('updateProjectContents', function () { - beforeEach(async function () { - this.req = { + beforeEach(async function (ctx) { + ctx.req = { params: { - 0: (this.path = 'chapters/main.tex'), - project_id: (this.project_id = 'project-id-123'), + 0: (ctx.path = 'chapters/main.tex'), + project_id: (ctx.project_id = 'project-id-123'), }, session: { destroy: sinon.stub(), }, headers: { - 'x-update-source': (this.source = 'github'), + 'x-update-source': (ctx.source = 'github'), }, } - this.res = { + ctx.res = { json: sinon.stub(), sendStatus: sinon.stub(), } - await this.TpdsController.promises.updateProjectContents( - this.req, - this.res - ) + await ctx.TpdsController.promises.updateProjectContents(ctx.req, ctx.res) }) - it('should merge the update', function () { - this.UpdateMerger.promises.mergeUpdate.should.be.calledWith( + it('should merge the update', function (ctx) { + ctx.UpdateMerger.promises.mergeUpdate.should.be.calledWith( null, - this.project_id, - `/${this.path}`, - this.req, - this.source + ctx.project_id, + `/${ctx.path}`, + ctx.req, + ctx.source ) }) - it('should return a success', function () { - this.res.json.should.be.calledWith({ - entityId: this.metadata.entityId.toString(), - rev: this.metadata.rev, + it('should return a success', function (ctx) { + ctx.res.json.should.be.calledWith({ + entityId: ctx.metadata.entityId.toString(), + rev: ctx.metadata.rev, }) }) }) describe('deleteProjectContents', function () { - beforeEach(async function () { - this.req = { + beforeEach(async function (ctx) { + ctx.req = { params: { - 0: (this.path = 'chapters/main.tex'), - project_id: (this.project_id = 'project-id-123'), + 0: (ctx.path = 'chapters/main.tex'), + project_id: (ctx.project_id = 'project-id-123'), }, session: { destroy: sinon.stub(), }, headers: { - 'x-update-source': (this.source = 'github'), + 'x-update-source': (ctx.source = 'github'), }, } - this.res = { + ctx.res = { sendStatus: sinon.stub(), json: sinon.stub(), } - await this.TpdsController.promises.deleteProjectContents( - this.req, - this.res - ) + await ctx.TpdsController.promises.deleteProjectContents(ctx.req, ctx.res) }) - it('should delete the file', function () { - this.UpdateMerger.promises.deleteUpdate.should.be.calledWith( + it('should delete the file', function (ctx) { + ctx.UpdateMerger.promises.deleteUpdate.should.be.calledWith( null, - this.project_id, - `/${this.path}`, - this.source + ctx.project_id, + `/${ctx.path}`, + ctx.source ) }) - it('should return a success', function () { - this.res.json.should.be.calledWith({ - entityId: this.metadata.entityId, + it('should return a success', function (ctx) { + ctx.res.json.should.be.calledWith({ + entityId: ctx.metadata.entityId, }) }) }) describe('getQueues', function () { - beforeEach(function () { - this.req = {} - this.res = { json: sinon.stub() } - this.next = sinon.stub() + beforeEach(function (ctx) { + ctx.req = {} + ctx.res = { json: sinon.stub() } + ctx.next = sinon.stub() }) describe('success', function () { - beforeEach(function (done) { - this.res.json.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + resolve() + }) + ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next) }) - this.TpdsController.getQueues(this.req, this.res, this.next) }) - it('should use userId from session', function () { - this.SessionManager.getLoggedInUserId.should.have.been.calledOnce - this.TpdsQueueManager.promises.getQueues.should.have.been.calledWith( + it('should use userId from session', function (ctx) { + ctx.SessionManager.getLoggedInUserId.should.have.been.calledOnce + ctx.TpdsQueueManager.promises.getQueues.should.have.been.calledWith( 'user-id' ) }) - it('should call json with response', function () { - this.res.json.should.have.been.calledWith('queues') - this.next.should.not.have.been.called + it('should call json with response', function (ctx) { + ctx.res.json.should.have.been.calledWith('queues') + ctx.next.should.not.have.been.called }) }) describe('error', function () { - beforeEach(function (done) { - this.err = new Error() - this.TpdsQueueManager.promises.getQueues = sinon - .stub() - .rejects(this.err) - this.next.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.err = new Error() + ctx.TpdsQueueManager.promises.getQueues = sinon + .stub() + .rejects(ctx.err) + ctx.next.callsFake(() => { + resolve() + }) + ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next) }) - this.TpdsController.getQueues(this.req, this.res, this.next) }) - it('should call next with error', function () { - this.res.json.should.not.have.been.called - this.next.should.have.been.calledWith(this.err) + it('should call next with error', function (ctx) { + ctx.res.json.should.not.have.been.called + ctx.next.should.have.been.calledWith(ctx.err) }) }) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs index a5ca099b5b..96cc22279e 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -9,120 +9,158 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('TpdsUpdateHandler', function () { - beforeEach(async function () { - this.projectName = 'My recipes' - this.projects = { - active1: { _id: new ObjectId(), name: this.projectName }, - active2: { _id: new ObjectId(), name: this.projectName }, + beforeEach(async function (ctx) { + ctx.projectName = 'My recipes' + ctx.projects = { + active1: { _id: new ObjectId(), name: ctx.projectName }, + active2: { _id: new ObjectId(), name: ctx.projectName }, archived1: { _id: new ObjectId(), - name: this.projectName, - archived: [this.userId], + name: ctx.projectName, + archived: [ctx.userId], }, archived2: { _id: new ObjectId(), - name: this.projectName, - archived: [this.userId], + name: ctx.projectName, + archived: [ctx.userId], }, } - this.userId = new ObjectId() - this.source = 'dropbox' - this.path = `/some/file` - this.update = {} - this.folderPath = '/some/folder' - this.folder = { + ctx.userId = new ObjectId() + ctx.source = 'dropbox' + ctx.path = `/some/file` + ctx.update = {} + ctx.folderPath = '/some/folder' + ctx.folder = { _id: new ObjectId(), parentFolder_id: new ObjectId(), } - this.CooldownManager = { + ctx.CooldownManager = { promises: { isProjectOnCooldown: sinon.stub().resolves(false), }, } - this.FileTypeManager = { + ctx.FileTypeManager = { promises: { shouldIgnore: sinon.stub().resolves(false), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves() }, }, } - this.notification = { + ctx.notification = { create: sinon.stub().resolves(), } - this.NotificationsBuilder = { + ctx.NotificationsBuilder = { promises: { - dropboxDuplicateProjectNames: sinon.stub().returns(this.notification), + dropboxDuplicateProjectNames: sinon.stub().returns(ctx.notification), }, } - this.ProjectCreationHandler = { + ctx.ProjectCreationHandler = { promises: { - createBlankProject: sinon.stub().resolves(this.projects.active1), + createBlankProject: sinon.stub().resolves(ctx.projects.active1), }, } - this.ProjectDeleter = { + ctx.ProjectDeleter = { promises: { markAsDeletedByExternalSource: sinon.stub().resolves(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { findUsersProjectsByName: sinon.stub(), findAllUsersProjects: sinon .stub() - .resolves({ owned: [this.projects.active1], readAndWrite: [] }), + .resolves({ owned: [ctx.projects.active1], readAndWrite: [] }), }, } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchivedOrTrashed: sinon.stub().returns(false), } - this.ProjectHelper.isArchivedOrTrashed - .withArgs(this.projects.archived1, this.userId) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(ctx.projects.archived1, ctx.userId) .returns(true) - this.ProjectHelper.isArchivedOrTrashed - .withArgs(this.projects.archived2, this.userId) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(ctx.projects.archived2, ctx.userId) .returns(true) - this.RootDocManager = { + ctx.RootDocManager = { setRootDocAutomaticallyInBackground: sinon.stub(), } - this.UpdateMerger = { + ctx.UpdateMerger = { promises: { deleteUpdate: sinon.stub().resolves(), mergeUpdate: sinon.stub().resolves(), - createFolder: sinon.stub().resolves(this.folder), + createFolder: sinon.stub().resolves(ctx.folder), }, } - this.TpdsUpdateHandler = await esmock.strict(MODULE_PATH, { - '.../../../../app/src/Features/Cooldown/CooldownManager': - this.CooldownManager, - '../../../../app/src/Features/Uploads/FileTypeManager': - this.FileTypeManager, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationsBuilder, - '../../../../app/src/Features/Project/ProjectCreationHandler': - this.ProjectCreationHandler, - '../../../../app/src/Features/Project/ProjectDeleter': - this.ProjectDeleter, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectHelper': this.ProjectHelper, - '../../../../app/src/Features/Project/ProjectRootDocManager': - this.RootDocManager, - '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger': - this.UpdateMerger, - }) + vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({ + default: ctx.CooldownManager, + })) + + vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({ + default: ctx.FileTypeManager, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: ctx.ProjectDeleter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ + default: ctx.RootDocManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger', + () => ({ + default: ctx.UpdateMerger, + }) + ) + + ctx.TpdsUpdateHandler = (await import(MODULE_PATH)).default }) describe('getting an update', function () { describe('byId', function () { describe('with no matching project', function () { - beforeEach(function () { - this.projectId = new ObjectId().toString() + beforeEach(function (ctx) { + ctx.projectId = new ObjectId().toString() }) receiveUpdateById() expectProjectNotCreated() @@ -130,8 +168,8 @@ describe('TpdsUpdateHandler', function () { }) describe('with one matching active project', function () { - beforeEach(function () { - this.projectId = this.projects.active1._id.toString() + beforeEach(function (ctx) { + ctx.projectId = ctx.projects.active1._id.toString() }) receiveUpdateById() expectProjectNotCreated() @@ -187,8 +225,8 @@ describe('TpdsUpdateHandler', function () { describe('update to a file that should be ignored', async function () { setupMatchingProjects(['active1']) - beforeEach(function () { - this.FileTypeManager.promises.shouldIgnore.resolves(true) + beforeEach(function (ctx) { + ctx.FileTypeManager.promises.shouldIgnore.resolves(true) }) receiveUpdate() expectProjectNotCreated() @@ -199,15 +237,15 @@ describe('TpdsUpdateHandler', function () { describe('update to a project on cooldown', async function () { setupMatchingProjects(['active1']) setupProjectOnCooldown() - beforeEach(async function () { + beforeEach(async function (ctx) { await expect( - this.TpdsUpdateHandler.promises.newUpdate( - this.userId, + ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.update, - this.source + ctx.projectName, + ctx.path, + ctx.update, + ctx.source ) ).to.be.rejectedWith(Errors.TooManyRequestsError) }) @@ -218,8 +256,8 @@ describe('TpdsUpdateHandler', function () { describe('getting a file delete', function () { describe('byId', function () { describe('with no matching project', function () { - beforeEach(function () { - this.projectId = new ObjectId().toString() + beforeEach(function (ctx) { + ctx.projectId = new ObjectId().toString() }) receiveFileDeleteById() expectDeleteNotProcessed() @@ -227,8 +265,8 @@ describe('TpdsUpdateHandler', function () { }) describe('with one matching active project', function () { - beforeEach(function () { - this.projectId = this.projects.active1._id.toString() + beforeEach(function (ctx) { + ctx.projectId = ctx.projects.active1._id.toString() }) receiveFileDeleteById() expectDeleteProcessed() @@ -379,13 +417,13 @@ describe('TpdsUpdateHandler', function () { describe('update to a project on cooldown', async function () { setupMatchingProjects(['active1']) setupProjectOnCooldown() - beforeEach(async function () { + beforeEach(async function (ctx) { await expect( - this.TpdsUpdateHandler.promises.createFolder( - this.userId, - this.projectId, - this.projectName, - this.path + ctx.TpdsUpdateHandler.promises.createFolder( + ctx.userId, + ctx.projectId, + ctx.projectName, + ctx.path ) ).to.be.rejectedWith(Errors.TooManyRequestsError) }) @@ -397,18 +435,18 @@ describe('TpdsUpdateHandler', function () { /* Setup helpers */ function setupMatchingProjects(projectKeys) { - beforeEach(function () { - const projects = projectKeys.map(key => this.projects[key]) - this.ProjectGetter.promises.findUsersProjectsByName - .withArgs(this.userId, this.projectName) + beforeEach(function (ctx) { + const projects = projectKeys.map(key => ctx.projects[key]) + ctx.ProjectGetter.promises.findUsersProjectsByName + .withArgs(ctx.userId, ctx.projectName) .resolves(projects) }) } function setupProjectOnCooldown() { - beforeEach(function () { - this.CooldownManager.promises.isProjectOnCooldown - .withArgs(this.projects.active1._id) + beforeEach(function (ctx) { + ctx.CooldownManager.promises.isProjectOnCooldown + .withArgs(ctx.projects.active1._id) .resolves(true) }) } @@ -416,76 +454,77 @@ function setupProjectOnCooldown() { /* Test helpers */ function receiveUpdate() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.newUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.update, - this.source + ctx.projectName, + ctx.path, + ctx.update, + ctx.source ) }) } function receiveUpdateById() { - beforeEach(function (done) { - this.TpdsUpdateHandler.newUpdate( - this.userId, - this.projectId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, + ctx.projectId, '', // projectName - this.path, - this.update, - this.source, - done + ctx.path, + ctx.update, + ctx.source ) }) } function receiveFileDelete() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.deleteUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.deleteUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.source + ctx.projectName, + ctx.path, + ctx.source ) }) } function receiveFileDeleteById() { - beforeEach(function (done) { - this.TpdsUpdateHandler.deleteUpdate( - this.userId, - this.projectId, - '', // projectName - this.path, - this.source, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.deleteUpdate( + ctx.userId, + ctx.projectId, + '', // projectName + ctx.path, + ctx.source, + resolve + ) + }) }) } function receiveProjectDelete() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.deleteUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.deleteUpdate( + ctx.userId, '', // projectId - this.projectName, + ctx.projectName, '/', - this.source + ctx.source ) }) } function receiveFolderUpdate() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.createFolder( - this.userId, - this.projectId, - this.projectName, - this.folderPath + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.createFolder( + ctx.userId, + ctx.projectId, + ctx.projectName, + ctx.folderPath ) }) } @@ -493,121 +532,121 @@ function receiveFolderUpdate() { /* Expectations */ function expectProjectCreated() { - it('creates a project', function () { + it('creates a project', function (ctx) { expect( - this.ProjectCreationHandler.promises.createBlankProject - ).to.have.been.calledWith(this.userId, this.projectName) + ctx.ProjectCreationHandler.promises.createBlankProject + ).to.have.been.calledWith(ctx.userId, ctx.projectName) }) - it('sets the root doc', function () { + it('sets the root doc', function (ctx) { expect( - this.RootDocManager.setRootDocAutomaticallyInBackground - ).to.have.been.calledWith(this.projects.active1._id) + ctx.RootDocManager.setRootDocAutomaticallyInBackground + ).to.have.been.calledWith(ctx.projects.active1._id) }) } function expectProjectNotCreated() { - it('does not create a project', function () { - expect(this.ProjectCreationHandler.promises.createBlankProject).not.to.have + it('does not create a project', function (ctx) { + expect(ctx.ProjectCreationHandler.promises.createBlankProject).not.to.have .been.called }) - it('does not set the root doc', function () { - expect(this.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have + it('does not set the root doc', function (ctx) { + expect(ctx.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have .been.called }) } function expectUpdateProcessed() { - it('processes the update', function () { - expect(this.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith( - this.userId, - this.projects.active1._id, - this.path, - this.update, - this.source + it('processes the update', function (ctx) { + expect(ctx.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith( + ctx.userId, + ctx.projects.active1._id, + ctx.path, + ctx.update, + ctx.source ) }) } function expectUpdateNotProcessed() { - it('does not process the update', function () { - expect(this.UpdateMerger.promises.mergeUpdate).not.to.have.been.called + it('does not process the update', function (ctx) { + expect(ctx.UpdateMerger.promises.mergeUpdate).not.to.have.been.called }) } function expectFolderUpdateProcessed() { - it('processes the folder update', function () { - expect(this.UpdateMerger.promises.createFolder).to.have.been.calledWith( - this.projects.active1._id, - this.folderPath, - this.userId + it('processes the folder update', function (ctx) { + expect(ctx.UpdateMerger.promises.createFolder).to.have.been.calledWith( + ctx.projects.active1._id, + ctx.folderPath, + ctx.userId ) }) } function expectFolderUpdateNotProcessed() { - it("doesn't process the folder update", function () { - expect(this.UpdateMerger.promises.createFolder).not.to.have.been.called + it("doesn't process the folder update", function (ctx) { + expect(ctx.UpdateMerger.promises.createFolder).not.to.have.been.called }) } function expectDropboxUnlinked() { - it('unlinks Dropbox', function () { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + it('unlinks Dropbox', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'removeDropbox', - this.userId, + ctx.userId, 'duplicate-projects' ) }) - it('creates a notification that dropbox was unlinked', function () { + it('creates a notification that dropbox was unlinked', function (ctx) { expect( - this.NotificationsBuilder.promises.dropboxDuplicateProjectNames - ).to.have.been.calledWith(this.userId) - expect(this.notification.create).to.have.been.calledWith(this.projectName) + ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames + ).to.have.been.calledWith(ctx.userId) + expect(ctx.notification.create).to.have.been.calledWith(ctx.projectName) }) } function expectDropboxNotUnlinked() { - it('does not unlink Dropbox', function () { - expect(this.Modules.promises.hooks.fire).not.to.have.been.called + it('does not unlink Dropbox', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).not.to.have.been.called }) - it('does not create a notification that dropbox was unlinked', function () { - expect(this.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not + it('does not create a notification that dropbox was unlinked', function (ctx) { + expect(ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not .to.have.been.called }) } function expectDeleteProcessed() { - it('processes the delete', function () { - expect(this.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith( - this.userId, - this.projects.active1._id, - this.path, - this.source + it('processes the delete', function (ctx) { + expect(ctx.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith( + ctx.userId, + ctx.projects.active1._id, + ctx.path, + ctx.source ) }) } function expectDeleteNotProcessed() { - it('does not process the delete', function () { - expect(this.UpdateMerger.promises.deleteUpdate).not.to.have.been.called + it('does not process the delete', function (ctx) { + expect(ctx.UpdateMerger.promises.deleteUpdate).not.to.have.been.called }) } function expectProjectDeleted() { - it('deletes the project', function () { + it('deletes the project', function (ctx) { expect( - this.ProjectDeleter.promises.markAsDeletedByExternalSource - ).to.have.been.calledWith(this.projects.active1._id) + ctx.ProjectDeleter.promises.markAsDeletedByExternalSource + ).to.have.been.calledWith(ctx.projects.active1._id) }) } function expectProjectNotDeleted() { - it('does not delete the project', function () { - expect(this.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to + it('does not delete the project', function (ctx) { + expect(ctx.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to .have.been.called }) } diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs index 8097218076..3408c3bb32 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -13,27 +13,27 @@ const MODULE_PATH = '../../../../app/src/Features/TokenAccess/TokenAccessController' describe('TokenAccessController', function () { - beforeEach(async function () { - this.token = 'abc123' - this.user = { _id: new ObjectId() } - this.project = { + beforeEach(async function (ctx) { + ctx.token = 'abc123' + ctx.user = { _id: new ObjectId() } + ctx.project = { _id: new ObjectId(), - owner_ref: this.user._id, + owner_ref: ctx.user._id, name: 'test', tokenAccessReadAndWrite_refs: [], tokenAccessReadOnly_refs: [], } - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub().returns() + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub().returns() - this.Settings = { + ctx.Settings = { siteUrl: 'https://www.dev-overleaf.com', adminPrivilegeAvailable: false, adminUrl: 'https://admin.dev-overleaf.com', adminDomains: ['overleaf.com'], } - this.TokenAccessHandler = { + ctx.TokenAccessHandler = { TOKEN_TYPES: { READ_ONLY: 'readOnly', READ_AND_WRITE: 'readAndWrite', @@ -46,7 +46,7 @@ describe('TokenAccessController', function () { grantSessionTokenAccess: sinon.stub(), promises: { addReadOnlyUserToProject: sinon.stub().resolves(), - getProjectByToken: sinon.stub().resolves(this.project), + getProjectByToken: sinon.stub().resolves(ctx.project), getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }), getV1DocInfo: sinon.stub(), removeReadAndWriteUserFromProject: sinon.stub().resolves(), @@ -54,16 +54,16 @@ describe('TokenAccessController', function () { }, } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.user._id), } - this.AuthenticationController = { + ctx.AuthenticationController = { setRedirectInSession: sinon.stub(), } - this.AuthorizationManager = { + ctx.AuthorizationManager = { promises: { getPrivilegeLevelForProject: sinon .stub() @@ -71,35 +71,35 @@ describe('TokenAccessController', function () { }, } - this.AuthorizationMiddleware = {} + ctx.AuthorizationMiddleware = {} - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.CollaboratorsInviteHandler = { + ctx.CollaboratorsInviteHandler = { promises: { revokeInviteForUser: sinon.stub().resolves(), }, } - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { addUserIdToProject: sinon.stub().resolves(), setCollaboratorPrivilegeLevel: sinon.stub().resolves(), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { userIsReadWriteTokenMember: sinon.stub().resolves(), isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(), @@ -107,24 +107,24 @@ describe('TokenAccessController', function () { }, } - this.EditorRealTimeController = { emitToRoom: sinon.stub() } + ctx.EditorRealTimeController = { emitToRoom: sinon.stub() } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.AnalyticsManager = { + ctx.AnalyticsManager = { recordEventForSession: sinon.stub(), recordEventForUserInBackground: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub().callsFake(async (userId, filter) => { - if (userId === this.userId) { - return this.user + if (userId === ctx.userId) { + return ctx.user } else { return null } @@ -134,322 +134,415 @@ describe('TokenAccessController', function () { }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAcceptEditCollaboratorInvite: sinon.stub().resolves(), }, } - this.TokenAccessController = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.Settings, - '../../../../app/src/Features/TokenAccess/TokenAccessHandler': - this.TokenAccessHandler, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Authorization/AuthorizationManager': - this.AuthorizationManager, - '../../../../app/src/Features/Authorization/AuthorizationMiddleware': - this.AuthorizationMiddleware, - '../../../../app/src/Features/Project/ProjectAuditLogHandler': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/Errors/Errors': (this.Errors = { + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authorization/AuthorizationManager', + () => ({ + default: ctx.AuthorizationManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authorization/AuthorizationMiddleware', + () => ({ + default: ctx.AuthorizationMiddleware, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/Errors', () => ({ + default: (ctx.Errors = { NotFoundError: sinon.stub(), }), - '../../../../app/src/Features/Collaborators/CollaboratorsHandler': - this.CollaboratorsHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler': - this.CollaboratorsInviteHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter': - this.CollaboratorsGetter, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Helpers/AsyncFormHelper': - (this.AsyncFormHelper = { - redirect: sinon.stub(), - }), - '../../../../app/src/Features/Helpers/AdminAuthorizationHelper': - (this.AdminAuthorizationHelper = { - canRedirectToAdminDomain: sinon.stub(), - }), - '../../../../app/src/Features/Helpers/UrlHelper': (this.UrlHelper = { - getSafeAdminDomainRedirect: sinon - .stub() - .callsFake( - path => `${this.Settings.adminUrl}${getSafeRedirectPath(path)}` - ), + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler', + () => ({ + default: ctx.CollaboratorsInviteHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Helpers/AsyncFormHelper', () => ({ + default: (ctx.AsyncFormHelper = { + redirect: sinon.stub(), }), - '../../../../app/src/Features/Analytics/AnalyticsManager': - this.AnalyticsManager, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Subscription/LimitationsManager': - this.LimitationsManager, - }) + })) + + vi.doMock( + '../../../../app/src/Features/Helpers/AdminAuthorizationHelper', + () => + (ctx.AdminAuthorizationHelper = { + canRedirectToAdminDomain: sinon.stub(), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Helpers/UrlHelper', + () => + (ctx.UrlHelper = { + getSafeAdminDomainRedirect: sinon + .stub() + .callsFake( + path => `${ctx.Settings.adminUrl}${getSafeRedirectPath(path)}` + ), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + ctx.TokenAccessController = (await import(MODULE_PATH)).default }) describe('grantTokenAccessReadAndWrite', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) }) describe('normal case (edit slot available)', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - true - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + true + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read and write invited member', function () { + it('adds the user as a read and write invited member', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readAndWrite' } ) }) - it('records a project-joined event for the user', function () { + it('records a project-joined event for the user', function (ctx) { expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(ctx.user._id, 'project-joined', { mode: 'edit', - projectId: this.project._id.toString(), - ownerId: this.project.owner_ref.toString(), + projectId: ctx.project._id.toString(), + ownerId: ctx.project.owner_ref.toString(), role: PrivilegeLevels.READ_AND_WRITE, source: 'link-sharing', }) }) - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('emits a project membership changed event', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'project:membership:changed', { members: true, invites: true } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when there are no edit collaborator slots available', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - false - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + false + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read only invited member instead (pendingEditor)', function () { + it('adds the user as a read only invited member instead (pendingEditor)', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_ONLY, { pendingEditor: true } ) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readOnly', pendingEditor: true } ) }) - it('records a project-joined event for the user', function () { + it('records a project-joined event for the user', function (ctx) { expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(ctx.user._id, 'project-joined', { mode: 'view', - projectId: this.project._id.toString(), + projectId: ctx.project._id.toString(), pendingEditor: true, - ownerId: this.project.owner_ref.toString(), + ownerId: ctx.project.owner_ref.toString(), role: PrivilegeLevels.READ_ONLY, source: 'link-sharing', }) }) - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('emits a project membership changed event', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'project:membership:changed', { members: true, invites: true } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when the access was already granted', function () { - beforeEach(function (done) { - this.project.tokenAccessReadAndWrite_refs.push(this.user._id) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.tokenAccessReadAndWrite_refs.push(ctx.user._id) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readAndWrite' } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('hash prefix missing in request', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read and write invited member', function () { + it('adds the user as a read and write invited member', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) }) - it('checks the hash prefix', function () { + it('checks the hash prefix', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('user is owner of project', function () { - beforeEach(function (done) { - this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( - PrivilegeLevels.OWNER - ) - this.req.params = { token: this.token } - this.req.body = {} - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( + PrivilegeLevels.OWNER + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = {} + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('checks token hash and includes log data', function () { + it('checks token hash and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, + ctx.user._id, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'user already has higher or same privilege', } ) @@ -457,33 +550,35 @@ describe('TokenAccessController', function () { }) describe('when user is not logged in', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.SessionManager.getLoggedInUserId.returns(null) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } }) describe('ANONYMOUS_READ_AND_WRITE_ENABLED is undefined', function () { - beforeEach(function (done) { - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to restricted', function () { - expect(this.res.json).to.have.been.calledWith({ + it('redirects to restricted', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ redirect: '/restricted', anonWriteAccessDenied: true, }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', null, @@ -493,42 +588,44 @@ describe('TokenAccessController', function () { ) }) - it('saves redirect URL with URL fragment', function () { + it('saves redirect URL with URL fragment', function (ctx) { expect( - this.AuthenticationController.setRedirectInSession.lastCall.args[1] + ctx.AuthenticationController.setRedirectInSession.lastCall.args[1] ).to.equal('/#prefix') }) }) describe('ANONYMOUS_READ_AND_WRITE_ENABLED is true', function () { - beforeEach(function (done) { - this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true - this.res.callback = done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true + ctx.res.callback = resolve - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to project', function () { - expect(this.res.json).to.have.been.calledWith({ - redirect: `/project/${this.project._id}`, + it('redirects to project', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `/project/${ctx.project._id}`, grantAnonymousAccess: 'readAndWrite', }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', null, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'granting read-write anonymous access', } ) @@ -537,44 +634,48 @@ describe('TokenAccessController', function () { }) describe('when Overleaf SaaS', function () { - beforeEach(function () { - this.Settings.overleaf = {} + beforeEach(function (ctx) { + ctx.Settings.overleaf = {} }) describe('when token is for v1 project', function () { - beforeEach(function (done) { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.TokenAccessHandler.promises.getV1DocInfo.resolves({ - exists: true, - has_owner: true, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves( + undefined + ) + ctx.TokenAccessHandler.promises.getV1DocInfo.resolves({ + exists: true, + has_owner: true, + }) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) }) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) }) - it('returns v1 import data', function () { - expect(this.res.json).to.have.been.calledWith({ + it('returns v1 import data', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ v1Import: { status: 'canDownloadZip', - projectId: this.token, + projectId: ctx.token, hasOwner: true, name: 'Untitled', brandInfo: undefined, }, }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, + ctx.user._id, { action: 'import v1', } @@ -583,31 +684,35 @@ describe('TokenAccessController', function () { }) describe('when token is not for a v1 or v2 project', function () { - beforeEach(function (done) { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.TokenAccessHandler.promises.getV1DocInfo.resolves({ - exists: false, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves( + undefined + ) + ctx.TokenAccessHandler.promises.getV1DocInfo.resolves({ + exists: false, + }) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) }) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) }) - it('returns 404', function () { - expect(this.res.sendStatus).to.have.been.calledWith(404) + it('returns 404', function (ctx) { + expect(ctx.res.sendStatus).to.have.been.calledWith(404) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, + ctx.user._id, { action: '404', } @@ -617,62 +722,67 @@ describe('TokenAccessController', function () { }) describe('not Overleaf SaaS', function () { - beforeEach(function () { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } }) - it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (done) { - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) + it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { - action: '404', - } - ) + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readAndWrite', + ctx.user._id, + { + action: '404', + } + ) - done() - } - ) + resolve() + } + ) + }) }) }) describe('when user is admin', function () { const admin = { _id: new ObjectId(), isAdmin: true } - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(admin._id) - this.SessionManager.getSessionUser.returns(admin) - this.AdminAuthorizationHelper.canRedirectToAdminDomain.returns(true) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.SessionManager.getLoggedInUserId.returns(admin._id) + ctx.SessionManager.getSessionUser.returns(admin) + ctx.AdminAuthorizationHelper.canRedirectToAdminDomain.returns(true) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } }) - it('redirects if project owner is non-admin', function () { - this.UserGetter.promises.getUserConfirmedEmails = sinon + it('redirects if project owner is non-admin', function (ctx) { + ctx.UserGetter.promises.getUserConfirmedEmails = sinon .stub() .resolves([{ email: 'test@not-overleaf.com' }]) - this.res.callback = () => { - expect(this.res.json).to.have.been.calledWith({ - redirect: `${this.Settings.adminUrl}/#prefix`, - }) - } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res - ) + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `${ctx.Settings.adminUrl}/#prefix`, + }) + resolve() + } + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res + ) + }) }) - it('grants access if project owner is an internal staff', function () { + it('grants access if project owner is an internal staff', function (ctx) { const internalStaff = { _id: new ObjectId(), isAdmin: true } const projectFromInternalStaff = { _id: new ObjectId(), @@ -681,16 +791,16 @@ describe('TokenAccessController', function () { tokenAccessReadOnly_refs: [], owner_ref: internalStaff._id, } - this.UserGetter.promises.getUser = sinon.stub().resolves(internalStaff) - this.UserGetter.promises.getUserConfirmedEmails = sinon + ctx.UserGetter.promises.getUser = sinon.stub().resolves(internalStaff) + ctx.UserGetter.promises.getUserConfirmedEmails = sinon .stub() .resolves([{ email: 'test@overleaf.com' }]) - this.TokenAccessHandler.promises.getProjectByToken = sinon + ctx.TokenAccessHandler.promises.getProjectByToken = sinon .stub() .resolves(projectFromInternalStaff) - this.res.callback = () => { + ctx.res.callback = () => { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( projectFromInternalStaff._id, undefined, @@ -698,327 +808,345 @@ describe('TokenAccessController', function () { PrivilegeLevels.READ_AND_WRITE ) } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res + ctx.TokenAccessController.grantTokenAccessReadAndWrite(ctx.req, ctx.res) + }) + }) + + it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) + + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readAndWrite', + ctx.user._id, + { + projectId: ctx.project._id, + action: 'token access not enabled', + } + ) + + resolve() + } ) }) }) - it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) { - this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) - - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { - projectId: this.project._id, - action: 'token access not enabled', - } - ) - - done() - } - ) - }) - - it('returns 400 when not using a read write token', function () { - this.TokenAccessHandler.isReadAndWriteToken.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res - ) - expect(this.res.sendStatus).to.have.been.calledWith(400) + it('returns 400 when not using a read write token', function (ctx) { + ctx.TokenAccessHandler.isReadAndWriteToken.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadAndWrite(ctx.req, ctx.res) + expect(ctx.res.sendStatus).to.have.been.calledWith(400) }) }) describe('grantTokenAccessReadOnly', function () { describe('normal case', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) + }) + + it('grants read-only access', function (ctx) { + expect( + ctx.TokenAccessHandler.promises.addReadOnlyUserToProject + ).to.have.been.calledWith( + ctx.user._id, + ctx.project._id, + ctx.project.owner_ref ) }) - it('grants read-only access', function () { + it('writes a project audit log', function (ctx) { expect( - this.TokenAccessHandler.promises.addReadOnlyUserToProject + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.user._id, - this.project._id, - this.project.owner_ref - ) - }) - - it('writes a project audit log', function () { - expect( - this.ProjectAuditLogHandler.promises.addEntry - ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'join-via-token', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readOnly' } ) }) - it('checks if hash prefix matches', function () { + it('checks if hash prefix matches', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readOnly', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when the access was already granted', function () { - beforeEach(function (done) { - this.project.tokenAccessReadOnly_refs.push(this.user._id) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.tokenAccessReadOnly_refs.push(ctx.user._id) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it("doesn't write a project audit log", function () { - expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been + it("doesn't write a project audit log", function (ctx) { + expect(ctx.ProjectAuditLogHandler.promises.addEntry).to.not.have.been .called }) - it('still checks if hash prefix matches', function () { + it('still checks if hash prefix matches', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readOnly', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) - it('returns 400 when not using a read only token', function () { - this.TokenAccessHandler.isReadOnlyToken.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadOnly(this.req, this.res) - expect(this.res.sendStatus).to.have.been.calledWith(400) + it('returns 400 when not using a read only token', function (ctx) { + ctx.TokenAccessHandler.isReadOnlyToken.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadOnly(ctx.req, ctx.res) + expect(ctx.res.sendStatus).to.have.been.calledWith(400) }) describe('anonymous users', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.SessionManager.getLoggedInUserId.returns(null) - this.res.callback = done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.SessionManager.getLoggedInUserId.returns(null) + ctx.res.callback = resolve - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('allows anonymous users and checks the token hash', function () { - expect(this.res.json).to.have.been.calledWith({ - redirect: `/project/${this.project._id}`, + it('allows anonymous users and checks the token hash', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `/project/${ctx.project._id}`, grantAnonymousAccess: 'readOnly', }) expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith(this.token, undefined, 'readOnly', null, { - projectId: this.project._id, + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith(ctx.token, undefined, 'readOnly', null, { + projectId: ctx.project._id, action: 'granting read-only anonymous access', }) }) }) describe('user is owner of project', function () { - beforeEach(function (done) { - this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( - PrivilegeLevels.OWNER - ) - this.req.params = { token: this.token } - this.req.body = {} - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( + PrivilegeLevels.OWNER + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = {} + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('checks token hash and includes log data', function () { + it('checks token hash and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readOnly', - this.user._id, + ctx.user._id, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'user already has higher or same privilege', } ) }) }) - it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) { - this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) + it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readOnly', - this.user._id, - { - projectId: this.project._id, - action: 'token access not enabled', - } - ) + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readOnly', + ctx.user._id, + { + projectId: ctx.project._id, + action: 'token access not enabled', + } + ) - done() - } - ) + resolve() + } + ) + }) }) }) describe('ensureUserCanUseSharingUpdatesConsentPage', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('when not in link sharing changes test', function () { - beforeEach(function (done) { - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AsyncFormHelper.redirect = sinon.stub().callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) describe('when link sharing changes test active', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ + beforeEach(function (ctx) { + ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'active', }) }) describe('when user is not an invited editor and is a read write token member', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - false - ) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.next.callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + false + ) + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + ctx.next.callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('calls next', function () { + it('calls next', function (ctx) { expect( - this.CollaboratorsGetter.promises + ctx.CollaboratorsGetter.promises .isUserInvitedReadWriteMemberOfProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember - ).to.have.been.calledWith(this.user._id, this.project._id) - expect(this.next).to.have.been.calledOnce - expect(this.next.firstCall.args[0]).to.not.exist + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) + expect(ctx.next).to.have.been.calledOnce + expect(ctx.next.firstCall.args[0]).to.not.exist }) }) describe('when user is already an invited editor', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - true - ) - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + true + ) + ctx.AsyncFormHelper.redirect = sinon + .stub() + .callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) describe('when user not a read write token member', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - false - ) - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + false + ) + ctx.AsyncFormHelper.redirect = sinon + .stub() + .callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) @@ -1026,116 +1154,122 @@ describe('TokenAccessController', function () { }) describe('moveReadWriteToCollaborators', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('when there are collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) }) describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToCollaborators( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('sets the privilege level to read and write for the invited viewer', function () { + it('sets the privilege level to read and write for the invited viewer', function (ctx) { expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ctx.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) - expect(this.res.sendStatus).to.have.been.calledWith(204) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) }) }) describe('when there are no edit collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( false ) }) describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToCollaborators( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('sets the privilege level to read only for the invited viewer (pendingEditor)', function () { + it('sets the privilege level to read only for the invited viewer (pendingEditor)', function (ctx) { expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ctx.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_ONLY, { pendingEditor: true } ) - expect(this.res.sendStatus).to.have.been.calledWith(204) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) }) }) }) describe('moveReadWriteToReadOnly', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('previously joined token access user moving to anonymous viewer', function () { - beforeEach(function (done) { - this.res.callback = done - this.TokenAccessController.moveReadWriteToReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('removes them from read write token access refs and adds them to read only token access refs', function () { + it('removes them from read write token access refs and adds them to read only token access refs', function (ctx) { expect( - this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly - ).to.have.been.calledWith(this.user._id, this.project._id) - expect(this.res.sendStatus).to.have.been.calledWith(204) + ctx.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'readonly-via-sharing-updates', - this.user._id, - this.req.ip + ctx.user._id, + ctx.req.ip ) }) }) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs index 35682f346c..1f6fd7adb9 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs @@ -2,15 +2,12 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS206: Consider reworking classes to avoid initClass * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ +import { vi } from 'vitest' import sinon from 'sinon' - import { expect } from 'chai' - -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import ArchiveErrors from '../../../../app/src/Features/Uploads/ArchiveErrors.js' @@ -19,12 +16,12 @@ const modulePath = '../../../../app/src/Features/Uploads/ProjectUploadController.mjs' describe('ProjectUploadController', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { let Timer - this.req = new MockRequest() - this.res = new MockResponse() - this.user_id = 'user-id-123' - this.metrics = { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.user_id = 'user-id-123' + ctx.metrics = { Timer: (Timer = (function () { Timer = class Timer { static initClass() { @@ -35,262 +32,298 @@ describe('ProjectUploadController', function () { return Timer })()), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user_id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user_id), } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: {}, } - this.EditorController = { + ctx.EditorController = { promises: {}, } - return (this.ProjectUploadController = await esmock.strict(modulePath, { - multer: sinon.stub(), - '@overleaf/settings': { path: {} }, - '../../../../app/src/Features/Uploads/ProjectUploadManager': - (this.ProjectUploadManager = {}), - '../../../../app/src/Features/Uploads/FileSystemImportManager': - (this.FileSystemImportManager = {}), - '@overleaf/metrics': this.metrics, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Uploads/ArchiveErrors': ArchiveErrors, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/Editor/EditorController': - this.EditorController, - fs: (this.fs = {}), + vi.doMock('multer', () => ({ + default: sinon.stub(), })) + + vi.doMock('@overleaf/settings', () => ({ + default: { path: {} }, + })) + + vi.doMock( + '../../../../app/src/Features/Uploads/ProjectUploadManager', + () => ({ + default: (ctx.ProjectUploadManager = {}), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Uploads/FileSystemImportManager', + () => ({ + default: (ctx.FileSystemImportManager = {}), + }) + ) + + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.metrics, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Uploads/ArchiveErrors', + () => ArchiveErrors + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + vi.doMock('fs', () => ({ + default: (ctx.fs = {}), + })) + + ctx.ProjectUploadController = (await import(modulePath)).default }) describe('uploadProject', function () { - beforeEach(function () { - this.path = '/path/to/file/on/disk.zip' - this.name = 'filename.zip' - this.req.file = { - path: this.path, + beforeEach(function (ctx) { + ctx.path = '/path/to/file/on/disk.zip' + ctx.fileName = 'filename.zip' + ctx.req.file = { + path: ctx.path, } - this.req.body = { - name: this.name, + ctx.req.body = { + name: ctx.fileName, } - this.req.session = { + ctx.req.session = { user: { - _id: this.user_id, + _id: ctx.user_id, }, } - this.project = { _id: (this.project_id = 'project-id-123') } + ctx.project = { _id: (ctx.project_id = 'project-id-123') } - return (this.fs.unlink = sinon.stub()) + ctx.fs.unlink = sinon.stub() }) describe('successfully', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() - .callsArgWith(3, null, this.project) - return this.ProjectUploadController.uploadProject(this.req, this.res) + .callsArgWith(3, null, ctx.project) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should create a project owned by the logged in user', function () { - return this.ProjectUploadManager.createProjectFromZipArchive - .calledWith(this.user_id) + it('should create a project owned by the logged in user', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive + .calledWith(ctx.user_id) .should.equal(true) }) - it('should create a project with the same name as the zip archive', function () { - return this.ProjectUploadManager.createProjectFromZipArchive + it('should create a project with the same name as the zip archive', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive .calledWith(sinon.match.any, 'filename', sinon.match.any) .should.equal(true) }) - it('should create a project from the zip archive', function () { - return this.ProjectUploadManager.createProjectFromZipArchive - .calledWith(sinon.match.any, sinon.match.any, this.path) + it('should create a project from the zip archive', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive + .calledWith(sinon.match.any, sinon.match.any, ctx.path) .should.equal(true) }) - it('should return a successful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a successful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: true, - project_id: this.project_id, + project_id: ctx.project_id, }) ) }) - it('should record the time taken to do the upload', function () { - return this.metrics.Timer.prototype.done.called.should.equal(true) + it('should record the time taken to do the upload', function (ctx) { + ctx.metrics.Timer.prototype.done.called.should.equal(true) }) - it('should remove the uploaded file', function () { - return this.fs.unlink.calledWith(this.path).should.equal(true) + it('should remove the uploaded file', function (ctx) { + ctx.fs.unlink.calledWith(ctx.path).should.equal(true) }) }) describe('when ProjectUploadManager.createProjectFromZipArchive fails', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() - .callsArgWith(3, new Error('Something went wrong'), this.project) - return this.ProjectUploadController.uploadProject(this.req, this.res) + .callsArgWith(3, new Error('Something went wrong'), ctx.project) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should return a failed response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a failed response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'upload_failed' }) ) }) }) describe('when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() .callsArgWith( 3, new ArchiveErrors.ZipContentsTooLargeError(), - this.project + ctx.project ) - return this.ProjectUploadController.uploadProject(this.req, this.res) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should return the reported error to the FileUploader client', function () { - expect(JSON.parse(this.res.body)).to.deep.equal({ + it('should return the reported error to the FileUploader client', function (ctx) { + expect(JSON.parse(ctx.res.body)).to.deep.equal({ success: false, error: 'zip_contents_too_large', }) }) - it("should return an 'unprocessable entity' status code", function () { - return expect(this.res.statusCode).to.equal(422) + it("should return an 'unprocessable entity' status code", function (ctx) { + expect(ctx.res.statusCode).to.equal(422) }) }) }) describe('uploadFile', function () { - beforeEach(function () { - this.project_id = 'project-id-123' - this.folder_id = 'folder-id-123' - this.path = '/path/to/file/on/disk.png' - this.name = 'filename.png' - this.req.file = { - path: this.path, + beforeEach(function (ctx) { + ctx.project_id = 'project-id-123' + ctx.folder_id = 'folder-id-123' + ctx.path = '/path/to/file/on/disk.png' + ctx.fileName = 'filename.png' + ctx.req.file = { + path: ctx.path, } - this.req.body = { - name: this.name, + ctx.req.body = { + name: ctx.fileName, } - this.req.session = { + ctx.req.session = { user: { - _id: this.user_id, + _id: ctx.user_id, }, } - this.req.params = { Project_id: this.project_id } - this.req.query = { folder_id: this.folder_id } - return (this.fs.unlink = sinon.stub()) + ctx.req.params = { Project_id: ctx.project_id } + ctx.req.query = { folder_id: ctx.folder_id } + ctx.fs.unlink = sinon.stub() }) describe('successfully', function () { - beforeEach(function () { - this.entity = { + beforeEach(function (ctx) { + ctx.entity = { _id: '1234', type: 'file', } - this.FileSystemImportManager.addEntity = sinon + ctx.FileSystemImportManager.addEntity = sinon .stub() - .callsArgWith(6, null, this.entity) - return this.ProjectUploadController.uploadFile(this.req, this.res) + .callsArgWith(6, null, ctx.entity) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should insert the file', function () { - return this.FileSystemImportManager.addEntity + it('should insert the file', function (ctx) { + return ctx.FileSystemImportManager.addEntity .calledWith( - this.user_id, - this.project_id, - this.folder_id, - this.name, - this.path + ctx.user_id, + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.path ) .should.equal(true) }) - it('should return a successful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a successful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: true, - entity_id: this.entity._id, + entity_id: ctx.entity._id, entity_type: 'file', }) ) }) - it('should time the request', function () { - return this.metrics.Timer.prototype.done.called.should.equal(true) + it('should time the request', function (ctx) { + ctx.metrics.Timer.prototype.done.called.should.equal(true) }) - it('should remove the uploaded file', function () { - return this.fs.unlink.calledWith(this.path).should.equal(true) + it('should remove the uploaded file', function (ctx) { + ctx.fs.unlink.calledWith(ctx.path).should.equal(true) }) }) describe('with folder structure', function () { - beforeEach(function (done) { - this.entity = { - _id: '1234', - type: 'file', - } - this.FileSystemImportManager.addEntity = sinon - .stub() - .callsArgWith(6, null, this.entity) - this.ProjectLocator.promises.findElement = sinon.stub().resolves({ - path: { fileSystem: '/test' }, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.entity = { + _id: '1234', + type: 'file', + } + ctx.FileSystemImportManager.addEntity = sinon + .stub() + .callsArgWith(6, null, ctx.entity) + ctx.ProjectLocator.promises.findElement = sinon.stub().resolves({ + path: { fileSystem: '/test' }, + }) + ctx.EditorController.promises.mkdirp = sinon.stub().resolves({ + lastFolder: { _id: 'folder-id' }, + }) + ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName + ctx.res.json = data => { + expect(data.success).to.be.true + resolve() + } + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - this.EditorController.promises.mkdirp = sinon.stub().resolves({ - lastFolder: { _id: 'folder-id' }, - }) - this.req.body.relativePath = 'foo/bar/' + this.name - this.res.json = data => { - expect(data.success).to.be.true - done() - } - this.ProjectUploadController.uploadFile(this.req, this.res) }) - it('should insert the file', function () { - this.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly( + it('should insert the file', function (ctx) { + ctx.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly( { - project_id: this.project_id, - element_id: this.folder_id, + project_id: ctx.project_id, + element_id: ctx.folder_id, type: 'folder', } ) - this.EditorController.promises.mkdirp.should.be.calledWith( - this.project_id, + ctx.EditorController.promises.mkdirp.should.be.calledWith( + ctx.project_id, '/test/foo/bar', - this.user_id + ctx.user_id ) - this.FileSystemImportManager.addEntity.should.be.calledOnceWith( - this.user_id, - this.project_id, + ctx.FileSystemImportManager.addEntity.should.be.calledOnceWith( + ctx.user_id, + ctx.project_id, 'folder-id', - this.name, - this.path + ctx.fileName, + ctx.path ) }) }) describe('when FileSystemImportManager.addEntity returns a generic error', function () { - beforeEach(function () { - this.FileSystemImportManager.addEntity = sinon + beforeEach(function (ctx) { + ctx.FileSystemImportManager.addEntity = sinon .stub() .callsArgWith(6, new Error('Sorry something went wrong')) - return this.ProjectUploadController.uploadFile(this.req, this.res) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return an unsuccessful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return an unsuccessful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, }) @@ -299,15 +332,15 @@ describe('ProjectUploadController', function () { }) describe('when FileSystemImportManager.addEntity returns a too many files error', function () { - beforeEach(function () { - this.FileSystemImportManager.addEntity = sinon + beforeEach(function (ctx) { + ctx.FileSystemImportManager.addEntity = sinon .stub() .callsArgWith(6, new Error('project_has_too_many_files')) - return this.ProjectUploadController.uploadFile(this.req, this.res) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return an unsuccessful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return an unsuccessful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'project_has_too_many_files', @@ -317,13 +350,13 @@ describe('ProjectUploadController', function () { }) describe('with an invalid filename', function () { - beforeEach(function () { - this.req.body.name = '' - return this.ProjectUploadController.uploadFile(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.body.name = '' + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return a a non success response', function () { - return expect(this.res.body).to.deep.equal( + it('should return a a non success response', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'invalid_filename', diff --git a/services/web/test/unit/src/User/UserPagesController.test.mjs b/services/web/test/unit/src/User/UserPagesController.test.mjs index 6b19ef03f5..181c9513ae 100644 --- a/services/web/test/unit/src/User/UserPagesController.test.mjs +++ b/services/web/test/unit/src/User/UserPagesController.test.mjs @@ -1,18 +1,15 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import assert from 'assert' import sinon from 'sinon' import { expect } from 'chai' import MockResponse from '../helpers/MockResponse.js' import MockRequest from '../helpers/MockRequest.js' -const modulePath = new URL( - '../../../../app/src/Features/User/UserPagesController', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/User/UserPagesController' describe('UserPagesController', function () { - beforeEach(async function () { - this.settings = { + beforeEach(async function (ctx) { + ctx.settings = { apis: { v1: { url: 'some.host', @@ -21,8 +18,8 @@ describe('UserPagesController', function () { }, }, } - this.user = { - _id: (this.user_id = 'kwjewkl'), + ctx.user = { + _id: (ctx.user_id = 'kwjewkl'), features: {}, email: 'joe@example.com', ip_address: '1.1.1.1', @@ -39,414 +36,507 @@ describe('UserPagesController', function () { papers: { encrypted: 'cccc' }, }, } - this.adminEmail = 'group-admin-email@overleaf.com' - this.subscriptionViewModel = { + ctx.adminEmail = 'group-admin-email@overleaf.com' + ctx.subscriptionViewModel = { memberGroupSubscriptions: [], } - this.UserGetter = { + ctx.UserGetter = { getUser: sinon.stub(), promises: { getUser: sinon.stub() }, } - this.UserSessionsManager = { getAllUserSessions: sinon.stub() } - this.dropboxStatus = {} - this.ErrorController = { notFound: sinon.stub() } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.user), + ctx.UserSessionsManager = { getAllUserSessions: sinon.stub() } + ctx.dropboxStatus = {} + ctx.ErrorController = { notFound: sinon.stub() } + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.user), } - this.NewsletterManager = { + ctx.NewsletterManager = { subscribed: sinon.stub().yields(), } - this.AuthenticationController = { + ctx.AuthenticationController = { getRedirectFromSession: sinon.stub(), setRedirectInSession: sinon.stub(), } - this.Features = { + ctx.Features = { hasFeature: sinon.stub().returns(false), } - this.PersonalAccessTokenManager = { + ctx.PersonalAccessTokenManager = { listTokens: sinon.stub().returns([]), } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { - getAdminEmail: sinon.stub().returns(this.adminEmail), + getAdminEmail: sinon.stub().returns(ctx.adminEmail), getMemberSubscriptions: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().returns('default'), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves(), }, }, } - this.UserPagesController = await esmock.strict(modulePath, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/User/UserSessionsManager': - this.UserSessionsManager, - '../../../../app/src/Features/Newsletter/NewsletterManager': - this.NewsletterManager, - '../../../../app/src/Features/Errors/ErrorController': - this.ErrorController, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/infrastructure/Features': this.Features, - '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager': - this.PersonalAccessTokenManager, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/infrastructure/Modules': this.Modules, - request: (this.request = sinon.stub()), - }) - this.req = new MockRequest() - this.req.session.user = this.user - this.res = new MockResponse() + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock( + '../../../../app/src/Features/Newsletter/NewsletterManager', + () => ({ + default: ctx.NewsletterManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: ctx.ErrorController, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock( + '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager', + () => ({ + default: ctx.PersonalAccessTokenManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + ctx.request = sinon.stub() + vi.doMock('request', () => ({ + default: ctx.request, + })) + + ctx.UserPagesController = (await import(modulePath)).default + ctx.req = new MockRequest() + ctx.req.session.user = ctx.user + ctx.res = new MockResponse() }) describe('registerPage', function () { - it('should render the register page', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/register') - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + it('should render the register page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/register') + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should set sharedProjectData', function (done) { - this.req.session.sharedProjectData = { - project_name: 'myProject', - user_first_name: 'user_first_name_here', - } + it('should set sharedProjectData', function (ctx) { + return new Promise(resolve => { + ctx.req.session.sharedProjectData = { + project_name: 'myProject', + user_first_name: 'user_first_name_here', + } - this.res.callback = () => { - this.res.renderedVariables.sharedProjectData.project_name.should.equal( - 'myProject' - ) - this.res.renderedVariables.sharedProjectData.user_first_name.should.equal( - 'user_first_name_here' - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + ctx.res.callback = () => { + ctx.res.renderedVariables.sharedProjectData.project_name.should.equal( + 'myProject' + ) + ctx.res.renderedVariables.sharedProjectData.user_first_name.should.equal( + 'user_first_name_here' + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should set newTemplateData', function (done) { - this.req.session.templateData = { templateName: 'templateName' } + it('should set newTemplateData', function (ctx) { + return new Promise(resolve => { + ctx.req.session.templateData = { templateName: 'templateName' } - this.res.callback = () => { - this.res.renderedVariables.newTemplateData.templateName.should.equal( - 'templateName' - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + ctx.res.callback = () => { + ctx.res.renderedVariables.newTemplateData.templateName.should.equal( + 'templateName' + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should not set the newTemplateData if there is nothing in the session', function (done) { - this.res.callback = () => { - assert.equal( - this.res.renderedVariables.newTemplateData.templateName, - undefined - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + it('should not set the newTemplateData if there is nothing in the session', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + assert.equal( + ctx.res.renderedVariables.newTemplateData.templateName, + undefined + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) }) describe('loginForm', function () { - it('should render the login page', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/login') - done() - } - this.UserPagesController.loginPage(this.req, this.res, done) + it('should render the login page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/login') + resolve() + } + ctx.UserPagesController.loginPage(ctx.req, ctx.res, resolve) + }) }) describe('when an explicit redirect is set via query string', function () { - beforeEach(function () { - this.AuthenticationController.getRedirectFromSession = sinon + beforeEach(function (ctx) { + ctx.AuthenticationController.getRedirectFromSession = sinon .stub() .returns(null) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.req.query.redir = '/somewhere/in/particular' + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.req.query.redir = '/somewhere/in/particular' }) - it('should set a redirect', function (done) { - this.res.callback = page => { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( - 1 - ) - expect( - this.AuthenticationController.setRedirectInSession.lastCall.args[1] - ).to.equal(this.req.query.redir) - done() - } - this.UserPagesController.loginPage(this.req, this.res, done) + it('should set a redirect', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = page => { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + expect( + ctx.AuthenticationController.setRedirectInSession.lastCall.args[1] + ).to.equal(ctx.req.query.redir) + resolve() + } + ctx.UserPagesController.loginPage(ctx.req, ctx.res, resolve) + }) }) }) }) describe('sessionsPage', function () { - beforeEach(function () { - this.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) + beforeEach(function (ctx) { + ctx.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) }) - it('should render user/sessions', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/sessions') - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should render user/sessions', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/sessions') + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) - it('should include current session data in the view', function (done) { - this.res.callback = () => { - expect(this.res.renderedVariables.currentSession).to.deep.equal({ - ip_address: '1.1.1.1', - session_created: 'timestamp', - }) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should include current session data in the view', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.currentSession).to.deep.equal({ + ip_address: '1.1.1.1', + session_created: 'timestamp', + }) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) - it('should have called getAllUserSessions', function (done) { - this.res.callback = page => { - this.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should have called getAllUserSessions', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = page => { + ctx.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) describe('when getAllUserSessions produces an error', function () { - beforeEach(function () { - this.UserSessionsManager.getAllUserSessions.callsArgWith( + beforeEach(function (ctx) { + ctx.UserSessionsManager.getAllUserSessions.callsArgWith( 2, new Error('woops') ) }) - it('should call next with an error', function (done) { - this.next = err => { - assert(err !== null) - assert(err instanceof Error) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, this.next) + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = err => { + assert(err !== null) + assert(err instanceof Error) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, ctx.next) + }) }) }) }) describe('emailPreferencesPage', function () { - beforeEach(function () { - this.UserGetter.getUser = sinon.stub().yields(null, this.user) + beforeEach(function (ctx) { + ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user) }) - it('render page with subscribed status', function (done) { - this.NewsletterManager.subscribed.yields(null, true) - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/email-preferences') - this.res.renderedVariables.title.should.equal('newsletter_info_title') - this.res.renderedVariables.subscribed.should.equal(true) - done() - } - this.UserPagesController.emailPreferencesPage(this.req, this.res, done) + it('render page with subscribed status', function (ctx) { + return new Promise(resolve => { + ctx.NewsletterManager.subscribed.yields(null, true) + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/email-preferences') + ctx.res.renderedVariables.title.should.equal('newsletter_info_title') + ctx.res.renderedVariables.subscribed.should.equal(true) + resolve() + } + ctx.UserPagesController.emailPreferencesPage(ctx.req, ctx.res, resolve) + }) }) - it('render page with unsubscribed status', function (done) { - this.NewsletterManager.subscribed.yields(null, false) - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/email-preferences') - this.res.renderedVariables.title.should.equal('newsletter_info_title') - this.res.renderedVariables.subscribed.should.equal(false) - done() - } - this.UserPagesController.emailPreferencesPage(this.req, this.res, done) + it('render page with unsubscribed status', function (ctx) { + return new Promise(resolve => { + ctx.NewsletterManager.subscribed.yields(null, false) + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/email-preferences') + ctx.res.renderedVariables.title.should.equal('newsletter_info_title') + ctx.res.renderedVariables.subscribed.should.equal(false) + resolve() + } + ctx.UserPagesController.emailPreferencesPage(ctx.req, ctx.res, resolve) + }) }) }) describe('settingsPage', function () { - beforeEach(function () { - this.request.get = sinon + beforeEach(function (ctx) { + ctx.request.get = sinon .stub() .callsArgWith(1, null, { statusCode: 200 }, { has_password: true }) - this.UserGetter.promises.getUser = sinon.stub().resolves(this.user) + ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user) }) - it('should render user/settings', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/settings') - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should render user/settings', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/settings') + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send user', function (done) { - this.res.callback = () => { - this.res.renderedVariables.user.id.should.equal(this.user._id) - this.res.renderedVariables.user.email.should.equal(this.user.email) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should send user', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.user.id.should.equal(ctx.user._id) + ctx.res.renderedVariables.user.email.should.equal(ctx.user.email) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it("should set 'shouldAllowEditingDetails' to true", function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal(true) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it("should set 'shouldAllowEditingDetails' to true", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(true) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should restructure thirdPartyIdentifiers data for template use', function (done) { - const expectedResult = { - google: 'testId', - } - this.res.callback = () => { - expect(this.res.renderedVariables.thirdPartyIds).to.include( - expectedResult - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should restructure thirdPartyIdentifiers data for template use', function (ctx) { + return new Promise(resolve => { + const expectedResult = { + google: 'testId', + } + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.thirdPartyIds).to.include( + expectedResult + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it("should set and clear 'projectSyncSuccessMessage'", function (done) { - this.req.session.projectSyncSuccessMessage = 'Some Sync Success' - this.res.callback = () => { - this.res.renderedVariables.projectSyncSuccessMessage.should.equal( - 'Some Sync Success' - ) - expect(this.req.session.projectSyncSuccessMessage).to.not.exist - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it("should set and clear 'projectSyncSuccessMessage'", function (ctx) { + return new Promise(resolve => { + ctx.req.session.projectSyncSuccessMessage = 'Some Sync Success' + ctx.res.callback = () => { + ctx.res.renderedVariables.projectSyncSuccessMessage.should.equal( + 'Some Sync Success' + ) + expect(ctx.req.session.projectSyncSuccessMessage).to.not.exist + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should cast refProviders to booleans', function (done) { - this.res.callback = () => { - expect(this.res.renderedVariables.user.refProviders).to.deep.equal({ - mendeley: true, - papers: true, - zotero: true, - }) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should cast refProviders to booleans', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.user.refProviders).to.deep.equal({ + mendeley: true, + papers: true, + zotero: true, + }) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send the correct managed user admin email', function (done) { - this.res.callback = () => { - expect( - this.res.renderedVariables.currentManagedUserAdminEmail - ).to.equal(this.adminEmail) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should send the correct managed user admin email', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect( + ctx.res.renderedVariables.currentManagedUserAdminEmail + ).to.equal(ctx.adminEmail) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send info for groups with SSO enabled', function (done) { - this.user.enrollment = { - sso: [ - { - groupId: 'abc123abc123', - primary: true, - linkedAt: new Date(), + it('should send info for groups with SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.user.enrollment = { + sso: [ + { + groupId: 'abc123abc123', + primary: true, + linkedAt: new Date(), + }, + ], + } + const group1 = { + _id: 'abc123abc123', + teamName: 'Group SSO Rulz', + admin_id: { + email: 'admin.email@ssolove.com', }, - ], - } - const group1 = { - _id: 'abc123abc123', - teamName: 'Group SSO Rulz', - admin_id: { - email: 'admin.email@ssolove.com', - }, - linked: true, - } - const group2 = { - _id: 'def456def456', - admin_id: { - email: 'someone.else@noname.co.uk', - }, - linked: false, - } - - this.Modules.promises.hooks.fire - .withArgs('getUserGroupsSSOEnrollmentStatus') - .resolves([[group1, group2]]) - - this.res.callback = () => { - expect( - this.res.renderedVariables.memberOfSSOEnabledGroups - ).to.deep.equal([ - { - groupId: 'abc123abc123', - groupName: 'Group SSO Rulz', - adminEmail: 'admin.email@ssolove.com', - linked: true, + linked: true, + } + const group2 = { + _id: 'def456def456', + admin_id: { + email: 'someone.else@noname.co.uk', }, - { - groupId: 'def456def456', - groupName: undefined, - adminEmail: 'someone.else@noname.co.uk', - linked: false, - }, - ]) - done() - } + linked: false, + } - this.UserPagesController.settingsPage(this.req, this.res, done) + ctx.Modules.promises.hooks.fire + .withArgs('getUserGroupsSSOEnrollmentStatus') + .resolves([[group1, group2]]) + + ctx.res.callback = () => { + expect( + ctx.res.renderedVariables.memberOfSSOEnabledGroups + ).to.deep.equal([ + { + groupId: 'abc123abc123', + groupName: 'Group SSO Rulz', + adminEmail: 'admin.email@ssolove.com', + linked: true, + }, + { + groupId: 'def456def456', + groupName: undefined, + adminEmail: 'someone.else@noname.co.uk', + linked: false, + }, + ]) + resolve() + } + + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) describe('when ldap.updateUserDetailsOnLogin is true', function () { - beforeEach(function () { - this.settings.ldap = { updateUserDetailsOnLogin: true } + beforeEach(function (ctx) { + ctx.settings.ldap = { updateUserDetailsOnLogin: true } }) - afterEach(function () { - delete this.settings.ldap + afterEach(function (ctx) { + delete ctx.settings.ldap }) - it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal( - false - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should set "shouldAllowEditingDetails" to false', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) }) describe('when saml.updateUserDetailsOnLogin is true', function () { - beforeEach(function () { - this.settings.saml = { updateUserDetailsOnLogin: true } + beforeEach(function (ctx) { + ctx.settings.saml = { updateUserDetailsOnLogin: true } }) - afterEach(function () { - delete this.settings.saml + afterEach(function (ctx) { + delete ctx.settings.saml }) - it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal( - false - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should set "shouldAllowEditingDetails" to false', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) }) }) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs b/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs index f6dedf2097..55bc62cd2d 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs +++ b/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.js' @@ -15,27 +15,39 @@ const assertCalledWith = sinon.assert.calledWith const modulePath = '../../../../app/src/Features/UserMembership/UserMembershipController.mjs' +vi.mock( + '../../../../app/src/Features/UserMembership/UserMembershipErrors.js', + () => + vi.importActual( + '../../../../app/src/Features/UserMembership/UserMembershipErrors.js' + ) +) + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('UserMembershipController', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.req.params.id = 'mock-entity-id' - this.user = { _id: 'mock-user-id' } - this.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' } - this.subscription = { + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.req.params.id = 'mock-entity-id' + ctx.user = { _id: 'mock-user-id' } + ctx.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' } + ctx.subscription = { _id: 'mock-subscription-id', admin_id: 'mock-admin-id', - fetchV1Data: callback => callback(null, this.subscription), + fetchV1Data: callback => callback(null, ctx.subscription), } - this.institution = { + ctx.institution = { _id: 'mock-institution-id', v1Id: 123, fetchV1Data: callback => { - const institution = Object.assign({}, this.institution) + const institution = Object.assign({}, ctx.institution) institution.name = 'Test Institution Name' callback(null, institution) }, } - this.users = [ + ctx.users = [ { _id: 'mock-member-id-1', email: 'mock-email-1@foo.com', @@ -50,106 +62,136 @@ describe('UserMembershipController', function () { }, ] - this.Settings = { + ctx.Settings = { managedUsers: { enabled: false, }, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.user), - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.user), + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.SSOConfig = { + ctx.SSOConfig = { findById: sinon .stub() .returns({ exec: sinon.stub().resolves({ enabled: true }) }), } - this.UserMembershipHandler = { - getEntity: sinon.stub().yields(null, this.subscription), - createEntity: sinon.stub().yields(null, this.institution), - getUsers: sinon.stub().yields(null, this.users), - addUser: sinon.stub().yields(null, this.newUser), + ctx.UserMembershipHandler = { + getEntity: sinon.stub().yields(null, ctx.subscription), + createEntity: sinon.stub().yields(null, ctx.institution), + getUsers: sinon.stub().yields(null, ctx.users), + addUser: sinon.stub().yields(null, ctx.newUser), removeUser: sinon.stub().yields(null), promises: { - getUsers: sinon.stub().resolves(this.users), + getUsers: sinon.stub().resolves(ctx.users), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, getAssignment: sinon.stub().yields(null, { variant: 'default' }), } - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { getSubscription: sinon.stub().resolves({}), }, } - this.UserMembershipController = await esmock.strict(modulePath, { - '../../../../app/src/Features/UserMembership/UserMembershipErrors': { + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipErrors', + () => ({ UserIsManagerError, UserNotFoundError, UserAlreadyAddedError, - }, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/UserMembership/UserMembershipHandler': - this.UserMembershipHandler, - '../../../../app/src/Features/Subscription/RecurlyClient': - this.RecurlyClient, - '@overleaf/settings': this.Settings, - '../../../../app/src/models/SSOConfig': { SSOConfig: this.SSOConfig }, - }) + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipHandler', + () => ({ + default: ctx.UserMembershipHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock('../../../../app/src/models/SSOConfig', () => ({ + SSOConfig: ctx.SSOConfig, + })) + + ctx.UserMembershipController = (await import(modulePath)).default }) describe('index', function () { - beforeEach(function () { - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.group + beforeEach(function (ctx) { + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.group }) - it('get users', async function () { - await this.UserMembershipController.manageGroupMembers(this.req, { + it('get users', async function (ctx) { + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: () => { sinon.assert.calledWithMatch( - this.UserMembershipHandler.promises.getUsers, - this.subscription, + ctx.UserMembershipHandler.promises.getUsers, + ctx.subscription, { modelName: 'Subscription' } ) }, }) }) - it('render group view', async function () { - this.subscription.managedUsersEnabled = false - await this.UserMembershipController.manageGroupMembers(this.req, { + it('render group view', async function (ctx) { + ctx.subscription.managedUsersEnabled = false + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-members-react') - expect(viewParams.users).to.deep.equal(this.users) - expect(viewParams.groupSize).to.equal(this.subscription.membersLimit) + expect(viewParams.users).to.deep.equal(ctx.users) + expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit) expect(viewParams.managedUsersActive).to.equal(false) }, }) }) - it('render group view with managed users', async function () { - this.subscription.managedUsersEnabled = true - await this.UserMembershipController.manageGroupMembers(this.req, { + it('render group view with managed users', async function (ctx) { + ctx.subscription.managedUsersEnabled = true + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-members-react') - expect(viewParams.users).to.deep.equal(this.users) - expect(viewParams.groupSize).to.equal(this.subscription.membersLimit) + expect(viewParams.users).to.deep.equal(ctx.users) + expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit) expect(viewParams.managedUsersActive).to.equal(true) }, }) }) - it('render group managers view', async function () { - this.req.entityConfig = EntityConfigs.groupManagers - await this.UserMembershipController.manageGroupManagers(this.req, { + it('render group managers view', async function (ctx) { + ctx.req.entityConfig = EntityConfigs.groupManagers + await ctx.UserMembershipController.manageGroupManagers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-managers-react') expect(viewParams.groupSize).to.equal(undefined) @@ -157,10 +199,10 @@ describe('UserMembershipController', function () { }) }) - it('render institution view', async function () { - this.req.entity = this.institution - this.req.entityConfig = EntityConfigs.institution - await this.UserMembershipController.manageInstitutionManagers(this.req, { + it('render institution view', async function (ctx) { + ctx.req.entity = ctx.institution + ctx.req.entityConfig = EntityConfigs.institution + await ctx.UserMembershipController.manageInstitutionManagers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal( 'user_membership/institution-managers-react' @@ -173,207 +215,233 @@ describe('UserMembershipController', function () { }) describe('add', function () { - beforeEach(function () { - this.req.body.email = this.newUser.email - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers + beforeEach(function (ctx) { + ctx.req.body.email = ctx.newUser.email + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers }) - it('add user', function (done) { - this.UserMembershipController.add(this.req, { - json: () => { - sinon.assert.calledWithMatch( - this.UserMembershipHandler.addUser, - this.subscription, - { modelName: 'Subscription' }, - this.newUser.email - ) - done() - }, - }) - }) - - it('return user object', function (done) { - this.UserMembershipController.add(this.req, { - json: payload => { - payload.user.should.equal(this.newUser) - done() - }, - }) - }) - - it('handle readOnly entity', function (done) { - this.req.entityConfig = EntityConfigs.group - this.UserMembershipController.add(this.req, null, error => { - expect(error).to.exist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - }) - }) - - it('handle user already added', function (done) { - this.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError()) - this.UserMembershipController.add(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('user_already_added') - done() + it('add user', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.add(ctx.req, { + json: () => { + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.addUser, + ctx.subscription, + { modelName: 'Subscription' }, + ctx.newUser.email + ) + resolve() }, - }), + }) }) }) - it('handle user not found', function (done) { - this.UserMembershipHandler.addUser.yields(new UserNotFoundError()) - this.UserMembershipController.add(this.req, { - status: () => ({ + it('return user object', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.add(ctx.req, { json: payload => { - expect(payload.error.code).to.equal('user_not_found') - done() + payload.user.should.equal(ctx.newUser) + resolve() }, - }), + }) }) }) - it('handle invalid email', function (done) { - this.req.body.email = 'not_valid_email' - this.UserMembershipController.add(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('invalid_email') - done() - }, - }), + it('handle readOnly entity', function (ctx) { + return new Promise(resolve => { + ctx.req.entityConfig = EntityConfigs.group + ctx.UserMembershipController.add(ctx.req, null, error => { + expect(error).to.exist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + resolve() + }) + }) + }) + + it('handle user already added', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError()) + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_already_added') + resolve() + }, + }), + }) + }) + }) + + it('handle user not found', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.addUser.yields(new UserNotFoundError()) + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_not_found') + resolve() + }, + }), + }) + }) + }) + + it('handle invalid email', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = 'not_valid_email' + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('invalid_email') + resolve() + }, + }), + }) }) }) }) describe('remove', function () { - beforeEach(function () { - this.req.params.userId = this.newUser._id - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers + beforeEach(function (ctx) { + ctx.req.params.userId = ctx.newUser._id + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers }) - it('remove user', function (done) { - this.UserMembershipController.remove(this.req, { - sendStatus: () => { - sinon.assert.calledWithMatch( - this.UserMembershipHandler.removeUser, - this.subscription, - { modelName: 'Subscription' }, - this.newUser._id - ) - done() - }, - }) - }) - - it('handle readOnly entity', function (done) { - this.req.entityConfig = EntityConfigs.group - this.UserMembershipController.remove(this.req, null, error => { - expect(error).to.exist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - }) - }) - - it('prevent self removal', function (done) { - this.req.params.userId = this.user._id - this.UserMembershipController.remove(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('managers_cannot_remove_self') - done() + it('remove user', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.remove(ctx.req, { + sendStatus: () => { + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.removeUser, + ctx.subscription, + { modelName: 'Subscription' }, + ctx.newUser._id + ) + resolve() }, - }), + }) }) }) - it('prevent admin removal', function (done) { - this.UserMembershipHandler.removeUser.yields(new UserIsManagerError()) - this.UserMembershipController.remove(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('managers_cannot_remove_admin') - done() - }, - }), + it('handle readOnly entity', function (ctx) { + return new Promise(resolve => { + ctx.req.entityConfig = EntityConfigs.group + ctx.UserMembershipController.remove(ctx.req, null, error => { + expect(error).to.exist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + resolve() + }) + }) + }) + + it('prevent self removal', function (ctx) { + return new Promise(resolve => { + ctx.req.params.userId = ctx.user._id + ctx.UserMembershipController.remove(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('managers_cannot_remove_self') + resolve() + }, + }), + }) + }) + }) + + it('prevent admin removal', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.removeUser.yields(new UserIsManagerError()) + ctx.UserMembershipController.remove(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal( + 'managers_cannot_remove_admin' + ) + resolve() + }, + }), + }) }) }) }) describe('exportCsv', function () { - beforeEach(function () { - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers - this.res = new MockResponse() - this.UserMembershipController.exportCsv(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers + ctx.res = new MockResponse() + ctx.UserMembershipController.exportCsv(ctx.req, ctx.res) }) - it('get users', function () { + it('get users', function (ctx) { sinon.assert.calledWithMatch( - this.UserMembershipHandler.getUsers, - this.subscription, + ctx.UserMembershipHandler.getUsers, + ctx.subscription, { modelName: 'Subscription' } ) }) - it('should set the correct content type on the request', function () { - assertCalledWith(this.res.contentType, 'text/csv; charset=utf-8') + it('should set the correct content type on the request', function (ctx) { + assertCalledWith(ctx.res.contentType, 'text/csv; charset=utf-8') }) - it('should name the exported csv file', function () { + it('should name the exported csv file', function (ctx) { assertCalledWith( - this.res.header, + ctx.res.header, 'Content-Disposition', 'attachment; filename="Group.csv"' ) }) - it('should export the correct csv', function () { + it('should export the correct csv', function (ctx) { assertCalledWith( - this.res.send, + ctx.res.send, '"email","last_logged_in_at","last_active_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z"' ) }) }) describe('new', function () { - beforeEach(function () { - this.req.params.name = 'publisher' - this.req.params.id = 'abc' + beforeEach(function (ctx) { + ctx.req.params.name = 'publisher' + ctx.req.params.id = 'abc' }) - it('renders view', function (done) { - this.UserMembershipController.new(this.req, { - render: (viewPath, data) => { - expect(data.entityName).to.eq('publisher') - expect(data.entityId).to.eq('abc') - done() - }, + it('renders view', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.new(ctx.req, { + render: (viewPath, data) => { + expect(data.entityName).to.eq('publisher') + expect(data.entityId).to.eq('abc') + resolve() + }, + }) }) }) }) describe('create', function () { - beforeEach(function () { - this.req.params.name = 'institution' - this.req.entityConfig = EntityConfigs.institution - this.req.params.id = 123 + beforeEach(function (ctx) { + ctx.req.params.name = 'institution' + ctx.req.entityConfig = EntityConfigs.institution + ctx.req.params.id = 123 }) - it('creates institution', function (done) { - this.UserMembershipController.create(this.req, { - redirect: path => { - expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index) - sinon.assert.calledWithMatch( - this.UserMembershipHandler.createEntity, - 123, - { modelName: 'Institution' } - ) - done() - }, + it('creates institution', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.create(ctx.req, { + redirect: path => { + expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index) + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.createEntity, + 123, + { modelName: 'Institution' } + ) + resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs b/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs index 01fe5d7a0d..4d8479a9cb 100644 --- a/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs +++ b/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs @@ -1,24 +1,22 @@ -import { strict as esmock } from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import Path from 'node:path' -import { fileURLToPath } from 'node:url' import sinon from 'sinon' import MockResponse from '../helpers/MockResponse.js' import MockRequest from '../helpers/MockRequest.js' -const __dirname = fileURLToPath(new URL('.', import.meta.url)) const modulePath = Path.join( - __dirname, + import.meta.dirname, '../../../../app/src/infrastructure/ServeStaticWrapper' ) describe('ServeStaticWrapperTests', function () { let error = null - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.express = { + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.express = { static: () => (req, res, next) => { if (error) { next(error) @@ -27,36 +25,39 @@ describe('ServeStaticWrapperTests', function () { } }, } - this.serveStaticWrapper = await esmock(modulePath, { - express: this.express, - }) + + vi.doMock('express', () => ({ + default: ctx.express, + })) + + ctx.serveStaticWrapper = (await import(modulePath)).default }) - this.afterEach(() => { + afterEach(() => { error = null }) - it('Premature close error thrown', async function () { + it('Premature close error thrown', async function (ctx) { error = new Error() error.code = 'ERR_STREAM_PREMATURE_CLOSE' - const middleware = this.serveStaticWrapper('test_folder', {}) + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next.called).to.be.false }) - it('No error thrown', async function () { - const middleware = this.serveStaticWrapper('test_folder', {}) + it('No error thrown', async function (ctx) { + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next).to.be.calledWith() }) - it('Other error thrown', async function () { + it('Other error thrown', async function (ctx) { error = new Error() - const middleware = this.serveStaticWrapper('test_folder', {}) + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next).to.be.calledWith(error) }) })