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 7c07334aee..f789d0227f 100644 --- a/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs +++ b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs @@ -1,4 +1,4 @@ -import { expect, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import * as path from 'node:path' import sinon from 'sinon' import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js' diff --git a/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs b/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs index 940ce1467a..72f1e7ab33 100644 --- a/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs +++ b/services/web/test/unit/src/Authentication/AuthenticationController.test.mjs @@ -1,31 +1,42 @@ -const sinon = require('sinon') -const { expect } = require('chai') +import { beforeEach, describe, it, vi, expect } from 'vitest' +import sinon from 'sinon' +import tk from 'timekeeper' +import MockRequest from '../helpers/MockRequest.js' +import MockResponse from '../helpers/MockResponse.js' +import mongodb from 'mongodb-legacy' +import AuthenticationErrors from '../../../../app/src/Features/Authentication/AuthenticationErrors.js' const modulePath = - '../../../../app/src/Features/Authentication/AuthenticationController.js' -const SandboxedModule = require('sandboxed-module') -const tk = require('timekeeper') -const MockRequest = require('../helpers/MockRequest') -const MockResponse = require('../helpers/MockResponse') -const { ObjectId } = require('mongodb-legacy') -const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors') + '../../../../app/src/Features/Authentication/AuthenticationController.mjs' + +const { ObjectId } = mongodb + +vi.mock( + '../../../../app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js', + () => ({ + default: { + clearInbound: vi.fn(), + clearSource: vi.fn(), + }, + }) +) describe('AuthenticationController', function () { - beforeEach(function () { + beforeEach(async function (ctx) { tk.freeze(Date.now()) - this.UserModel = { findOne: sinon.stub() } - this.httpAuthUsers = { + ctx.UserModel = { findOne: sinon.stub() } + ctx.httpAuthUsers = { 'valid-test-user': Math.random().toString(16).slice(2), } - this.user = { + ctx.user = { _id: new ObjectId(), - email: (this.email = 'USER@example.com'), + email: (ctx.email = 'USER@example.com'), first_name: 'bob', last_name: 'brown', referal_id: 1234, isAdmin: false, } - this.staffUser = { - ...this.user, + ctx.staffUser = { + ...ctx.user, staffAccess: { publisherMetrics: true, publisherManagement: false, @@ -38,8 +49,8 @@ describe('AuthenticationController', function () { splitTestManagement: true, }, } - this.noStaffAccessUser = { - ...this.user, + ctx.noStaffAccessUser = { + ...ctx.user, staffAccess: { publisherMetrics: false, publisherManagement: false, @@ -52,99 +63,173 @@ describe('AuthenticationController', function () { splitTestManagement: false, }, } - this.password = 'banana' - this.req = new MockRequest() - this.res = new MockResponse() - this.callback = sinon.stub() - this.next = sinon.stub() - this.req.session.analyticsId = 'abc-123' + ctx.password = 'banana' + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.callback = sinon.stub() + ctx.next = sinon.stub() + ctx.req.session.analyticsId = 'abc-123' - this.AuthenticationController = SandboxedModule.require(modulePath, { - requires: { - '../Helpers/AdminAuthorizationHelper': (this.AdminAuthorizationHelper = - { - hasAdminAccess: sinon.stub().returns(false), - }), - './AuthenticationErrors': AuthenticationErrors, - '../User/UserAuditLogHandler': (this.UserAuditLogHandler = { - addEntry: sinon.stub().yields(null), - promises: { - addEntry: sinon.stub().resolves(), - }, + vi.doMock( + '../../../../app/src/Features/Helpers/AdminAuthorizationHelper', + () => ({ + default: (ctx.AdminAuthorizationHelper = { + hasAdminAccess: sinon.stub().returns(false), }), - '../Helpers/AsyncFormHelper': (this.AsyncFormHelper = { - redirect: sinon.stub(), - }), - '../../infrastructure/RequestContentTypeDetection': { - acceptsJson: (this.acceptsJson = sinon.stub().returns(false)), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationErrors', + () => AuthenticationErrors + ) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: (ctx.UserAuditLogHandler = { + addEntry: sinon.stub().yields(null), + promises: { + addEntry: sinon.stub().resolves(), }, - './AuthenticationManager': (this.AuthenticationManager = { + }), + })) + + vi.doMock('../../../../app/src/Features/Helpers/AsyncFormHelper', () => ({ + default: (ctx.AsyncFormHelper = { + redirect: sinon.stub(), + }), + })) + + vi.doMock( + '../../../../app/src/infrastructure/RequestContentTypeDetection', + () => ({ + acceptsJson: (ctx.acceptsJson = sinon.stub().returns(false)), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: (ctx.AuthenticationManager = { promises: {}, }), - '../User/UserUpdater': (this.UserUpdater = { - updateUser: sinon.stub(), - }), - '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }), - '../Security/LoginRateLimiter': (this.LoginRateLimiter = { + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: (ctx.UserUpdater = { + updateUser: sinon.stub(), + }), + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.Metrics = { inc: sinon.stub() }), + })) + + vi.doMock('../../../../app/src/Features/Security/LoginRateLimiter', () => ({ + default: (ctx.LoginRateLimiter = { + processLoginRequest: sinon.stub(), + recordSuccessfulLogin: sinon.stub(), + promises: { processLoginRequest: sinon.stub(), recordSuccessfulLogin: sinon.stub(), - promises: { - processLoginRequest: sinon.stub(), - recordSuccessfulLogin: sinon.stub(), - }, - }), - '../User/UserHandler': (this.UserHandler = { - promises: { - populateTeamInvites: sinon.stub().resolves(), - }, - }), - '../Analytics/AnalyticsManager': (this.AnalyticsManager = { + }, + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserHandler', () => ({ + default: (ctx.UserHandler = { + promises: { + populateTeamInvites: sinon.stub().resolves(), + }, + }), + })) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: (ctx.AnalyticsManager = { recordEventForUserInBackground: sinon.stub(), identifyUser: sinon.stub(), - getIdsFromSession: sinon.stub().returns({ userId: this.user._id }), + getIdsFromSession: sinon.stub().returns({ userId: ctx.user._id }), }), - '@overleaf/settings': (this.Settings = { - siteUrl: 'http://www.foo.bar', - httpAuthUsers: this.httpAuthUsers, - elevateAccountSecurityAfterFailedLogin: 90 * 24 * 60 * 60 * 1000, - }), - passport: (this.passport = { - authenticate: sinon.stub().returns(sinon.stub()), - }), - '../User/UserSessionsManager': (this.UserSessionsManager = { - trackSession: sinon.stub(), - untrackSession: sinon.stub(), - removeSessionsFromRedis: sinon.stub().yields(null), - }), - '../../infrastructure/Modules': (this.Modules = { - hooks: { fire: sinon.stub().yields(null, []) }, - promises: { hooks: { fire: sinon.stub().resolves([]) } }, - }), - '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = { + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.Settings = { + siteUrl: 'http://www.foo.bar', + httpAuthUsers: ctx.httpAuthUsers, + elevateAccountSecurityAfterFailedLogin: 90 * 24 * 60 * 60 * 1000, + }), + })) + + vi.doMock('passport', () => ({ + default: (ctx.passport = { + authenticate: sinon.stub().returns(sinon.stub()), + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: (ctx.UserSessionsManager = { + trackSession: sinon.stub(), + untrackSession: sinon.stub(), + removeSessionsFromRedis: sinon.stub().yields(null), + }), + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { + hooks: { fire: sinon.stub().yields(null, []) }, + promises: { hooks: { fire: sinon.stub().resolves([]) } }, + }), + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: (ctx.NotificationsBuilder = { ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }), }), - '../../models/User': { User: this.UserModel }, - '../../../../modules/oauth2-server/app/src/Oauth2Server': - (this.Oauth2Server = { - Request: sinon.stub(), - Response: sinon.stub(), - server: { - authenticate: sinon.stub(), - }, - }), - '../Helpers/UrlHelper': (this.UrlHelper = { - getSafeRedirectPath: sinon.stub(), - }), - './SessionManager': (this.SessionManager = { - isUserLoggedIn: sinon.stub().returns(true), - getSessionUser: sinon.stub().returns(this.user), - }), + }) + ) + + vi.doMock('../../../../app/src/models/User', () => ({ + default: { User: ctx.UserModel }, + })) + + ctx.Oauth2Server = { + Request: sinon.stub(), + Response: sinon.stub(), + server: { + authenticate: sinon.stub(), }, - }) - this.UrlHelper.getSafeRedirectPath + } + + vi.doMock('../../../../modules/oauth2-server/app/src/Oauth2Server', () => ({ + default: ctx.Oauth2Server, + })) + + vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({ + default: (ctx.UrlHelper = { + getSafeRedirectPath: sinon.stub(), + }), + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: (ctx.SessionManager = { + isUserLoggedIn: sinon.stub().returns(true), + getSessionUser: sinon.stub().returns(ctx.user), + }), + }) + ) + + ctx.AuthenticationController = (await import(modulePath)).default + ctx.UrlHelper.getSafeRedirectPath .withArgs('https://evil.com') .returns(undefined) - this.UrlHelper.getSafeRedirectPath.returnsArg(0) + ctx.UrlHelper.getSafeRedirectPath.returnsArg(0) }) afterEach(function () { @@ -152,87 +237,97 @@ describe('AuthenticationController', function () { }) describe('validateAdmin', function () { - beforeEach(function () { - this.Settings.adminDomains = ['good.example.com'] - this.goodAdmin = { + beforeEach(function (ctx) { + ctx.Settings.adminDomains = ['good.example.com'] + ctx.goodAdmin = { email: 'alice@good.example.com', isAdmin: true, } - this.badAdmin = { + ctx.badAdmin = { email: 'beatrice@bad.example.com', isAdmin: true, } - this.normalUser = { + ctx.normalUser = { email: 'claire@whatever.example.com', isAdmin: false, } }) - it('should skip when adminDomains are not configured', function (done) { - this.Settings.adminDomains = [] - this.SessionManager.getSessionUser = sinon.stub().returns(this.normalUser) - this.AuthenticationController.validateAdmin(this.req, this.res, err => { - this.SessionManager.getSessionUser.called.should.equal(false) - expect(err).to.not.exist - done() + it('should skip when adminDomains are not configured', async function (ctx) { + await new Promise(resolve => { + ctx.Settings.adminDomains = [] + ctx.SessionManager.getSessionUser = sinon.stub().returns(ctx.normalUser) + ctx.AuthenticationController.validateAdmin(ctx.req, ctx.res, err => { + ctx.SessionManager.getSessionUser.called.should.equal(false) + expect(err).to.not.exist + resolve() + }) }) }) - it('should skip non-admin user', function (done) { - this.SessionManager.getSessionUser = sinon.stub().returns(this.normalUser) - this.AuthenticationController.validateAdmin(this.req, this.res, err => { - this.SessionManager.getSessionUser.called.should.equal(true) - expect(err).to.not.exist - done() + it('should skip non-admin user', async function (ctx) { + await new Promise(resolve => { + ctx.SessionManager.getSessionUser = sinon.stub().returns(ctx.normalUser) + ctx.AuthenticationController.validateAdmin(ctx.req, ctx.res, err => { + ctx.SessionManager.getSessionUser.called.should.equal(true) + expect(err).to.not.exist + resolve() + }) }) }) - it('should permit an admin with the right doman', function (done) { - this.SessionManager.getSessionUser = sinon.stub().returns(this.goodAdmin) - this.AuthenticationController.validateAdmin(this.req, this.res, err => { - this.SessionManager.getSessionUser.called.should.equal(true) - expect(err).to.not.exist - done() + it('should permit an admin with the right doman', async function (ctx) { + await new Promise(resolve => { + ctx.SessionManager.getSessionUser = sinon.stub().returns(ctx.goodAdmin) + ctx.AuthenticationController.validateAdmin(ctx.req, ctx.res, err => { + ctx.SessionManager.getSessionUser.called.should.equal(true) + expect(err).to.not.exist + resolve() + }) }) }) - it('should block an admin with a missing email', function (done) { - this.SessionManager.getSessionUser = sinon - .stub() - .returns({ isAdmin: true }) - this.AdminAuthorizationHelper.hasAdminAccess.returns(true) - this.AuthenticationController.validateAdmin(this.req, this.res, err => { - this.SessionManager.getSessionUser.called.should.equal(true) - expect(err).to.exist - done() + it('should block an admin with a missing email', async function (ctx) { + await new Promise(resolve => { + ctx.SessionManager.getSessionUser = sinon + .stub() + .returns({ isAdmin: true }) + ctx.AdminAuthorizationHelper.hasAdminAccess.returns(true) + ctx.AuthenticationController.validateAdmin(ctx.req, ctx.res, err => { + ctx.SessionManager.getSessionUser.called.should.equal(true) + expect(err).to.exist + resolve() + }) }) }) - it('should block an admin with a bad domain', function (done) { - this.SessionManager.getSessionUser = sinon.stub().returns(this.badAdmin) - this.AdminAuthorizationHelper.hasAdminAccess.returns(true) - this.AuthenticationController.validateAdmin(this.req, this.res, err => { - this.SessionManager.getSessionUser.called.should.equal(true) - expect(err).to.exist - done() + it('should block an admin with a bad domain', async function (ctx) { + await new Promise(resolve => { + ctx.SessionManager.getSessionUser = sinon.stub().returns(ctx.badAdmin) + ctx.AdminAuthorizationHelper.hasAdminAccess.returns(true) + ctx.AuthenticationController.validateAdmin(ctx.req, ctx.res, err => { + ctx.SessionManager.getSessionUser.called.should.equal(true) + expect(err).to.exist + resolve() + }) }) }) }) describe('serializeUser', function () { describe('when isAdmin is false', function () { - it('does not return an isAdmin field', function () { + it('does not return an isAdmin field', function (ctx) { const isAdminMatcher = sinon.match(value => { return !('isAdmin' in value) }) - this.AuthenticationController.serializeUser(this.user, this.callback) - expect(this.callback).to.have.been.calledWith(null, isAdminMatcher) + ctx.AuthenticationController.serializeUser(ctx.user, ctx.callback) + expect(ctx.callback).to.have.been.calledWith(null, isAdminMatcher) }) }) describe('when staffAccess fields are provided', function () { - it('only returns the fields set to true', function () { + it('only returns the fields set to true', function (ctx) { const expectedStaffAccess = { publisherMetrics: true, institutionMetrics: true, @@ -247,160 +342,145 @@ describe('AuthenticationController', function () { ) }) - this.AuthenticationController.serializeUser( - this.staffUser, - this.callback - ) - expect(this.callback).to.have.been.calledWith(null, staffAccessMatcher) + ctx.AuthenticationController.serializeUser(ctx.staffUser, ctx.callback) + expect(ctx.callback).to.have.been.calledWith(null, staffAccessMatcher) }) }) describe('when all staffAccess fields are false', function () { - it('no staffAccess attribute is set', function () { + it('no staffAccess attribute is set', function (ctx) { const staffAccessMatcher = sinon.match(value => { return !('staffAccess' in value) }) - this.AuthenticationController.serializeUser( - this.noStaffAccessUser, - this.callback + ctx.AuthenticationController.serializeUser( + ctx.noStaffAccessUser, + ctx.callback ) - expect(this.callback).to.have.been.calledWith(null, staffAccessMatcher) + expect(ctx.callback).to.have.been.calledWith(null, staffAccessMatcher) }) }) }) describe('passportLogin', function () { - beforeEach(function () { - this.info = null - this.req.login = sinon.stub().yields(null) - this.res.json = sinon.stub() - this.req.session = { - passport: { user: this.user }, + beforeEach(function (ctx) { + ctx.info = null + ctx.req.login = sinon.stub().yields(null) + ctx.res.json = sinon.stub() + ctx.req.session = { + passport: { user: ctx.user }, postLoginRedirect: '/path/to/redir/to', } - this.req.session.destroy = sinon.stub().yields(null) - this.req.session.save = sinon.stub().yields(null) - this.req.sessionStore = { generate: sinon.stub() } - this.AuthenticationController.promises.finishLogin = sinon.stub() - this.passport.authenticate.yields(null, this.user, this.info) - this.err = new Error('woops') + ctx.req.session.destroy = sinon.stub().yields(null) + ctx.req.session.save = sinon.stub().yields(null) + ctx.req.sessionStore = { generate: sinon.stub() } + ctx.AuthenticationController.promises.finishLogin = sinon.stub() + ctx.passport.authenticate.yields(null, ctx.user, ctx.info) + ctx.err = new Error('woops') }) - it('should call passport.authenticate', function () { - this.AuthenticationController.passportLogin(this.req, this.res, this.next) - this.passport.authenticate.callCount.should.equal(1) + it('should call passport.authenticate', function (ctx) { + ctx.AuthenticationController.passportLogin(ctx.req, ctx.res, ctx.next) + ctx.passport.authenticate.callCount.should.equal(1) }) describe('when authenticate produces an error', function () { - beforeEach(function () { - this.passport.authenticate.yields(this.err) + beforeEach(function (ctx) { + ctx.passport.authenticate.yields(ctx.err) }) - it('should return next with an error', function () { - this.AuthenticationController.passportLogin( - this.req, - this.res, - this.next - ) - this.next.calledWith(this.err).should.equal(true) + it('should return next with an error', function (ctx) { + ctx.AuthenticationController.passportLogin(ctx.req, ctx.res, ctx.next) + ctx.next.calledWith(ctx.err).should.equal(true) }) }) describe('when authenticate produces a user', function () { - beforeEach(function () { - this.req.session.postLoginRedirect = 'some_redirect' - this.passport.authenticate.yields(null, this.user, this.info) + beforeEach(function (ctx) { + ctx.req.session.postLoginRedirect = 'some_redirect' + ctx.passport.authenticate.yields(null, ctx.user, ctx.info) }) - afterEach(function () { - delete this.req.session.postLoginRedirect + afterEach(function (ctx) { + delete ctx.req.session.postLoginRedirect }) - it('should call finishLogin', function (done) { - this.AuthenticationController.promises.finishLogin.callsFake(() => { - this.AuthenticationController.promises.finishLogin.callCount.should.equal( - 1 - ) - this.AuthenticationController.promises.finishLogin - .calledWith(this.user, this.req, this.res) - .should.equal(true) - done() + it('should call finishLogin', async function (ctx) { + await new Promise(resolve => { + ctx.AuthenticationController.promises.finishLogin.callsFake(() => { + ctx.AuthenticationController.promises.finishLogin.callCount.should.equal( + 1 + ) + ctx.AuthenticationController.promises.finishLogin + .calledWith(ctx.user, ctx.req, ctx.res) + .should.equal(true) + resolve() + }) + ctx.AuthenticationController.passportLogin(ctx.req, ctx.res, ctx.next) }) - this.AuthenticationController.passportLogin( - this.req, - this.res, - this.next - ) }) }) describe('when authenticate does not produce a user', function () { - beforeEach(function () { - this.info = { text: 'a', type: 'b' } - this.passport.authenticate.yields(null, false, this.info) + beforeEach(function (ctx) { + ctx.info = { text: 'a', type: 'b' } + ctx.passport.authenticate.yields(null, false, ctx.info) }) - it('should not call finishLogin', function () { - this.AuthenticationController.passportLogin( - this.req, - this.res, - this.next - ) - this.AuthenticationController.promises.finishLogin.callCount.should.equal( + it('should not call finishLogin', function (ctx) { + ctx.AuthenticationController.passportLogin(ctx.req, ctx.res, ctx.next) + ctx.AuthenticationController.promises.finishLogin.callCount.should.equal( 0 ) }) - it('should not send a json response with redirect', function () { - this.AuthenticationController.passportLogin( - this.req, - this.res, - this.next - ) - this.res.json.callCount.should.equal(1) - this.res.json.should.have.been.calledWith({ message: this.info }) - expect(this.res.json.lastCall.args[0].redir != null).to.equal(false) + it('should not send a json response with redirect', function (ctx) { + ctx.AuthenticationController.passportLogin(ctx.req, ctx.res, ctx.next) + ctx.res.json.callCount.should.equal(1) + ctx.res.json.should.have.been.calledWith({ message: ctx.info }) + expect(ctx.res.json.lastCall.args[0].redir != null).to.equal(false) }) }) }) describe('doPassportLogin', function () { - beforeEach(function () { - this.AuthenticationController._recordFailedLogin = sinon.stub() - this.AuthenticationController._recordSuccessfulLogin = sinon.stub() - this.req.body = { - email: this.email, - password: this.password, + beforeEach(function (ctx) { + ctx.AuthenticationController._recordFailedLogin = sinon.stub() + ctx.AuthenticationController._recordSuccessfulLogin = sinon.stub() + ctx.req.body = { + email: ctx.email, + password: ctx.password, session: { postLoginRedirect: '/path/to/redir/to', }, } - this.req.__authAuditInfo = { captcha: 'disabled' } - this.cb = sinon.stub() + ctx.req.__authAuditInfo = { captcha: 'disabled' } + ctx.cb = sinon.stub() }) describe('when the authentication errors', function () { - beforeEach(function () { - this.LoginRateLimiter.promises.processLoginRequest.resolves(true) - this.errorsWith = (error, done) => { - this.AuthenticationManager.promises.authenticate = sinon + beforeEach(function (ctx) { + ctx.LoginRateLimiter.promises.processLoginRequest.resolves(true) + ctx.errorsWith = (error, done) => { + ctx.AuthenticationManager.promises.authenticate = sinon .stub() .rejects(error) - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb.callsFake(() => done()) + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb.callsFake(() => done()) ) } }) describe('with "password is too long"', function () { - beforeEach(function (done) { - this.errorsWith(new Error('password is too long'), done) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.errorsWith(new Error('password is too long'), resolve) + }) }) - it('should send a 429', function () { - this.cb.should.have.been.calledWith(undefined, false, { + it('should send a 429', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, false, { status: 422, type: 'error', key: 'password-too-long', @@ -409,21 +489,31 @@ describe('AuthenticationController', function () { }) }) describe('with ParallelLoginError', function () { - beforeEach(function (done) { - this.errorsWith(new AuthenticationErrors.ParallelLoginError(), done) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.errorsWith( + new AuthenticationErrors.ParallelLoginError(), + resolve + ) + }) }) - it('should send a 429', function () { - this.cb.should.have.been.calledWith(undefined, false, { + it('should send a 429', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, false, { status: 429, }) }) }) describe('with PasswordReusedError', function () { - beforeEach(function (done) { - this.errorsWith(new AuthenticationErrors.PasswordReusedError(), done) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.errorsWith( + new AuthenticationErrors.PasswordReusedError(), + resolve + ) + }) }) - it('should send a 400', function () { - this.cb.should.have.been.calledWith(undefined, false, { + it('should send a 400', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, false, { status: 400, type: 'error', key: 'password-compromised', @@ -433,95 +523,105 @@ describe('AuthenticationController', function () { }) describe('with another error', function () { const err = new Error('unhandled error') - beforeEach(function (done) { - this.errorsWith(err, done) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.errorsWith(err, resolve) + }) }) - it('should send a 400', function () { - this.cb.should.have.been.calledWith(err) + it('should send a 400', function (ctx) { + ctx.cb.should.have.been.calledWith(err) }) }) }) describe('when the user is authenticated', function () { - beforeEach(function () { - this.cb = sinon.stub() - this.LoginRateLimiter.promises.processLoginRequest.resolves(true) - this.AuthenticationManager.promises.authenticate = sinon + beforeEach(function (ctx) { + ctx.cb = sinon.stub() + ctx.LoginRateLimiter.promises.processLoginRequest.resolves(true) + ctx.AuthenticationManager.promises.authenticate = sinon .stub() - .resolves({ user: this.user }) - this.req.sessionID = Math.random() + .resolves({ user: ctx.user }) + ctx.req.sessionID = Math.random() }) describe('happy path', function () { - beforeEach(function (done) { - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb.callsFake(() => done()) - ) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb.callsFake(() => resolve()) + ) + }) }) - it('should attempt to authorise the user', function () { - this.AuthenticationManager.promises.authenticate - .calledWith({ email: this.email.toLowerCase() }, this.password) + it('should attempt to authorise the user', function (ctx) { + ctx.AuthenticationManager.promises.authenticate + .calledWith({ email: ctx.email.toLowerCase() }, ctx.password) .should.equal(true) }) - it("should establish the user's session", function () { - this.cb.calledWith(undefined, this.user).should.equal(true) + it("should establish the user's session", function (ctx) { + ctx.cb.calledWith(undefined, ctx.user).should.equal(true) }) }) describe('with a user having a recent failed login ', function () { - beforeEach(function () { - this.user.lastFailedLogin = new Date() + beforeEach(function (ctx) { + ctx.user.lastFailedLogin = new Date() }) describe('with captcha disabled', function () { - beforeEach(function (done) { - this.req.__authAuditInfo.captcha = 'disabled' - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb.callsFake(() => done()) - ) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.req.__authAuditInfo.captcha = 'disabled' + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb.callsFake(() => resolve()) + ) + }) }) - it('should let the user log in', function () { - this.cb.should.have.been.calledWith(undefined, this.user) + it('should let the user log in', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, ctx.user) }) }) describe('with a solved captcha', function () { - beforeEach(function (done) { - this.req.__authAuditInfo.captcha = 'solved' - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb.callsFake(() => done()) - ) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.req.__authAuditInfo.captcha = 'solved' + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb.callsFake(() => resolve()) + ) + }) }) - it('should let the user log in', function () { - this.cb.should.have.been.calledWith(undefined, this.user) + it('should let the user log in', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, ctx.user) }) }) describe('with a skipped captcha', function () { - beforeEach(function (done) { - this.req.__authAuditInfo.captcha = 'skipped' - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb.callsFake(() => done()) - ) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.req.__authAuditInfo.captcha = 'skipped' + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb.callsFake(() => resolve()) + ) + }) }) - it('should request a captcha', function () { - this.cb.should.have.been.calledWith(undefined, false, { + it('should request a captcha', function (ctx) { + ctx.cb.should.have.been.calledWith(undefined, false, { text: 'cannot_verify_user_not_robot', type: 'error', errorReason: 'cannot_verify_user_not_robot', @@ -533,583 +633,596 @@ describe('AuthenticationController', function () { }) describe('when the user is not authenticated', function () { - beforeEach(function (done) { - this.LoginRateLimiter.promises.processLoginRequest.resolves(true) - this.AuthenticationManager.promises.authenticate = sinon - .stub() - .resolves({ user: null }) - this.cb = sinon.stub().callsFake(() => done()) - this.AuthenticationController.doPassportLogin( - this.req, - this.req.body.email, - this.req.body.password, - this.cb - ) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.LoginRateLimiter.promises.processLoginRequest.resolves(true) + ctx.AuthenticationManager.promises.authenticate = sinon + .stub() + .resolves({ user: null }) + ctx.cb = sinon.stub().callsFake(() => resolve()) + ctx.AuthenticationController.doPassportLogin( + ctx.req, + ctx.req.body.email, + ctx.req.body.password, + ctx.cb + ) + }) }) - it('should not establish the login', function () { - this.cb.callCount.should.equal(1) - this.cb.calledWith(null, false) - expect(this.cb.lastCall.args[2]).to.deep.equal({ + it('should not establish the login', function (ctx) { + ctx.cb.callCount.should.equal(1) + ctx.cb.calledWith(null, false) + expect(ctx.cb.lastCall.args[2]).to.deep.equal({ type: 'error', key: 'invalid-password-retry-or-reset', status: 401, }) }) - it('should not setup the user data in the background', function () { - this.UserHandler.promises.populateTeamInvites.called.should.equal(false) + it('should not setup the user data in the background', function (ctx) { + ctx.UserHandler.promises.populateTeamInvites.called.should.equal(false) }) - it('should record a failed login', function () { - this.AuthenticationController._recordFailedLogin.called.should.equal( + it('should record a failed login', function (ctx) { + ctx.AuthenticationController._recordFailedLogin.called.should.equal( true ) }) - it('should log the failed login', function () { - this.logger.debug - .calledWith({ email: this.email.toLowerCase() }, 'failed log in') - .should.equal(true) + it('should log the failed login', function (ctx) { + expect(ctx.logger.debug).toBeCalledWith( + { email: ctx.email.toLowerCase() }, + 'failed log in' + ) }) }) }) describe('requireLogin', function () { - beforeEach(function () { - this.user = { + beforeEach(function (ctx) { + ctx.user = { _id: 'user-id-123', email: 'user@overleaf.com', } - this.middleware = this.AuthenticationController.requireLogin() + ctx.middleware = ctx.AuthenticationController.requireLogin() }) describe('when the user is logged in', function () { - beforeEach(function () { - this.req.session = { - user: (this.user = { + beforeEach(function (ctx) { + ctx.req.session = { + user: (ctx.user = { _id: 'user-id-123', email: 'user@overleaf.com', }), } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('should call the next method in the chain', function () { - this.next.called.should.equal(true) + it('should call the next method in the chain', function (ctx) { + ctx.next.called.should.equal(true) }) }) describe('when the user is not logged in', function () { - beforeEach(function () { - this.req.session = {} - this.AuthenticationController._redirectToLoginOrRegisterPage = + beforeEach(function (ctx) { + ctx.req.session = {} + ctx.AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - this.req.query = {} - this.SessionManager.isUserLoggedIn = sinon.stub().returns(false) - this.middleware(this.req, this.res, this.next) + ctx.req.query = {} + ctx.SessionManager.isUserLoggedIn = sinon.stub().returns(false) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('should redirect to the register or login page', function () { - this.AuthenticationController._redirectToLoginOrRegisterPage - .calledWith(this.req, this.res) + it('should redirect to the register or login page', function (ctx) { + ctx.AuthenticationController._redirectToLoginOrRegisterPage + .calledWith(ctx.req, ctx.res) .should.equal(true) }) }) }) describe('requireOauth', function () { - beforeEach(function () { - this.res.json = sinon.stub() - this.res.status = sinon.stub().returns(this.res) - this.res.sendStatus = sinon.stub() - this.middleware = this.AuthenticationController.requireOauth('scope') + beforeEach(function (ctx) { + ctx.res.json = sinon.stub() + ctx.res.status = sinon.stub().returns(ctx.res) + ctx.res.sendStatus = sinon.stub() + ctx.middleware = ctx.AuthenticationController.requireOauth('scope') }) describe('when Oauth2Server authenticates', function () { - beforeEach(function (done) { - this.token = { + beforeEach(async function (ctx) { + ctx.token = { accessToken: 'token', user: 'user', } - this.Oauth2Server.server.authenticate = sinon - .stub() - .resolves(this.token) - this.middleware(this.req, this.res, () => done()) + ctx.Oauth2Server.server.authenticate.resolves(ctx.token) + await ctx.middleware(ctx.req, ctx.res) }) - it('should set oauth_token on request', function () { - this.req.oauth_token.should.equal(this.token) + it('should set oauth_token on request', function (ctx) { + ctx.req.oauth_token.should.equal(ctx.token) }) - it('should set oauth on request', function () { - this.req.oauth.access_token.should.equal(this.token.accessToken) + it('should set oauth on request', function (ctx) { + ctx.req.oauth.access_token.should.equal(ctx.token.accessToken) }) - it('should set oauth_user on request', function () { - this.req.oauth_user.should.equal('user') + it('should set oauth_user on request', function (ctx) { + ctx.req.oauth_user.should.equal('user') }) }) describe('when Oauth2Server returns 401 error', function () { - beforeEach(function (done) { - this.res.json.callsFake(() => done()) - this.Oauth2Server.server.authenticate.rejects({ code: 401 }) - this.middleware(this.req, this.res, this.next) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.res.json.callsFake(() => resolve()) + ctx.Oauth2Server.server.authenticate.rejects({ code: 401 }) + ctx.middleware(ctx.req, ctx.res, ctx.next) + }) }) - it('should return 401 error', function () { - this.res.status.should.have.been.calledWith(401) + it('should return 401 error', function (ctx) { + ctx.res.status.should.have.been.calledWith(401) }) - it('should not call next', function () { - this.next.should.have.not.been.calledOnce + it('should not call next', function (ctx) { + ctx.next.should.have.not.been.calledOnce }) }) }) describe('requireGlobalLogin', function () { - beforeEach(function () { - this.req.headers = {} - this.middleware = sinon.stub() - this.AuthenticationController.requirePrivateApiAuth = sinon + beforeEach(function (ctx) { + ctx.req.headers = {} + ctx.middleware = sinon.stub() + ctx.AuthenticationController.requirePrivateApiAuth = sinon .stub() - .returns(this.middleware) - this.setRedirect = sinon.spy( - this.AuthenticationController, + .returns(ctx.middleware) + ctx.setRedirect = sinon.spy( + ctx.AuthenticationController, 'setRedirectInSession' ) }) - afterEach(function () { - this.setRedirect.restore() + afterEach(function (ctx) { + ctx.setRedirect.restore() }) describe('with white listed url', function () { - beforeEach(function () { - this.AuthenticationController.addEndpointToLoginWhitelist('/login') - this.req._parsedUrl.pathname = '/login' - this.AuthenticationController.requireGlobalLogin( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + ctx.AuthenticationController.addEndpointToLoginWhitelist('/login') + ctx.req._parsedUrl.pathname = '/login' + ctx.AuthenticationController.requireGlobalLogin( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next() directly', function () { - this.next.called.should.equal(true) + it('should call next() directly', function (ctx) { + ctx.next.called.should.equal(true) }) }) describe('with white listed url and a query string', function () { - beforeEach(function () { - this.AuthenticationController.addEndpointToLoginWhitelist('/login') - this.req._parsedUrl.pathname = '/login' - this.req.url = '/login?query=something' - this.AuthenticationController.requireGlobalLogin( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + ctx.AuthenticationController.addEndpointToLoginWhitelist('/login') + ctx.req._parsedUrl.pathname = '/login' + ctx.req.url = '/login?query=something' + ctx.AuthenticationController.requireGlobalLogin( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next() directly', function () { - this.next.called.should.equal(true) + it('should call next() directly', function (ctx) { + ctx.next.called.should.equal(true) }) }) describe('with http auth', function () { - beforeEach(function () { - this.req.headers.authorization = 'Mock Basic Auth' - this.AuthenticationController.requireGlobalLogin( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + ctx.req.headers.authorization = 'Mock Basic Auth' + ctx.AuthenticationController.requireGlobalLogin( + ctx.req, + ctx.res, + ctx.next ) }) - it('should pass the request onto requirePrivateApiAuth middleware', function () { - this.middleware - .calledWith(this.req, this.res, this.next) - .should.equal(true) + it('should pass the request onto requirePrivateApiAuth middleware', function (ctx) { + ctx.middleware.calledWith(ctx.req, ctx.res, ctx.next).should.equal(true) }) }) describe('with a user session', function () { - beforeEach(function () { - this.req.session = { user: { mock: 'user', _id: 'some_id' } } - this.AuthenticationController.requireGlobalLogin( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + ctx.req.session = { user: { mock: 'user', _id: 'some_id' } } + ctx.AuthenticationController.requireGlobalLogin( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next() directly', function () { - this.next.called.should.equal(true) + it('should call next() directly', function (ctx) { + ctx.next.called.should.equal(true) }) }) describe('with no login credentials', function () { - beforeEach(function () { - this.req.session = {} - this.SessionManager.isUserLoggedIn = sinon.stub().returns(false) - this.AuthenticationController.requireGlobalLogin( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + ctx.req.session = {} + ctx.SessionManager.isUserLoggedIn = sinon.stub().returns(false) + ctx.AuthenticationController.requireGlobalLogin( + ctx.req, + ctx.res, + ctx.next ) }) - it('should have called setRedirectInSession', function () { - this.setRedirect.callCount.should.equal(1) + it('should have called setRedirectInSession', function (ctx) { + ctx.setRedirect.callCount.should.equal(1) }) - it('should redirect to the /login page', function () { - this.res.redirectedTo.should.equal('/login') + it('should redirect to the /login page', function (ctx) { + ctx.res.redirectedTo.should.equal('/login') }) }) }) describe('requireBasicAuth', function () { - beforeEach(function () { - this.basicAuthUsers = { + beforeEach(function (ctx) { + ctx.basicAuthUsers = { 'basic-auth-user': 'basic-auth-password', } - this.middleware = this.AuthenticationController.requireBasicAuth( - this.basicAuthUsers + ctx.middleware = ctx.AuthenticationController.requireBasicAuth( + ctx.basicAuthUsers ) }) describe('with http auth', function () { - it('should error with incorrect user', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from('user:nope').toString('base64')}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should error with incorrect user', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from('user:nope').toString('base64')}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should error with incorrect password', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from('basic-auth-user:nope').toString( - 'base64' - )}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should error with incorrect password', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from( + 'basic-auth-user:nope' + ).toString('base64')}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should fail with empty pass', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from(`basic-auth-user:`).toString( - 'base64' - )}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should fail with empty pass', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from(`basic-auth-user:`).toString( + 'base64' + )}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should fail with empty user and password of "undefined"', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from(`:undefined`).toString( - 'base64' - )}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should fail with empty user and password of "undefined"', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from(`:undefined`).toString( + 'base64' + )}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should fail with empty user and empty password', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from(`:`).toString('base64')}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should fail with empty user and empty password', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from(`:`).toString('base64')}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should fail with a user that is not a valid property', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from( - `constructor:[Function ]` - ).toString('base64')}`, - } - this.req.sendStatus = status => { - expect(status).to.equal(401) - done() - } - this.middleware(this.req, this.req) + it('should fail with a user that is not a valid property', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from( + `constructor:[Function ]` + ).toString('base64')}`, + } + ctx.req.sendStatus = status => { + expect(status).to.equal(401) + resolve() + } + ctx.middleware(ctx.req, ctx.req) + }) }) - it('should succeed with correct user/pass', function (done) { - this.req.headers = { - authorization: `Basic ${Buffer.from( - `basic-auth-user:${this.basicAuthUsers['basic-auth-user']}` - ).toString('base64')}`, - } - this.middleware(this.req, this.res, done) + it('should succeed with correct user/pass', async function (ctx) { + await new Promise(resolve => { + ctx.req.headers = { + authorization: `Basic ${Buffer.from( + `basic-auth-user:${ctx.basicAuthUsers['basic-auth-user']}` + ).toString('base64')}`, + } + ctx.middleware(ctx.req, ctx.res, resolve) + }) }) }) }) describe('requirePrivateApiAuth', function () { - beforeEach(function () { - this.AuthenticationController.requireBasicAuth = sinon.stub() + beforeEach(function (ctx) { + ctx.AuthenticationController.requireBasicAuth = sinon.stub() }) - it('should call requireBasicAuth with the private API user details', function () { - this.AuthenticationController.requirePrivateApiAuth() - this.AuthenticationController.requireBasicAuth - .calledWith(this.httpAuthUsers) + it('should call requireBasicAuth with the private API user details', function (ctx) { + ctx.AuthenticationController.requirePrivateApiAuth() + ctx.AuthenticationController.requireBasicAuth + .calledWith(ctx.httpAuthUsers) .should.equal(true) }) }) describe('_redirectToLoginOrRegisterPage', function () { - beforeEach(function () { - this.middleware = this.AuthenticationController.requireLogin( - (this.options = { load_from_db: false }) + beforeEach(function (ctx) { + ctx.middleware = ctx.AuthenticationController.requireLogin( + (ctx.options = { load_from_db: false }) ) - this.req.session = {} - this.AuthenticationController._redirectToRegisterPage = sinon.stub() - this.AuthenticationController._redirectToLoginPage = sinon.stub() - this.req.query = {} + ctx.req.session = {} + ctx.AuthenticationController._redirectToRegisterPage = sinon.stub() + ctx.AuthenticationController._redirectToLoginPage = sinon.stub() + ctx.req.query = {} }) describe('they have come directly to the url', function () { - beforeEach(function () { - this.req.query = {} - this.SessionManager.isUserLoggedIn = sinon.stub().returns(false) - this.middleware(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.query = {} + ctx.SessionManager.isUserLoggedIn = sinon.stub().returns(false) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('should redirect to the login page', function () { - this.AuthenticationController._redirectToRegisterPage - .calledWith(this.req, this.res) + it('should redirect to the login page', function (ctx) { + ctx.AuthenticationController._redirectToRegisterPage + .calledWith(ctx.req, ctx.res) .should.equal(false) - this.AuthenticationController._redirectToLoginPage - .calledWith(this.req, this.res) + ctx.AuthenticationController._redirectToLoginPage + .calledWith(ctx.req, ctx.res) .should.equal(true) }) }) describe('they have come via a templates link', function () { - beforeEach(function () { - this.req.query.zipUrl = 'something' - this.SessionManager.isUserLoggedIn = sinon.stub().returns(false) - this.middleware(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.query.zipUrl = 'something' + ctx.SessionManager.isUserLoggedIn = sinon.stub().returns(false) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('should redirect to the register page', function () { - this.AuthenticationController._redirectToRegisterPage - .calledWith(this.req, this.res) + it('should redirect to the register page', function (ctx) { + ctx.AuthenticationController._redirectToRegisterPage + .calledWith(ctx.req, ctx.res) .should.equal(true) - this.AuthenticationController._redirectToLoginPage - .calledWith(this.req, this.res) + ctx.AuthenticationController._redirectToLoginPage + .calledWith(ctx.req, ctx.res) .should.equal(false) }) }) describe('they have been invited to a project', function () { - beforeEach(function () { - this.req.session.sharedProjectData = { + beforeEach(function (ctx) { + ctx.req.session.sharedProjectData = { project_name: 'something', user_first_name: 'else', } - this.SessionManager.isUserLoggedIn = sinon.stub().returns(false) - this.middleware(this.req, this.res, this.next) + ctx.SessionManager.isUserLoggedIn = sinon.stub().returns(false) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('should redirect to the register page', function () { - this.AuthenticationController._redirectToRegisterPage - .calledWith(this.req, this.res) + it('should redirect to the register page', function (ctx) { + ctx.AuthenticationController._redirectToRegisterPage + .calledWith(ctx.req, ctx.res) .should.equal(true) - this.AuthenticationController._redirectToLoginPage - .calledWith(this.req, this.res) + ctx.AuthenticationController._redirectToLoginPage + .calledWith(ctx.req, ctx.res) .should.equal(false) }) }) }) describe('_redirectToRegisterPage', function () { - beforeEach(function () { - this.req.path = '/target/url' - this.req.query = { extra_query: 'foo' } - this.AuthenticationController._redirectToRegisterPage(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.path = '/target/url' + ctx.req.query = { extra_query: 'foo' } + ctx.AuthenticationController._redirectToRegisterPage(ctx.req, ctx.res) }) - it('should redirect to the register page with a query string attached', function () { - this.req.session.postLoginRedirect.should.equal( + it('should redirect to the register page with a query string attached', function (ctx) { + ctx.req.session.postLoginRedirect.should.equal( '/target/url?extra_query=foo' ) - this.res.redirectedTo.should.equal('/register?extra_query=foo') + ctx.res.redirectedTo.should.equal('/register?extra_query=foo') }) - it('should log out a message', function () { - this.logger.debug - .calledWith( - { url: this.url }, - 'user not logged in so redirecting to register page' - ) - .should.equal(true) + it('should log out a message', function (ctx) { + expect(ctx.logger.debug).toBeCalledWith( + { url: ctx.url }, + 'user not logged in so redirecting to register page' + ) }) }) describe('_redirectToLoginPage', function () { - beforeEach(function () { - this.req.path = '/target/url' - this.req.query = { extra_query: 'foo' } - this.AuthenticationController._redirectToLoginPage(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.path = '/target/url' + ctx.req.query = { extra_query: 'foo' } + ctx.AuthenticationController._redirectToLoginPage(ctx.req, ctx.res) }) - it('should redirect to the register page with a query string attached', function () { - this.req.session.postLoginRedirect.should.equal( + it('should redirect to the register page with a query string attached', function (ctx) { + ctx.req.session.postLoginRedirect.should.equal( '/target/url?extra_query=foo' ) - this.res.redirectedTo.should.equal('/login?extra_query=foo') + ctx.res.redirectedTo.should.equal('/login?extra_query=foo') }) }) describe('_recordSuccessfulLogin', function () { - beforeEach(function () { - this.UserUpdater.updateUser = sinon.stub().yields() - this.AuthenticationController._recordSuccessfulLogin( - this.user._id, - this.callback + beforeEach(function (ctx) { + ctx.UserUpdater.updateUser = sinon.stub().yields() + ctx.AuthenticationController._recordSuccessfulLogin( + ctx.user._id, + ctx.callback ) }) - it('should increment the user.login.success metric', function () { - this.Metrics.inc.calledWith('user.login.success').should.equal(true) + it('should increment the user.login.success metric', function (ctx) { + ctx.Metrics.inc.calledWith('user.login.success').should.equal(true) }) - it("should update the user's login count and last logged in date", function () { - this.UserUpdater.updateUser.args[0][1].$set.lastLoggedIn.should.not.equal( + it("should update the user's login count and last logged in date", function (ctx) { + ctx.UserUpdater.updateUser.args[0][1].$set.lastLoggedIn.should.not.equal( undefined ) - this.UserUpdater.updateUser.args[0][1].$inc.loginCount.should.equal(1) + ctx.UserUpdater.updateUser.args[0][1].$inc.loginCount.should.equal(1) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('_recordFailedLogin', function () { - beforeEach(function () { - this.AuthenticationController._recordFailedLogin(this.callback) + beforeEach(function (ctx) { + ctx.AuthenticationController._recordFailedLogin(ctx.callback) }) - it('should increment the user.login.failed metric', function () { - this.Metrics.inc.calledWith('user.login.failed').should.equal(true) + it('should increment the user.login.failed metric', function (ctx) { + ctx.Metrics.inc.calledWith('user.login.failed').should.equal(true) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('setRedirectInSession', function () { - beforeEach(function () { - this.req = { session: {} } - this.req.path = '/somewhere' - this.req.query = { one: '1' } + beforeEach(function (ctx) { + ctx.req = { session: {} } + ctx.req.path = '/somewhere' + ctx.req.query = { one: '1' } }) - it('should set redirect property on session', function () { - this.AuthenticationController.setRedirectInSession(this.req) - expect(this.req.session.postLoginRedirect).to.equal('/somewhere?one=1') + it('should set redirect property on session', function (ctx) { + ctx.AuthenticationController.setRedirectInSession(ctx.req) + expect(ctx.req.session.postLoginRedirect).to.equal('/somewhere?one=1') }) - it('should set the supplied value', function () { - this.AuthenticationController.setRedirectInSession( - this.req, + it('should set the supplied value', function (ctx) { + ctx.AuthenticationController.setRedirectInSession( + ctx.req, '/somewhere/specific' ) - expect(this.req.session.postLoginRedirect).to.equal('/somewhere/specific') + expect(ctx.req.session.postLoginRedirect).to.equal('/somewhere/specific') }) - it('should not allow open redirects', function () { - this.AuthenticationController.setRedirectInSession( - this.req, + it('should not allow open redirects', function (ctx) { + ctx.AuthenticationController.setRedirectInSession( + ctx.req, 'https://evil.com' ) - expect(this.req.session.postLoginRedirect).to.be.undefined + expect(ctx.req.session.postLoginRedirect).to.be.undefined }) describe('with a png', function () { - beforeEach(function () { - this.req = { session: {} } + beforeEach(function (ctx) { + ctx.req = { session: {} } }) - it('should not set the redirect', function () { - this.AuthenticationController.setRedirectInSession( - this.req, + it('should not set the redirect', function (ctx) { + ctx.AuthenticationController.setRedirectInSession( + ctx.req, '/something.png' ) - expect(this.req.session.postLoginRedirect).to.equal(undefined) + expect(ctx.req.session.postLoginRedirect).to.equal(undefined) }) }) describe('with a js path', function () { - beforeEach(function () { - this.req = { session: {} } + beforeEach(function (ctx) { + ctx.req = { session: {} } }) - it('should not set the redirect', function () { - this.AuthenticationController.setRedirectInSession( - this.req, + it('should not set the redirect', function (ctx) { + ctx.AuthenticationController.setRedirectInSession( + ctx.req, '/js/something.js' ) - expect(this.req.session.postLoginRedirect).to.equal(undefined) + expect(ctx.req.session.postLoginRedirect).to.equal(undefined) }) }) }) describe('getRedirectFromSession', function () { - it('should get redirect property from session', function () { - this.req = { session: { postLoginRedirect: '/a?b=c' } } + it('should get redirect property from session', function (ctx) { + ctx.req = { session: { postLoginRedirect: '/a?b=c' } } expect( - this.AuthenticationController.getRedirectFromSession(this.req) + ctx.AuthenticationController.getRedirectFromSession(ctx.req) ).to.equal('/a?b=c') }) - it('should not allow open redirects', function () { - this.req = { session: { postLoginRedirect: 'https://evil.com' } } - expect(this.AuthenticationController.getRedirectFromSession(this.req)).to - .be.null + it('should not allow open redirects', function (ctx) { + ctx.req = { session: { postLoginRedirect: 'https://evil.com' } } + expect(ctx.AuthenticationController.getRedirectFromSession(ctx.req)).to.be + .null }) - it('handle null values', function () { - this.req = { session: {} } - expect(this.AuthenticationController.getRedirectFromSession(this.req)).to - .be.null + it('handle null values', function (ctx) { + ctx.req = { session: {} } + expect(ctx.AuthenticationController.getRedirectFromSession(ctx.req)).to.be + .null }) }) describe('_clearRedirectFromSession', function () { - beforeEach(function () { - this.req = { session: { postLoginRedirect: '/a?b=c' } } + beforeEach(function (ctx) { + ctx.req = { session: { postLoginRedirect: '/a?b=c' } } }) - it('should remove the redirect property from session', function () { - this.AuthenticationController._clearRedirectFromSession(this.req) - expect(this.req.session.postLoginRedirect).to.equal(undefined) + it('should remove the redirect property from session', function (ctx) { + ctx.AuthenticationController._clearRedirectFromSession(ctx.req) + expect(ctx.req.session.postLoginRedirect).to.equal(undefined) }) }) @@ -1119,101 +1232,95 @@ describe('AuthenticationController', function () { // - afterLoginSessionSetup // - clear redirect // - issue redir, two ways - beforeEach(function () { - this.AuthenticationController.getRedirectFromSession = sinon + beforeEach(function (ctx) { + ctx.AuthenticationController.getRedirectFromSession = sinon .stub() .returns('/some/page') - this.req.sessionID = 'thisisacryptographicallysecurerandomid' - this.req.session = { + ctx.req.sessionID = 'thisisacryptographicallysecurerandomid' + ctx.req.session = { passport: { user: { _id: 'one' } }, } - this.req.session.destroy = sinon.stub().yields(null) - this.req.session.save = sinon.stub().yields(null) - this.req.sessionStore = { generate: sinon.stub() } - this.req.login = sinon.stub().yields(null) + ctx.req.session.destroy = sinon.stub().yields(null) + ctx.req.session.save = sinon.stub().yields(null) + ctx.req.sessionStore = { generate: sinon.stub() } + ctx.req.login = sinon.stub().yields(null) - this.AuthenticationController._clearRedirectFromSession = sinon.stub() - this.AuthenticationController._redirectToReconfirmPage = sinon.stub() - this.UserSessionsManager.trackSession = sinon.stub() - this.UserHandler.promises.populateTeamInvites = sinon.stub().resolves() - this.LoginRateLimiter.recordSuccessfulLogin = sinon.stub() - this.AuthenticationController._recordSuccessfulLogin = sinon.stub() - this.AnalyticsManager.recordEvent = sinon.stub() - this.AnalyticsManager.identifyUser = sinon.stub() - this.acceptsJson.returns(true) - this.res.json = sinon.stub() - this.res.redirect = sinon.stub() + ctx.AuthenticationController._clearRedirectFromSession = sinon.stub() + ctx.AuthenticationController._redirectToReconfirmPage = sinon.stub() + ctx.UserSessionsManager.trackSession = sinon.stub() + ctx.UserHandler.promises.populateTeamInvites = sinon.stub().resolves() + ctx.LoginRateLimiter.recordSuccessfulLogin = sinon.stub() + ctx.AuthenticationController._recordSuccessfulLogin = sinon.stub() + ctx.AnalyticsManager.recordEvent = sinon.stub() + ctx.AnalyticsManager.identifyUser = sinon.stub() + ctx.acceptsJson.returns(true) + ctx.res.json = sinon.stub() + ctx.res.redirect = sinon.stub() }) - it('should extract the redirect from the session', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should extract the redirect from the session', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.AuthenticationController.getRedirectFromSession.callCount + ctx.AuthenticationController.getRedirectFromSession.callCount ).to.equal(1) expect( - this.AuthenticationController.getRedirectFromSession.calledWith( - this.req + ctx.AuthenticationController.getRedirectFromSession.calledWith(ctx.req) + ).to.equal(true) + }) + + it('should clear redirect from session', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next + ) + expect( + ctx.AuthenticationController._clearRedirectFromSession.callCount + ).to.equal(1) + expect( + ctx.AuthenticationController._clearRedirectFromSession.calledWith( + ctx.req ) ).to.equal(true) }) - it('should clear redirect from session', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should issue a json response with a redirect', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.AuthenticationController._clearRedirectFromSession.callCount - ).to.equal(1) - expect( - this.AuthenticationController._clearRedirectFromSession.calledWith( - this.req - ) - ).to.equal(true) - }) - - it('should issue a json response with a redirect', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next - ) - expect( - this.AsyncFormHelper.redirect.calledWith( - this.req, - this.res, - '/some/page' - ) + ctx.AsyncFormHelper.redirect.calledWith(ctx.req, ctx.res, '/some/page') ).to.equal(true) }) describe('with a non-json request', function () { - beforeEach(function () { - this.acceptsJson.returns(false) - this.res.json = sinon.stub() - this.res.redirect = sinon.stub() + beforeEach(function (ctx) { + ctx.acceptsJson.returns(false) + ctx.res.json = sinon.stub() + ctx.res.redirect = sinon.stub() }) - it('should issue a plain redirect', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should issue a plain redirect', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.AsyncFormHelper.redirect.calledWith( - this.req, - this.res, + ctx.AsyncFormHelper.redirect.calledWith( + ctx.req, + ctx.res, '/some/page' ) ).to.equal(true) @@ -1221,153 +1328,157 @@ describe('AuthenticationController', function () { }) describe('when user is flagged to reconfirm', function () { - beforeEach(function () { - this.req.session = {} - this.user.must_reconfirm = true + beforeEach(function (ctx) { + ctx.req.session = {} + ctx.user.must_reconfirm = true }) - it('should redirect to reconfirm page', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should redirect to reconfirm page', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.AuthenticationController._redirectToReconfirmPage.calledWith( - this.req + ctx.AuthenticationController._redirectToReconfirmPage.calledWith( + ctx.req ) ).to.equal(true) }) }) describe('when user account is suspended', function () { - beforeEach(function () { - this.req.session = {} - this.user.suspended = true + beforeEach(function (ctx) { + ctx.req.session = {} + ctx.user.suspended = true }) - it('should not log in and instead redirect to suspended account page', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should not log in and instead redirect to suspended account page', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - sinon.assert.notCalled(this.req.login) + sinon.assert.notCalled(ctx.req.login) sinon.assert.calledWith( - this.AsyncFormHelper.redirect, - this.req, - this.res, + ctx.AsyncFormHelper.redirect, + ctx.req, + ctx.res, '/account-suspended' ) }) }) describe('preFinishLogin hook', function () { - it('call hook and proceed', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('call hook and proceed', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) sinon.assert.calledWith( - this.Modules.promises.hooks.fire, + ctx.Modules.promises.hooks.fire, 'preFinishLogin', - this.req, - this.res, - this.user + ctx.req, + ctx.res, + ctx.user ) - expect(this.AsyncFormHelper.redirect.called).to.equal(true) + expect(ctx.AsyncFormHelper.redirect.called).to.equal(true) }) - it('stop if hook has redirected', async function () { - this.Modules.promises.hooks.fire = sinon + it('stop if hook has redirected', async function (ctx) { + ctx.Modules.promises.hooks.fire = sinon .stub() .resolves([{ doNotFinish: true }]) - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - expect(this.next.callCount).to.equal(0) - expect(this.res.json.callCount).to.equal(0) + expect(ctx.next.callCount).to.equal(0) + expect(ctx.res.json.callCount).to.equal(0) }) - it('call next with hook errors', function (done) { - this.Modules.promises.hooks.fire = sinon.stub().yields(new Error()) - this.AuthenticationController.promises - .finishLogin(this.user, this.req, this.res) - .catch(err => { - expect(err).to.exist - expect(this.res.json.callCount).to.equal(0) - done() - }) + it('call next with hook errors', async function (ctx) { + await new Promise(resolve => { + ctx.Modules.promises.hooks.fire = sinon.stub().yields(new Error()) + ctx.AuthenticationController.promises + .finishLogin(ctx.user, ctx.req, ctx.res) + .catch(err => { + expect(err).to.exist + expect(ctx.res.json.callCount).to.equal(0) + resolve() + }) + }) }) }) describe('UserAuditLog', function () { - it('should add an audit log entry', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should add an audit log entry', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.UserAuditLogHandler.promises.addEntry + ctx.UserAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.user._id, + ctx.user._id, 'login', - this.user._id, + ctx.user._id, '42.42.42.42' ) }) - it('should add an audit log entry before logging the user in', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should add an audit log entry before logging the user in', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) expect( - this.UserAuditLogHandler.promises.addEntry - ).to.have.been.calledBefore(this.req.login) + ctx.UserAuditLogHandler.promises.addEntry + ).to.have.been.calledBefore(ctx.req.login) }) - it('should not log the user in without an audit log entry', function (done) { - const theError = new Error() - this.UserAuditLogHandler.promises.addEntry.rejects(theError) - this.next.callsFake(err => { - expect(err).to.equal(theError) - expect(this.next).to.have.been.calledWith(theError) - expect(this.req.login).to.not.have.been.called - done() + it('should not log the user in without an audit log entry', async function (ctx) { + await new Promise(resolve => { + const theError = new Error() + ctx.UserAuditLogHandler.promises.addEntry.rejects(theError) + ctx.next.callsFake(err => { + expect(err).to.equal(theError) + expect(ctx.next).to.have.been.calledWith(theError) + expect(ctx.req.login).to.not.have.been.called + resolve() + }) + ctx.AuthenticationController.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next + ) }) - this.AuthenticationController.finishLogin( - this.user, - this.req, - this.res, - this.next - ) }) - it('should pass along auditInfo when present', async function () { - this.AuthenticationController.setAuditInfo(this.req, { + it('should pass along auditInfo when present', async function (ctx) { + ctx.AuthenticationController.setAuditInfo(ctx.req, { method: 'Login', }) - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res ) expect( - this.UserAuditLogHandler.promises.addEntry + ctx.UserAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.user._id, + ctx.user._id, 'login', - this.user._id, + ctx.user._id, '42.42.42.42', { method: 'Login' } ) @@ -1377,126 +1488,128 @@ describe('AuthenticationController', function () { describe('_afterLoginSessionSetup', function () { beforeEach(function () {}) - it('should call req.login', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should call req.login', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - this.req.login.callCount.should.equal(1) + ctx.req.login.callCount.should.equal(1) }) - it('should erase the CSRF secret', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should erase the CSRF secret', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - expect(this.req.session.csrfSecret).to.not.exist + expect(ctx.req.session.csrfSecret).to.not.exist }) - it('should call req.session.save', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should call req.session.save', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - this.req.session.save.callCount.should.equal(1) + ctx.req.session.save.callCount.should.equal(1) }) - it('should call UserSessionsManager.trackSession', async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + it('should call UserSessionsManager.trackSession', async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) - this.UserSessionsManager.trackSession.callCount.should.equal(1) + ctx.UserSessionsManager.trackSession.callCount.should.equal(1) }) describe('when req.session.save produces an error', function () { - beforeEach(function () { - this.req.session.save = sinon.stub().yields(new Error('woops')) + beforeEach(function (ctx) { + ctx.req.session.save = sinon.stub().yields(new Error('woops')) }) - it('should produce an error', function (done) { - this.AuthenticationController.promises - .finishLogin(this.user, this.req, this.res) - .catch(err => { - expect(err).to.not.be.oneOf([null, undefined]) - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function (ctx) { + await new Promise(resolve => { + ctx.AuthenticationController.promises + .finishLogin(ctx.user, ctx.req, ctx.res) + .catch(err => { + expect(err).to.not.be.oneOf([null, undefined]) + expect(err).to.be.instanceof(Error) + resolve() + }) + }) }) - it('should not call UserSessionsManager.trackSession', function (done) { - this.AuthenticationController.promises - .finishLogin(this.user, this.req, this.res) - .catch(err => { - expect(err).to.exist - this.UserSessionsManager.trackSession.callCount.should.equal(0) - done() - }) + it('should not call UserSessionsManager.trackSession', async function (ctx) { + await new Promise(resolve => { + ctx.AuthenticationController.promises + .finishLogin(ctx.user, ctx.req, ctx.res) + .catch(err => { + expect(err).to.exist + ctx.UserSessionsManager.trackSession.callCount.should.equal(0) + resolve() + }) + }) }) }) }) describe('_loginAsyncHandlers', function () { - beforeEach(async function () { - await this.AuthenticationController.promises.finishLogin( - this.user, - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + await ctx.AuthenticationController.promises.finishLogin( + ctx.user, + ctx.req, + ctx.res, + ctx.next ) }) - it('should call identifyUser', function () { + it('should call identifyUser', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.identifyUser, - this.user._id, - this.req.session.analyticsId + ctx.AnalyticsManager.identifyUser, + ctx.user._id, + ctx.req.session.analyticsId ) }) - it('should setup the user data in the background', function () { - this.UserHandler.promises.populateTeamInvites - .calledWith(this.user) + it('should setup the user data in the background', function (ctx) { + ctx.UserHandler.promises.populateTeamInvites + .calledWith(ctx.user) .should.equal(true) }) - it('should set res.session.justLoggedIn', function () { - this.req.session.justLoggedIn.should.equal(true) + it('should set res.session.justLoggedIn', function (ctx) { + ctx.req.session.justLoggedIn.should.equal(true) }) - it('should record the successful login', function () { - this.AuthenticationController._recordSuccessfulLogin - .calledWith(this.user._id) + it('should record the successful login', function (ctx) { + ctx.AuthenticationController._recordSuccessfulLogin + .calledWith(ctx.user._id) .should.equal(true) }) - it('should tell the rate limiter that there was a success for that email', function () { - this.LoginRateLimiter.recordSuccessfulLogin - .calledWith(this.user.email) + it('should tell the rate limiter that there was a success for that email', function (ctx) { + ctx.LoginRateLimiter.recordSuccessfulLogin + .calledWith(ctx.user.email) .should.equal(true) }) - it('should log the successful login', function () { - this.logger.debug - .calledWith( - { email: this.user.email, userId: this.user._id.toString() }, - 'successful log in' - ) - .should.equal(true) + it('should log the successful login', function (ctx) { + expect(ctx.logger.debug).toBeCalledWith( + { email: ctx.user.email, userId: ctx.user._id.toString() }, + 'successful log in' + ) }) - it('should track the login event', function () { + it('should track the login event', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForUserInBackground, - this.user._id, + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.user._id, 'user-logged-in' ) }) @@ -1504,33 +1617,33 @@ describe('AuthenticationController', function () { }) describe('checkCredentials', function () { - beforeEach(function () { - this.userDetailsMap = new Map() - this.logger.err = sinon.stub() - this.Metrics.inc = sinon.stub() + beforeEach(function (ctx) { + ctx.userDetailsMap = new Map() + ctx.logger.err = sinon.stub() + ctx.Metrics.inc = sinon.stub() }) describe('with valid credentials', function () { describe('single password', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', 'correctpassword') - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', 'correctpassword') + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'correctpassword' ) }) - it('should return true', function () { - this.result.should.equal(true) + it('should return true', function (ctx) { + ctx.result.should.equal(true) }) - it('should not log an error', function () { - this.logger.err.called.should.equal(false) + it('should not log an error', function (ctx) { + ctx.logger.err.called.should.equal(false) }) - it('should record success metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record success metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1542,25 +1655,25 @@ describe('AuthenticationController', function () { }) describe('array with primary password', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['primary', 'fallback']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['primary', 'fallback']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'primary' ) }) - it('should return true', function () { - this.result.should.equal(true) + it('should return true', function (ctx) { + ctx.result.should.equal(true) }) - it('should not log an error', function () { - this.logger.err.called.should.equal(false) + it('should not log an error', function (ctx) { + ctx.logger.err.called.should.equal(false) }) - it('should record success metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record success metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1572,25 +1685,25 @@ describe('AuthenticationController', function () { }) describe('array with fallback password', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['primary', 'fallback']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['primary', 'fallback']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'fallback' ) }) - it('should return true', function () { - this.result.should.equal(true) + it('should return true', function (ctx) { + ctx.result.should.equal(true) }) - it('should not log an error', function () { - this.logger.err.called.should.equal(false) + it('should not log an error', function (ctx) { + ctx.logger.err.called.should.equal(false) }) - it('should record success metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record success metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1604,28 +1717,28 @@ describe('AuthenticationController', function () { describe('with invalid credentials', function () { describe('unknown user', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', 'correctpassword') - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', 'correctpassword') + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'unknownuser', 'anypassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'unknownuser' }, 'invalid login details' ) }) - it('should record failure metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record failure metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1637,28 +1750,28 @@ describe('AuthenticationController', function () { }) describe('wrong password', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', 'correctpassword') - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', 'correctpassword') + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'wrongpassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'testuser' }, 'invalid login details' ) }) - it('should record failure metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record failure metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1670,28 +1783,28 @@ describe('AuthenticationController', function () { }) describe('wrong password with array', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['primary', 'fallback']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['primary', 'fallback']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'wrongpassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'testuser' }, 'invalid login details' ) }) - it('should record failure metrics', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record failure metrics', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1703,28 +1816,28 @@ describe('AuthenticationController', function () { }) describe('null user entry', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', null) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', null) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'anypassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'testuser' }, 'invalid login details' ) }) - it('should record failure metrics for unknown user', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record failure metrics for unknown user', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { @@ -1736,59 +1849,59 @@ describe('AuthenticationController', function () { }) describe('empty primary password in array', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['', 'fallback']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['', 'fallback']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'fallback' ) }) - it('should return true with fallback password', function () { - this.result.should.equal(true) + it('should return true with fallback password', function (ctx) { + ctx.result.should.equal(true) }) - it('should not log an error', function () { - this.logger.err.called.should.equal(false) + it('should not log an error', function (ctx) { + ctx.logger.err.called.should.equal(false) }) }) describe('empty fallback password in array', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['primary', '']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['primary', '']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'primary' ) }) - it('should return true with primary password', function () { - this.result.should.equal(true) + it('should return true with primary password', function (ctx) { + ctx.result.should.equal(true) }) - it('should not log an error', function () { - this.logger.err.called.should.equal(false) + it('should not log an error', function (ctx) { + ctx.logger.err.called.should.equal(false) }) }) describe('both passwords empty in array', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', ['', '']) - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', ['', '']) + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'anypassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'testuser' }, 'invalid login details' ) @@ -1796,28 +1909,28 @@ describe('AuthenticationController', function () { }) describe('empty single password', function () { - beforeEach(function () { - this.userDetailsMap.set('testuser', '') - this.result = this.AuthenticationController.checkCredentials( - this.userDetailsMap, + beforeEach(function (ctx) { + ctx.userDetailsMap.set('testuser', '') + ctx.result = ctx.AuthenticationController.checkCredentials( + ctx.userDetailsMap, 'testuser', 'anypassword' ) }) - it('should return false', function () { - this.result.should.equal(false) + it('should return false', function (ctx) { + ctx.result.should.equal(false) }) - it('should log an error', function () { - this.logger.err.should.have.been.calledWith( + it('should log an error', function (ctx) { + ctx.logger.err.should.have.been.calledWith( { user: 'testuser' }, 'invalid login details' ) }) - it('should record failure metrics for unknown user', function () { - this.Metrics.inc.should.have.been.calledWith( + it('should record failure metrics for unknown user', function (ctx) { + ctx.Metrics.inc.should.have.been.calledWith( 'security.http-auth.check-credentials', 1, { diff --git a/services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs b/services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs index 435ebcc011..aff0849083 100644 --- a/services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs +++ b/services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs @@ -1,31 +1,32 @@ -const sinon = require('sinon') -const { expect } = require('chai') +import { beforeEach, describe, it, vi, expect } from 'vitest' +import sinon from 'sinon' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.js' +import PublicAccessLevels from '../../../../app/src/Features/Authorization/PublicAccessLevels.js' +import mongodb from 'mongodb-legacy' const modulePath = - '../../../../app/src/Features/Authorization/AuthorizationManager.js' -const SandboxedModule = require('sandboxed-module') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels') -const PublicAccessLevels = require('../../../../app/src/Features/Authorization/PublicAccessLevels') -const { ObjectId } = require('mongodb-legacy') + '../../../../app/src/Features/Authorization/AuthorizationManager.mjs' + +const { ObjectId } = mongodb describe('AuthorizationManager', function () { - beforeEach(function () { - this.user = { _id: new ObjectId() } - this.project = { _id: new ObjectId() } - this.doc = { _id: new ObjectId() } - this.thread = { _id: new ObjectId() } - this.token = 'some-token' + beforeEach(async function (ctx) { + ctx.user = { _id: new ObjectId() } + ctx.project = { _id: new ObjectId() } + ctx.doc = { _id: new ObjectId() } + ctx.thread = { _id: new ObjectId() } + ctx.token = 'some-token' - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProject: sinon.stub().resolves(null), }, } - this.ProjectGetter.promises.getProject - .withArgs(this.project._id) - .resolves(this.project) + ctx.ProjectGetter.promises.getProject + .withArgs(ctx.project._id) + .resolves(ctx.project) - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getProjectAccess: sinon.stub().resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), @@ -34,16 +35,16 @@ describe('AuthorizationManager', function () { }, } - this.CollaboratorsHandler = {} + ctx.CollaboratorsHandler = {} - this.User = { + ctx.User = { findOne: sinon.stub().returns({ exec: sinon.stub().resolves(null) }), } - this.User.findOne - .withArgs({ _id: this.user._id }) - .returns({ exec: sinon.stub().resolves(this.user) }) + ctx.User.findOne + .withArgs({ _id: ctx.user._id }) + .returns({ exec: sinon.stub().resolves(ctx.user) }) - this.TokenAccessHandler = { + ctx.TokenAccessHandler = { promises: { validateTokenForAnonymousAccess: sinon .stub() @@ -51,37 +52,78 @@ describe('AuthorizationManager', function () { }, } - this.ChatApiHandler = { + ctx.ChatApiHandler = { promises: { getThread: sinon .stub() .resolves({ messages: [{ user_id: new ObjectId() }] }), }, } - this.Modules = { promises: { hooks: { fire: sinon.stub() } } } - this.settings = { + ctx.Features = { + hasFeature: sinon.stub().returns(true), + } + ctx.Modules = { promises: { hooks: { fire: sinon.stub() } } } + ctx.settings = { passwordStrengthOptions: {}, adminPrivilegeAvailable: true, adminRolesEnabled: false, moduleImportSequence: [], } - this.AuthorizationManager = SandboxedModule.require(modulePath, { - requires: { - 'mongodb-legacy': { ObjectId }, - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, - '../Project/ProjectGetter': this.ProjectGetter, - '../../models/User': { User: this.User }, - '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, - '../Chat/ChatApiHandler': this.ChatApiHandler, - '../../infrastructure/Modules': this.Modules, - '@overleaf/settings': this.settings, - }, - }) + + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({ + default: ctx.ChatApiHandler, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + ctx.AuthorizationManager = (await import(modulePath)).default }) describe('isRestrictedUser', function () { - it('should produce the correct values', function () { + it('should produce the correct values', function (ctx) { const notRestrictedScenarios = [ [null, 'readAndWrite', false, false], ['id', 'readAndWrite', true, false], @@ -100,12 +142,12 @@ describe('AuthorizationManager', function () { ] for (const notRestrictedArgs of notRestrictedScenarios) { expect( - this.AuthorizationManager.isRestrictedUser(...notRestrictedArgs) + ctx.AuthorizationManager.isRestrictedUser(...notRestrictedArgs) ).to.equal(false) } for (const restrictedArgs of restrictedScenarios) { expect( - this.AuthorizationManager.isRestrictedUser(...restrictedArgs) + ctx.AuthorizationManager.isRestrictedUser(...restrictedArgs) ).to.equal(true) } }) @@ -113,402 +155,402 @@ describe('AuthorizationManager', function () { describe('getPrivilegeLevelForProject', function () { describe('with a token-based project', function () { - beforeEach(function () { - this.project.publicAccesLevel = 'tokenBased' + beforeEach(function (ctx) { + ctx.project.publicAccesLevel = 'tokenBased' }) describe('with a user id with a privilege level', function () { - beforeEach(async function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon .stub() .returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_ONLY), }) - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it("should return the user's privilege level", function () { - expect(this.result).to.equal('readOnly') + it("should return the user's privilege level", function (ctx) { + expect(ctx.result).to.equal('readOnly') }) }) describe('with a user id with no privilege level', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return false', function () { - expect(this.result).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.result).to.equal(false) }) }) describe('with a user id who is an admin', function () { - beforeEach(async function () { - this.user.isAdmin = true - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.user.isAdmin = true + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return the user as an owner', function () { - expect(this.result).to.equal('owner') + it('should return the user as an owner', function (ctx) { + expect(ctx.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { describe('when the token is not valid', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should check if the token is valid', function () { - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( - this.project._id, - this.token + it('should check if the token is valid', function (ctx) { + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + ctx.project._id, + ctx.token ) }) - it('should return false', function () { - expect(this.result).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.result).to.equal(false) }) }) describe('when the token is valid for read-and-write', function () { - beforeEach(async function () { - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess = + beforeEach(async function (ctx) { + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess = sinon .stub() - .withArgs(this.project._id, this.token) + .withArgs(ctx.project._id, ctx.token) .resolves({ isValidReadAndWrite: true, isValidReadOnly: false }) - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should check if the token is valid', function () { - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( - this.project._id, - this.token + it('should check if the token is valid', function (ctx) { + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + ctx.project._id, + ctx.token ) }) - it('should give read-write access', function () { - expect(this.result).to.equal('readAndWrite') + it('should give read-write access', function (ctx) { + expect(ctx.result).to.equal('readAndWrite') }) }) describe('when the token is valid for read-only', function () { - beforeEach(async function () { - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess = + beforeEach(async function (ctx) { + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess = sinon .stub() - .withArgs(this.project._id, this.token) + .withArgs(ctx.project._id, ctx.token) .resolves({ isValidReadAndWrite: false, isValidReadOnly: true }) - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should check if the token is valid', function () { - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( - this.project._id, - this.token + it('should check if the token is valid', function (ctx) { + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess.should.have.been.calledWith( + ctx.project._id, + ctx.token ) }) - it('should give read-only access', function () { - expect(this.result).to.equal('readOnly') + it('should give read-only access', function (ctx) { + expect(ctx.result).to.equal('readOnly') }) }) }) }) describe('with a private project', function () { - beforeEach(function () { - this.project.publicAccesLevel = 'private' + beforeEach(function (ctx) { + ctx.project.publicAccesLevel = 'private' }) describe('with a user id with a privilege level', function () { - beforeEach(async function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon .stub() .returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_ONLY), }) - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it("should return the user's privilege level", function () { - expect(this.result).to.equal('readOnly') + it("should return the user's privilege level", function (ctx) { + expect(ctx.result).to.equal('readOnly') }) }) describe('with a user id with no privilege level', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return false', function () { - expect(this.result).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.result).to.equal(false) }) }) describe('with a user id who is an admin', function () { - beforeEach(async function () { - this.user.isAdmin = true - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.user.isAdmin = true + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return the user as an owner', function () { - expect(this.result).to.equal('owner') + it('should return the user as an owner', function (ctx) { + expect(ctx.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should return false', function () { - expect(this.result).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.result).to.equal(false) }) }) }) describe('with a public project', function () { - beforeEach(function () { - this.project.publicAccesLevel = 'readAndWrite' - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(function (ctx) { + ctx.project.publicAccesLevel = 'readAndWrite' + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon .stub() - .returns(this.project.publicAccesLevel), + .returns(ctx.project.publicAccesLevel), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.NONE), }) }) describe('with a user id with a privilege level', function () { - beforeEach(async function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon .stub() - .returns(this.project.publicAccesLevel), + .returns(ctx.project.publicAccesLevel), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_ONLY), }) - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it("should return the user's privilege level", function () { - expect(this.result).to.equal('readOnly') + it("should return the user's privilege level", function (ctx) { + expect(ctx.result).to.equal('readOnly') }) }) describe('with a user id with no privilege level', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return the public privilege level', function () { - expect(this.result).to.equal('readAndWrite') + it('should return the public privilege level', function (ctx) { + expect(ctx.result).to.equal('readAndWrite') }) }) describe('with a user id who is an admin', function () { - beforeEach(async function () { - this.user.isAdmin = true - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, - this.project._id, - this.token + beforeEach(async function (ctx) { + ctx.user.isAdmin = true + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, + ctx.project._id, + ctx.token ) }) - it('should return the user as an owner', function () { - expect(this.result).to.equal('owner') + it('should return the user as an owner', function (ctx) { + expect(ctx.result).to.equal('owner') }) }) describe('with no user (anonymous)', function () { - beforeEach(async function () { - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + beforeEach(async function (ctx) { + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should return the public privilege level', function () { - expect(this.result).to.equal('readAndWrite') + it('should return the public privilege level', function (ctx) { + expect(ctx.result).to.equal('readAndWrite') }) }) describe('with link-sharing disabled', function () { - beforeEach(async function () { - this.settings.disableLinkSharing = true - this.result = - await this.AuthorizationManager.promises.getPrivilegeLevelForProject( + beforeEach(async function (ctx) { + ctx.Features.hasFeature.withArgs('link-sharing').returns(false) + ctx.result = + await ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( null, - this.project._id, - this.token + ctx.project._id, + ctx.token ) }) - it('should not call CollaboratorsGetter.getProjectAccess', function () { - this.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( + it('should not call CollaboratorsGetter.getProjectAccess', function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.called.should.equal( false ) }) - it('should return false', function () { - expect(this.result).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.result).to.equal(false) }) }) }) describe("when the project doesn't exist", function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.getProjectAccess.rejects( + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess.rejects( new Errors.NotFoundError() ) }) - it('should return a NotFoundError', async function () { + it('should return a NotFoundError', async function (ctx) { const someOtherId = new ObjectId() await expect( - this.AuthorizationManager.promises.getPrivilegeLevelForProject( - this.user._id, + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.user._id, someOtherId, - this.token + ctx.token ) ).to.be.rejectedWith(Errors.NotFoundError) }) }) describe('when the project id is not valid', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_ONLY), }) }) - it('should return a error', async function () { + it('should return a error', async function (ctx) { await expect( - this.AuthorizationManager.promises.getPrivilegeLevelForProject( + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject( undefined, 'not project id', - this.token + ctx.token ) ).to.be.rejected }) @@ -560,128 +602,126 @@ describe('AuthorizationManager', function () { describe('isUserSiteAdmin', function () { describe('when user is admin', function () { - beforeEach(function () { - this.user.isAdmin = true + beforeEach(function (ctx) { + ctx.user.isAdmin = true }) - it('should return true', async function () { - const isAdmin = - await this.AuthorizationManager.promises.isUserSiteAdmin( - this.user._id - ) + it('should return true', async function (ctx) { + const isAdmin = await ctx.AuthorizationManager.promises.isUserSiteAdmin( + ctx.user._id + ) expect(isAdmin).to.equal(true) }) }) describe('when user is not admin', function () { - it('should return false', async function () { - const isAdmin = - await this.AuthorizationManager.promises.isUserSiteAdmin( - this.user._id - ) + it('should return false', async function (ctx) { + const isAdmin = await ctx.AuthorizationManager.promises.isUserSiteAdmin( + ctx.user._id + ) expect(isAdmin).to.equal(false) }) }) describe('when user is not found', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const someOtherId = new ObjectId() const isAdmin = - await this.AuthorizationManager.promises.isUserSiteAdmin(someOtherId) + await ctx.AuthorizationManager.promises.isUserSiteAdmin(someOtherId) expect(isAdmin).to.equal(false) }) }) describe('when no user is passed', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const isAdmin = - await this.AuthorizationManager.promises.isUserSiteAdmin(null) + await ctx.AuthorizationManager.promises.isUserSiteAdmin(null) expect(isAdmin).to.equal(false) }) }) }) describe('canUserDeleteOrResolveThread', function () { - it('should return true when user has write permissions', async function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + it('should return true when user has write permissions', async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_AND_WRITE), }) const canResolve = - await this.AuthorizationManager.promises.canUserDeleteOrResolveThread( - this.user._id, - this.project._id, - this.thread._id, - this.token + await ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread( + ctx.user._id, + ctx.project._id, + ctx.thread._id, + ctx.token ) expect(canResolve).to.equal(true) }) - it('should return false when user has read permission', async function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + it('should return false when user has read permission', async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.READ_ONLY), }) const canResolve = - await this.AuthorizationManager.promises.canUserDeleteOrResolveThread( - this.user._id, - this.project._id, - this.thread._id, - this.token + await ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread( + ctx.user._id, + ctx.project._id, + ctx.thread._id, + ctx.token ) expect(canResolve).to.equal(false) }) describe('when user has review permission', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(PrivilegeLevels.REVIEW), }) }) - it('should return false when user is not the comment author', async function () { + it('should return false when user is not the comment author', async function (ctx) { const canResolve = - await this.AuthorizationManager.promises.canUserDeleteOrResolveThread( - this.user._id, - this.project._id, - this.thread._id, - this.token + await ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread( + ctx.user._id, + ctx.project._id, + ctx.thread._id, + ctx.token ) expect(canResolve).to.equal(false) }) - it('should return true when user is the thread author', async function () { - this.ChatApiHandler.promises.getThread - .withArgs(this.project._id, this.thread._id) - .resolves({ messages: [{ user_id: this.user._id }] }) + it('should return true when user is the thread author', async function (ctx) { + ctx.ChatApiHandler.promises.getThread + .withArgs(ctx.project._id, ctx.thread._id) + .resolves({ messages: [{ user_id: ctx.user._id }] }) const canResolve = - await this.AuthorizationManager.promises.canUserDeleteOrResolveThread( - this.user._id, - this.project._id, - this.thread._id, - this.token + await ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread( + ctx.user._id, + ctx.project._id, + ctx.thread._id, + ctx.token ) expect(canResolve).to.equal(true) @@ -694,36 +734,37 @@ function testPermission(permission, privilegeLevels) { describe(permission, function () { describe('when authenticated', function () { describe('when user is site admin', function () { - beforeEach('set user as site admin', function () { - this.user.isAdmin = true + beforeEach(function (ctx) { + ctx.annotate('Set user as site admin') + ctx.user.isAdmin = true }) expectPermission(permission, privilegeLevels.siteAdmin || false) }) describe('admin without permissions', function () { - beforeEach(function () { - this.user.isAdmin = true - this.settings.adminRolesEnabled = true - this.Modules.promises.hooks.fire + beforeEach(function (ctx) { + ctx.user.isAdmin = true + ctx.settings.adminRolesEnabled = true + ctx.Modules.promises.hooks.fire .withArgs('getAdminCapabilities') .resolves([]) }) expectPermission(permission, false) }) describe('admin with `view-project-content`', function () { - beforeEach(function () { - this.user.isAdmin = true - this.settings.adminRolesEnabled = true - this.Modules.promises.hooks.fire + beforeEach(function (ctx) { + ctx.user.isAdmin = true + ctx.settings.adminRolesEnabled = true + ctx.Modules.promises.hooks.fire .withArgs('getAdminCapabilities') .resolves([['view-project-content']]) }) expectPermission(permission, privilegeLevels.readOnly || false) }) describe('admin with `modify-project`', function () { - beforeEach(function () { - this.user.isAdmin = true - this.settings.adminRolesEnabled = true - this.Modules.promises.hooks.fire + beforeEach(function (ctx) { + ctx.user.isAdmin = true + ctx.settings.adminRolesEnabled = true + ctx.Modules.promises.hooks.fire .withArgs('getAdminCapabilities') .resolves([ [ @@ -771,12 +812,12 @@ function testPermission(permission, privilegeLevels) { }) describe('when user is not found', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const otherUserId = new ObjectId() - const value = await this.AuthorizationManager.promises[permission]( + const value = await ctx.AuthorizationManager.promises[permission]( otherUserId, - this.project._id, - this.token + ctx.project._id, + ctx.token ) expect(value).to.equal(false) }) @@ -784,8 +825,8 @@ function testPermission(permission, privilegeLevels) { }) describe('when anonymous', function () { - beforeEach(function () { - this.user = null + beforeEach(function (ctx) { + ctx.user = null }) describe('with read-write access through a token', function () { @@ -815,36 +856,39 @@ function testPermission(permission, privilegeLevels) { } function setupUserPrivilegeLevel(privilegeLevel) { - beforeEach(`set user privilege level to ${privilegeLevel}`, function () { - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(function (ctx) { + ctx.annotate(`set user privilege level to ${privilegeLevel}`) + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ publicAccessLevel: sinon.stub().returns(PublicAccessLevels.PRIVATE), privilegeLevelForUser: sinon .stub() - .withArgs(this.user._id) + .withArgs(ctx.user._id) .returns(privilegeLevel), }) }) } function setupPublicAccessLevel(level) { - beforeEach(`set public access level to ${level}`, function () { - this.project.publicAccesLevel = level - this.CollaboratorsGetter.promises.getProjectAccess - .withArgs(this.project._id) + beforeEach(function (ctx) { + ctx.annotate(`set public access level to ${level}`) + ctx.project.publicAccesLevel = level + ctx.CollaboratorsGetter.promises.getProjectAccess + .withArgs(ctx.project._id) .resolves({ - publicAccessLevel: sinon.stub().returns(this.project.publicAccesLevel), + publicAccessLevel: sinon.stub().returns(ctx.project.publicAccesLevel), privilegeLevelForUser: sinon.stub().returns(PrivilegeLevels.NONE), }) }) } function setupTokenAccessLevel(level) { - beforeEach(`set token access level to ${level}`, function () { - this.project.publicAccesLevel = PublicAccessLevels.TOKEN_BASED - this.TokenAccessHandler.promises.validateTokenForAnonymousAccess - .withArgs(this.project._id, this.token) + beforeEach(function (ctx) { + ctx.annotate(`set token access level to ${level}`) + ctx.project.publicAccesLevel = PublicAccessLevels.TOKEN_BASED + ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess + .withArgs(ctx.project._id, ctx.token) .resolves({ isValidReadAndWrite: level === 'readAndWrite', isValidReadOnly: level === 'readOnly', @@ -853,11 +897,11 @@ function setupTokenAccessLevel(level) { } function expectPermission(permission, expectedValue) { - it(`should return ${expectedValue}`, async function () { - const value = await this.AuthorizationManager.promises[permission]( - this.user && this.user._id, - this.project._id, - this.token + it(`should return ${expectedValue}`, async function (ctx) { + const value = await ctx.AuthorizationManager.promises[permission]( + ctx.user && ctx.user._id, + ctx.project._id, + ctx.token ) expect(value).to.equal(expectedValue) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs index 0d0b320154..c027b0b834 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs @@ -476,13 +476,10 @@ describe('CollaboratorsController', function () { }) it('returns 204 on success', async function (ctx) { - await new Promise(resolve => { - ctx.res.sendStatus = status => { - expect(status).to.equal(204) - resolve() - } - ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) - }) + ctx.res.sendStatus = vi.fn() + + await ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) + expect(ctx.res.sendStatus).toHaveBeenCalledWith(204) }) it('returns 404 if the project does not exist', async function (ctx) { diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs index f5ac06fb58..02fd445098 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs @@ -1,46 +1,53 @@ -const Path = require('path') -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') -const { Project } = require('../helpers/models/Project') -const Errors = require('../../../../app/src/Features/Errors/Errors') +import { vi, expect } from 'vitest' +import Path from 'path' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' +import Errors from '../../../../app/src/Features/Errors/Errors.js' + +const { Project } = indirectlyImportModels(['Project']) + +const { ObjectId } = mongodb + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) const MODULE_PATH = Path.join( - __dirname, + import.meta.dirname, '../../../../app/src/Features/Collaborators/CollaboratorsGetter' ) describe('CollaboratorsGetter', function () { - beforeEach(function () { - this.userId = 'efb93a186e9a06f15fea5abd' - this.ownerRef = new ObjectId() - this.readOnlyRef1 = new ObjectId() - this.readOnlyRef2 = new ObjectId() - this.pendingEditorRef = new ObjectId() - this.pendingReviewerRef = new ObjectId() - this.readWriteRef1 = new ObjectId() - this.readWriteRef2 = new ObjectId() - this.reviewer1Ref = new ObjectId() - this.reviewer2Ref = new ObjectId() - this.readOnlyTokenRef = new ObjectId() - this.readWriteTokenRef = new ObjectId() - this.nonMemberRef = new ObjectId() - this.project = { + beforeEach(async function (ctx) { + ctx.userId = 'efb93a186e9a06f15fea5abd' + ctx.ownerRef = new ObjectId() + ctx.readOnlyRef1 = new ObjectId() + ctx.readOnlyRef2 = new ObjectId() + ctx.pendingEditorRef = new ObjectId() + ctx.pendingReviewerRef = new ObjectId() + ctx.readWriteRef1 = new ObjectId() + ctx.readWriteRef2 = new ObjectId() + ctx.reviewer1Ref = new ObjectId() + ctx.reviewer2Ref = new ObjectId() + ctx.readOnlyTokenRef = new ObjectId() + ctx.readWriteTokenRef = new ObjectId() + ctx.nonMemberRef = new ObjectId() + ctx.project = { _id: new ObjectId(), - owner_ref: [this.ownerRef], + owner_ref: [ctx.ownerRef], readOnly_refs: [ - this.readOnlyRef1, - this.readOnlyRef2, - this.pendingEditorRef, - this.pendingReviewerRef, + ctx.readOnlyRef1, + ctx.readOnlyRef2, + ctx.pendingEditorRef, + ctx.pendingReviewerRef, ], - pendingEditor_refs: [this.pendingEditorRef], - pendingReviewer_refs: [this.pendingReviewerRef], - collaberator_refs: [this.readWriteRef1, this.readWriteRef2], - reviewer_refs: [this.reviewer1Ref, this.reviewer2Ref], - tokenAccessReadAndWrite_refs: [this.readWriteTokenRef], - tokenAccessReadOnly_refs: [this.readOnlyTokenRef], + pendingEditor_refs: [ctx.pendingEditorRef], + pendingReviewer_refs: [ctx.pendingReviewerRef], + collaberator_refs: [ctx.readWriteRef1, ctx.readWriteRef2], + reviewer_refs: [ctx.reviewer1Ref, ctx.reviewer2Ref], + tokenAccessReadAndWrite_refs: [ctx.readWriteTokenRef], + tokenAccessReadOnly_refs: [ctx.readOnlyTokenRef], publicAccesLevel: 'tokenBased', tokens: { readOnly: 'ro', @@ -49,98 +56,114 @@ describe('CollaboratorsGetter', function () { }, } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(null), getUsers: sinon.stub().resolves([]), }, } - this.ProjectMock = sinon.mock(Project) - this.ProjectGetter = { + ctx.ProjectMock = sinon.mock(Project) + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.ProjectEditorHandler = { + ctx.ProjectEditorHandler = { buildUserModelView: sinon.stub(), } - this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, { - requires: { - 'mongodb-legacy': { ObjectId }, - '../User/UserGetter': this.UserGetter, - '../../models/Project': { Project }, - '../Project/ProjectGetter': this.ProjectGetter, - '../Project/ProjectEditorHandler': this.ProjectEditorHandler, - }, - }) + + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEditorHandler', + () => ({ + default: ctx.ProjectEditorHandler, + }) + ) + + ctx.CollaboratorsGetter = (await import(MODULE_PATH)).default }) - afterEach(function () { - this.ProjectMock.verify() + afterEach(function (ctx) { + ctx.ProjectMock.verify() }) describe('getMemberIdsWithPrivilegeLevels', function () { describe('with project', function () { - it('should return an array of member ids with their privilege levels', async function () { + it('should return an array of member ids with their privilege levels', async function (ctx) { const result = - await this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( - this.project._id + await ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( + ctx.project._id ) expect(result).to.have.deep.members([ { - id: this.ownerRef.toString(), + id: ctx.ownerRef.toString(), privilegeLevel: 'owner', source: 'owner', }, { - id: this.readWriteRef1.toString(), + id: ctx.readWriteRef1.toString(), privilegeLevel: 'readAndWrite', source: 'invite', }, { - id: this.readWriteRef2.toString(), + id: ctx.readWriteRef2.toString(), privilegeLevel: 'readAndWrite', source: 'invite', }, { - id: this.readOnlyRef1.toString(), + id: ctx.readOnlyRef1.toString(), privilegeLevel: 'readOnly', source: 'invite', }, { - id: this.readOnlyRef2.toString(), + id: ctx.readOnlyRef2.toString(), privilegeLevel: 'readOnly', source: 'invite', }, { - id: this.pendingEditorRef.toString(), + id: ctx.pendingEditorRef.toString(), privilegeLevel: 'readOnly', source: 'invite', pendingEditor: true, }, { - id: this.pendingReviewerRef.toString(), + id: ctx.pendingReviewerRef.toString(), privilegeLevel: 'readOnly', source: 'invite', pendingReviewer: true, }, { - id: this.readOnlyTokenRef.toString(), + id: ctx.readOnlyTokenRef.toString(), privilegeLevel: 'readOnly', source: 'token', }, { - id: this.readWriteTokenRef.toString(), + id: ctx.readWriteTokenRef.toString(), privilegeLevel: 'readAndWrite', source: 'token', }, { - id: this.reviewer1Ref.toString(), + id: ctx.reviewer1Ref.toString(), privilegeLevel: 'review', source: 'invite', }, { - id: this.reviewer2Ref.toString(), + id: ctx.reviewer2Ref.toString(), privilegeLevel: 'review', source: 'invite', }, @@ -149,14 +172,14 @@ describe('CollaboratorsGetter', function () { }) describe('with a missing project', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject.resolves(null) + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(null) }) - it('should return a NotFoundError', async function () { + it('should return a NotFoundError', async function (ctx) { await expect( - this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( - this.project._id + ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( + ctx.project._id ) ).to.be.rejectedWith(Errors.NotFoundError) }) @@ -164,89 +187,89 @@ describe('CollaboratorsGetter', function () { }) describe('getMemberIds', function () { - it('should return the ids', async function () { - const memberIds = await this.CollaboratorsGetter.promises.getMemberIds( - this.project._id + it('should return the ids', async function (ctx) { + const memberIds = await ctx.CollaboratorsGetter.promises.getMemberIds( + ctx.project._id ) expect(memberIds).to.have.members([ - this.ownerRef.toString(), - this.readOnlyRef1.toString(), - this.readOnlyRef2.toString(), - this.readWriteRef1.toString(), - this.readWriteRef2.toString(), - this.pendingEditorRef.toString(), - this.pendingReviewerRef.toString(), - this.readWriteTokenRef.toString(), - this.readOnlyTokenRef.toString(), - this.reviewer1Ref.toString(), - this.reviewer2Ref.toString(), + ctx.ownerRef.toString(), + ctx.readOnlyRef1.toString(), + ctx.readOnlyRef2.toString(), + ctx.readWriteRef1.toString(), + ctx.readWriteRef2.toString(), + ctx.pendingEditorRef.toString(), + ctx.pendingReviewerRef.toString(), + ctx.readWriteTokenRef.toString(), + ctx.readOnlyTokenRef.toString(), + ctx.reviewer1Ref.toString(), + ctx.reviewer2Ref.toString(), ]) }) }) describe('getInvitedMemberIds', function () { - it('should return the invited ids', async function () { + it('should return the invited ids', async function (ctx) { const memberIds = - await this.CollaboratorsGetter.promises.getInvitedMemberIds( - this.project._id + await ctx.CollaboratorsGetter.promises.getInvitedMemberIds( + ctx.project._id ) expect(memberIds).to.have.members([ - this.ownerRef.toString(), - this.readOnlyRef1.toString(), - this.readOnlyRef2.toString(), - this.readWriteRef1.toString(), - this.readWriteRef2.toString(), - this.pendingEditorRef.toString(), - this.pendingReviewerRef.toString(), - this.reviewer1Ref.toString(), - this.reviewer2Ref.toString(), + ctx.ownerRef.toString(), + ctx.readOnlyRef1.toString(), + ctx.readOnlyRef2.toString(), + ctx.readWriteRef1.toString(), + ctx.readWriteRef2.toString(), + ctx.pendingEditorRef.toString(), + ctx.pendingReviewerRef.toString(), + ctx.reviewer1Ref.toString(), + ctx.reviewer2Ref.toString(), ]) }) }) describe('getMemberIdPrivilegeLevel', function () { - it('should return the privilege level if it exists', async function () { + it('should return the privilege level if it exists', async function (ctx) { const level = - await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( - this.readOnlyRef1, - this.project._id + await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + ctx.readOnlyRef1, + ctx.project._id ) expect(level).to.equal('readOnly') }) - it('should return review privilege level', async function () { + it('should return review privilege level', async function (ctx) { const level = - await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( - this.reviewer1Ref, - this.project._id + await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + ctx.reviewer1Ref, + ctx.project._id ) expect(level).to.equal('review') }) - it('should return false if the member has no privilege level', async function () { + it('should return false if the member has no privilege level', async function (ctx) { const level = - await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( - this.nonMemberRef, - this.project._id + await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + ctx.nonMemberRef, + ctx.project._id ) expect(level).to.equal(false) }) - it('should return review privilege level when user is both reviewer and token member', async function () { + it('should return review privilege level when user is both reviewer and token member', async function (ctx) { const userWhoIsBothReviewerAndToken = new ObjectId() const projectWithDuplicateUser = { - ...this.project, + ...ctx.project, reviewer_refs: [userWhoIsBothReviewerAndToken], tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken], } - this.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser) + ctx.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser) const level = - await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( userWhoIsBothReviewerAndToken, - this.project._id + ctx.project._id ) expect(level).to.equal('review') @@ -255,20 +278,20 @@ describe('CollaboratorsGetter', function () { describe('isUserInvitedMemberOfProject', function () { describe('when user is a member of the project', function () { - it('should return true and the privilegeLevel', async function () { + it('should return true and the privilegeLevel', async function (ctx) { const isMember = - await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( - this.readOnlyRef1 + await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( + ctx.readOnlyRef1 ) expect(isMember).to.equal(true) }) }) describe('when user is not a member of the project', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const isMember = - await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( - this.nonMemberRef + await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( + ctx.nonMemberRef ) expect(isMember).to.equal(false) }) @@ -277,30 +300,30 @@ describe('CollaboratorsGetter', function () { describe('isUserInvitedReadWriteMemberOfProject', function () { describe('when user is a read write member of the project', function () { - it('should return true', async function () { + it('should return true', async function (ctx) { const isMember = - await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( - this.readWriteRef1 + await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( + ctx.readWriteRef1 ) expect(isMember).to.equal(true) }) }) describe('when user is a read only member of the project', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const isMember = - await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( - this.readOnlyRef1 + await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( + ctx.readOnlyRef1 ) expect(isMember).to.equal(false) }) }) describe('when user is not a member of the project', function () { - it('should return false', async function () { + it('should return false', async function (ctx) { const isMember = - await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( - this.nonMemberRef + await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( + ctx.nonMemberRef ) expect(isMember).to.equal(false) }) @@ -308,42 +331,42 @@ describe('CollaboratorsGetter', function () { }) describe('getProjectsUserIsMemberOf', function () { - beforeEach(function () { - this.fields = 'mock fields' - this.ProjectMock.expects('find') - .withArgs({ collaberator_refs: this.userId }, this.fields) + beforeEach(function (ctx) { + ctx.fields = 'mock fields' + ctx.ProjectMock.expects('find') + .withArgs({ collaberator_refs: ctx.userId }, ctx.fields) .chain('exec') .resolves(['mock-read-write-project-1', 'mock-read-write-project-2']) - this.ProjectMock.expects('find') - .withArgs({ readOnly_refs: this.userId }, this.fields) + ctx.ProjectMock.expects('find') + .withArgs({ readOnly_refs: ctx.userId }, ctx.fields) .chain('exec') .resolves(['mock-read-only-project-1', 'mock-read-only-project-2']) - this.ProjectMock.expects('find') - .withArgs({ reviewer_refs: this.userId }, this.fields) + ctx.ProjectMock.expects('find') + .withArgs({ reviewer_refs: ctx.userId }, ctx.fields) .chain('exec') .resolves(['mock-review-project-1', 'mock-review-project-2']) - this.ProjectMock.expects('find') + ctx.ProjectMock.expects('find') .withArgs( { - tokenAccessReadAndWrite_refs: this.userId, + tokenAccessReadAndWrite_refs: ctx.userId, publicAccesLevel: 'tokenBased', }, - this.fields + ctx.fields ) .chain('exec') .resolves([ 'mock-token-read-write-project-1', 'mock-token-read-write-project-2', ]) - this.ProjectMock.expects('find') + ctx.ProjectMock.expects('find') .withArgs( { - tokenAccessReadOnly_refs: this.userId, + tokenAccessReadOnly_refs: ctx.userId, publicAccesLevel: 'tokenBased', }, - this.fields + ctx.fields ) .chain('exec') .resolves([ @@ -352,11 +375,11 @@ describe('CollaboratorsGetter', function () { ]) }) - it('should call the callback with the projects', async function () { + it('should call the callback with the projects', async function (ctx) { const projects = - await this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf( - this.userId, - this.fields + await ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf( + ctx.userId, + ctx.fields ) expect(projects).to.deep.equal({ readAndWrite: [ @@ -378,101 +401,98 @@ describe('CollaboratorsGetter', function () { }) describe('getAllInvitedMembers', function () { - beforeEach(async function () { - this.owningUser = { - _id: this.ownerRef, + beforeEach(async function (ctx) { + ctx.owningUser = { + _id: ctx.ownerRef, email: 'owner@example.com', features: { a: 1 }, } - this.readWriteUser = { - _id: this.readWriteRef1, + ctx.readWriteUser = { + _id: ctx.readWriteRef1, email: 'readwrite@example.com', } - this.reviewUser = { - _id: this.reviewer1Ref, + ctx.reviewUser = { + _id: ctx.reviewer1Ref, email: 'review@example.com', } - this.members = [ - { user: this.owningUser, privilegeLevel: 'owner' }, - { user: this.readWriteUser, privilegeLevel: 'readAndWrite' }, - { user: this.reviewUser, privilegeLevel: 'review' }, + ctx.members = [ + { user: ctx.owningUser, privilegeLevel: 'owner' }, + { user: ctx.readWriteUser, privilegeLevel: 'readAndWrite' }, + { user: ctx.reviewUser, privilegeLevel: 'review' }, ] - this.memberViews = [ - { _id: this.readWriteUser._id, email: this.readWriteUser.email }, - { _id: this.reviewUser._id, email: this.reviewUser.email }, + ctx.memberViews = [ + { _id: ctx.readWriteUser._id, email: ctx.readWriteUser.email }, + { _id: ctx.reviewUser._id, email: ctx.reviewUser.email }, ] - this.UserGetter.promises.getUsers.resolves([ - this.owningUser, - this.readWriteUser, - this.reviewUser, + ctx.UserGetter.promises.getUsers.resolves([ + ctx.owningUser, + ctx.readWriteUser, + ctx.reviewUser, ]) - this.ProjectEditorHandler.buildUserModelView - .withArgs(this.members[1]) - .returns(this.memberViews[0]) - this.ProjectEditorHandler.buildUserModelView - .withArgs(this.members[2]) - .returns(this.memberViews[1]) - this.result = - await this.CollaboratorsGetter.promises.getAllInvitedMembers( - this.project._id - ) + ctx.ProjectEditorHandler.buildUserModelView + .withArgs(ctx.members[1]) + .returns(ctx.memberViews[0]) + ctx.ProjectEditorHandler.buildUserModelView + .withArgs(ctx.members[2]) + .returns(ctx.memberViews[1]) + ctx.result = await ctx.CollaboratorsGetter.promises.getAllInvitedMembers( + ctx.project._id + ) }) - it('should produce a list of members', function () { - expect(this.result).to.deep.equal(this.memberViews) + it('should produce a list of members', function (ctx) { + expect(ctx.result).to.deep.equal(ctx.memberViews) }) - it('should call ProjectEditorHandler.buildUserModelView', function () { - expect(this.ProjectEditorHandler.buildUserModelView).to.have.been + it('should call ProjectEditorHandler.buildUserModelView', function (ctx) { + expect(ctx.ProjectEditorHandler.buildUserModelView).to.have.been .calledTwice expect( - this.ProjectEditorHandler.buildUserModelView - ).to.have.been.calledWith(this.members[1]) + ctx.ProjectEditorHandler.buildUserModelView + ).to.have.been.calledWith(ctx.members[1]) expect( - this.ProjectEditorHandler.buildUserModelView - ).to.have.been.calledWith(this.members[2]) + ctx.ProjectEditorHandler.buildUserModelView + ).to.have.been.calledWith(ctx.members[2]) }) }) describe('userIsTokenMember', function () { - it('should return true when the project is found', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(this.project) - const isMember = - await this.CollaboratorsGetter.promises.userIsTokenMember( - this.userId, - this.project._id - ) + it('should return true when the project is found', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project) + const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember( + ctx.userId, + ctx.project._id + ) expect(isMember).to.be.true }) - it('should return false when the project is not found', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(null) - const isMember = - await this.CollaboratorsGetter.promises.userIsTokenMember( - this.userId, - this.project._id - ) + it('should return false when the project is not found', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(null) + const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember( + ctx.userId, + ctx.project._id + ) expect(isMember).to.be.false }) }) describe('userIsReadWriteTokenMember', function () { - it('should return true when the project is found', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(this.project) + it('should return true when the project is found', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project) const isMember = - await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember( - this.userId, - this.project._id + await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember( + ctx.userId, + ctx.project._id ) expect(isMember).to.be.true }) - it('should return false when the project is not found', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(null) + it('should return false when the project is not found', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(null) const isMember = - await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember( - this.userId, - this.project._id + await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember( + ctx.userId, + ctx.project._id ) expect(isMember).to.be.false }) @@ -481,53 +501,53 @@ describe('CollaboratorsGetter', function () { describe('getPublicShareTokens', function () { const userMock = new ObjectId() - it('should return null when the project is not found', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(undefined) + it('should return null when the project is not found', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(undefined) const tokens = - await this.CollaboratorsGetter.promises.getPublicShareTokens( + await ctx.CollaboratorsGetter.promises.getPublicShareTokens( userMock, - this.project._id + ctx.project._id ) expect(tokens).to.be.null }) - it('should return an empty object when the user is not owner or read-only collaborator', async function () { - this.ProjectMock.expects('findOne').chain('exec').resolves(this.project) + it('should return an empty object when the user is not owner or read-only collaborator', async function (ctx) { + ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project) const tokens = - await this.CollaboratorsGetter.promises.getPublicShareTokens( + await ctx.CollaboratorsGetter.promises.getPublicShareTokens( userMock, - this.project._id + ctx.project._id ) expect(tokens).to.deep.equal({}) }) describe('when the user is a read-only token collaborator', function () { - it('should return the read-only token', async function () { - this.ProjectMock.expects('findOne') + it('should return the read-only token', async function (ctx) { + ctx.ProjectMock.expects('findOne') .chain('exec') - .resolves({ hasTokenReadOnlyAccess: true, ...this.project }) + .resolves({ hasTokenReadOnlyAccess: true, ...ctx.project }) const tokens = - await this.CollaboratorsGetter.promises.getPublicShareTokens( + await ctx.CollaboratorsGetter.promises.getPublicShareTokens( userMock, - this.project._id + ctx.project._id ) expect(tokens).to.deep.equal({ readOnly: tokens.readOnly }) }) }) describe('when the user is the owner of the project', function () { - beforeEach(function () { - this.ProjectMock.expects('findOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('findOne') .chain('exec') - .resolves({ isOwner: true, ...this.project }) + .resolves({ isOwner: true, ...ctx.project }) }) - it('should return all the tokens', async function () { + it('should return all the tokens', async function (ctx) { const tokens = - await this.CollaboratorsGetter.promises.getPublicShareTokens( + await ctx.CollaboratorsGetter.promises.getPublicShareTokens( userMock, - this.project._id + ctx.project._id ) expect(tokens).to.deep.equal(tokens) }) @@ -535,20 +555,20 @@ describe('CollaboratorsGetter', function () { }) describe('getInvitedEditCollaboratorCount', function () { - it('should return the count of invited edit collaborators (readAndWrite, review)', async function () { + it('should return the count of invited edit collaborators (readAndWrite, review)', async function (ctx) { const count = - await this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount( - this.project._id + await ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount( + ctx.project._id ) expect(count).to.equal(4) }) }) describe('getInvitedPendingEditorCount', function () { - it('should return the count of pending editors and reviewers', async function () { + it('should return the count of pending editors and reviewers', async function (ctx) { const count = - await this.CollaboratorsGetter.promises.getInvitedPendingEditorCount( - this.project._id + await ctx.CollaboratorsGetter.promises.getInvitedPendingEditorCount( + ctx.project._id ) expect(count).to.equal(2) }) @@ -556,11 +576,11 @@ describe('CollaboratorsGetter', function () { describe('ProjectAccess', function () { describe('privilegeLevelForUser', function () { - it('should return reviewer privilege when user is both reviewer and token member', function () { + it('should return reviewer privilege when user is both reviewer and token member', function (ctx) { const userWhoIsBothReviewerAndToken = new ObjectId() const projectWithDuplicateUser = { - owner_ref: this.ownerRef, + owner_ref: ctx.ownerRef, collaberator_refs: [], readOnly_refs: [], tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken], @@ -571,7 +591,7 @@ describe('CollaboratorsGetter', function () { pendingReviewer_refs: [], } - const projectAccess = new this.CollaboratorsGetter.ProjectAccess( + const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess( projectWithDuplicateUser ) const privilegeLevel = projectAccess.privilegeLevelForUser( @@ -581,11 +601,11 @@ describe('CollaboratorsGetter', function () { expect(privilegeLevel).to.equal('review') }) - it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function () { + it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function (ctx) { const userWhoIsBothReadOnlyAndTokenRW = new ObjectId() const projectWithDuplicateUser = { - owner_ref: this.ownerRef, + owner_ref: ctx.ownerRef, collaberator_refs: [], readOnly_refs: [userWhoIsBothReadOnlyAndTokenRW], tokenAccessReadAndWrite_refs: [userWhoIsBothReadOnlyAndTokenRW], @@ -596,7 +616,7 @@ describe('CollaboratorsGetter', function () { pendingReviewer_refs: [], } - const projectAccess = new this.CollaboratorsGetter.ProjectAccess( + const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess( projectWithDuplicateUser ) const privilegeLevel = projectAccess.privilegeLevelForUser( @@ -607,12 +627,12 @@ describe('CollaboratorsGetter', function () { expect(privilegeLevel).to.equal('readOnly') }) - it('should return none for non-members', function () { - const projectAccess = new this.CollaboratorsGetter.ProjectAccess( - this.project + it('should return none for non-members', function (ctx) { + const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess( + ctx.project ) const privilegeLevel = projectAccess.privilegeLevelForUser( - this.nonMemberRef + ctx.nonMemberRef ) expect(privilegeLevel).to.equal(false) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs index c97e4f2fe8..f3f924125e 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs @@ -1,106 +1,144 @@ -const { promisify } = require('util') -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const sinon = require('sinon') -const { expect } = require('chai') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const { Project } = require('../helpers/models/Project') -const { ObjectId } = require('mongodb-legacy') +import { vi, expect } from 'vitest' +import path from 'path' +import sinon from 'sinon' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' +import mongodb from 'mongodb-legacy' +import { setTimeout } from 'node:timers/promises' +const { ObjectId } = mongodb +const { Project } = indirectlyImportModels(['Project']) +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) const MODULE_PATH = path.join( - __dirname, + import.meta.dirname, '../../../../app/src/Features/Collaborators/CollaboratorsHandler' ) -const sleep = promisify(setTimeout) +const sleep = setTimeout describe('CollaboratorsHandler', function () { - beforeEach(function () { - this.userId = new ObjectId() - this.addingUserId = new ObjectId() - this.project = { + beforeEach(async function (ctx) { + ctx.userId = new ObjectId() + ctx.addingUserId = new ObjectId() + ctx.project = { _id: new ObjectId(), - owner_ref: this.addingUserId, + owner_ref: ctx.addingUserId, name: 'Foo', } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(null), }, } - this.ContactManager = { + ctx.ContactManager = { addContact: sinon.stub(), } - this.ProjectMock = sinon.mock(Project) - this.TpdsProjectFlusher = { + ctx.ProjectMock = sinon.mock(Project) + ctx.TpdsProjectFlusher = { promises: { flushProjectToTpds: sinon.stub().resolves(), }, } - this.TpdsUpdateSender = { + ctx.TpdsUpdateSender = { promises: { createProject: sinon.stub().resolves(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { dangerouslyGetAllProjectsUserIsMemberOf: sinon.stub(), getMemberIdsWithPrivilegeLevels: sinon.stub().resolves([]), }, } - this.EditorRealTimeController = { emitToRoom: sinon.stub() } - this.CollaboratorsHandler = SandboxedModule.require(MODULE_PATH, { - requires: { - '../User/UserGetter': this.UserGetter, - '../Contacts/ContactManager': this.ContactManager, - '../../models/Project': { Project }, - '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher, - '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender, - '../Project/ProjectGetter': this.ProjectGetter, - '../Editor/EditorRealTimeController': this.EditorRealTimeController, - './CollaboratorsGetter': this.CollaboratorsGetter, - }, - }) + ctx.EditorRealTimeController = { emitToRoom: sinon.stub() } + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Contacts/ContactManager', () => ({ + default: ctx.ContactManager, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher', + () => ({ + default: ctx.TpdsProjectFlusher, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender', + () => ({ + default: ctx.TpdsUpdateSender, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + ctx.CollaboratorsHandler = (await import(MODULE_PATH)).default }) - afterEach(function () { - this.ProjectMock.verify() + afterEach(function (ctx) { + ctx.ProjectMock.verify() }) describe('removeUserFromProject', function () { describe('a non-archived project', function () { - it('should remove the user from mongo', async function () { - this.ProjectMock.expects('updateOne') + it('should remove the user from mongo', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { $pull: { - collaberator_refs: this.userId, - reviewer_refs: this.userId, - readOnly_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, - tokenAccessReadOnly_refs: this.userId, - tokenAccessReadAndWrite_refs: this.userId, - archived: this.userId, - trashed: this.userId, + collaberator_refs: ctx.userId, + reviewer_refs: ctx.userId, + readOnly_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, + tokenAccessReadOnly_refs: ctx.userId, + tokenAccessReadAndWrite_refs: ctx.userId, + archived: ctx.userId, + trashed: ctx.userId, }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.removeUserFromProject( - this.project._id, - this.userId + await ctx.CollaboratorsHandler.promises.removeUserFromProject( + ctx.project._id, + ctx.userId ) }) }) @@ -108,70 +146,70 @@ describe('CollaboratorsHandler', function () { describe('addUserIdToProject', function () { describe('as readOnly', function () { - beforeEach(async function () { - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { - $addToSet: { readOnly_refs: this.userId }, + $addToSet: { readOnly_refs: ctx.userId }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readOnly' ) }) - it('should create the project folder in dropbox', function () { + it('should create the project folder in dropbox', function (ctx) { expect( - this.TpdsUpdateSender.promises.createProject + ctx.TpdsUpdateSender.promises.createProject ).to.have.been.calledWith({ - projectId: this.project._id, - projectName: this.project.name, - ownerId: this.addingUserId, - userId: this.userId, + projectId: ctx.project._id, + projectName: ctx.project.name, + ownerId: ctx.addingUserId, + userId: ctx.userId, }) }) - it('should flush the project to the TPDS', function () { + it('should flush the project to the TPDS', function (ctx) { expect( - this.TpdsProjectFlusher.promises.flushProjectToTpds - ).to.have.been.calledWith(this.project._id) + ctx.TpdsProjectFlusher.promises.flushProjectToTpds + ).to.have.been.calledWith(ctx.project._id) }) - it('should add the user as a contact for the adding user', function () { - expect(this.ContactManager.addContact).to.have.been.calledWith( - this.addingUserId, - this.userId + it('should add the user as a contact for the adding user', function (ctx) { + expect(ctx.ContactManager.addContact).to.have.been.calledWith( + ctx.addingUserId, + ctx.userId ) }) describe('and with pendingEditor flag', function () { - it('should add them to the pending editor refs', async function () { - this.ProjectMock.expects('updateOne') + it('should add them to the pending editor refs', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { $addToSet: { - readOnly_refs: this.userId, - pendingEditor_refs: this.userId, + readOnly_refs: ctx.userId, + pendingEditor_refs: ctx.userId, }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readOnly', { pendingEditor: true } ) @@ -179,25 +217,25 @@ describe('CollaboratorsHandler', function () { }) describe('with pendingReviewer flag', function () { - it('should add them to the pending reviewer refs', async function () { - this.ProjectMock.expects('updateOne') + it('should add them to the pending reviewer refs', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { $addToSet: { - readOnly_refs: this.userId, - pendingReviewer_refs: this.userId, + readOnly_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readOnly', { pendingReviewer: true } ) @@ -206,77 +244,77 @@ describe('CollaboratorsHandler', function () { }) describe('as readAndWrite', function () { - beforeEach(async function () { - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { - $addToSet: { collaberator_refs: this.userId }, + $addToSet: { collaberator_refs: ctx.userId }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readAndWrite' ) }) - it('should flush the project to the TPDS', function () { + it('should flush the project to the TPDS', function (ctx) { expect( - this.TpdsProjectFlusher.promises.flushProjectToTpds - ).to.have.been.calledWith(this.project._id) + ctx.TpdsProjectFlusher.promises.flushProjectToTpds + ).to.have.been.calledWith(ctx.project._id) }) }) describe('as reviewer', function () { - beforeEach(async function () { - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { - track_changes: { [this.userId]: true }, - $addToSet: { reviewer_refs: this.userId }, + track_changes: { [ctx.userId]: true }, + $addToSet: { reviewer_refs: ctx.userId }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'review' ) }) - it('should update the client with new track changes settings', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project._id, 'toggle-track-changes', { - [this.userId]: true, + it('should update the client with new track changes settings', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project._id, 'toggle-track-changes', { + [ctx.userId]: true, }) .should.equal(true) }) - it('should flush the project to the TPDS', function () { + it('should flush the project to the TPDS', function (ctx) { expect( - this.TpdsProjectFlusher.promises.flushProjectToTpds - ).to.have.been.calledWith(this.project._id) + ctx.TpdsProjectFlusher.promises.flushProjectToTpds + ).to.have.been.calledWith(ctx.project._id) }) }) describe('with invalid privilegeLevel', function () { - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { await expect( - this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'notValid' ) ).to.be.rejected @@ -284,15 +322,15 @@ describe('CollaboratorsHandler', function () { }) describe('when user already exists as a collaborator', function () { - beforeEach(function () { - this.project.collaberator_refs = [this.userId] + beforeEach(function (ctx) { + ctx.project.collaberator_refs = [ctx.userId] }) - it('should not add the user again', async function () { - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + it('should not add the user again', async function (ctx) { + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readAndWrite' ) // Project.updateOne() should not be called. If it is, it will fail because @@ -301,71 +339,71 @@ describe('CollaboratorsHandler', function () { }) describe('when user already exists as a reviewer', function () { - beforeEach(function () { - this.project.collaberator_refs = [] - this.project.reviewer_refs = [this.userId] - this.project.readOnly_refs = [] + beforeEach(function (ctx) { + ctx.project.collaberator_refs = [] + ctx.project.reviewer_refs = [ctx.userId] + ctx.project.readOnly_refs = [] }) - it('should not add the user again', async function () { - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + it('should not add the user again', async function (ctx) { + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readAndWrite' ) }) }) describe('when user already exists as a read-only user', function () { - beforeEach(function () { - this.project.collaberator_refs = [] - this.project.reviewer_refs = [] - this.project.readOnly_refs = [this.userId] + beforeEach(function (ctx) { + ctx.project.collaberator_refs = [] + ctx.project.reviewer_refs = [] + ctx.project.readOnly_refs = [ctx.userId] }) - it('should not add the user again', async function () { - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, - this.addingUserId, - this.userId, + it('should not add the user again', async function (ctx) { + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, + ctx.addingUserId, + ctx.userId, 'readAndWrite' ) }) }) describe('with null addingUserId', function () { - beforeEach(async function () { - this.project.collaberator_refs = [] - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.project.collaberator_refs = [] + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { - $addToSet: { collaberator_refs: this.userId }, + $addToSet: { collaberator_refs: ctx.userId }, } ) .chain('exec') .resolves() - await this.CollaboratorsHandler.promises.addUserIdToProject( - this.project._id, + await ctx.CollaboratorsHandler.promises.addUserIdToProject( + ctx.project._id, null, - this.userId, + ctx.userId, 'readAndWrite' ) }) - it('should not add the adding user as a contact', function () { - expect(this.ContactManager.addContact).not.to.have.been.called + it('should not add the adding user as a contact', function (ctx) { + expect(ctx.ContactManager.addContact).not.to.have.been.called }) }) }) describe('removeUserFromAllProjects', function () { - it('should remove the user from each project', async function () { - this.CollaboratorsGetter.promises.dangerouslyGetAllProjectsUserIsMemberOf - .withArgs(this.userId, { _id: 1 }) + it('should remove the user from each project', async function (ctx) { + ctx.CollaboratorsGetter.promises.dangerouslyGetAllProjectsUserIsMemberOf + .withArgs(ctx.userId, { _id: 1 }) .resolves({ readAndWrite: [ { _id: 'read-and-write-0' }, @@ -392,38 +430,38 @@ describe('CollaboratorsHandler', function () { 'token-read-only-1', ] for (const projectId of expectedProjects) { - this.ProjectMock.expects('updateOne') + ctx.ProjectMock.expects('updateOne') .withArgs( { _id: projectId, }, { $pull: { - collaberator_refs: this.userId, - reviewer_refs: this.userId, - readOnly_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, - tokenAccessReadOnly_refs: this.userId, - tokenAccessReadAndWrite_refs: this.userId, - archived: this.userId, - trashed: this.userId, + collaberator_refs: ctx.userId, + reviewer_refs: ctx.userId, + readOnly_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, + tokenAccessReadOnly_refs: ctx.userId, + tokenAccessReadAndWrite_refs: ctx.userId, + archived: ctx.userId, + trashed: ctx.userId, }, } ) .resolves() } - await this.CollaboratorsHandler.promises.removeUserFromAllProjects( - this.userId + await ctx.CollaboratorsHandler.promises.removeUserFromAllProjects( + ctx.userId ) }) }) describe('transferProjects', function () { - beforeEach(function () { - this.fromUserId = new ObjectId() - this.toUserId = new ObjectId() - this.projects = [ + beforeEach(function (ctx) { + ctx.fromUserId = new ObjectId() + ctx.toUserId = new ObjectId() + ctx.projects = [ { _id: new ObjectId(), }, @@ -431,91 +469,91 @@ describe('CollaboratorsHandler', function () { _id: new ObjectId(), }, ] - this.ProjectMock.expects('find') + ctx.ProjectMock.expects('find') .withArgs({ $or: [ - { owner_ref: this.fromUserId }, - { collaberator_refs: this.fromUserId }, - { readOnly_refs: this.fromUserId }, + { owner_ref: ctx.fromUserId }, + { collaberator_refs: ctx.fromUserId }, + { readOnly_refs: ctx.fromUserId }, ], }) .chain('exec') - .resolves(this.projects) - this.ProjectMock.expects('updateMany') + .resolves(ctx.projects) + ctx.ProjectMock.expects('updateMany') .withArgs( - { owner_ref: this.fromUserId }, - { $set: { owner_ref: this.toUserId } } + { owner_ref: ctx.fromUserId }, + { $set: { owner_ref: ctx.toUserId } } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { collaberator_refs: this.fromUserId }, + { collaberator_refs: ctx.fromUserId }, { - $addToSet: { collaberator_refs: this.toUserId }, + $addToSet: { collaberator_refs: ctx.toUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { collaberator_refs: this.fromUserId }, + { collaberator_refs: ctx.fromUserId }, { - $pull: { collaberator_refs: this.fromUserId }, + $pull: { collaberator_refs: ctx.fromUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { readOnly_refs: this.fromUserId }, + { readOnly_refs: ctx.fromUserId }, { - $addToSet: { readOnly_refs: this.toUserId }, + $addToSet: { readOnly_refs: ctx.toUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { readOnly_refs: this.fromUserId }, + { readOnly_refs: ctx.fromUserId }, { - $pull: { readOnly_refs: this.fromUserId }, + $pull: { readOnly_refs: ctx.fromUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { pendingEditor_refs: this.fromUserId }, + { pendingEditor_refs: ctx.fromUserId }, { - $addToSet: { pendingEditor_refs: this.toUserId }, + $addToSet: { pendingEditor_refs: ctx.toUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { pendingEditor_refs: this.fromUserId }, + { pendingEditor_refs: ctx.fromUserId }, { - $pull: { pendingEditor_refs: this.fromUserId }, + $pull: { pendingEditor_refs: ctx.fromUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { pendingReviewer_refs: this.fromUserId }, + { pendingReviewer_refs: ctx.fromUserId }, { - $addToSet: { pendingReviewer_refs: this.toUserId }, + $addToSet: { pendingReviewer_refs: ctx.toUserId }, } ) .chain('exec') .resolves() - this.ProjectMock.expects('updateMany') + ctx.ProjectMock.expects('updateMany') .withArgs( - { pendingReviewer_refs: this.fromUserId }, + { pendingReviewer_refs: ctx.fromUserId }, { - $pull: { pendingReviewer_refs: this.fromUserId }, + $pull: { pendingReviewer_refs: ctx.fromUserId }, } ) .chain('exec') @@ -523,254 +561,254 @@ describe('CollaboratorsHandler', function () { }) describe('successfully', function () { - it('should flush each project to the TPDS', async function () { - await this.CollaboratorsHandler.promises.transferProjects( - this.fromUserId, - this.toUserId + it('should flush each project to the TPDS', async function (ctx) { + await ctx.CollaboratorsHandler.promises.transferProjects( + ctx.fromUserId, + ctx.toUserId ) await sleep(10) // let the background tasks run - for (const project of this.projects) { + for (const project of ctx.projects) { expect( - this.TpdsProjectFlusher.promises.flushProjectToTpds + ctx.TpdsProjectFlusher.promises.flushProjectToTpds ).to.have.been.calledWith(project._id) } }) }) describe('when flushing to TPDS fails', function () { - it('should log an error but not fail', async function () { - this.TpdsProjectFlusher.promises.flushProjectToTpds.rejects( + it('should log an error but not fail', async function (ctx) { + ctx.TpdsProjectFlusher.promises.flushProjectToTpds.rejects( new Error('oops') ) - await this.CollaboratorsHandler.promises.transferProjects( - this.fromUserId, - this.toUserId + await ctx.CollaboratorsHandler.promises.transferProjects( + ctx.fromUserId, + ctx.toUserId ) await sleep(10) // let the background tasks run - expect(this.logger.err).to.have.been.called + expect(ctx.logger.err).toHaveBeenCalled() }) }) }) describe('setCollaboratorPrivilegeLevel', function () { - it('sets a collaborator to read-only', async function () { - this.ProjectMock.expects('updateOne') + it('sets a collaborator to read-only', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { $pull: { - collaberator_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, - reviewer_refs: this.userId, + collaberator_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, + reviewer_refs: ctx.userId, }, - $addToSet: { readOnly_refs: this.userId }, + $addToSet: { readOnly_refs: ctx.userId }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'readOnly' ) }) - it('sets a collaborator to read-write', async function () { - this.ProjectMock.expects('updateOne') + it('sets a collaborator to read-write', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { - $addToSet: { collaberator_refs: this.userId }, + $addToSet: { collaberator_refs: ctx.userId }, $pull: { - readOnly_refs: this.userId, - reviewer_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, + readOnly_refs: ctx.userId, + reviewer_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'readAndWrite' ) }) describe('sets a collaborator to reviewer when track changes is enabled for everyone', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject = sinon.stub().resolves({ + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves({ _id: new ObjectId(), - owner_ref: this.addingUserId, + owner_ref: ctx.addingUserId, name: 'Foo', track_changes: true, }) }) - it('should correctly update the project', async function () { - this.ProjectMock.expects('updateOne') + it('should correctly update the project', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { - $addToSet: { reviewer_refs: this.userId }, - $set: { track_changes: { [this.userId]: true } }, + $addToSet: { reviewer_refs: ctx.userId }, + $set: { track_changes: { [ctx.userId]: true } }, $pull: { - readOnly_refs: this.userId, - collaberator_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, + readOnly_refs: ctx.userId, + collaberator_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'review' ) }) }) describe('sets a collaborator to reviewer when track changes is not enabled for everyone', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject = sinon.stub().resolves({ + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves({ _id: new ObjectId(), - owner_ref: this.addingUserId, + owner_ref: ctx.addingUserId, name: 'Foo', track_changes: { - [this.userId]: true, + [ctx.userId]: true, }, }) }) - it('should correctly update the project', async function () { - this.ProjectMock.expects('updateOne') + it('should correctly update the project', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { - $addToSet: { reviewer_refs: this.userId }, - $set: { [`track_changes.${this.userId}`]: true }, + $addToSet: { reviewer_refs: ctx.userId }, + $set: { [`track_changes.${ctx.userId}`]: true }, $pull: { - readOnly_refs: this.userId, - collaberator_refs: this.userId, - pendingEditor_refs: this.userId, - pendingReviewer_refs: this.userId, + readOnly_refs: ctx.userId, + collaberator_refs: ctx.userId, + pendingEditor_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'review' ) }) }) - it('sets a collaborator to read-only as a pendingEditor', async function () { - this.ProjectMock.expects('updateOne') + it('sets a collaborator to read-only as a pendingEditor', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { $addToSet: { - readOnly_refs: this.userId, - pendingEditor_refs: this.userId, + readOnly_refs: ctx.userId, + pendingEditor_refs: ctx.userId, }, $pull: { - collaberator_refs: this.userId, - reviewer_refs: this.userId, - pendingReviewer_refs: this.userId, + collaberator_refs: ctx.userId, + reviewer_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'readOnly', { pendingEditor: true } ) }) - it('sets a collaborator to read-only as a pendingReviewer', async function () { - this.ProjectMock.expects('updateOne') + it('sets a collaborator to read-only as a pendingReviewer', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, $or: [ - { collaberator_refs: this.userId }, - { readOnly_refs: this.userId }, - { reviewer_refs: this.userId }, + { collaberator_refs: ctx.userId }, + { readOnly_refs: ctx.userId }, + { reviewer_refs: ctx.userId }, ], }, { $addToSet: { - readOnly_refs: this.userId, - pendingReviewer_refs: this.userId, + readOnly_refs: ctx.userId, + pendingReviewer_refs: ctx.userId, }, $pull: { - collaberator_refs: this.userId, - reviewer_refs: this.userId, - pendingEditor_refs: this.userId, + collaberator_refs: ctx.userId, + reviewer_refs: ctx.userId, + pendingEditor_refs: ctx.userId, }, } ) .chain('exec') .resolves({ matchedCount: 1 }) - await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'readOnly', { pendingReviewer: true } ) }) - it('throws a NotFoundError if the project or collaborator does not exist', async function () { - this.ProjectMock.expects('updateOne') + it('throws a NotFoundError if the project or collaborator does not exist', async function (ctx) { + ctx.ProjectMock.expects('updateOne') .chain('exec') .resolves({ matchedCount: 0 }) await expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( - this.project._id, - this.userId, + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + ctx.project._id, + ctx.userId, 'readAndWrite' ) ).to.be.rejectedWith(Errors.NotFoundError) diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs index 1066a729bd..60b629f099 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs @@ -22,9 +22,12 @@ describe('DocumentUpdaterController', function () { default: ctx.settings, })) - vi.doMock('../../../../app/src/Features/Project/ProjectLocator.mjs', () => ({ - default: ctx.ProjectLocator, - })) + vi.doMock( + '../../../../app/src/Features/Project/ProjectLocator.mjs', + () => ({ + default: ctx.ProjectLocator, + }) + ) vi.doMock( '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs', diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs index 3335ea7152..6246b948ab 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs @@ -1,26 +1,28 @@ -const sinon = require('sinon') -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') -const nock = require('nock') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import path from 'path' +import mongodb from 'mongodb-legacy' +import nock from 'nock' + +const { ObjectId } = mongodb + const modulePath = path.join( - __dirname, + import.meta.dirname, '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler' ) describe('DocumentUpdaterHandler', function () { - beforeEach(function () { - this.project_id = 'project-id-923' - this.projectHistoryId = 'ol-project-id-1' - this.doc_id = 'doc-id-394' - this.lines = ['one', 'two', 'three'] - this.version = 42 - this.user_id = 'mock-user-id-123' - this.project = { _id: this.project_id } + beforeEach(async function (ctx) { + ctx.project_id = 'project-id-923' + ctx.projectHistoryId = 'ol-project-id-1' + ctx.doc_id = 'doc-id-394' + ctx.lines = ['one', 'two', 'three'] + ctx.version = 42 + ctx.user_id = 'mock-user-id-123' + ctx.project = { _id: ctx.project_id } - this.projectEntityHandler = {} - this.settings = { + ctx.projectEntityHandler = {} + ctx.settings = { apis: { documentupdater: { url: 'http://document_updater.example.com', @@ -31,41 +33,60 @@ describe('DocumentUpdaterHandler', function () { }, moduleImportSequence: [], } - this.source = 'dropbox' - this.docUpdaterMock = nock(this.settings.apis.documentupdater.url) + ctx.source = 'dropbox' + ctx.docUpdaterMock = nock(ctx.settings.apis.documentupdater.url) - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProjectWithoutLock: sinon.stub(), }, } - this.ProjectGetter.promises.getProjectWithoutLock - .withArgs(this.project_id) - .resolves(this.project) + ctx.ProjectGetter.promises.getProjectWithoutLock + .withArgs(ctx.project_id) + .resolves(ctx.project) - this.handler = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/settings': this.settings, - '../Project/ProjectEntityHandler': this.projectEntityHandler, - '../../models/Project': { - Project: (this.Project = {}), + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.projectEntityHandler, + }) + ) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: (ctx.Project = {}), + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: {}, + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: { + Timer: class { + done() {} }, - '../Project/ProjectGetter': this.ProjectGetter, - '../../Features/Project/ProjectLocator': {}, - '@overleaf/metrics': { - Timer: class { - done() {} - }, - }, - '../../infrastructure/Modules': { - promises: { - hooks: { - fire: sinon.stub().resolves(), - }, + }, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: { + promises: { + hooks: { + fire: sinon.stub().resolves(), }, }, }, - }) + })) + + ctx.handler = (await import(modulePath)).default }) afterEach(function () { @@ -74,36 +95,36 @@ describe('DocumentUpdaterHandler', function () { describe('flushProjectToMongo', function () { describe('successfully', function () { - beforeEach(async function () { - this.docUpdaterMock.post(`/project/${this.project_id}/flush`).reply(204) - await this.handler.promises.flushProjectToMongo(this.project_id) + beforeEach(async function (ctx) { + ctx.docUpdaterMock.post(`/project/${ctx.project_id}/flush`).reply(204) + await ctx.handler.promises.flushProjectToMongo(ctx.project_id) }) - it('should flush the document from the document updater', function () { - expect(this.docUpdaterMock.isDone()).to.be.true + it('should flush the document from the document updater', function (ctx) { + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/flush`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/flush`) .replyWithError('boom') }) - it('should reject with an error', async function () { - await expect(this.handler.promises.flushProjectToMongo(this.project_id)) + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.flushProjectToMongo(ctx.project_id)) .to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock.post(`/project/${this.project_id}/flush`).reply(500) + beforeEach(function (ctx) { + ctx.docUpdaterMock.post(`/project/${ctx.project_id}/flush`).reply(500) }) - it('should reject with an error', async function () { - await expect(this.handler.promises.flushProjectToMongo(this.project_id)) + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.flushProjectToMongo(ctx.project_id)) .to.be.rejected }) }) @@ -111,40 +132,38 @@ describe('DocumentUpdaterHandler', function () { describe('flushProjectToMongoAndDelete', function () { describe('successfully', function () { - beforeEach(async function () { - this.docUpdaterMock.delete(`/project/${this.project_id}`).reply(204) - await this.handler.promises.flushProjectToMongoAndDelete( - this.project_id - ) + beforeEach(async function (ctx) { + ctx.docUpdaterMock.delete(`/project/${ctx.project_id}`).reply(204) + await ctx.handler.promises.flushProjectToMongoAndDelete(ctx.project_id) }) - it('should delete the project from the document updater', function () { - expect(this.docUpdaterMock.isDone()).to.be.true + it('should delete the project from the document updater', function (ctx) { + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .delete(`/project/${this.project_id}`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .delete(`/project/${ctx.project_id}`) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.flushProjectToMongoAndDelete(this.project_id) + ctx.handler.promises.flushProjectToMongoAndDelete(ctx.project_id) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock.delete(`/project/${this.project_id}`).reply(500) + beforeEach(function (ctx) { + ctx.docUpdaterMock.delete(`/project/${ctx.project_id}`).reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.flushProjectToMongoAndDelete(this.project_id) + ctx.handler.promises.flushProjectToMongoAndDelete(ctx.project_id) ).to.be.rejected }) }) @@ -152,45 +171,42 @@ describe('DocumentUpdaterHandler', function () { describe('flushDocToMongo', function () { describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/flush`) .reply(204) }) - it('should flush the document from the document updater', async function () { - await this.handler.promises.flushDocToMongo( - this.project_id, - this.doc_id - ) - expect(this.docUpdaterMock.isDone()).to.be.true + it('should flush the document from the document updater', async function (ctx) { + await ctx.handler.promises.flushDocToMongo(ctx.project_id, ctx.doc_id) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/flush`) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.flushDocToMongo(this.project_id, this.doc_id) + ctx.handler.promises.flushDocToMongo(ctx.project_id, ctx.doc_id) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/flush`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/flush`) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.flushDocToMongo(this.project_id, this.doc_id) + ctx.handler.promises.flushDocToMongo(ctx.project_id, ctx.doc_id) ).to.be.rejected }) }) @@ -198,142 +214,132 @@ describe('DocumentUpdaterHandler', function () { describe('deleteDoc', function () { describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock - .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .delete(`/project/${ctx.project_id}/doc/${ctx.doc_id}`) .reply(204) }) - it('should delete the document from the document updater', async function () { - await this.handler.promises.deleteDoc(this.project_id, this.doc_id) - expect(this.docUpdaterMock.isDone()).to.be.true + it('should delete the document from the document updater', async function (ctx) { + await ctx.handler.promises.deleteDoc(ctx.project_id, ctx.doc_id) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .delete(`/project/${ctx.project_id}/doc/${ctx.doc_id}`) .replyWithError('boom') }) - it('should reject with an error', async function () { - await expect( - this.handler.promises.deleteDoc(this.project_id, this.doc_id) - ).to.be.rejected + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.deleteDoc(ctx.project_id, ctx.doc_id)) + .to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock - .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .delete(`/project/${ctx.project_id}/doc/${ctx.doc_id}`) .reply(500) }) - it('should reject with an error', async function () { - await expect( - this.handler.promises.deleteDoc(this.project_id, this.doc_id) - ).to.be.rejected + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.deleteDoc(ctx.project_id, ctx.doc_id)) + .to.be.rejected }) }) describe("with 'ignoreFlushErrors' option", function () { - it('when option is true, should send a `ignore_flush_errors=true` URL query to document-updater', async function () { - this.docUpdaterMock + it('when option is true, should send a `ignore_flush_errors=true` URL query to document-updater', async function (ctx) { + ctx.docUpdaterMock .delete( - `/project/${this.project_id}/doc/${this.doc_id}?ignore_flush_errors=true` + `/project/${ctx.project_id}/doc/${ctx.doc_id}?ignore_flush_errors=true` ) .reply(204) - await this.handler.promises.deleteDoc( - this.project_id, - this.doc_id, - true - ) - expect(this.docUpdaterMock.isDone()).to.be.true + await ctx.handler.promises.deleteDoc(ctx.project_id, ctx.doc_id, true) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it("when option is false, shouldn't send any URL query to document-updater", async function () { - this.docUpdaterMock - .delete(`/project/${this.project_id}/doc/${this.doc_id}`) + it("when option is false, shouldn't send any URL query to document-updater", async function (ctx) { + ctx.docUpdaterMock + .delete(`/project/${ctx.project_id}/doc/${ctx.doc_id}`) .reply(204) - await this.handler.promises.deleteDoc( - this.project_id, - this.doc_id, - false - ) - expect(this.docUpdaterMock.isDone()).to.be.true + await ctx.handler.promises.deleteDoc(ctx.project_id, ctx.doc_id, false) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) }) describe('setDocument', function () { describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}`, { - lines: this.lines, - source: this.source, - user_id: this.user_id, + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}`, { + lines: ctx.lines, + source: ctx.source, + user_id: ctx.user_id, }) .reply(204) }) - it('should set the document in the document updater', async function () { - await this.handler.promises.setDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + it('should set the document in the document updater', async function (ctx) { + await ctx.handler.promises.setDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}`, { - lines: this.lines, - source: this.source, - user_id: this.user_id, + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}`, { + lines: ctx.lines, + source: ctx.source, + user_id: ctx.user_id, }) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.setDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + ctx.handler.promises.setDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}`, { - lines: this.lines, - source: this.source, - user_id: this.user_id, + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}`, { + lines: ctx.lines, + source: ctx.source, + user_id: ctx.user_id, }) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.setDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + ctx.handler.promises.setDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) ).to.be.rejected }) @@ -341,90 +347,90 @@ describe('DocumentUpdaterHandler', function () { }) describe('getComment', function () { - beforeEach(function () { - this.comment = { id: new ObjectId().toString() } - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.comment = { id: new ObjectId().toString() } + ctx.docUpdaterMock .get( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.comment.id}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.comment.id}` ) - .reply(200, this.comment) + .reply(200, ctx.comment) }) - it('should get the comment from the document updater', async function () { - const body = await this.handler.promises.getComment( - this.project_id, - this.doc_id, - this.comment.id + it('should get the comment from the document updater', async function (ctx) { + const body = await ctx.handler.promises.getComment( + ctx.project_id, + ctx.doc_id, + ctx.comment.id ) - expect(body).to.deep.equal(this.comment) + expect(body).to.deep.equal(ctx.comment) }) }) describe('getDocument', function () { - beforeEach(function () { - this.doc = { - lines: this.lines, - version: this.version, + beforeEach(function (ctx) { + ctx.doc = { + lines: ctx.lines, + version: ctx.version, ops: ['mock-op-1', 'mock-op-2'], ranges: { mock: 'ranges' }, } - this.fromVersion = 2 + ctx.fromVersion = 2 }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .get( - `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}?fromVersion=${ctx.fromVersion}` ) - .reply(200, this.doc) + .reply(200, ctx.doc) }) - it('should return the lines and version', async function () { - const doc = await this.handler.promises.getDocument( - this.project_id, - this.doc_id, - this.fromVersion + it('should return the lines and version', async function (ctx) { + const doc = await ctx.handler.promises.getDocument( + ctx.project_id, + ctx.doc_id, + ctx.fromVersion ) - expect(doc).to.deep.equal(this.doc) + expect(doc).to.deep.equal(ctx.doc) }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .get( - `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}?fromVersion=${ctx.fromVersion}` ) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.getDocument( - this.project_id, - this.doc_id, - this.fromVersion + ctx.handler.promises.getDocument( + ctx.project_id, + ctx.doc_id, + ctx.fromVersion ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .get( - `/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}?fromVersion=${ctx.fromVersion}` ) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.getDocument( - this.project_id, - this.doc_id, - this.fromVersion + ctx.handler.promises.getDocument( + ctx.project_id, + ctx.doc_id, + ctx.fromVersion ) ).to.be.rejected }) @@ -432,66 +438,66 @@ describe('DocumentUpdaterHandler', function () { }) describe('getProjectDocsIfMatch', function () { - beforeEach(function () { - this.project_state_hash = '1234567890abcdef' - this.doc0 = { - _id: this.doc_id, - lines: this.lines, - v: this.version, + beforeEach(function (ctx) { + ctx.project_state_hash = '1234567890abcdef' + ctx.doc0 = { + _id: ctx.doc_id, + lines: ctx.lines, + v: ctx.version, } - this.docs = [this.doc0, this.doc0, this.doc0] + ctx.docs = [ctx.doc0, ctx.doc0, ctx.doc0] }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` + `/project/${ctx.project_id}/get_and_flush_if_old?state=${ctx.project_state_hash}` ) - .reply(200, this.docs) + .reply(200, ctx.docs) }) - it('should call the callback with the documents', async function () { - const docs = await this.handler.promises.getProjectDocsIfMatch( - this.project_id, - this.project_state_hash + it('should call the callback with the documents', async function (ctx) { + const docs = await ctx.handler.promises.getProjectDocsIfMatch( + ctx.project_id, + ctx.project_state_hash ) - expect(docs).to.deep.equal(this.docs) + expect(docs).to.deep.equal(ctx.docs) }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` + `/project/${ctx.project_id}/get_and_flush_if_old?state=${ctx.project_state_hash}` ) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.getProjectDocsIfMatch( - this.project_id, - this.project_state_hash + ctx.handler.promises.getProjectDocsIfMatch( + ctx.project_id, + ctx.project_state_hash ) ).to.be.rejected }) }) describe('when the document updater returns a conflict error code', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}` + `/project/${ctx.project_id}/get_and_flush_if_old?state=${ctx.project_state_hash}` ) .reply(409) }) - it('should return no documents', async function () { - const response = await this.handler.promises.getProjectDocsIfMatch( - this.project_id, - this.project_state_hash + it('should return no documents', async function (ctx) { + const response = await ctx.handler.promises.getProjectDocsIfMatch( + ctx.project_id, + ctx.project_state_hash ) expect(response).to.be.undefined }) @@ -500,99 +506,94 @@ describe('DocumentUpdaterHandler', function () { describe('clearProjectState', function () { describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/clearState`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/clearState`) .reply(200) }) - it('should clear the project state from the document updater', async function () { - await this.handler.promises.clearProjectState(this.project_id) - expect(this.docUpdaterMock.isDone()).to.be.true + it('should clear the project state from the document updater', async function (ctx) { + await ctx.handler.promises.clearProjectState(ctx.project_id) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/clearState`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/clearState`) .replyWithError('boom') }) - it('should reject with an error', async function () { - await expect(this.handler.promises.clearProjectState(this.project_id)) - .to.be.rejected + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.clearProjectState(ctx.project_id)).to + .be.rejected }) }) describe('when the document updater returns an error code', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/clearState`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/clearState`) .reply(500) }) - it('should reject with an error', async function () { - await expect(this.handler.promises.clearProjectState(this.project_id)) - .to.be.rejected + it('should reject with an error', async function (ctx) { + await expect(ctx.handler.promises.clearProjectState(ctx.project_id)).to + .be.rejected }) }) }) describe('acceptChanges', function () { - beforeEach(function () { - this.change_id = 'mock-change-id-1' + beforeEach(function (ctx) { + ctx.change_id = 'mock-change-id-1' }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock - .post( - `/project/${this.project_id}/doc/${this.doc_id}/change/accept`, - { - change_ids: [this.change_id], - } - ) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/change/accept`, { + change_ids: [ctx.change_id], + }) .reply(200) }) - it('should accept the change in the document updater', async function () { - await this.handler.promises.acceptChanges( - this.project_id, - this.doc_id, - [this.change_id] - ) - expect(this.docUpdaterMock.isDone()).to.be.true + it('should accept the change in the document updater', async function (ctx) { + await ctx.handler.promises.acceptChanges(ctx.project_id, ctx.doc_id, [ + ctx.change_id, + ]) + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/change/accept`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/change/accept`) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.acceptChanges(this.project_id, this.doc_id, [ - this.change_id, + ctx.handler.promises.acceptChanges(ctx.project_id, ctx.doc_id, [ + ctx.change_id, ]) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/change/accept`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/change/accept`) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.acceptChanges(this.project_id, this.doc_id, [ - this.change_id, + ctx.handler.promises.acceptChanges(ctx.project_id, ctx.doc_id, [ + ctx.change_id, ]) ).to.be.rejected }) @@ -600,67 +601,67 @@ describe('DocumentUpdaterHandler', function () { }) describe('deleteThread', function () { - beforeEach(function () { - this.thread_id = 'mock-thread-id-1' + beforeEach(function (ctx) { + ctx.thread_id = 'mock-thread-id-1' }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .delete( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}` ) .reply(200) }) - it('should delete the thread in the document updater', async function () { - await this.handler.promises.deleteThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + it('should delete the thread in the document updater', async function (ctx) { + await ctx.handler.promises.deleteThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .delete( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}` ) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.deleteThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.deleteThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .delete( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}` ) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.deleteThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.deleteThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) @@ -668,67 +669,67 @@ describe('DocumentUpdaterHandler', function () { }) describe('resolveThread', function () { - beforeEach(function () { - this.thread_id = 'mock-thread-id-1' + beforeEach(function (ctx) { + ctx.thread_id = 'mock-thread-id-1' }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/resolve` ) .reply(200) }) - it('should resolve the thread in the document updater', async function () { - await this.handler.promises.resolveThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + it('should resolve the thread in the document updater', async function (ctx) { + await ctx.handler.promises.resolveThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/resolve` ) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.resolveThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.resolveThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/resolve` ) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.resolveThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.resolveThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) @@ -736,67 +737,67 @@ describe('DocumentUpdaterHandler', function () { }) describe('reopenThread', function () { - beforeEach(function () { - this.thread_id = 'mock-thread-id-1' + beforeEach(function (ctx) { + ctx.thread_id = 'mock-thread-id-1' }) describe('successfully', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/reopen` ) .reply(200) }) - it('should reopen the thread in the document updater', async function () { - await this.handler.promises.reopenThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + it('should reopen the thread in the document updater', async function (ctx) { + await ctx.handler.promises.reopenThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/reopen` ) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.reopenThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.reopenThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock + beforeEach(function (ctx) { + ctx.docUpdaterMock .post( - `/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen` + `/project/${ctx.project_id}/doc/${ctx.doc_id}/comment/${ctx.thread_id}/reopen` ) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.reopenThread( - this.project_id, - this.doc_id, - this.thread_id, - this.user_id + ctx.handler.promises.reopenThread( + ctx.project_id, + ctx.doc_id, + ctx.thread_id, + ctx.user_id ) ).to.be.rejected }) @@ -804,34 +805,34 @@ describe('DocumentUpdaterHandler', function () { }) describe('updateProjectStructure ', function () { - beforeEach(function () { - this.user_id = 1234 - this.version = 999 + beforeEach(function (ctx) { + ctx.user_id = 1234 + ctx.version = 999 }) describe('with project history disabled', function () { - beforeEach(function () { - this.settings.apis.project_history.sendProjectStructureOps = false + beforeEach(function (ctx) { + ctx.settings.apis.project_history.sendProjectStructureOps = false }) - it('returns early', async function () { - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + it('returns early', async function (ctx) { + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, {}, - this.source + ctx.source ) }) }) describe('with project history enabled', function () { - beforeEach(function () { - this.settings.apis.project_history.sendProjectStructureOps = true + beforeEach(function (ctx) { + ctx.settings.apis.project_history.sendProjectStructureOps = true }) describe('when an entity has changed name', function () { - it('should send the structure update to the document updater', async function () { + it('should send the structure update to the document updater', async function (ctx) { const docIdA = new ObjectId() const docIdB = new ObjectId() const changes = { @@ -850,7 +851,7 @@ describe('DocumentUpdaterHandler', function () { doc: { _id: new ObjectId(docIdB.toString()) }, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } const updates = [ @@ -862,32 +863,32 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when a doc has been added', function () { - it('should send the structure update to the document updater', async function () { + it('should send the structure update to the document updater', async function (ctx) { const docId = new ObjectId() const changes = { newDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } const updates = [ @@ -904,29 +905,29 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when a file has been added', function () { - it('should send the structure update to the document updater', async function () { + it('should send the structure update to the document updater', async function (ctx) { const fileId = new ObjectId() const changes = { newFiles: [ @@ -935,7 +936,7 @@ describe('DocumentUpdaterHandler', function () { file: { _id: fileId, hash: '12345' }, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } const updates = [ @@ -952,34 +953,34 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when an entity has been deleted', function () { - it('should end the structure update to the document updater', async function () { + it('should end the structure update to the document updater', async function (ctx) { const docId = new ObjectId() const changes = { oldDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } const updates = [ @@ -991,30 +992,30 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when a file is converted to a doc', function () { - it('should send the delete first', async function () { + it('should send the delete first', async function (ctx) { const docId = new ObjectId() const fileId = new ObjectId() const changes = { @@ -1031,7 +1032,7 @@ describe('DocumentUpdaterHandler', function () { doc: { _id: docId }, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } const updates = [ @@ -1054,50 +1055,50 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the project version is missing', function () { - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { const docId = new ObjectId() const changes = { oldDocs: [{ path: '/foo', docLines: 'a\nb', doc: { _id: docId } }], } await expect( - this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, + ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, changes, - this.source + ctx.source ) ).to.be.rejectedWith('did not receive project version in changes') }) }) describe('when ranges are present', function () { - beforeEach(function () { - this.docId = new ObjectId() - this.ranges = { + beforeEach(function (ctx) { + ctx.docId = new ObjectId() + ctx.ranges = { changes: [ { op: { p: 0, i: 'foo' }, @@ -1115,106 +1116,106 @@ describe('DocumentUpdaterHandler', function () { }, ], } - this.changes = { + ctx.changes = { newDocs: [ { path: '/foo', docLines: 'foo\nbar', - doc: { _id: this.docId }, - ranges: this.ranges, + doc: { _id: ctx.docId }, + ranges: ctx.ranges, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } }) - it('should forward ranges', async function () { + it('should forward ranges', async function (ctx) { const updates = [ { type: 'add-doc', - id: this.docId.toString(), + id: ctx.docId.toString(), pathname: '/foo', docLines: 'foo\nbar', historyRangesSupport: false, hash: undefined, - ranges: this.ranges, + ranges: ctx.ranges, metadata: undefined, createdBlob: true, }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, + ctx.changes, + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it('should include flag when history ranges support is enabled', async function () { - this.ProjectGetter.promises.getProjectWithoutLock - .withArgs(this.project_id) + it('should include flag when history ranges support is enabled', async function (ctx) { + ctx.ProjectGetter.promises.getProjectWithoutLock + .withArgs(ctx.project_id) .resolves({ - _id: this.project_id, + _id: ctx.project_id, overleaf: { history: { rangesSupportEnabled: true } }, }) const updates = [ { type: 'add-doc', - id: this.docId.toString(), + id: ctx.docId.toString(), pathname: '/foo', docLines: 'foo\nbar', historyRangesSupport: true, hash: undefined, - ranges: this.ranges, + ranges: ctx.ranges, metadata: undefined, createdBlob: true, }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, + ctx.changes, + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('with filestore disabled', function () { - beforeEach(function () { - this.fileId = new ObjectId() + beforeEach(function (ctx) { + ctx.fileId = new ObjectId() const updates = [ { type: 'add-file', - id: this.fileId.toString(), + id: ctx.fileId.toString(), pathname: '/bar', docLines: undefined, historyRangesSupport: false, @@ -1225,58 +1226,58 @@ describe('DocumentUpdaterHandler', function () { }, ] - this.docUpdaterMock - .post(`/project/${this.project_id}`, { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}`, { updates, - userId: this.user_id, - version: this.version, - projectHistoryId: this.projectHistoryId, - source: this.source, + userId: ctx.user_id, + version: ctx.version, + projectHistoryId: ctx.projectHistoryId, + source: ctx.source, }) .reply(204) }) - it('should add files without URL and with createdBlob', async function () { - this.changes = { + it('should add files without URL and with createdBlob', async function (ctx) { + ctx.changes = { newFiles: [ { path: '/bar', - file: { _id: this.fileId, hash: '12345' }, + file: { _id: ctx.fileId, hash: '12345' }, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } - await this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source + await ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, + ctx.changes, + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it('should flag files without hash', async function () { - this.fileId = new ObjectId() - this.changes = { + it('should flag files without hash', async function (ctx) { + ctx.fileId = new ObjectId() + ctx.changes = { newFiles: [ { path: '/bar', - file: { _id: this.fileId }, + file: { _id: ctx.fileId }, }, ], - newProject: { version: this.version }, + newProject: { version: ctx.version }, } await expect( - this.handler.promises.updateProjectStructure( - this.project_id, - this.projectHistoryId, - this.user_id, - this.changes, - this.source + ctx.handler.promises.updateProjectStructure( + ctx.project_id, + ctx.projectHistoryId, + ctx.user_id, + ctx.changes, + ctx.source ) ).to.be.rejected }) @@ -1285,7 +1286,7 @@ describe('DocumentUpdaterHandler', function () { }) describe('resyncProjectHistory', function () { - it('should add docs', async function () { + it('should add docs', async function (ctx) { const docId1 = new ObjectId() const docId2 = new ObjectId() const docs = [ @@ -1296,7 +1297,7 @@ describe('DocumentUpdaterHandler', function () { const projectId = new ObjectId() const projectHistoryId = 99 - this.docUpdaterMock + ctx.docUpdaterMock .post(`/project/${projectId}/history/resync`, { docs: [ { doc: docId1.toString(), path: 'main.tex' }, @@ -1307,7 +1308,7 @@ describe('DocumentUpdaterHandler', function () { }) .reply(200) - await this.handler.promises.resyncProjectHistory( + await ctx.handler.promises.resyncProjectHistory( projectId, projectHistoryId, docs, @@ -1315,10 +1316,10 @@ describe('DocumentUpdaterHandler', function () { {} ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it('should add files', async function () { + it('should add files', async function (ctx) { const fileId1 = new ObjectId() const fileId2 = new ObjectId() const fileId3 = new ObjectId() @@ -1358,7 +1359,7 @@ describe('DocumentUpdaterHandler', function () { const projectId = new ObjectId() const projectHistoryId = 99 - this.docUpdaterMock + ctx.docUpdaterMock .post(`/project/${projectId}/history/resync`, { docs: [], files: [ @@ -1396,7 +1397,7 @@ describe('DocumentUpdaterHandler', function () { }) .reply(200) - await this.handler.promises.resyncProjectHistory( + await ctx.handler.promises.resyncProjectHistory( projectId, projectHistoryId, docs, @@ -1404,10 +1405,10 @@ describe('DocumentUpdaterHandler', function () { {} ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it('should add files without URL', async function () { + it('should add files without URL', async function (ctx) { const fileId1 = new ObjectId() const fileId2 = new ObjectId() const fileId3 = new ObjectId() @@ -1447,7 +1448,7 @@ describe('DocumentUpdaterHandler', function () { const projectId = new ObjectId() const projectHistoryId = 99 - this.docUpdaterMock + ctx.docUpdaterMock .post(`/project/${projectId}/history/resync`, { docs: [], files: [ @@ -1484,17 +1485,17 @@ describe('DocumentUpdaterHandler', function () { projectHistoryId, }) .reply(200) - await this.handler.promises.resyncProjectHistory( + await ctx.handler.promises.resyncProjectHistory( projectId, projectHistoryId, docs, files, {} ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) - it('should flag files with missing hashes', async function () { + it('should flag files with missing hashes', async function (ctx) { const fileId1 = new ObjectId() const fileId2 = new ObjectId() const fileId3 = new ObjectId() @@ -1533,7 +1534,7 @@ describe('DocumentUpdaterHandler', function () { const projectId = new ObjectId() const projectHistoryId = 99 await expect( - this.handler.promises.resyncProjectHistory( + ctx.handler.promises.resyncProjectHistory( projectId, projectHistoryId, docs, @@ -1546,64 +1547,64 @@ describe('DocumentUpdaterHandler', function () { describe('appendToDocument', function () { describe('successfully', function () { - beforeEach(function () { - this.body = { rev: 1 } - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/append`, { - lines: this.lines, - source: this.source, - user_id: this.user_id, + beforeEach(function (ctx) { + ctx.body = { rev: 1 } + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/append`, { + lines: ctx.lines, + source: ctx.source, + user_id: ctx.user_id, }) .reply(200) }) - it('should append to the document in the document updater', async function () { - await this.handler.promises.appendToDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + it('should append to the document in the document updater', async function (ctx) { + await ctx.handler.promises.appendToDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) - expect(this.docUpdaterMock.isDone()).to.be.true + expect(ctx.docUpdaterMock.isDone()).to.be.true }) }) describe('when the document updater API returns an error', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/append`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/append`) .replyWithError('boom') }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.appendToDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + ctx.handler.promises.appendToDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) ).to.be.rejected }) }) describe('when the document updater returns a failure error code', function () { - beforeEach(function () { - this.docUpdaterMock - .post(`/project/${this.project_id}/doc/${this.doc_id}/append`) + beforeEach(function (ctx) { + ctx.docUpdaterMock + .post(`/project/${ctx.project_id}/doc/${ctx.doc_id}/append`) .reply(500) }) - it('should reject with an error', async function () { + it('should reject with an error', async function (ctx) { await expect( - this.handler.promises.appendToDocument( - this.project_id, - this.doc_id, - this.user_id, - this.lines, - this.source + ctx.handler.promises.appendToDocument( + ctx.project_id, + ctx.doc_id, + ctx.user_id, + ctx.lines, + ctx.source ) ).to.be.rejected }) diff --git a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs index e958abb915..ab38897fb7 100644 --- a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs @@ -43,9 +43,12 @@ describe('ProjectZipStreamManager', function () { }) ) - vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({ - default: (ctx.HistoryManager = {}), - })) + vi.doMock( + '../../../../app/src/Features/History/HistoryManager.mjs', + () => ({ + default: (ctx.HistoryManager = {}), + }) + ) vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ default: (ctx.ProjectGetter = {}), diff --git a/services/web/test/unit/src/Editor/EditorController.test.mjs b/services/web/test/unit/src/Editor/EditorController.test.mjs index 2dd11bdf0f..383c462ca2 100644 --- a/services/web/test/unit/src/Editor/EditorController.test.mjs +++ b/services/web/test/unit/src/Editor/EditorController.test.mjs @@ -1,234 +1,250 @@ -/* eslint-disable - n/handle-callback-err, - max-len, - no-return-assign, - no-unused-vars, -*/ -// 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 - */ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') -const OError = require('@overleaf/o-error') +import { beforeEach, describe, expect, it, vi } from 'vitest' +import sinon from 'sinon' +import OError from '@overleaf/o-error' +import mongodb from 'mongodb-legacy' -const modulePath = require('path').join( - __dirname, - '../../../../app/src/Features/Editor/EditorController' -) -const MockClient = require('../helpers/MockClient') -const assert = require('assert') -const { ObjectId } = require('mongodb-legacy') +const modulePath = '../../../../app/src/Features/Editor/EditorController' + +const { ObjectId } = mongodb describe('EditorController', function () { - beforeEach(function () { - this.project_id = 'test-project-id' - this.source = 'dropbox' - this.user_id = new ObjectId() + beforeEach(async function (ctx) { + ctx.project_id = 'test-project-id' + ctx.source = 'dropbox' + ctx.user_id = new ObjectId() - this.doc = { _id: (this.doc_id = 'test-doc-id') } - this.docName = 'doc.tex' - this.docLines = ['1234', 'dskl'] - this.file = { _id: (this.file_id = 'dasdkjk') } - this.fileName = 'file.png' - this.fsPath = '/folder/file.png' - this.linkedFileData = { provider: 'url' } + ctx.doc = { _id: (ctx.doc_id = 'test-doc-id') } + ctx.docName = 'doc.tex' + ctx.docLines = ['1234', 'dskl'] + ctx.file = { _id: (ctx.file_id = 'dasdkjk') } + ctx.fileName = 'file.png' + ctx.fsPath = '/folder/file.png' + ctx.linkedFileData = { provider: 'url' } - this.newFile = { _id: 'new-file-id' } + ctx.newFile = { _id: 'new-file-id' } - this.folder_id = '123ksajdn' - this.folder = { _id: this.folder_id } - this.folderName = 'folder' + ctx.folder_id = '123ksajdn' + ctx.folder = { _id: ctx.folder_id } + ctx.folderName = 'folder' - this.callback = sinon.stub() + ctx.callback = sinon.stub() - return (this.EditorController = SandboxedModule.require(modulePath, { - requires: { - '../Project/ProjectEntityUpdateHandler': - (this.ProjectEntityUpdateHandler = {}), - '../Project/ProjectOptionsHandler': (this.ProjectOptionsHandler = { + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', + () => ({ + default: (ctx.ProjectEntityUpdateHandler = {}), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectOptionsHandler', + () => ({ + default: (ctx.ProjectOptionsHandler = { setCompiler: sinon.stub().yields(), setImageName: sinon.stub().yields(), setSpellCheckLanguage: sinon.stub().yields(), }), - '../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = { + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: (ctx.ProjectDetailsHandler = { setProjectDescription: sinon.stub().yields(), renameProject: sinon.stub().yields(), setPublicAccessLevel: sinon.stub().yields(), }), - '../Project/ProjectDeleter': (this.ProjectDeleter = {}), - '../DocumentUpdater/DocumentUpdaterHandler': - (this.DocumentUpdaterHandler = { - flushDocToMongo: sinon.stub().yields(), - setDocument: sinon.stub().yields(), - }), - './EditorRealTimeController': (this.EditorRealTimeController = { + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: (ctx.ProjectDeleter = { + deleteProject: sinon.stub(), + }), + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: (ctx.DocumentUpdaterHandler = { + flushDocToMongo: sinon.stub().yields(), + setDocument: sinon.stub().yields(), + }), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: (ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), }), - '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }), - }, + }) + ) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.Metrics = { inc: sinon.stub() }), })) + + ctx.EditorController = (await import(modulePath)).default }) describe('addDoc', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.addDocWithRanges = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.addDocWithRanges = sinon .stub() - .yields(null, this.doc, this.folder_id) - return this.EditorController.addDoc( - this.project_id, - this.folder_id, - this.docName, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, ctx.folder_id) + return ctx.EditorController.addDoc( + ctx.project_id, + ctx.folder_id, + ctx.docName, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should add the doc using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.addDocWithRanges + it('should add the doc using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.addDocWithRanges .calledWith( - this.project_id, - this.folder_id, - this.docName, - this.docLines, + ctx.project_id, + ctx.folder_id, + ctx.docName, + ctx.docLines, {}, - this.user_id, - this.source + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('should send the update out to the users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('should send the update out to the users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewDoc', - this.folder_id, - this.doc, - this.source, - this.user_id + ctx.folder_id, + ctx.doc, + ctx.source, + ctx.user_id ) .should.equal(true) }) - it('calls the callback', function () { - return this.callback.calledWith(null, this.doc).should.equal(true) + it('calls the callback', function (ctx) { + return ctx.callback.calledWith(null, ctx.doc).should.equal(true) }) }) describe('addFile', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.addFile = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.addFile = sinon .stub() - .yields(null, this.file, this.folder_id) - return this.EditorController.addFile( - this.project_id, - this.folder_id, - this.fileName, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.file, ctx.folder_id) + return ctx.EditorController.addFile( + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should add the folder using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.addFile + it('should add the folder using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.addFile .calledWith( - this.project_id, - this.folder_id, - this.fileName, - this.fsPath, - this.linkedFileData, - this.user_id, - this.source + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.fsPath, + ctx.linkedFileData, + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('should send the update of a new folder out to the users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('should send the update of a new folder out to the users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFile', - this.folder_id, - this.file, - this.source, - this.linkedFileData, - this.user_id + ctx.folder_id, + ctx.file, + ctx.source, + ctx.linkedFileData, + ctx.user_id ) .should.equal(true) }) - it('calls the callback', function () { - return this.callback.calledWith(null, this.file).should.equal(true) + it('calls the callback', function (ctx) { + return ctx.callback.calledWith(null, ctx.file).should.equal(true) }) }) describe('upsertDoc', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertDoc = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertDoc = sinon .stub() - .yields(null, this.doc, false) - return this.EditorController.upsertDoc( - this.project_id, - this.folder_id, - this.docName, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, false) + return ctx.EditorController.upsertDoc( + ctx.project_id, + ctx.folder_id, + ctx.docName, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('upserts the doc using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.upsertDoc + it('upserts the doc using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.upsertDoc .calledWith( - this.project_id, - this.folder_id, - this.docName, - this.docLines, - this.source + ctx.project_id, + ctx.folder_id, + ctx.docName, + ctx.docLines, + ctx.source ) .should.equal(true) }) - it('returns the doc', function () { - return this.callback.calledWith(null, this.doc).should.equal(true) + it('returns the doc', function (ctx) { + return ctx.callback.calledWith(null, ctx.doc).should.equal(true) }) describe('doc does not exist', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertDoc = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertDoc = sinon .stub() - .yields(null, this.doc, true) - return this.EditorController.upsertDoc( - this.project_id, - this.folder_id, - this.docName, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, true) + return ctx.EditorController.upsertDoc( + ctx.project_id, + ctx.folder_id, + ctx.docName, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('sends an update out to users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('sends an update out to users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewDoc', - this.folder_id, - this.doc, - this.source, - this.user_id + ctx.folder_id, + ctx.doc, + ctx.source, + ctx.user_id ) .should.equal(true) }) @@ -236,67 +252,67 @@ describe('EditorController', function () { }) describe('upsertFile', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertFile = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertFile = sinon .stub() - .yields(null, this.newFile, false, this.file) - return this.EditorController.upsertFile( - this.project_id, - this.folder_id, - this.fileName, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.newFile, false, ctx.file) + return ctx.EditorController.upsertFile( + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('upserts the file using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.upsertFile + it('upserts the file using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.upsertFile .calledWith( - this.project_id, - this.folder_id, - this.fileName, - this.fsPath, - this.linkedFileData, - this.user_id, - this.source + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.fsPath, + ctx.linkedFileData, + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('returns the file', function () { - return this.callback.calledWith(null, this.newFile).should.equal(true) + it('returns the file', function (ctx) { + return ctx.callback.calledWith(null, ctx.newFile).should.equal(true) }) describe('file does not exist', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertFile = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertFile = sinon .stub() - .yields(null, this.file, true) - return this.EditorController.upsertFile( - this.project_id, - this.folder_id, - this.fileName, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.file, true) + return ctx.EditorController.upsertFile( + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should send the update out to users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('should send the update out to users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFile', - this.folder_id, - this.file, - this.source, - this.linkedFileData, - this.user_id + ctx.folder_id, + ctx.file, + ctx.source, + ctx.linkedFileData, + ctx.user_id ) .should.equal(true) }) @@ -304,91 +320,91 @@ describe('EditorController', function () { }) describe('upsertDocWithPath', function () { - beforeEach(function () { - this.docPath = '/folder/doc' + beforeEach(function (ctx) { + ctx.docPath = '/folder/doc' - this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() - .yields(null, this.doc, false, [], this.folder) - return this.EditorController.upsertDocWithPath( - this.project_id, - this.docPath, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, false, [], ctx.folder) + return ctx.EditorController.upsertDocWithPath( + ctx.project_id, + ctx.docPath, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('upserts the doc using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.upsertDocWithPath - .calledWith(this.project_id, this.docPath, this.docLines, this.source) + it('upserts the doc using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.upsertDocWithPath + .calledWith(ctx.project_id, ctx.docPath, ctx.docLines, ctx.source) .should.equal(true) }) describe('doc does not exist', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() - .yields(null, this.doc, true, [], this.folder) - return this.EditorController.upsertDocWithPath( - this.project_id, - this.docPath, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, true, [], ctx.folder) + return ctx.EditorController.upsertDocWithPath( + ctx.project_id, + ctx.docPath, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should send the update for the doc out to users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('should send the update for the doc out to users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewDoc', - this.folder_id, - this.doc, - this.source, - this.user_id + ctx.folder_id, + ctx.doc, + ctx.source, + ctx.user_id ) .should.equal(true) }) }) describe('folders required for doc do not exist', function () { - beforeEach(function () { + beforeEach(function (ctx) { const folders = [ - (this.folderA = { _id: 2, parentFolder_id: 1 }), - (this.folderB = { _id: 3, parentFolder_id: 2 }), + (ctx.folderA = { _id: 2, parentFolder_id: 1 }), + (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] - this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + ctx.ProjectEntityUpdateHandler.upsertDocWithPath = sinon .stub() - .yields(null, this.doc, true, folders, this.folderB) - return this.EditorController.upsertDocWithPath( - this.project_id, - this.docPath, - this.docLines, - this.source, - this.user_id, - this.callback + .yields(null, ctx.doc, true, folders, ctx.folderB) + return ctx.EditorController.upsertDocWithPath( + ctx.project_id, + ctx.docPath, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should send the update for each folder to users in the project', function () { - this.EditorRealTimeController.emitToRoom + it('should send the update for each folder to users in the project', function (ctx) { + ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFolder', - this.folderA.parentFolder_id, - this.folderA + ctx.folderA.parentFolder_id, + ctx.folderA ) .should.equal(true) - return this.EditorRealTimeController.emitToRoom + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFolder', - this.folderB.parentFolder_id, - this.folderB + ctx.folderB.parentFolder_id, + ctx.folderB ) .should.equal(true) }) @@ -396,102 +412,102 @@ describe('EditorController', function () { }) describe('upsertFileWithPath', function () { - beforeEach(function () { - this.filePath = '/folder/file' + beforeEach(function (ctx) { + ctx.filePath = '/folder/file' - this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() - .yields(null, this.newFile, false, this.file, [], this.folder) - return this.EditorController.upsertFileWithPath( - this.project_id, - this.filePath, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.newFile, false, ctx.file, [], ctx.folder) + return ctx.EditorController.upsertFileWithPath( + ctx.project_id, + ctx.filePath, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('upserts the file using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.upsertFileWithPath + it('upserts the file using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.upsertFileWithPath .calledWith( - this.project_id, - this.filePath, - this.fsPath, - this.linkedFileData, - this.user_id, - this.source + ctx.project_id, + ctx.filePath, + ctx.fsPath, + ctx.linkedFileData, + ctx.user_id, + ctx.source ) .should.equal(true) }) describe('file does not exist', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() - .yields(null, this.file, true, undefined, [], this.folder) - return this.EditorController.upsertFileWithPath( - this.project_id, - this.filePath, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.file, true, undefined, [], ctx.folder) + return ctx.EditorController.upsertFileWithPath( + ctx.project_id, + ctx.filePath, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should send the update for the file out to users in the project', function () { - return this.EditorRealTimeController.emitToRoom + it('should send the update for the file out to users in the project', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFile', - this.folder_id, - this.file, - this.source, - this.linkedFileData, - this.user_id + ctx.folder_id, + ctx.file, + ctx.source, + ctx.linkedFileData, + ctx.user_id ) .should.equal(true) }) }) describe('folders required for file do not exist', function () { - beforeEach(function () { + beforeEach(function (ctx) { const folders = [ - (this.folderA = { _id: 2, parentFolder_id: 1 }), - (this.folderB = { _id: 3, parentFolder_id: 2 }), + (ctx.folderA = { _id: 2, parentFolder_id: 1 }), + (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] - this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + ctx.ProjectEntityUpdateHandler.upsertFileWithPath = sinon .stub() - .yields(null, this.file, true, undefined, folders, this.folderB) - return this.EditorController.upsertFileWithPath( - this.project_id, - this.filePath, - this.fsPath, - this.linkedFileData, - this.source, - this.user_id, - this.callback + .yields(null, ctx.file, true, undefined, folders, ctx.folderB) + return ctx.EditorController.upsertFileWithPath( + ctx.project_id, + ctx.filePath, + ctx.fsPath, + ctx.linkedFileData, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should send the update for each folder to users in the project', function () { - this.EditorRealTimeController.emitToRoom + it('should send the update for each folder to users in the project', function (ctx) { + ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFolder', - this.folderA.parentFolder_id, - this.folderA + ctx.folderA.parentFolder_id, + ctx.folderA ) .should.equal(true) - return this.EditorRealTimeController.emitToRoom + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveNewFolder', - this.folderB.parentFolder_id, - this.folderB + ctx.folderB.parentFolder_id, + ctx.folderB ) .should.equal(true) }) @@ -499,377 +515,364 @@ describe('EditorController', function () { }) describe('addFolder', function () { - beforeEach(function () { - this.EditorController._notifyProjectUsersOfNewFolder = sinon + beforeEach(function (ctx) { + ctx.EditorController._notifyProjectUsersOfNewFolder = sinon .stub() .yields() - this.ProjectEntityUpdateHandler.addFolder = sinon + ctx.ProjectEntityUpdateHandler.addFolder = sinon .stub() - .yields(null, this.folder, this.folder_id) - return this.EditorController.addFolder( - this.project_id, - this.folder_id, - this.folderName, - this.source, - this.user_id, - this.callback + .yields(null, ctx.folder, ctx.folder_id) + return ctx.EditorController.addFolder( + ctx.project_id, + ctx.folder_id, + ctx.folderName, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should add the folder using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.addFolder - .calledWith( - this.project_id, - this.folder_id, - this.folderName, - this.user_id - ) + it('should add the folder using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.addFolder + .calledWith(ctx.project_id, ctx.folder_id, ctx.folderName, ctx.user_id) .should.equal(true) }) - it('should notifyProjectUsersOfNewFolder', function () { - return this.EditorController._notifyProjectUsersOfNewFolder - .calledWith(this.project_id, this.folder_id, this.folder, this.user_id) + it('should notifyProjectUsersOfNewFolder', function (ctx) { + return ctx.EditorController._notifyProjectUsersOfNewFolder + .calledWith(ctx.project_id, ctx.folder_id, ctx.folder, ctx.user_id) .should.equal(true) }) - it('should return the folder in the callback', function () { - return this.callback.calledWith(null, this.folder).should.equal(true) + it('should return the folder in the callback', function (ctx) { + return ctx.callback.calledWith(null, ctx.folder).should.equal(true) }) }) describe('mkdirp', function () { - beforeEach(function () { - this.path = 'folder1/folder2' - this.folders = [ - (this.folderA = { _id: 2, parentFolder_id: 1 }), - (this.folderB = { _id: 3, parentFolder_id: 2 }), + beforeEach(function (ctx) { + ctx.path = 'folder1/folder2' + ctx.folders = [ + (ctx.folderA = { _id: 2, parentFolder_id: 1 }), + (ctx.folderB = { _id: 3, parentFolder_id: 2 }), ] - this.userId = new ObjectId().toString() - this.EditorController._notifyProjectUsersOfNewFolders = sinon + ctx.userId = new ObjectId().toString() + ctx.EditorController._notifyProjectUsersOfNewFolders = sinon .stub() .yields() - this.ProjectEntityUpdateHandler.mkdirp = sinon + ctx.ProjectEntityUpdateHandler.mkdirp = sinon .stub() - .yields(null, this.folders, this.folder) - return this.EditorController.mkdirp( - this.project_id, - this.path, - this.userId, - this.callback + .yields(null, ctx.folders, ctx.folder) + return ctx.EditorController.mkdirp( + ctx.project_id, + ctx.path, + ctx.userId, + ctx.callback ) }) - it('should create the folder using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.mkdirp - .calledWith(this.project_id, this.path, this.userId) + it('should create the folder using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.mkdirp + .calledWith(ctx.project_id, ctx.path, ctx.userId) .should.equal(true) }) - it('should notifyProjectUsersOfNewFolder', function () { - return this.EditorController._notifyProjectUsersOfNewFolders.calledWith( - this.project_id, - this.folders + it('should notifyProjectUsersOfNewFolder', function (ctx) { + return ctx.EditorController._notifyProjectUsersOfNewFolders.calledWith( + ctx.project_id, + ctx.folders ) }) - it('should return the folder in the callback', function () { - return this.callback - .calledWith(null, this.folders, this.folder) + it('should return the folder in the callback', function (ctx) { + return ctx.callback + .calledWith(null, ctx.folders, ctx.folder) .should.equal(true) }) }) describe('deleteEntity', function () { - beforeEach(function () { - this.entity_id = 'entity_id_here' - this.type = 'doc' - this.ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() - return this.EditorController.deleteEntity( - this.project_id, - this.entity_id, - this.type, - this.source, - this.user_id, - this.callback + beforeEach(function (ctx) { + ctx.entity_id = 'entity_id_here' + ctx.type = 'doc' + ctx.ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() + return ctx.EditorController.deleteEntity( + ctx.project_id, + ctx.entity_id, + ctx.type, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should delete the folder using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.deleteEntity + it('should delete the folder using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.deleteEntity .calledWith( - this.project_id, - this.entity_id, - this.type, - this.user_id, - this.source + ctx.project_id, + ctx.entity_id, + ctx.type, + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('notify users an entity has been deleted', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith( - this.project_id, - 'removeEntity', - this.entity_id, - this.source - ) + it('notify users an entity has been deleted', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'removeEntity', ctx.entity_id, ctx.source) .should.equal(true) }) }) describe('deleteEntityWithPath', function () { - beforeEach(function () { - this.entity_id = 'entity_id_here' - this.ProjectEntityUpdateHandler.deleteEntityWithPath = sinon + beforeEach(function (ctx) { + ctx.entity_id = 'entity_id_here' + ctx.ProjectEntityUpdateHandler.deleteEntityWithPath = sinon .stub() - .yields(null, this.entity_id) - this.path = 'folder1/folder2' - return this.EditorController.deleteEntityWithPath( - this.project_id, - this.path, - this.source, - this.user_id, - this.callback + .yields(null, ctx.entity_id) + ctx.path = 'folder1/folder2' + return ctx.EditorController.deleteEntityWithPath( + ctx.project_id, + ctx.path, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('should delete the folder using the project entity handler', function () { - return this.ProjectEntityUpdateHandler.deleteEntityWithPath - .calledWith(this.project_id, this.path, this.user_id, this.source) + it('should delete the folder using the project entity handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.deleteEntityWithPath + .calledWith(ctx.project_id, ctx.path, ctx.user_id, ctx.source) .should.equal(true) }) - it('notify users an entity has been deleted', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith( - this.project_id, - 'removeEntity', - this.entity_id, - this.source - ) + it('notify users an entity has been deleted', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'removeEntity', ctx.entity_id, ctx.source) .should.equal(true) }) }) describe('updateProjectDescription', function () { - beforeEach(function () { - this.description = 'new description' - return this.EditorController.updateProjectDescription( - this.project_id, - this.description, - this.callback + beforeEach(function (ctx) { + ctx.description = 'new description' + return ctx.EditorController.updateProjectDescription( + ctx.project_id, + ctx.description, + ctx.callback ) }) - it('should send the new description to the project details handler', function () { - return this.ProjectDetailsHandler.setProjectDescription - .calledWith(this.project_id, this.description) + it('should send the new description to the project details handler', function (ctx) { + return ctx.ProjectDetailsHandler.setProjectDescription + .calledWith(ctx.project_id, ctx.description) .should.equal(true) }) - it('should notify the other clients about the updated description', function () { - return this.EditorRealTimeController.emitToRoom + it('should notify the other clients about the updated description', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'projectDescriptionUpdated', - this.description + ctx.description ) .should.equal(true) }) }) describe('deleteProject', function () { - beforeEach(function () { - this.err = 'errro' - return (this.ProjectDeleter.deleteProject = sinon - .stub() - .callsArgWith(1, this.err)) + beforeEach(function (ctx) { + ctx.err = 'errro' + ctx.ProjectDeleter.deleteProject.callsArgWith(1, ctx.err) }) - it('should call the project handler', function (done) { - return this.EditorController.deleteProject(this.project_id, err => { - err.should.equal(this.err) - this.ProjectDeleter.deleteProject - .calledWith(this.project_id) - .should.equal(true) - return done() + it('should call the project handler', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.deleteProject(ctx.project_id, err => { + err.should.equal(ctx.err) + ctx.ProjectDeleter.deleteProject + .calledWith(ctx.project_id) + .should.equal(true) + resolve() + }) }) }) }) describe('renameEntity', function () { - beforeEach(function (done) { - this.entity_id = 'entity_id_here' - this.entityType = 'doc' - this.newName = 'bobsfile.tex' - this.ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.entity_id = 'entity_id_here' + ctx.entityType = 'doc' + ctx.newName = 'bobsfile.tex' + ctx.ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() - return this.EditorController.renameEntity( - this.project_id, - this.entity_id, - this.entityType, - this.newName, - this.user_id, - this.source, - done - ) + return ctx.EditorController.renameEntity( + ctx.project_id, + ctx.entity_id, + ctx.entityType, + ctx.newName, + ctx.user_id, + ctx.source, + resolve + ) + }) }) - it('should call the project handler', function () { - return this.ProjectEntityUpdateHandler.renameEntity + it('should call the project handler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.renameEntity .calledWith( - this.project_id, - this.entity_id, - this.entityType, - this.newName, - this.user_id, - this.source + ctx.project_id, + ctx.entity_id, + ctx.entityType, + ctx.newName, + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('should emit the update to the room', function () { - return this.EditorRealTimeController.emitToRoom + it('should emit the update to the room', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveEntityRename', - this.entity_id, - this.newName + ctx.entity_id, + ctx.newName ) .should.equal(true) }) }) describe('moveEntity', function () { - beforeEach(function () { - this.entity_id = 'entity_id_here' - this.entityType = 'doc' - this.ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() - return this.EditorController.moveEntity( - this.project_id, - this.entity_id, - this.folder_id, - this.entityType, - this.user_id, - this.source, - this.callback + beforeEach(function (ctx) { + ctx.entity_id = 'entity_id_here' + ctx.entityType = 'doc' + ctx.ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() + return ctx.EditorController.moveEntity( + ctx.project_id, + ctx.entity_id, + ctx.folder_id, + ctx.entityType, + ctx.user_id, + ctx.source, + ctx.callback ) }) - it('should call the ProjectEntityUpdateHandler', function () { - return this.ProjectEntityUpdateHandler.moveEntity + it('should call the ProjectEntityUpdateHandler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.moveEntity .calledWith( - this.project_id, - this.entity_id, - this.folder_id, - this.entityType, - this.user_id, - this.source + ctx.project_id, + ctx.entity_id, + ctx.folder_id, + ctx.entityType, + ctx.user_id, + ctx.source ) .should.equal(true) }) - it('should emit the update to the room', function () { - return this.EditorRealTimeController.emitToRoom + it('should emit the update to the room', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'reciveEntityMove', - this.entity_id, - this.folder_id + ctx.entity_id, + ctx.folder_id ) .should.equal(true) }) - it('calls the callback', function () { - return this.callback.called.should.equal(true) + it('calls the callback', function (ctx) { + return ctx.callback.called.should.equal(true) }) }) describe('renameProject', function () { - beforeEach(function () { - this.err = 'errro' - this.newName = 'new name here' - return this.EditorController.renameProject( - this.project_id, - this.newName, - this.callback + beforeEach(function (ctx) { + ctx.err = 'errro' + ctx.newName = 'new name here' + return ctx.EditorController.renameProject( + ctx.project_id, + ctx.newName, + ctx.callback ) }) - it('should call the EditorController', function () { - return this.ProjectDetailsHandler.renameProject - .calledWith(this.project_id, this.newName) + it('should call the EditorController', function (ctx) { + return ctx.ProjectDetailsHandler.renameProject + .calledWith(ctx.project_id, ctx.newName) .should.equal(true) }) - it('should emit the update to the room', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'projectNameUpdated', this.newName) + it('should emit the update to the room', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'projectNameUpdated', ctx.newName) .should.equal(true) }) }) describe('setCompiler', function () { - beforeEach(function () { - this.compiler = 'latex' - return this.EditorController.setCompiler( - this.project_id, - this.compiler, - this.callback + beforeEach(function (ctx) { + ctx.compiler = 'latex' + return ctx.EditorController.setCompiler( + ctx.project_id, + ctx.compiler, + ctx.callback ) }) - it('should send the new compiler and project id to the project options handler', function () { - this.ProjectOptionsHandler.setCompiler - .calledWith(this.project_id, this.compiler) + it('should send the new compiler and project id to the project options handler', function (ctx) { + ctx.ProjectOptionsHandler.setCompiler + .calledWith(ctx.project_id, ctx.compiler) .should.equal(true) - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'compilerUpdated', this.compiler) + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'compilerUpdated', ctx.compiler) .should.equal(true) }) }) describe('setImageName', function () { - beforeEach(function () { - this.imageName = 'texlive-1234.5' - return this.EditorController.setImageName( - this.project_id, - this.imageName, - this.callback + beforeEach(function (ctx) { + ctx.imageName = 'texlive-1234.5' + return ctx.EditorController.setImageName( + ctx.project_id, + ctx.imageName, + ctx.callback ) }) - it('should send the new imageName and project id to the project options handler', function () { - this.ProjectOptionsHandler.setImageName - .calledWith(this.project_id, this.imageName) + it('should send the new imageName and project id to the project options handler', function (ctx) { + ctx.ProjectOptionsHandler.setImageName + .calledWith(ctx.project_id, ctx.imageName) .should.equal(true) - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'imageNameUpdated', this.imageName) + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'imageNameUpdated', ctx.imageName) .should.equal(true) }) }) describe('setSpellCheckLanguage', function () { - beforeEach(function () { - this.languageCode = 'fr' - return this.EditorController.setSpellCheckLanguage( - this.project_id, - this.languageCode, - this.callback + beforeEach(function (ctx) { + ctx.languageCode = 'fr' + return ctx.EditorController.setSpellCheckLanguage( + ctx.project_id, + ctx.languageCode, + ctx.callback ) }) - it('should send the new languageCode and project id to the project options handler', function () { - this.ProjectOptionsHandler.setSpellCheckLanguage - .calledWith(this.project_id, this.languageCode) + it('should send the new languageCode and project id to the project options handler', function (ctx) { + ctx.ProjectOptionsHandler.setSpellCheckLanguage + .calledWith(ctx.project_id, ctx.languageCode) .should.equal(true) - return this.EditorRealTimeController.emitToRoom + return ctx.EditorRealTimeController.emitToRoom .calledWith( - this.project_id, + ctx.project_id, 'spellCheckLanguageUpdated', - this.languageCode + ctx.languageCode ) .should.equal(true) }) @@ -877,212 +880,210 @@ describe('EditorController', function () { describe('setPublicAccessLevel', function () { describe('when setting to private', function () { - beforeEach(function () { - this.newAccessLevel = 'private' - this.ProjectDetailsHandler.ensureTokensArePresent = sinon - .stub() - .yields() - return this.EditorController.setPublicAccessLevel( - this.project_id, - this.newAccessLevel, - this.callback + beforeEach(function (ctx) { + ctx.newAccessLevel = 'private' + ctx.ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields() + return ctx.EditorController.setPublicAccessLevel( + ctx.project_id, + ctx.newAccessLevel, + ctx.callback ) }) - it('should set the access level', function () { - return this.ProjectDetailsHandler.setPublicAccessLevel - .calledWith(this.project_id, this.newAccessLevel) + it('should set the access level', function (ctx) { + return ctx.ProjectDetailsHandler.setPublicAccessLevel + .calledWith(ctx.project_id, ctx.newAccessLevel) .should.equal(true) }) - it('should broadcast the access level change', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'project:publicAccessLevel:changed') + it('should broadcast the access level change', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'project:publicAccessLevel:changed') .should.equal(true) }) - it('should not ensure tokens are present for project', function () { - return this.ProjectDetailsHandler.ensureTokensArePresent - .calledWith(this.project_id) + it('should not ensure tokens are present for project', function (ctx) { + return ctx.ProjectDetailsHandler.ensureTokensArePresent + .calledWith(ctx.project_id) .should.equal(false) }) }) describe('when setting to tokenBased', function () { - beforeEach(function () { - this.newAccessLevel = 'tokenBased' - this.tokens = { readOnly: 'aaa', readAndWrite: '42bbb' } - this.ProjectDetailsHandler.ensureTokensArePresent = sinon - .stub() - .yields() - return this.EditorController.setPublicAccessLevel( - this.project_id, - this.newAccessLevel, - this.callback + beforeEach(function (ctx) { + ctx.newAccessLevel = 'tokenBased' + ctx.tokens = { readOnly: 'aaa', readAndWrite: '42bbb' } + ctx.ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields() + return ctx.EditorController.setPublicAccessLevel( + ctx.project_id, + ctx.newAccessLevel, + ctx.callback ) }) - it('should set the access level', function () { - return this.ProjectDetailsHandler.setPublicAccessLevel - .calledWith(this.project_id, this.newAccessLevel) + it('should set the access level', function (ctx) { + return ctx.ProjectDetailsHandler.setPublicAccessLevel + .calledWith(ctx.project_id, ctx.newAccessLevel) .should.equal(true) }) - it('should broadcast the access level change', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'project:publicAccessLevel:changed') + it('should broadcast the access level change', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'project:publicAccessLevel:changed') .should.equal(true) }) - it('should ensure tokens are present for project', function () { - return this.ProjectDetailsHandler.ensureTokensArePresent - .calledWith(this.project_id) + it('should ensure tokens are present for project', function (ctx) { + return ctx.ProjectDetailsHandler.ensureTokensArePresent + .calledWith(ctx.project_id) .should.equal(true) }) }) }) describe('setRootDoc', function () { - beforeEach(function () { - this.newRootDocID = '21312321321' - this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() - return this.EditorController.setRootDoc( - this.project_id, - this.newRootDocID, - this.callback + beforeEach(function (ctx) { + ctx.newRootDocID = '21312321321' + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() + return ctx.EditorController.setRootDoc( + ctx.project_id, + ctx.newRootDocID, + ctx.callback ) }) - it('should call the ProjectEntityUpdateHandler', function () { - return this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.newRootDocID) + it('should call the ProjectEntityUpdateHandler', function (ctx) { + return ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.newRootDocID) .should.equal(true) }) - it('should emit the update to the room', function () { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'rootDocUpdated', this.newRootDocID) + it('should emit the update to the room', function (ctx) { + return ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.project_id, 'rootDocUpdated', ctx.newRootDocID) .should.equal(true) }) }) describe('setMainBibliographyDoc', function () { describe('on success', function () { - beforeEach(function (done) { - this.mainBibliographyId = 'bib-doc-id' - this.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon - .stub() - .yields() + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.mainBibliographyId = 'bib-doc-id' + ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon + .stub() + .yields() - this.callback = sinon.stub().callsFake(done) - this.EditorController.setMainBibliographyDoc( - this.project_id, - this.mainBibliographyId, - this.callback - ) + ctx.callback = sinon.stub().callsFake(resolve) + ctx.EditorController.setMainBibliographyDoc( + ctx.project_id, + ctx.mainBibliographyId, + ctx.callback + ) + }) }) - it('should forward the call to the ProjectEntityUpdateHandler', function () { + it('should forward the call to the ProjectEntityUpdateHandler', function (ctx) { expect( - this.ProjectEntityUpdateHandler.setMainBibliographyDoc - ).to.have.been.calledWith(this.project_id, this.mainBibliographyId) + ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc + ).to.have.been.calledWith(ctx.project_id, ctx.mainBibliographyId) }) - it('should emit the update to the room', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project_id, + it('should emit the update to the room', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project_id, 'mainBibliographyDocUpdated', - this.mainBibliographyId + ctx.mainBibliographyId ) }) - it('should return nothing', function () { - expect(this.callback).to.have.been.calledWithExactly() + it('should return nothing', function (ctx) { + expect(ctx.callback).to.have.been.calledWithExactly() }) }) describe('on error', function () { - beforeEach(function (done) { - this.mainBibliographyId = 'bib-doc-id' - this.error = new Error('oh no') - this.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon - .stub() - .yields(this.error) + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.mainBibliographyId = 'bib-doc-id' + ctx.error = new Error('oh no') + ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon + .stub() + .yields(ctx.error) - this.callback = sinon.stub().callsFake(() => done()) - this.EditorController.setMainBibliographyDoc( - this.project_id, - this.mainBibliographyId, - this.callback - ) + ctx.callback = sinon.stub().callsFake(() => resolve()) + ctx.EditorController.setMainBibliographyDoc( + ctx.project_id, + ctx.mainBibliographyId, + ctx.callback + ) + }) }) - it('should forward the call to the ProjectEntityUpdateHandler', function () { + it('should forward the call to the ProjectEntityUpdateHandler', function (ctx) { expect( - this.ProjectEntityUpdateHandler.setMainBibliographyDoc - ).to.have.been.calledWith(this.project_id, this.mainBibliographyId) + ctx.ProjectEntityUpdateHandler.setMainBibliographyDoc + ).to.have.been.calledWith(ctx.project_id, ctx.mainBibliographyId) }) - it('should return the error', function () { - expect(this.callback).to.have.been.calledWithExactly(this.error) + it('should return the error', function (ctx) { + expect(ctx.callback).to.have.been.calledWithExactly(ctx.error) }) - it('should not emit the update to the room', function () { - expect(this.EditorRealTimeController.emitToRoom).to.not.have.been.called + it('should not emit the update to the room', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.not.have.been.called }) }) }) describe('appendToDoc', function () { describe('on success', function () { - beforeEach(function () { - this.docId = 'doc-1' - this.ProjectEntityUpdateHandler.appendToDoc = sinon + beforeEach(function (ctx) { + ctx.docId = 'doc-1' + ctx.ProjectEntityUpdateHandler.appendToDoc = sinon .stub() .yields(null, { rev: '1' }) - this.EditorController.appendToDoc( - this.project_id, - this.docId, - this.docLines, - this.source, - this.user_id, - this.callback + ctx.EditorController.appendToDoc( + ctx.project_id, + ctx.docId, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('appends to the doc using the project entity handler', function () { - this.ProjectEntityUpdateHandler.appendToDoc - .calledWith(this.project_id, this.docId, this.docLines, this.source) + it('appends to the doc using the project entity handler', function (ctx) { + ctx.ProjectEntityUpdateHandler.appendToDoc + .calledWith(ctx.project_id, ctx.docId, ctx.docLines, ctx.source) .should.equal(true) }) }) describe('on error', function () { - beforeEach(function () { - this.docId = 'doc-1' - this.ProjectEntityUpdateHandler.appendToDoc = sinon + beforeEach(function (ctx) { + ctx.docId = 'doc-1' + ctx.ProjectEntityUpdateHandler.appendToDoc = sinon .stub() .yields(new Error('foo')) - this.EditorController.appendToDoc( - this.project_id, - this.docId, - this.docLines, - this.source, - this.user_id, - this.callback + ctx.EditorController.appendToDoc( + ctx.project_id, + ctx.docId, + ctx.docLines, + ctx.source, + ctx.user_id, + ctx.callback ) }) - it('tries to append to the doc using the project entity handler', function () { - this.ProjectEntityUpdateHandler.appendToDoc - .calledWith(this.project_id, this.docId, this.docLines, this.source) + it('tries to append to the doc using the project entity handler', function (ctx) { + ctx.ProjectEntityUpdateHandler.appendToDoc + .calledWith(ctx.project_id, ctx.docId, ctx.docLines, ctx.source) .should.equal(true) }) - it('tags the error', function () { - this.callback.calledWith(sinon.match.instanceOf(OError)) + it('tags the error', function (ctx) { + ctx.callback.calledWith(sinon.match.instanceOf(OError)) }) }) }) diff --git a/services/web/test/unit/src/Editor/EditorHttpController.test.mjs b/services/web/test/unit/src/Editor/EditorHttpController.test.mjs index 3bea466169..d52d284dd7 100644 --- a/services/web/test/unit/src/Editor/EditorHttpController.test.mjs +++ b/services/web/test/unit/src/Editor/EditorHttpController.test.mjs @@ -168,9 +168,12 @@ describe('EditorHttpController', function () { vi.mock('../../../../app/src/Features/Errors/Errors.js', () => vi.importActual('../../../../app/src/Features/Errors/Errors.js') ) - vi.doMock('../../../../app/src/Features/Project/ProjectDeleter.mjs', () => ({ - default: ctx.ProjectDeleter, - })) + vi.doMock( + '../../../../app/src/Features/Project/ProjectDeleter.mjs', + () => ({ + default: ctx.ProjectDeleter, + }) + ) vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({ default: ctx.ProjectGetter, })) diff --git a/services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs b/services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs index ae0c1d5fa1..ef8801851b 100644 --- a/services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs +++ b/services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs @@ -1,13 +1,13 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const SandboxedModule = require('sandboxed-module') +import { beforeEach, describe, it, vi, expect } from 'vitest' +import sinon from 'sinon' -const MODULE_PATH = '../../../../app/src/Features/FileStore/FileStoreHandler.js' +const MODULE_PATH = + '../../../../app/src/Features/FileStore/FileStoreHandler.mjs' describe('FileStoreHandler', function () { - beforeEach(function () { - this.fileSize = 999 - this.fs = { + beforeEach(async function (ctx) { + ctx.fileSize = 999 + ctx.fs = { createReadStream: sinon.stub(), lstat: sinon.stub().callsArgWith(1, null, { isFile() { @@ -16,10 +16,10 @@ describe('FileStoreHandler', function () { isDirectory() { return false }, - size: this.fileSize, + size: ctx.fileSize, }), } - this.writeStream = { + ctx.writeStream = { my: 'writeStream', on(type, fn) { if (type === 'response') { @@ -27,25 +27,24 @@ describe('FileStoreHandler', function () { } }, } - this.readStream = { my: 'readStream', on: sinon.stub() } - this.request = sinon.stub() - this.request.head = sinon.stub() - this.filestoreUrl = 'http://filestore.overleaf.test' - this.settings = { - apis: { filestore: { url: this.filestoreUrl } }, + ctx.readStream = { my: 'readStream', on: sinon.stub() } + ctx.request = sinon.stub() + ctx.request.head = sinon.stub() + ctx.filestoreUrl = 'http://filestore.overleaf.test' + ctx.settings = { + apis: { filestore: { url: ctx.filestoreUrl } }, } - this.hashValue = '0123456789' - this.fileArgs = { name: 'upload-filename' } - this.fileId = 'file_id_here' - this.projectId = '1312312312' - this.historyId = 123 - this.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' - this.fsPath = 'uploads/myfile.eps' - this.getFileUrl = (projectId, fileId) => - `${this.filestoreUrl}/project/${projectId}/file/${fileId}` - this.getProjectUrl = projectId => - `${this.filestoreUrl}/project/${projectId}` - this.FileModel = class File { + ctx.hashValue = '0123456789' + ctx.fileArgs = { name: 'upload-filename' } + ctx.fileId = 'file_id_here' + ctx.projectId = '1312312312' + ctx.historyId = 123 + ctx.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' + ctx.fsPath = 'uploads/myfile.eps' + ctx.getFileUrl = (projectId, fileId) => + `${ctx.filestoreUrl}/project/${projectId}/file/${fileId}` + ctx.getProjectUrl = projectId => `${ctx.filestoreUrl}/project/${projectId}` + ctx.FileModel = class File { constructor(options) { ;({ name: this.name, hash: this.hash } = options) this._id = 'file_id_here' @@ -55,53 +54,75 @@ describe('FileStoreHandler', function () { } } } - this.FileHashManager = { - computeHash: sinon.stub().callsArgWith(1, null, this.hashValue), + ctx.FileHashManager = { + computeHash: sinon.stub().callsArgWith(1, null, ctx.hashValue), } - this.HistoryManager = { + ctx.HistoryManager = { uploadBlobFromDisk: sinon.stub().callsArg(4), } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { getDetails: sinon.stub().callsArgWith(1, null, { - overleaf: { history: { id: this.historyId } }, + overleaf: { history: { id: ctx.historyId } }, }), } - this.Features = { + ctx.Features = { hasFeature: sinon.stub(), } - this.Modules = { + ctx.Modules = { hooks: { fire: sinon.stub().callsArgWith(2, null), }, } - this.handler = SandboxedModule.require(MODULE_PATH, { - requires: { - '@overleaf/settings': this.settings, - request: this.request, - '../History/HistoryManager': this.HistoryManager, - '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler, - './FileHashManager': this.FileHashManager, - '../../infrastructure/Features': this.Features, - '../../infrastructure/Modules': this.Modules, - // FIXME: need to stub File object here - '../../models/File': { - File: this.FileModel, - }, - fs: this.fs, - }, - }) + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('request', () => ({ + default: ctx.request, + })) + + vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({ + default: ctx.HistoryManager, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/FileStore/FileHashManager', () => ({ + default: ctx.FileHashManager, + })) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock('../../../../app/src/models/File', () => ({ + File: ctx.FileModel, + })) + + vi.doMock('node:fs', () => ({ default: ctx.fs })) + + ctx.handler = (await import(MODULE_PATH)).default }) describe('uploadFileFromDisk', function () { - beforeEach(function () { - this.request.returns(this.writeStream) + beforeEach(function (ctx) { + ctx.request.returns(ctx.writeStream) }) - it('should get the project details', async function () { - this.fs.createReadStream.returns({ + it('should get the project details', async function (ctx) { + ctx.fs.createReadStream.returns({ pipe() {}, on(type, cb) { if (type === 'open') { @@ -109,18 +130,18 @@ describe('FileStoreHandler', function () { } }, }) - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - this.ProjectDetailsHandler.getDetails - .calledWith(this.projectId) + ctx.ProjectDetailsHandler.getDetails + .calledWith(ctx.projectId) .should.equal(true) }) - it('should compute the file hash', async function () { - this.fs.createReadStream.returns({ + it('should compute the file hash', async function (ctx) { + ctx.fs.createReadStream.returns({ pipe() {}, on(type, cb) { if (type === 'open') { @@ -128,18 +149,16 @@ describe('FileStoreHandler', function () { } }, }) - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - this.FileHashManager.computeHash - .calledWith(this.fsPath) - .should.equal(true) + ctx.FileHashManager.computeHash.calledWith(ctx.fsPath).should.equal(true) }) - it('should call the preUploadFile hook', async function () { - this.fs.createReadStream.returns({ + it('should call the preUploadFile hook', async function (ctx) { + ctx.fs.createReadStream.returns({ pipe() {}, on(type, cb) { if (type === 'open') { @@ -147,24 +166,24 @@ describe('FileStoreHandler', function () { } }, }) - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - this.Modules.hooks.fire + ctx.Modules.hooks.fire .calledWith('preUploadFile', { - projectId: this.projectId, - historyId: this.historyId, - fileArgs: this.fileArgs, - fsPath: this.fsPath, - size: this.fileSize, + projectId: ctx.projectId, + historyId: ctx.historyId, + fileArgs: ctx.fileArgs, + fsPath: ctx.fsPath, + size: ctx.fileSize, }) .should.equal(true) }) - it('should upload the file to the history store as a blob', async function () { - this.fs.createReadStream.returns({ + it('should upload the file to the history store as a blob', async function (ctx) { + ctx.fs.createReadStream.returns({ pipe() {}, on(type, cb) { if (type === 'open') { @@ -172,37 +191,37 @@ describe('FileStoreHandler', function () { } }, }) - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - this.HistoryManager.uploadBlobFromDisk - .calledWith(this.historyId, this.hashValue, this.fileSize, this.fsPath) + ctx.HistoryManager.uploadBlobFromDisk + .calledWith(ctx.historyId, ctx.hashValue, ctx.fileSize, ctx.fsPath) .should.equal(true) }) - it('should not open file handle', async function () { - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + it('should not open file handle', async function (ctx) { + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - expect(this.fs.createReadStream).to.not.have.been.called + expect(ctx.fs.createReadStream).to.not.have.been.called }) - it('should not talk to filestore', async function () { - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + it('should not talk to filestore', async function (ctx) { + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - expect(this.request).to.not.have.been.called + expect(ctx.request).to.not.have.been.called }) - it('should call the postUploadFile hook', async function () { - this.fs.createReadStream.returns({ + it('should call the postUploadFile hook', async function (ctx) { + ctx.fs.createReadStream.returns({ pipe() {}, on(type, cb) { if (type === 'open') { @@ -210,33 +229,33 @@ describe('FileStoreHandler', function () { } }, }) - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - this.Modules.hooks.fire + ctx.Modules.hooks.fire .calledWith('postUploadFile', { - projectId: this.projectId, - fileRef: sinon.match.instanceOf(this.FileModel), - size: this.fileSize, + projectId: ctx.projectId, + fileRef: sinon.match.instanceOf(ctx.FileModel), + size: ctx.fileSize, }) .should.equal(true) }) - it('should resolve with the url and fileRef', async function () { - const { fileRef } = await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + it('should resolve with the url and fileRef', async function (ctx) { + const { fileRef } = await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) - expect(fileRef._id).to.equal(this.fileId) - expect(fileRef.hash).to.equal(this.hashValue) + expect(fileRef._id).to.equal(ctx.fileId) + expect(fileRef.hash).to.equal(ctx.hashValue) }) describe('symlink', function () { - it('should not read file if it is symlink', async function () { - this.fs.lstat = sinon.stub().callsArgWith(1, null, { + it('should not read file if it is symlink', async function (ctx) { + ctx.fs.lstat = sinon.stub().callsArgWith(1, null, { isFile() { return false }, @@ -248,10 +267,10 @@ describe('FileStoreHandler', function () { let error try { - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) } catch (err) { error = err @@ -259,18 +278,18 @@ describe('FileStoreHandler', function () { expect(error).to.exist - this.fs.createReadStream.called.should.equal(false) + ctx.fs.createReadStream.called.should.equal(false) }) - it('should not read file stat returns nothing', async function () { - this.fs.lstat = sinon.stub().callsArgWith(1, null, null) + it('should not read file stat returns nothing', async function (ctx) { + ctx.fs.lstat = sinon.stub().callsArgWith(1, null, null) let error try { - await this.handler.promises.uploadFileFromDisk( - this.projectId, - this.fileArgs, - this.fsPath + await ctx.handler.promises.uploadFileFromDisk( + ctx.projectId, + ctx.fileArgs, + ctx.fsPath ) } catch (err) { error = err @@ -278,7 +297,7 @@ describe('FileStoreHandler', function () { expect(error).to.exist - this.fs.createReadStream.called.should.equal(false) + ctx.fs.createReadStream.called.should.equal(false) }) }) }) diff --git a/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs b/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs index 691bd20dd1..9ffd126363 100644 --- a/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs +++ b/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs @@ -1,32 +1,35 @@ -const { expect } = require('chai') -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const MockRequest = require('../helpers/MockRequest') -const MockResponse = require('../helpers/MockResponse') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import MockRequest from '../helpers/MockRequest.js' +import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Helpers/AdminAuthorizationHelper' describe('AdminAuthorizationHelper', function () { - beforeEach(function () { - this.fireHook = sinon.stub().resolves([]) - this.settings = { + beforeEach(async function (ctx) { + ctx.fireHook = sinon.stub().resolves([]) + ctx.settings = { adminPrivilegeAvailable: true, adminUrl: 'https://admin.overleaf.com', adminRolesEnabled: true, } - this.AdminAuthorizationHelper = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/settings': this.settings, - '../../infrastructure/Modules': { - promises: { - hooks: { - fire: this.fireHook, - }, + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: { + promises: { + hooks: { + fire: ctx.fireHook, }, }, }, - }) + })) + + ctx.AdminAuthorizationHelper = (await import(modulePath)).default }) describe('getAdminCapabilities', function () { describe('when modules return capabilities', function () { @@ -34,9 +37,9 @@ describe('AdminAuthorizationHelper', function () { const module1Capabilities = ['capability1', 'capability2'] const module2Capabilities = ['capability2', 'capability3'] - beforeEach(async function () { - this.fireHook.resolves([module1Capabilities, module2Capabilities]) - result = await this.AdminAuthorizationHelper.getAdminCapabilities({}) + beforeEach(async function (ctx) { + ctx.fireHook.resolves([module1Capabilities, module2Capabilities]) + result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({}) }) it('returns true for adminCapabilitiesAvailable', async function () { expect(result.adminCapabilitiesAvailable).to.be.true @@ -49,8 +52,8 @@ describe('AdminAuthorizationHelper', function () { }) describe('when no module returns capabilities', function () { let result - beforeEach(async function () { - result = await this.AdminAuthorizationHelper.getAdminCapabilities({}) + beforeEach(async function (ctx) { + result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({}) }) it('returns false for adminCapabilitiesAvailable', function () { @@ -64,204 +67,203 @@ describe('AdminAuthorizationHelper', function () { describe('useAdminCapabilities', function () { describe('when admin capabilities are not available', function () { describe('user is null', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.req.session = { + ctx.req.session = { user: null, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('does not define adminCapabilitiesAvailable on req', function () { - expect(this.req).not.to.have.property('adminCapabilitiesAvailable') + it('does not define adminCapabilitiesAvailable on req', function (ctx) { + expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable') }) - it('defines adminCapabilities as an empty array on req', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.be.empty + it('defines adminCapabilities as an empty array on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.be.empty }) }) describe('user is not an admin', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.user = { + ctx.user = { isAdmin: false, } - this.req.session = { - user: this.user, + ctx.req.session = { + user: ctx.user, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('does not define adminCapabilitiesAvailable on req', function () { - expect(this.req).not.to.have.property('adminCapabilitiesAvailable') + it('does not define adminCapabilitiesAvailable on req', function (ctx) { + expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable') }) - it('defines adminCapabilities as an empty array on req', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.be.empty + it('defines adminCapabilities as an empty array on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.be.empty }) }) describe('user is an admin', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.user = { + ctx.user = { isAdmin: true, } - this.req.session = { - user: this.user, + ctx.req.session = { + user: ctx.user, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('defines adminCapabilitiesAvailable as false on req', function () { - expect(this.req).to.have.property('adminCapabilitiesAvailable', false) + it('defines adminCapabilitiesAvailable as false on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilitiesAvailable', false) }) - it('defines adminCapabilities as an empty array', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.be.empty + it('defines adminCapabilities as an empty array', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.be.empty }) }) }) describe('when admin capabilities are available', function () { - beforeEach(function () { - this.fireHook.resolves(['capability1', 'capability2']) + beforeEach(function (ctx) { + ctx.fireHook.resolves(['capability1', 'capability2']) }) describe('user is not an admin', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.user = { + ctx.user = { isAdmin: false, } - this.req.session = { - user: this.user, + ctx.req.session = { + user: ctx.user, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('does not define adminCapabilitiesAvailable on req', function () { - expect(this.req).not.to.have.property('adminCapabilitiesAvailable') + it('does not define adminCapabilitiesAvailable on req', function (ctx) { + expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable') }) - it('defines adminCapabilities as an empty array on req', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.be.empty + it('defines adminCapabilities as an empty array on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.be.empty }) }) describe('user is an admin', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.user = { + ctx.user = { isAdmin: true, } - this.req.session = { - user: this.user, + ctx.req.session = { + user: ctx.user, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('defines adminCapabilitiesAvailable as true on req', function () { - expect(this.req).to.have.property('adminCapabilitiesAvailable', true) + it('defines adminCapabilitiesAvailable as true on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true) }) - it('defines adminCapabilities with the capabilities returned from modules', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.include('capability1') - expect(this.req.adminCapabilities).to.include('capability2') + it('defines adminCapabilities with the capabilities returned from modules', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.include('capability1') + expect(ctx.req.adminCapabilities).to.include('capability2') }) }) }) describe('when getting capabilities from modules throws an error', function () { - beforeEach(async function () { - this.fireHook.rejects(new Error('Module error')) + beforeEach(async function (ctx) { + ctx.fireHook.rejects(new Error('Module error')) - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() - this.user = { + ctx.user = { isAdmin: true, } - this.req.logger = { + ctx.req.logger = { warn: sinon.stub(), } - this.req.session = { - user: this.user, + ctx.req.session = { + user: ctx.user, } - await this.AdminAuthorizationHelper.useAdminCapabilities( - this.req, - this.res, - this.next + await ctx.AdminAuthorizationHelper.useAdminCapabilities( + ctx.req, + ctx.res, + ctx.next ) }) - it('logs the error', function () { - expect(this.logger.warn).to.have.been.calledWith( - sinon.match.has('err', sinon.match.instanceOf(Error)) - ) + it('logs the error', function (ctx) { + expect(ctx.logger.warn).toHaveBeenCalled() + expect(ctx.logger.warn.mock.calls[0][0].err).toBeInstanceOf(Error) }) - it('defines adminCapabilitiesAvailable as true on req', function () { - expect(this.req).to.have.property('adminCapabilitiesAvailable', true) + it('defines adminCapabilitiesAvailable as true on req', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true) }) - it('defines adminCapabilities as an empty array', function () { - expect(this.req).to.have.property('adminCapabilities') - expect(this.req.adminCapabilities).to.be.an('array') - expect(this.req.adminCapabilities).to.be.empty + it('defines adminCapabilities as an empty array', function (ctx) { + expect(ctx.req).to.have.property('adminCapabilities') + expect(ctx.req.adminCapabilities).to.be.an('array') + expect(ctx.req.adminCapabilities).to.be.empty }) }) }) describe('useHasAdminCapability', function () { - it('adds hasAdminCapability to res.locals', function () { + it('adds hasAdminCapability to res.locals', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals).to.have.property('hasAdminCapability') expect(res.locals.hasAdminCapability).to.be.a('function') @@ -269,7 +271,7 @@ describe('AdminAuthorizationHelper', function () { describe('when the user is not an admin', function () { describe('when req.adminCapabilitiesAvailable is true', function () { - it('returns false for any capability', function () { + it('returns false for any capability', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() @@ -279,14 +281,14 @@ describe('AdminAuthorizationHelper', function () { req.session.user = { isAdmin: false } - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals.hasAdminCapability('capability1')).to.be.false }) }) describe('when req.adminCapabilitiesAvailable is false', function () { - it('returns false for any capability', function () { + it('returns false for any capability', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() @@ -296,21 +298,21 @@ describe('AdminAuthorizationHelper', function () { req.session.user = { isAdmin: false } - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals.hasAdminCapability('capability1')).to.be.false }) }) describe('when req.adminCapabilitiesAvailable is undefined', function () { - it('returns false for any capability', function () { + it('returns false for any capability', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() req.session.user = { isAdmin: false } - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals.hasAdminCapability('capability1')).to.be.false }) @@ -319,7 +321,7 @@ describe('AdminAuthorizationHelper', function () { describe('user is an admin', function () { describe('when req.adminCapabilitiesAvailable is false', function () { - it('returns true for any capability', function () { + it('returns true for any capability', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() @@ -327,21 +329,21 @@ describe('AdminAuthorizationHelper', function () { req.session.user = { isAdmin: true } req.adminCapabilitiesAvailable = false - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals.hasAdminCapability('capability1')).to.be.true }) }) describe('when req.adminCapabilitiesAvailable is undefined', function () { - it('returns true for any capability', function () { + it('returns true for any capability', function (ctx) { const req = new MockRequest() const res = new MockResponse() const next = sinon.stub() req.session.user = { isAdmin: true } - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) expect(res.locals.hasAdminCapability('capability1')).to.be.true }) @@ -349,7 +351,7 @@ describe('AdminAuthorizationHelper', function () { describe('when req.adminCapabilitiesAvailable is true', function () { let req, res, next - beforeEach(function () { + beforeEach(function (ctx) { req = new MockRequest() res = new MockResponse() next = sinon.stub() @@ -358,7 +360,7 @@ describe('AdminAuthorizationHelper', function () { req.adminCapabilitiesAvailable = true req.adminCapabilities = ['capability1', 'capability2'] - this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) + ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next) }) it('returns true for a capability the user has', function () { @@ -373,20 +375,20 @@ describe('AdminAuthorizationHelper', function () { }) describe('hasAdminCapability', function () { describe('when user is not an admin', function () { - it('returns false', function () { + it('returns false', function (ctx) { const req = { session: { user: { isAdmin: false }, }, } expect( - this.AdminAuthorizationHelper.hasAdminCapability('capability')(req) + ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req) ).to.be.false }) }) describe('when user is an admin', function () { describe('when adminCapabilitiesAvailable is falsey', function () { - it('returns true', function () { + it('returns true', function (ctx) { const req = { session: { user: { isAdmin: true }, @@ -394,22 +396,22 @@ describe('AdminAuthorizationHelper', function () { adminCapabilitiesAvailable: false, } expect( - this.AdminAuthorizationHelper.hasAdminCapability('capability')(req) + ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req) ).to.be.true }) - it('ignores the "requireAdminRoles" argument', function () { + it('ignores the "requireAdminRoles" argument', function (ctx) { const req = { session: { user: { isAdmin: true } }, adminCapabilitiesAvailable: false, } expect( - this.AdminAuthorizationHelper.hasAdminCapability( + ctx.AdminAuthorizationHelper.hasAdminCapability( 'capability', true )(req) ).to.be.true expect( - this.AdminAuthorizationHelper.hasAdminCapability( + ctx.AdminAuthorizationHelper.hasAdminCapability( 'capability', false )(req) @@ -418,30 +420,26 @@ describe('AdminAuthorizationHelper', function () { }) describe('when adminCapabilitiesAvailable is true', function () { describe('when user has the requested capability', function () { - it('returns true', function () { + it('returns true', function (ctx) { const req = { session: { user: { isAdmin: true } }, adminCapabilitiesAvailable: true, adminCapabilities: ['capability'], } expect( - this.AdminAuthorizationHelper.hasAdminCapability('capability')( - req - ) + ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req) ).to.be.true }) }) describe('when user does not have the requested capability', function () { - it('returns false', function () { + it('returns false', function (ctx) { const req = { session: { user: { isAdmin: true } }, adminCapabilitiesAvailable: true, adminCapabilities: ['other-capability'], } expect( - this.AdminAuthorizationHelper.hasAdminCapability('capability')( - req - ) + ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req) ).to.be.false }) }) @@ -449,26 +447,26 @@ describe('AdminAuthorizationHelper', function () { }) describe('when admin roles are not enabled', function () { - beforeEach(function () { - this.settings.adminRolesEnabled = false + beforeEach(function (ctx) { + ctx.settings.adminRolesEnabled = false }) - it('returns false even for admins', function () { + it('returns false even for admins', function (ctx) { const req = { session: { user: { isAdmin: true } } } expect( - this.AdminAuthorizationHelper.hasAdminCapability('capability')(req) + ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req) ).to.be.false expect( - this.AdminAuthorizationHelper.hasAdminCapability( + ctx.AdminAuthorizationHelper.hasAdminCapability( 'capability', true )(req) ).to.be.false }) - it('returns true when requireAdminRoles=false', function () { + it('returns true when requireAdminRoles=false', function (ctx) { const req = { session: { user: { isAdmin: true } } } expect( - this.AdminAuthorizationHelper.hasAdminCapability( + ctx.AdminAuthorizationHelper.hasAdminCapability( 'capability', false )(req) diff --git a/services/web/test/unit/src/History/HistoryController.test.mjs b/services/web/test/unit/src/History/HistoryController.test.mjs index f7e033f6d4..f2c6d30de8 100644 --- a/services/web/test/unit/src/History/HistoryController.test.mjs +++ b/services/web/test/unit/src/History/HistoryController.test.mjs @@ -82,9 +82,12 @@ describe('HistoryController', function () { }) ) - vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({ - default: ctx.HistoryManager, - })) + vi.doMock( + '../../../../app/src/Features/History/HistoryManager.mjs', + () => ({ + default: ctx.HistoryManager, + }) + ) vi.doMock( '../../../../app/src/Features/Project/ProjectDetailsHandler.mjs', diff --git a/services/web/test/unit/src/History/HistoryManagerTests.mjs b/services/web/test/unit/src/History/HistoryManagerTests.mjs index b38984c75e..a4e0efe5a4 100644 --- a/services/web/test/unit/src/History/HistoryManagerTests.mjs +++ b/services/web/test/unit/src/History/HistoryManagerTests.mjs @@ -1,12 +1,14 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') -const { +import { expect } from 'chai' +import sinon from 'sinon' +import SandboxedModule from 'sandboxed-module' +import mongodb from 'mongodb-legacy' +import { cleanupTestDatabase, db, waitForDb, -} = require('../../../../app/src/infrastructure/mongodb') +} from '../../../../app/src/infrastructure/mongodb.js' + +const { ObjectId } = mongodb const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager' diff --git a/services/web/test/unit/src/History/RestoreManager.test.mjs b/services/web/test/unit/src/History/RestoreManager.test.mjs index eb1f54bd40..d7add704e7 100644 --- a/services/web/test/unit/src/History/RestoreManager.test.mjs +++ b/services/web/test/unit/src/History/RestoreManager.test.mjs @@ -23,61 +23,64 @@ describe('RestoreManager', function () { default: Errors, })) - vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({ - default: (ctx.HistoryManager = { - promises: { - getContentAtVersion: sinon.stub().resolves({ - // Raw snapshot data that will be passed to Snapshot.fromRaw - files: { - 'main.tex': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - editorId: 'test-editor', + vi.doMock( + '../../../../app/src/Features/History/HistoryManager.mjs', + () => ({ + default: (ctx.HistoryManager = { + promises: { + getContentAtVersion: sinon.stub().resolves({ + // Raw snapshot data that will be passed to Snapshot.fromRaw + files: { + 'main.tex': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + editorId: 'test-editor', + }, + }, + 'foo.tex': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + editorId: 'test-editor', + }, + }, + 'folder/file.tex': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + editorId: 'test-editor', + }, + }, + 'foo.png': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + provider: 'bar', + }, + }, + 'linkedFile.bib': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + provider: 'mendeley', + }, + }, + 'withMainTrue.tex': { + hash: 'abcdef1234567890abcdef1234567890abcdef12', + stringLength: 100, + metadata: { + main: true, + }, }, }, - 'foo.tex': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - editorId: 'test-editor', - }, - }, - 'folder/file.tex': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - editorId: 'test-editor', - }, - }, - 'foo.png': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - provider: 'bar', - }, - }, - 'linkedFile.bib': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - provider: 'mendeley', - }, - }, - 'withMainTrue.tex': { - hash: 'abcdef1234567890abcdef1234567890abcdef12', - stringLength: 100, - metadata: { - main: true, - }, - }, - }, - timestamp: new Date().toISOString(), - }), - requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }), - }, - }), - })) + timestamp: new Date().toISOString(), + }), + requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }), + }, + }), + }) + ) vi.doMock('../../../../app/src/infrastructure/Metrics.js', () => ({ default: { diff --git a/services/web/test/unit/src/Project/ProjectDeleter.test.mjs b/services/web/test/unit/src/Project/ProjectDeleter.test.mjs index 2482344945..5dbea99d6c 100644 --- a/services/web/test/unit/src/Project/ProjectDeleter.test.mjs +++ b/services/web/test/unit/src/Project/ProjectDeleter.test.mjs @@ -1,33 +1,40 @@ +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import tk from 'timekeeper' +import moment from 'moment' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' +import mongodb from 'mongodb-legacy' +import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/Project/ProjectDeleter' -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') -const tk = require('timekeeper') -const moment = require('moment') -const { Project } = require('../helpers/models/Project') -const { DeletedProject } = require('../helpers/models/DeletedProject') -const { ObjectId, ReadPreference } = require('mongodb-legacy') -const Errors = require('../../../../app/src/Features/Errors/Errors') + +const { Project, DeletedProject } = indirectlyImportModels([ + 'Project', + 'DeletedProject', +]) +const { ObjectId, ReadPreference } = mongodb +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) describe('ProjectDeleter', function () { - beforeEach(function () { + beforeEach(async function (ctx) { tk.freeze(Date.now()) - this.ip = '192.170.18.1' - this.project = dummyProject() - this.user = { + ctx.ip = '192.170.18.1' + ctx.project = dummyProject() + ctx.user = { _id: '588f3ddae8ebc1bac07c9fa4', first_name: 'bjkdsjfk', features: {}, } - this.doc = { + ctx.doc = { _id: '5bd975f54f62e803cb8a8fec', lines: ['a bunch of lines', 'for a sunny day', 'in London town'], ranges: {}, project_id: '5cf9270b4eff6e186cf8b05e', } - this.deletedProjects = [ + ctx.deletedProjects = [ { _id: '5cf7f145c1401f0ca0eb1aaa', deleterData: { @@ -36,7 +43,7 @@ describe('ProjectDeleter', function () { deleterId: '588f3ddae8ebc1bac07c9fa4', deleterIpAddress: '172.19.0.1', deletedProjectId: '5cf9270b4eff6e186cf8b05e', - deletedProjectOwnerId: this.user._id, + deletedProjectOwnerId: ctx.user._id, }, project: { _id: '5cf9270b4eff6e186cf8b05e', @@ -62,171 +69,224 @@ describe('ProjectDeleter', function () { }, ] - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { flushProjectToMongoAndDelete: sinon.stub().resolves(), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.TagsHandler = { + ctx.TagsHandler = { promises: { removeProjectFromAllTags: sinon.stub().resolves(), }, } - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { removeUserFromAllProjects: sinon.stub().resolves(), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getMemberIds: sinon .stub() - .withArgs(this.project._id) + .withArgs(ctx.project._id) .resolves(['member-id-1', 'member-id-2']), }, } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { promises: { - generateUniqueName: sinon.stub().resolves(this.project.name), + generateUniqueName: sinon.stub().resolves(ctx.project.name), }, } - this.db = { + ctx.db = { projects: { insertOne: sinon.stub().resolves(), }, } - this.DocstoreManager = { + ctx.DocstoreManager = { promises: { archiveProject: sinon.stub().resolves(), destroyProject: sinon.stub().resolves(), }, } - this.HistoryManager = { + ctx.HistoryManager = { promises: { deleteProject: sinon.stub().resolves(), }, } - this.ProjectMock = sinon.mock(Project) - this.DeletedProjectMock = sinon.mock(DeletedProject) - this.Features = { + ctx.ProjectMock = sinon.mock(Project) + ctx.DeletedProjectMock = sinon.mock(DeletedProject) + ctx.Features = { hasFeature: sinon.stub().returns(true), } - this.ChatApiHandler = { + ctx.ChatApiHandler = { promises: { destroyProject: sinon.stub().resolves(), }, } - this.ProjectAuditLogEntry = { + ctx.ProjectAuditLogEntry = { deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }), } - this.ProjectDeleter = SandboxedModule.require(modulePath, { - requires: { - '../../infrastructure/Modules': { - promises: { hooks: { fire: sinon.stub().resolves() } }, - }, - '../../infrastructure/Features': this.Features, - '../Editor/EditorRealTimeController': this.EditorRealTimeController, - '../../models/Project': { Project }, - '../../models/DeletedProject': { DeletedProject }, - '../DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../Tags/TagsHandler': this.TagsHandler, - '../Chat/ChatApiHandler': this.ChatApiHandler, - '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - '../Docstore/DocstoreManager': this.DocstoreManager, - './ProjectDetailsHandler': this.ProjectDetailsHandler, - '../../infrastructure/mongodb': { - db: this.db, - ObjectId, - READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode, - }, - '../History/HistoryManager': this.HistoryManager, - '../../models/ProjectAuditLogEntry': { - ProjectAuditLogEntry: this.ProjectAuditLogEntry, - }, + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: { + promises: { hooks: { fire: sinon.stub().resolves() } }, }, - }) + })) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project, + })) + + vi.doMock('../../../../app/src/models/DeletedProject', () => ({ + DeletedProject, + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({ + default: ctx.ChatApiHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({ + default: ctx.DocstoreManager, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({ + db: ctx.db, + ObjectId, + READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode, + })) + + vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({ + default: ctx.HistoryManager, + })) + + vi.doMock('../../../../app/src/models/ProjectAuditLogEntry', () => ({ + ProjectAuditLogEntry: ctx.ProjectAuditLogEntry, + })) + + ctx.ProjectDeleter = (await import(modulePath)).default }) - afterEach(function () { + afterEach(function (ctx) { tk.reset() - this.DeletedProjectMock.restore() - this.ProjectMock.restore() + ctx.DeletedProjectMock.restore() + ctx.ProjectMock.restore() }) describe('mark as deleted by external source', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { deletedByExternalDataSource: true } ) .chain('exec') .resolves() }) - it('should update the project with the flag set to true', async function () { - await this.ProjectDeleter.promises.markAsDeletedByExternalSource( - this.project._id + it('should update the project with the flag set to true', async function (ctx) { + await ctx.ProjectDeleter.promises.markAsDeletedByExternalSource( + ctx.project._id ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) - it('should tell the editor controler so users are notified', async function () { - await this.ProjectDeleter.promises.markAsDeletedByExternalSource( - this.project._id + it('should tell the editor controler so users are notified', async function (ctx) { + await ctx.ProjectDeleter.promises.markAsDeletedByExternalSource( + ctx.project._id ) - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.project._id, + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'projectRenamedOrDeletedByExternalSource' ) }) }) describe('unmarkAsDeletedByExternalSource', function () { - beforeEach(async function () { - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { deletedByExternalDataSource: false } ) .chain('exec') .resolves() - await this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource( - this.project._id + await ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource( + ctx.project._id ) }) - it('should remove the flag from the project', function () { - this.ProjectMock.verify() + it('should remove the flag from the project', function (ctx) { + ctx.ProjectMock.verify() }) }) describe('deleteUsersProjects', function () { - beforeEach(function () { - this.projects = [dummyProject(), dummyProject()] - this.ProjectMock.expects('find') - .withArgs({ owner_ref: this.user._id }) + beforeEach(function (ctx) { + ctx.projects = [dummyProject(), dummyProject()] + ctx.ProjectMock.expects('find') + .withArgs({ owner_ref: ctx.user._id }) .chain('exec') - .resolves(this.projects) - for (const project of this.projects) { - this.ProjectMock.expects('findOne') + .resolves(ctx.projects) + for (const project of ctx.projects) { + ctx.ProjectMock.expects('findOne') .withArgs({ _id: project._id }) .chain('exec') .resolves(project) - this.ProjectMock.expects('deleteOne') + ctx.ProjectMock.expects('deleteOne') .withArgs({ _id: project._id }) .chain('exec') .resolves() - this.DeletedProjectMock.expects('updateOne') + ctx.DeletedProjectMock.expects('updateOne') .withArgs( { 'deleterData.deletedProjectId': project._id }, { @@ -239,146 +299,146 @@ describe('ProjectDeleter', function () { } }) - it('should delete all projects owned by the user', async function () { - await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id) - this.ProjectMock.verify() - this.DeletedProjectMock.verify() + it('should delete all projects owned by the user', async function (ctx) { + await ctx.ProjectDeleter.promises.deleteUsersProjects(ctx.user._id) + ctx.ProjectMock.verify() + ctx.DeletedProjectMock.verify() }) - it('should remove any collaboration from this user', async function () { - await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id) + it('should remove any collaboration from this user', async function (ctx) { + await ctx.ProjectDeleter.promises.deleteUsersProjects(ctx.user._id) sinon.assert.calledWith( - this.CollaboratorsHandler.promises.removeUserFromAllProjects, - this.user._id + ctx.CollaboratorsHandler.promises.removeUserFromAllProjects, + ctx.user._id ) sinon.assert.calledOnce( - this.CollaboratorsHandler.promises.removeUserFromAllProjects + ctx.CollaboratorsHandler.promises.removeUserFromAllProjects ) }) }) describe('deleteProject', function () { - beforeEach(function () { - this.deleterData = { + beforeEach(function (ctx) { + ctx.deleterData = { deletedAt: new Date(), - deletedProjectId: this.project._id, - deletedProjectOwnerId: this.project.owner_ref, - deletedProjectCollaboratorIds: this.project.collaberator_refs, - deletedProjectReadOnlyIds: this.project.readOnly_refs, - deletedProjectReviewerIds: this.project.reviewer_refs, + deletedProjectId: ctx.project._id, + deletedProjectOwnerId: ctx.project.owner_ref, + deletedProjectCollaboratorIds: ctx.project.collaberator_refs, + deletedProjectReadOnlyIds: ctx.project.readOnly_refs, + deletedProjectReviewerIds: ctx.project.reviewer_refs, deletedProjectReadWriteTokenAccessIds: - this.project.tokenAccessReadAndWrite_refs, + ctx.project.tokenAccessReadAndWrite_refs, deletedProjectReadOnlyTokenAccessIds: - this.project.tokenAccessReadOnly_refs, - deletedProjectReadWriteToken: this.project.tokens.readAndWrite, - deletedProjectReadOnlyToken: this.project.tokens.readOnly, - deletedProjectOverleafId: this.project.overleaf.id, - deletedProjectOverleafHistoryId: this.project.overleaf.history.id, - deletedProjectLastUpdatedAt: this.project.lastUpdated, + ctx.project.tokenAccessReadOnly_refs, + deletedProjectReadWriteToken: ctx.project.tokens.readAndWrite, + deletedProjectReadOnlyToken: ctx.project.tokens.readOnly, + deletedProjectOverleafId: ctx.project.overleaf.id, + deletedProjectOverleafHistoryId: ctx.project.overleaf.history.id, + deletedProjectLastUpdatedAt: ctx.project.lastUpdated, } - this.ProjectMock.expects('findOne') - .withArgs({ _id: this.project._id }) + ctx.ProjectMock.expects('findOne') + .withArgs({ _id: ctx.project._id }) .chain('exec') - .resolves(this.project) + .resolves(ctx.project) }) - it('should save a DeletedProject with additional deleterData', async function () { - this.deleterData.deleterIpAddress = this.ip - this.deleterData.deleterId = this.user._id + it('should save a DeletedProject with additional deleterData', async function (ctx) { + ctx.deleterData.deleterIpAddress = ctx.ip + ctx.deleterData.deleterId = ctx.user._id - this.ProjectMock.expects('deleteOne').chain('exec').resolves() - this.DeletedProjectMock.expects('updateOne') + ctx.ProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('updateOne') .withArgs( - { 'deleterData.deletedProjectId': this.project._id }, + { 'deleterData.deletedProjectId': ctx.project._id }, { - project: this.project, - deleterData: this.deleterData, + project: ctx.project, + deleterData: ctx.deleterData, }, { upsert: true } ) .resolves() - await this.ProjectDeleter.promises.deleteProject(this.project._id, { - deleterUser: this.user, - ipAddress: this.ip, + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, { + deleterUser: ctx.user, + ipAddress: ctx.ip, }) - this.DeletedProjectMock.verify() + ctx.DeletedProjectMock.verify() }) - it('should flushProjectToMongoAndDelete in doc updater', async function () { - this.ProjectMock.expects('deleteOne').chain('exec').resolves() - this.DeletedProjectMock.expects('updateOne').resolves() + it('should flushProjectToMongoAndDelete in doc updater', async function (ctx) { + ctx.ProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('updateOne').resolves() - await this.ProjectDeleter.promises.deleteProject(this.project._id, { - deleterUser: this.user, - ipAddress: this.ip, + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, { + deleterUser: ctx.user, + ipAddress: ctx.ip, }) - this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete - .calledWith(this.project._id) + ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete + .calledWith(ctx.project._id) .should.equal(true) }) - it('should flush docs out of mongo', async function () { - this.ProjectMock.expects('deleteOne').chain('exec').resolves() - this.DeletedProjectMock.expects('updateOne').resolves() - await this.ProjectDeleter.promises.deleteProject(this.project._id, { - deleterUser: this.user, - ipAddress: this.ip, + it('should flush docs out of mongo', async function (ctx) { + ctx.ProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('updateOne').resolves() + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, { + deleterUser: ctx.user, + ipAddress: ctx.ip, }) expect( - this.DocstoreManager.promises.archiveProject - ).to.have.been.calledWith(this.project._id) + ctx.DocstoreManager.promises.archiveProject + ).to.have.been.calledWith(ctx.project._id) }) - it('should flush docs out of mongo and ignore errors', async function () { - this.ProjectMock.expects('deleteOne').chain('exec').resolves() - this.DeletedProjectMock.expects('updateOne').resolves() - this.DocstoreManager.promises.archiveProject.rejects(new Error('foo')) - await this.ProjectDeleter.promises.deleteProject(this.project._id, { - deleterUser: this.user, - ipAddress: this.ip, + it('should flush docs out of mongo and ignore errors', async function (ctx) { + ctx.ProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('updateOne').resolves() + ctx.DocstoreManager.promises.archiveProject.rejects(new Error('foo')) + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, { + deleterUser: ctx.user, + ipAddress: ctx.ip, }) }) - it('should removeProjectFromAllTags', async function () { - this.ProjectMock.expects('deleteOne').chain('exec').resolves() - this.DeletedProjectMock.expects('updateOne').resolves() + it('should removeProjectFromAllTags', async function (ctx) { + ctx.ProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('updateOne').resolves() - await this.ProjectDeleter.promises.deleteProject(this.project._id) + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id) sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectFromAllTags, + ctx.TagsHandler.promises.removeProjectFromAllTags, 'member-id-1', - this.project._id + ctx.project._id ) sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectFromAllTags, + ctx.TagsHandler.promises.removeProjectFromAllTags, 'member-id-2', - this.project._id + ctx.project._id ) }) - it('should remove the project from Mongo', async function () { - this.ProjectMock.expects('deleteOne') - .withArgs({ _id: this.project._id }) + it('should remove the project from Mongo', async function (ctx) { + ctx.ProjectMock.expects('deleteOne') + .withArgs({ _id: ctx.project._id }) .chain('exec') .resolves() - this.DeletedProjectMock.expects('updateOne').resolves() + ctx.DeletedProjectMock.expects('updateOne').resolves() - await this.ProjectDeleter.promises.deleteProject(this.project._id) - this.ProjectMock.verify() + await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id) + ctx.ProjectMock.verify() }) }) describe('expireDeletedProjectsAfterDuration', function () { - beforeEach(async function () { - for (const deletedProject of this.deletedProjects) { - this.ProjectMock.expects('findById') + beforeEach(async function (ctx) { + for (const deletedProject of ctx.deletedProjects) { + ctx.ProjectMock.expects('findById') .withArgs(deletedProject.deleterData.deletedProjectId) .chain('exec') .resolves(null) } - this.DeletedProjectMock.expects('find') + ctx.DeletedProjectMock.expects('find') .withArgs({ 'deleterData.deletedAt': { $lt: new Date(moment().subtract(90, 'days')), @@ -388,16 +448,16 @@ describe('ProjectDeleter', function () { }, }) .chain('exec') - .resolves(this.deletedProjects) + .resolves(ctx.deletedProjects) - for (const deletedProject of this.deletedProjects) { - this.DeletedProjectMock.expects('findOne') + for (const deletedProject of ctx.deletedProjects) { + ctx.DeletedProjectMock.expects('findOne') .withArgs({ 'deleterData.deletedProjectId': deletedProject.project._id, }) .chain('exec') .resolves(deletedProject) - this.DeletedProjectMock.expects('updateOne') + ctx.DeletedProjectMock.expects('updateOne') .withArgs( { _id: deletedProject._id, @@ -413,25 +473,25 @@ describe('ProjectDeleter', function () { .resolves() } - await this.ProjectDeleter.promises.expireDeletedProjectsAfterDuration() + await ctx.ProjectDeleter.promises.expireDeletedProjectsAfterDuration() }) - it('should expire projects older than 90 days', function () { - this.DeletedProjectMock.verify() + it('should expire projects older than 90 days', function (ctx) { + ctx.DeletedProjectMock.verify() }) }) describe('expireDeletedProject', function () { describe('on an inactive project', function () { - beforeEach(async function () { - this.ProjectMock.expects('findById') - .withArgs(this.deletedProjects[0].deleterData.deletedProjectId) + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('findById') + .withArgs(ctx.deletedProjects[0].deleterData.deletedProjectId) .chain('exec') .resolves(null) - this.DeletedProjectMock.expects('updateOne') + ctx.DeletedProjectMock.expects('updateOne') .withArgs( { - _id: this.deletedProjects[0]._id, + _id: ctx.deletedProjects[0]._id, }, { $set: { @@ -443,54 +503,54 @@ describe('ProjectDeleter', function () { .chain('exec') .resolves() - this.DeletedProjectMock.expects('findOne') + ctx.DeletedProjectMock.expects('findOne') .withArgs({ - 'deleterData.deletedProjectId': this.deletedProjects[0].project._id, + 'deleterData.deletedProjectId': ctx.deletedProjects[0].project._id, }) .chain('exec') - .resolves(this.deletedProjects[0]) + .resolves(ctx.deletedProjects[0]) - await this.ProjectDeleter.promises.expireDeletedProject( - this.deletedProjects[0].project._id + await ctx.ProjectDeleter.promises.expireDeletedProject( + ctx.deletedProjects[0].project._id ) }) - it('should find the specified deletedProject and remove its project and ip address', function () { - this.DeletedProjectMock.verify() + it('should find the specified deletedProject and remove its project and ip address', function (ctx) { + ctx.DeletedProjectMock.verify() }) - it('should destroy the docs in docstore', function () { + it('should destroy the docs in docstore', function (ctx) { expect( - this.DocstoreManager.promises.destroyProject - ).to.have.been.calledWith(this.deletedProjects[0].project._id) + ctx.DocstoreManager.promises.destroyProject + ).to.have.been.calledWith(ctx.deletedProjects[0].project._id) }) - it('should delete the project in history', function () { + it('should delete the project in history', function (ctx) { expect( - this.HistoryManager.promises.deleteProject + ctx.HistoryManager.promises.deleteProject ).to.have.been.calledWith( - this.deletedProjects[0].project._id, - this.deletedProjects[0].project.overleaf.history.id + ctx.deletedProjects[0].project._id, + ctx.deletedProjects[0].project.overleaf.history.id ) }) - it('should destroy the chat threads and messages', function () { + it('should destroy the chat threads and messages', function (ctx) { expect( - this.ChatApiHandler.promises.destroyProject - ).to.have.been.calledWith(this.deletedProjects[0].project._id) + ctx.ChatApiHandler.promises.destroyProject + ).to.have.been.calledWith(ctx.deletedProjects[0].project._id) }) - it('should delete audit logs', async function () { - expect(this.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({ - projectId: this.deletedProjects[0].project._id, + it('should delete audit logs', async function (ctx) { + expect(ctx.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({ + projectId: ctx.deletedProjects[0].project._id, }) }) - it('should log a completed deletion', async function () { - expect(this.logger.info).to.have.been.calledWith( + it('should log a completed deletion', async function (ctx) { + expect(ctx.logger.info).toHaveBeenCalledWith( { - projectId: this.deletedProjects[0].project._id, - userId: this.user._id, + projectId: ctx.deletedProjects[0].project._id, + userId: ctx.user._id, }, 'expired deleted project successfully' ) @@ -498,131 +558,131 @@ describe('ProjectDeleter', function () { }) describe('on an active project (from an incomplete delete)', function () { - beforeEach(async function () { - this.ProjectMock.expects('findById') - .withArgs(this.deletedProjects[0].deleterData.deletedProjectId) + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('findById') + .withArgs(ctx.deletedProjects[0].deleterData.deletedProjectId) .chain('exec') - .resolves(this.deletedProjects[0].project) - this.DeletedProjectMock.expects('deleteOne') + .resolves(ctx.deletedProjects[0].project) + ctx.DeletedProjectMock.expects('deleteOne') .withArgs({ - 'deleterData.deletedProjectId': this.deletedProjects[0].project._id, + 'deleterData.deletedProjectId': ctx.deletedProjects[0].project._id, }) .chain('exec') .resolves() - await this.ProjectDeleter.promises.expireDeletedProject( - this.deletedProjects[0].project._id + await ctx.ProjectDeleter.promises.expireDeletedProject( + ctx.deletedProjects[0].project._id ) }) - it('should delete the spurious deleted project record', function () { - this.DeletedProjectMock.verify() + it('should delete the spurious deleted project record', function (ctx) { + ctx.DeletedProjectMock.verify() }) - it('should not destroy the docs in docstore', function () { - expect(this.DocstoreManager.promises.destroyProject).to.not.have.been + it('should not destroy the docs in docstore', function (ctx) { + expect(ctx.DocstoreManager.promises.destroyProject).to.not.have.been .called }) - it('should not delete the project in history', function () { - expect(this.HistoryManager.promises.deleteProject).to.not.have.been + it('should not delete the project in history', function (ctx) { + expect(ctx.HistoryManager.promises.deleteProject).to.not.have.been .called }) - it('should not destroy the chat threads and messages', function () { - expect(this.ChatApiHandler.promises.destroyProject).to.not.have.been + it('should not destroy the chat threads and messages', function (ctx) { + expect(ctx.ChatApiHandler.promises.destroyProject).to.not.have.been .called }) }) }) describe('archiveProject', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { - $addToSet: { archived: new ObjectId(this.user._id) }, - $pull: { trashed: new ObjectId(this.user._id) }, + $addToSet: { archived: new ObjectId(ctx.user._id) }, + $pull: { trashed: new ObjectId(ctx.user._id) }, } ) .resolves() }) - it('should update the project', async function () { - await this.ProjectDeleter.promises.archiveProject( - this.project._id, - this.user._id + it('should update the project', async function (ctx) { + await ctx.ProjectDeleter.promises.archiveProject( + ctx.project._id, + ctx.user._id ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('unarchiveProject', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, - { $pull: { archived: new ObjectId(this.user._id) } } + { _id: ctx.project._id }, + { $pull: { archived: new ObjectId(ctx.user._id) } } ) .resolves() }) - it('should update the project', async function () { - await this.ProjectDeleter.promises.unarchiveProject( - this.project._id, - this.user._id + it('should update the project', async function (ctx) { + await ctx.ProjectDeleter.promises.unarchiveProject( + ctx.project._id, + ctx.user._id ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('trashProject', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { - $addToSet: { trashed: new ObjectId(this.user._id) }, - $pull: { archived: new ObjectId(this.user._id) }, + $addToSet: { trashed: new ObjectId(ctx.user._id) }, + $pull: { archived: new ObjectId(ctx.user._id) }, } ) .resolves() }) - it('should update the project', async function () { - await this.ProjectDeleter.promises.trashProject( - this.project._id, - this.user._id + it('should update the project', async function (ctx) { + await ctx.ProjectDeleter.promises.trashProject( + ctx.project._id, + ctx.user._id ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('untrashProject', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( - { _id: this.project._id }, - { $pull: { trashed: new ObjectId(this.user._id) } } + { _id: ctx.project._id }, + { $pull: { trashed: new ObjectId(ctx.user._id) } } ) .resolves() }) - it('should update the project', async function () { - await this.ProjectDeleter.promises.untrashProject( - this.project._id, - this.user._id + it('should update the project', async function (ctx) { + await ctx.ProjectDeleter.promises.untrashProject( + ctx.project._id, + ctx.user._id ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('restoreProject', function () { - beforeEach(function () { - this.ProjectMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { $unset: { archived: true }, @@ -632,115 +692,115 @@ describe('ProjectDeleter', function () { .resolves() }) - it('should unset the archive attribute', async function () { - await this.ProjectDeleter.promises.restoreProject(this.project._id) + it('should unset the archive attribute', async function (ctx) { + await ctx.ProjectDeleter.promises.restoreProject(ctx.project._id) }) }) describe('undeleteProject', function () { - beforeEach(function () { - this.unknownProjectId = new ObjectId() - this.purgedProjectId = new ObjectId() + beforeEach(function (ctx) { + ctx.unknownProjectId = new ObjectId() + ctx.purgedProjectId = new ObjectId() - this.deletedProject = { + ctx.deletedProject = { _id: 'deleted', - project: this.project, + project: ctx.project, deleterData: { - deletedProjectId: this.project._id, - deletedProjectOwnerId: this.project.owner_ref, + deletedProjectId: ctx.project._id, + deletedProjectOwnerId: ctx.project.owner_ref, }, } - this.purgedProject = { + ctx.purgedProject = { _id: 'purged', deleterData: { - deletedProjectId: this.purgedProjectId, + deletedProjectId: ctx.purgedProjectId, deletedProjectOwnerId: 'potato', }, } - this.DeletedProjectMock.expects('findOne') - .withArgs({ 'deleterData.deletedProjectId': this.project._id }) + ctx.DeletedProjectMock.expects('findOne') + .withArgs({ 'deleterData.deletedProjectId': ctx.project._id }) .chain('exec') - .resolves(this.deletedProject) - this.DeletedProjectMock.expects('findOne') - .withArgs({ 'deleterData.deletedProjectId': this.purgedProjectId }) + .resolves(ctx.deletedProject) + ctx.DeletedProjectMock.expects('findOne') + .withArgs({ 'deleterData.deletedProjectId': ctx.purgedProjectId }) .chain('exec') - .resolves(this.purgedProject) - this.DeletedProjectMock.expects('findOne') - .withArgs({ 'deleterData.deletedProjectId': this.unknownProjectId }) + .resolves(ctx.purgedProject) + ctx.DeletedProjectMock.expects('findOne') + .withArgs({ 'deleterData.deletedProjectId': ctx.unknownProjectId }) .chain('exec') .resolves(null) - this.DeletedProjectMock.expects('deleteOne').chain('exec').resolves() + ctx.DeletedProjectMock.expects('deleteOne').chain('exec').resolves() }) - it('should return not found if the project does not exist', async function () { + it('should return not found if the project does not exist', async function (ctx) { await expect( - this.ProjectDeleter.promises.undeleteProject( - this.unknownProjectId.toString() + ctx.ProjectDeleter.promises.undeleteProject( + ctx.unknownProjectId.toString() ) ).to.be.rejectedWith(Errors.NotFoundError, 'project_not_found') }) - it('should return not found if the project has been expired', async function () { + it('should return not found if the project has been expired', async function (ctx) { await expect( - this.ProjectDeleter.promises.undeleteProject( - this.purgedProjectId.toString() + ctx.ProjectDeleter.promises.undeleteProject( + ctx.purgedProjectId.toString() ) ).to.be.rejectedWith(Errors.NotFoundError, 'project_too_old_to_restore') }) - it('should insert the project into the collection', async function () { - await this.ProjectDeleter.promises.undeleteProject(this.project._id) + it('should insert the project into the collection', async function (ctx) { + await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id) sinon.assert.calledWith( - this.db.projects.insertOne, + ctx.db.projects.insertOne, sinon.match({ - _id: this.project._id, - name: this.project.name, + _id: ctx.project._id, + name: ctx.project.name, }) ) }) - it('should clear the archive bit', async function () { - this.project.archived = true - await this.ProjectDeleter.promises.undeleteProject(this.project._id) + it('should clear the archive bit', async function (ctx) { + ctx.project.archived = true + await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id) sinon.assert.calledWith( - this.db.projects.insertOne, + ctx.db.projects.insertOne, sinon.match({ archived: undefined }) ) }) - it('should generate a unique name for the project', async function () { - await this.ProjectDeleter.promises.undeleteProject(this.project._id) + it('should generate a unique name for the project', async function (ctx) { + await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id) sinon.assert.calledWith( - this.ProjectDetailsHandler.promises.generateUniqueName, - this.project.owner_ref + ctx.ProjectDetailsHandler.promises.generateUniqueName, + ctx.project.owner_ref ) }) - it('should add a suffix to the project name', async function () { - await this.ProjectDeleter.promises.undeleteProject(this.project._id) + it('should add a suffix to the project name', async function (ctx) { + await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id) sinon.assert.calledWith( - this.ProjectDetailsHandler.promises.generateUniqueName, - this.project.owner_ref, - this.project.name + ' (Restored)' + ctx.ProjectDetailsHandler.promises.generateUniqueName, + ctx.project.owner_ref, + ctx.project.name + ' (Restored)' ) }) - it('should remove the DeletedProject', async function () { + it('should remove the DeletedProject', async function (ctx) { // need to change the mock just to include the methods we want - this.DeletedProjectMock.restore() - this.DeletedProjectMock = sinon.mock(DeletedProject) - this.DeletedProjectMock.expects('findOne') - .withArgs({ 'deleterData.deletedProjectId': this.project._id }) + ctx.DeletedProjectMock.restore() + ctx.DeletedProjectMock = sinon.mock(DeletedProject) + ctx.DeletedProjectMock.expects('findOne') + .withArgs({ 'deleterData.deletedProjectId': ctx.project._id }) .chain('exec') - .resolves(this.deletedProject) - this.DeletedProjectMock.expects('deleteOne') + .resolves(ctx.deletedProject) + ctx.DeletedProjectMock.expects('deleteOne') .withArgs({ _id: 'deleted' }) .chain('exec') .resolves() - await this.ProjectDeleter.promises.undeleteProject(this.project._id) - this.DeletedProjectMock.verify() + await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id) + ctx.DeletedProjectMock.verify() }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs b/services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs index 51cdddd9eb..0c93d396d2 100644 --- a/services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs @@ -1,36 +1,41 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const ProjectHelper = require('../../../../app/src/Features/Project/ProjectHelper') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js' + +const { ObjectId } = mongodb const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('ProjectDetailsHandler', function () { - beforeEach(function () { - this.user = { + beforeEach(async function (ctx) { + ctx.user = { _id: new ObjectId(), email: 'user@example.com', features: 'mock-features', } - this.collaborator = { + ctx.collaborator = { _id: new ObjectId(), email: 'collaborator@example.com', } - this.project = { + ctx.project = { _id: new ObjectId(), name: 'project', description: 'this is a great project', something: 'should not exist', compiler: 'latexxxxxx', - owner_ref: this.user._id, - collaberator_refs: [this.collaborator._id], + owner_ref: ctx.user._id, + collaberator_refs: [ctx.collaborator._id], } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProjectWithoutDocLines: sinon.stub().resolves(this.project), - getProject: sinon.stub().resolves(this.project), + getProjectWithoutDocLines: sinon.stub().resolves(ctx.project), + getProject: sinon.stub().resolves(ctx.project), findAllUsersProjects: sinon.stub().resolves({ owned: [], readAndWrite: [], @@ -40,200 +45,221 @@ describe('ProjectDetailsHandler', function () { }), }, } - this.ProjectModelUpdateQuery = { + ctx.ProjectModelUpdateQuery = { exec: sinon.stub().resolves(), } - this.ProjectModel = { - updateOne: sinon.stub().returns(this.ProjectModelUpdateQuery), + ctx.ProjectModel = { + updateOne: sinon.stub().returns(ctx.ProjectModelUpdateQuery), } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUser: sinon.stub().resolves(this.user), + getUser: sinon.stub().resolves(ctx.user), }, } - this.TpdsUpdateSender = { + ctx.TpdsUpdateSender = { promises: { moveEntity: sinon.stub().resolves(), }, } - this.TokenGenerator = { + ctx.TokenGenerator = { readAndWriteToken: sinon.stub(), promises: { generateUniqueReadOnlyToken: sinon.stub(), }, } - this.settings = { + ctx.settings = { defaultFeatures: 'default-features', } - this.handler = SandboxedModule.require(MODULE_PATH, { - requires: { - './ProjectHelper': ProjectHelper, - './ProjectGetter': this.ProjectGetter, - '../../models/Project': { - Project: this.ProjectModel, - }, - '../User/UserGetter': this.UserGetter, - '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender, - '../TokenGenerator/TokenGenerator': this.TokenGenerator, - '@overleaf/settings': this.settings, - }, - }) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ProjectHelper, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: ctx.ProjectModel, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender', + () => ({ + default: ctx.TpdsUpdateSender, + }) + ) + + vi.doMock( + '../../../../app/src/Features/TokenGenerator/TokenGenerator', + () => ({ + default: ctx.TokenGenerator, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + ctx.handler = (await import(MODULE_PATH)).default }) describe('getDetails', function () { - it('should find the project and owner', async function () { - const details = await this.handler.promises.getDetails(this.project._id) - details.name.should.equal(this.project.name) - details.description.should.equal(this.project.description) - details.compiler.should.equal(this.project.compiler) - details.features.should.equal(this.user.features) + it('should find the project and owner', async function (ctx) { + const details = await ctx.handler.promises.getDetails(ctx.project._id) + details.name.should.equal(ctx.project.name) + details.description.should.equal(ctx.project.description) + details.compiler.should.equal(ctx.project.compiler) + details.features.should.equal(ctx.user.features) expect(details.something).to.be.undefined }) - it('should find overleaf metadata if it exists', async function () { - this.project.overleaf = { id: 'id' } - const details = await this.handler.promises.getDetails(this.project._id) - details.overleaf.should.equal(this.project.overleaf) + it('should find overleaf metadata if it exists', async function (ctx) { + ctx.project.overleaf = { id: 'id' } + const details = await ctx.handler.promises.getDetails(ctx.project._id) + details.overleaf.should.equal(ctx.project.overleaf) expect(details.something).to.be.undefined }) - it('should return an error for a non-existent project', async function () { - this.ProjectGetter.promises.getProject.resolves(null) + it('should return an error for a non-existent project', async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(null) await expect( - this.handler.promises.getDetails('0123456789012345678901234') + ctx.handler.promises.getDetails('0123456789012345678901234') ).to.be.rejectedWith(Errors.NotFoundError) }) - it('should return the default features if no owner found', async function () { - this.UserGetter.promises.getUser.resolves(null) - const details = await this.handler.promises.getDetails(this.project._id) - details.features.should.equal(this.settings.defaultFeatures) + it('should return the default features if no owner found', async function (ctx) { + ctx.UserGetter.promises.getUser.resolves(null) + const details = await ctx.handler.promises.getDetails(ctx.project._id) + details.features.should.equal(ctx.settings.defaultFeatures) }) - it('should rethrow any error', async function () { - this.ProjectGetter.promises.getProject.rejects(new Error('boom')) - await expect(this.handler.promises.getDetails(this.project._id)).to.be + it('should rethrow any error', async function (ctx) { + ctx.ProjectGetter.promises.getProject.rejects(new Error('boom')) + await expect(ctx.handler.promises.getDetails(ctx.project._id)).to.be .rejected }) }) describe('getProjectDescription', function () { - it('should make a call to mongo just for the description', async function () { - this.ProjectGetter.promises.getProject.resolves() - await this.handler.promises.getProjectDescription(this.project._id) - expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith( - this.project._id, + it('should make a call to mongo just for the description', async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves() + await ctx.handler.promises.getProjectDescription(ctx.project._id) + expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith( + ctx.project._id, { description: true } ) }) - it('should return what the mongo call returns', async function () { + it('should return what the mongo call returns', async function (ctx) { const expectedDescription = 'cool project' - this.ProjectGetter.promises.getProject.resolves({ + ctx.ProjectGetter.promises.getProject.resolves({ description: expectedDescription, }) - const description = await this.handler.promises.getProjectDescription( - this.project._id + const description = await ctx.handler.promises.getProjectDescription( + ctx.project._id ) expect(description).to.equal(expectedDescription) }) }) describe('setProjectDescription', function () { - beforeEach(function () { - this.description = 'updated teh description' + beforeEach(function (ctx) { + ctx.description = 'updated teh description' }) - it('should update the project detials', async function () { - await this.handler.promises.setProjectDescription( - this.project._id, - this.description + it('should update the project detials', async function (ctx) { + await ctx.handler.promises.setProjectDescription( + ctx.project._id, + ctx.description ) - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, - { description: this.description } + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, + { description: ctx.description } ) }) }) describe('renameProject', function () { - beforeEach(function () { - this.newName = 'new name here' + beforeEach(function (ctx) { + ctx.newName = 'new name here' }) - it('should update the project with the new name', async function () { - await this.handler.promises.renameProject(this.project._id, this.newName) - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, - { name: this.newName } + it('should update the project with the new name', async function (ctx) { + await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName) + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, + { name: ctx.newName } ) }) - it('should tell the TpdsUpdateSender', async function () { - await this.handler.promises.renameProject(this.project._id, this.newName) - expect(this.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith( - { - projectId: this.project._id, - projectName: this.project.name, - newProjectName: this.newName, - } - ) + it('should tell the TpdsUpdateSender', async function (ctx) { + await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName) + expect(ctx.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith({ + projectId: ctx.project._id, + projectName: ctx.project.name, + newProjectName: ctx.newName, + }) }) - it('should not do anything with an invalid name', async function () { - await expect(this.handler.promises.renameProject(this.project._id)).to.be + it('should not do anything with an invalid name', async function (ctx) { + await expect(ctx.handler.promises.renameProject(ctx.project._id)).to.be .rejected - expect(this.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called - expect(this.ProjectModel.updateOne).not.to.have.been.called + expect(ctx.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called + expect(ctx.ProjectModel.updateOne).not.to.have.been.called }) - it('should trim whitespace around name', async function () { - await this.handler.promises.renameProject( - this.project._id, - ` ${this.newName} ` + it('should trim whitespace around name', async function (ctx) { + await ctx.handler.promises.renameProject( + ctx.project._id, + ` ${ctx.newName} ` ) - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, - { name: this.newName } + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, + { name: ctx.newName } ) }) }) describe('validateProjectName', function () { - it('should reject undefined names', async function () { - await expect(this.handler.promises.validateProjectName(undefined)).to.be + it('should reject undefined names', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName(undefined)).to.be .rejected }) - it('should reject empty names', async function () { - await expect(this.handler.promises.validateProjectName('')).to.be.rejected + it('should reject empty names', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName('')).to.be.rejected }) - it('should reject names with /s', async function () { - await expect(this.handler.promises.validateProjectName('foo/bar')).to.be + it('should reject names with /s', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName('foo/bar')).to.be .rejected }) - it('should reject names with \\s', async function () { - await expect(this.handler.promises.validateProjectName('foo\\bar')).to.be + it('should reject names with \\s', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName('foo\\bar')).to.be .rejected }) - it('should reject long names', async function () { - await expect(this.handler.promises.validateProjectName('a'.repeat(1000))) + it('should reject long names', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName('a'.repeat(1000))) .to.be.rejected }) - it('should accept normal names', async function () { - await expect(this.handler.promises.validateProjectName('foobar')).to.be + it('should accept normal names', async function (ctx) { + await expect(ctx.handler.promises.validateProjectName('foobar')).to.be .fulfilled }) }) describe('generateUniqueName', function () { // actually testing `ProjectHelper.promises.ensureNameIsUnique()` - beforeEach(function () { - this.longName = 'x'.repeat(this.handler.MAX_PROJECT_NAME_LENGTH - 5) + beforeEach(function (ctx) { + ctx.longName = 'x'.repeat(ctx.handler.MAX_PROJECT_NAME_LENGTH - 5) const usersProjects = { owned: [ { _id: 1, name: 'name' }, @@ -290,116 +316,116 @@ describe('ProjectDetailsHandler', function () { tokenReadOnly: [ { _id: 10, name: 'name5' }, { _id: 11, name: 'name55' }, - { _id: 12, name: this.longName }, + { _id: 12, name: ctx.longName }, ], } - this.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects) + ctx.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects) }) - it('should leave a unique name unchanged', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should leave a unique name unchanged', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'unique-name', ['-test-suffix'] ) expect(name).to.equal('unique-name') }) - it('should append a suffix to an existing name', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should append a suffix to an existing name', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'name1', ['-test-suffix'] ) expect(name).to.equal('name1-test-suffix') }) - it('should fallback to a second suffix when needed', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should fallback to a second suffix when needed', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'name1', ['1', '-test-suffix'] ) expect(name).to.equal('name1-test-suffix') }) - it('should truncate the name when append a suffix if the result is too long', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, - this.longName, + it('should truncate the name when append a suffix if the result is too long', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, + ctx.longName, ['-test-suffix'] ) expect(name).to.equal( - this.longName.substr(0, this.handler.MAX_PROJECT_NAME_LENGTH - 12) + + ctx.longName.substr(0, ctx.handler.MAX_PROJECT_NAME_LENGTH - 12) + '-test-suffix' ) }) - it('should use a numeric index if no suffix is supplied', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should use a numeric index if no suffix is supplied', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'name1', [] ) expect(name).to.equal('name1 (1)') }) - it('should use a numeric index if all suffixes are exhausted', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should use a numeric index if all suffixes are exhausted', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'name', ['1', '11'] ) expect(name).to.equal('name (1)') }) - it('should find the next lowest available numeric index for the base name', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should find the next lowest available numeric index for the base name', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'numeric', [] ) expect(name).to.equal('numeric (21)') }) - it('should not find a numeric index lower than the one already present', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should not find a numeric index lower than the one already present', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'numeric (31)', [] ) expect(name).to.equal('numeric (41)') }) - it('should handle years in name', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should handle years in name', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'unique-name (2021)', [] ) expect(name).to.equal('unique-name (2021)') }) - it('should handle duplicating with year in name', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should handle duplicating with year in name', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'Yearbook (2021)', [] ) expect(name).to.equal('Yearbook (2021) (2)') }) describe('title with that causes invalid regex', function () { - it('should create the project with a suffix when project name exists', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should create the project with a suffix when project name exists', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'Resume (2020', [] ) expect(name).to.equal('Resume (2020 (1)') }) - it('should create the project with the provided name', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should create the project with the provided name', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'Yearbook (2021', [] ) @@ -409,18 +435,18 @@ describe('ProjectDetailsHandler', function () { describe('numeric index is already present', function () { describe('when there is 1 project "x (2)"', function () { - beforeEach(function () { + beforeEach(function (ctx) { const usersProjects = { owned: [{ _id: 1, name: 'x (2)' }], } - this.ProjectGetter.promises.findAllUsersProjects.resolves( + ctx.ProjectGetter.promises.findAllUsersProjects.resolves( usersProjects ) }) - it('should produce "x (3)" uploading a zip with name "x (2)"', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should produce "x (3)" uploading a zip with name "x (2)"', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'x (2)', [] ) @@ -429,21 +455,21 @@ describe('ProjectDetailsHandler', function () { }) describe('when there are 2 projects "x (2)" and "x (3)"', function () { - beforeEach(function () { + beforeEach(function (ctx) { const usersProjects = { owned: [ { _id: 1, name: 'x (2)' }, { _id: 2, name: 'x (3)' }, ], } - this.ProjectGetter.promises.findAllUsersProjects.resolves( + ctx.ProjectGetter.promises.findAllUsersProjects.resolves( usersProjects ) }) - it('should produce "x (4)" when uploading a zip with name "x (2)"', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should produce "x (4)" when uploading a zip with name "x (2)"', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'x (2)', [] ) @@ -452,30 +478,30 @@ describe('ProjectDetailsHandler', function () { }) describe('when there are 2 projects "x (2)" and "x (4)"', function () { - beforeEach(function () { + beforeEach(function (ctx) { const usersProjects = { owned: [ { _id: 1, name: 'x (2)' }, { _id: 2, name: 'x (4)' }, ], } - this.ProjectGetter.promises.findAllUsersProjects.resolves( + ctx.ProjectGetter.promises.findAllUsersProjects.resolves( usersProjects ) }) - it('should produce "x (3)" when uploading a zip with name "x (2)"', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should produce "x (3)" when uploading a zip with name "x (2)"', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'x (2)', [] ) expect(name).to.equal('x (3)') }) - it('should produce "x (5)" when uploading a zip with name "x (4)"', async function () { - const name = await this.handler.promises.generateUniqueName( - this.user._id, + it('should produce "x (5)" when uploading a zip with name "x (4)"', async function (ctx) { + const name = await ctx.handler.promises.generateUniqueName( + ctx.user._id, 'x (4)', [] ) @@ -486,70 +512,70 @@ describe('ProjectDetailsHandler', function () { }) describe('fixProjectName', function () { - it('should change empty names to Untitled', function () { - expect(this.handler.fixProjectName('')).to.equal('Untitled') + it('should change empty names to Untitled', function (ctx) { + expect(ctx.handler.fixProjectName('')).to.equal('Untitled') }) - it('should replace / with -', function () { - expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar') + it('should replace / with -', function (ctx) { + expect(ctx.handler.fixProjectName('foo/bar')).to.equal('foo-bar') }) - it("should replace \\ with ''", function () { - expect(this.handler.fixProjectName('foo \\ bar')).to.equal('foo bar') + it("should replace \\ with ''", function (ctx) { + expect(ctx.handler.fixProjectName('foo \\ bar')).to.equal('foo bar') }) - it('should truncate long names', function () { - expect(this.handler.fixProjectName('a'.repeat(1000))).to.equal( + it('should truncate long names', function (ctx) { + expect(ctx.handler.fixProjectName('a'.repeat(1000))).to.equal( 'a'.repeat(150) ) }) - it('should accept normal names', function () { - expect(this.handler.fixProjectName('foobar')).to.equal('foobar') + it('should accept normal names', function (ctx) { + expect(ctx.handler.fixProjectName('foobar')).to.equal('foobar') }) - it('should trim name after truncation', function () { - expect(this.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal( + it('should trim name after truncation', function (ctx) { + expect(ctx.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal( 'a'.repeat(149) ) }) }) describe('setPublicAccessLevel', function () { - beforeEach(function () { - this.accessLevel = 'tokenBased' + beforeEach(function (ctx) { + ctx.accessLevel = 'tokenBased' }) - it('should update the project with the new level', async function () { - await this.handler.promises.setPublicAccessLevel( - this.project._id, - this.accessLevel + it('should update the project with the new level', async function (ctx) { + await ctx.handler.promises.setPublicAccessLevel( + ctx.project._id, + ctx.accessLevel ) - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, - { publicAccesLevel: this.accessLevel } + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, + { publicAccesLevel: ctx.accessLevel } ) }) - it('should not produce an error', async function () { + it('should not produce an error', async function (ctx) { await expect( - this.handler.promises.setPublicAccessLevel( - this.project._id, - this.accessLevel + ctx.handler.promises.setPublicAccessLevel( + ctx.project._id, + ctx.accessLevel ) ).to.be.fulfilled }) describe('when update produces an error', function () { - beforeEach(function () { - this.ProjectModelUpdateQuery.exec.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectModelUpdateQuery.exec.rejects(new Error('woops')) }) - it('should produce an error', async function () { + it('should produce an error', async function (ctx) { await expect( - this.handler.promises.setPublicAccessLevel( - this.project._id, - this.accessLevel + ctx.handler.promises.setPublicAccessLevel( + ctx.project._id, + ctx.accessLevel ) ).to.be.rejected }) @@ -558,76 +584,76 @@ describe('ProjectDetailsHandler', function () { describe('ensureTokensArePresent', function () { describe('when the project has tokens', function () { - beforeEach(function () { - this.project = { - _id: this.project._id, + beforeEach(function (ctx) { + ctx.project = { + _id: ctx.project._id, tokens: { readOnly: 'aaa', readAndWrite: '42bbb', readAndWritePrefix: '42', }, } - this.ProjectGetter.promises.getProject.resolves(this.project) + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) }) - it('should get the project', async function () { - await this.handler.promises.ensureTokensArePresent(this.project._id) - expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce - expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith( - this.project._id, + it('should get the project', async function (ctx) { + await ctx.handler.promises.ensureTokensArePresent(ctx.project._id) + expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce + expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith( + ctx.project._id, { tokens: 1, } ) }) - it('should not update the project with new tokens', async function () { - await this.handler.promises.ensureTokensArePresent(this.project._id) - expect(this.ProjectModel.updateOne).not.to.have.been.called + it('should not update the project with new tokens', async function (ctx) { + await ctx.handler.promises.ensureTokensArePresent(ctx.project._id) + expect(ctx.ProjectModel.updateOne).not.to.have.been.called }) }) describe('when tokens are missing', function () { - beforeEach(function () { - this.project = { _id: this.project._id } - this.ProjectGetter.promises.getProject.resolves(this.project) - this.readOnlyToken = 'abc' - this.readAndWriteToken = '42def' - this.readAndWriteTokenPrefix = '42' - this.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves( - this.readOnlyToken + beforeEach(function (ctx) { + ctx.project = { _id: ctx.project._id } + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.readOnlyToken = 'abc' + ctx.readAndWriteToken = '42def' + ctx.readAndWriteTokenPrefix = '42' + ctx.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves( + ctx.readOnlyToken ) - this.TokenGenerator.readAndWriteToken.returns({ - token: this.readAndWriteToken, - numericPrefix: this.readAndWriteTokenPrefix, + ctx.TokenGenerator.readAndWriteToken.returns({ + token: ctx.readAndWriteToken, + numericPrefix: ctx.readAndWriteTokenPrefix, }) }) - it('should get the project', async function () { - await this.handler.promises.ensureTokensArePresent(this.project._id) - expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce - expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith( - this.project._id, + it('should get the project', async function (ctx) { + await ctx.handler.promises.ensureTokensArePresent(ctx.project._id) + expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce + expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith( + ctx.project._id, { tokens: 1, } ) }) - it('should update the project with new tokens', async function () { - await this.handler.promises.ensureTokensArePresent(this.project._id) - expect(this.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have + it('should update the project with new tokens', async function (ctx) { + await ctx.handler.promises.ensureTokensArePresent(ctx.project._id) + expect(ctx.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have .been.calledOnce - expect(this.TokenGenerator.readAndWriteToken).to.have.been.calledOnce - expect(this.ProjectModel.updateOne).to.have.been.calledOnce - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, + expect(ctx.TokenGenerator.readAndWriteToken).to.have.been.calledOnce + expect(ctx.ProjectModel.updateOne).to.have.been.calledOnce + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, { $set: { tokens: { - readOnly: this.readOnlyToken, - readAndWrite: this.readAndWriteToken, - readAndWritePrefix: this.readAndWriteTokenPrefix, + readOnly: ctx.readOnlyToken, + readAndWrite: ctx.readAndWriteToken, + readAndWritePrefix: ctx.readAndWriteTokenPrefix, }, }, } @@ -637,10 +663,10 @@ describe('ProjectDetailsHandler', function () { }) describe('clearTokens', function () { - it('clears the tokens from the project', async function () { - await this.handler.promises.clearTokens(this.project._id) - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, + it('clears the tokens from the project', async function (ctx) { + await ctx.handler.promises.clearTokens(ctx.project._id) + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } } ) }) diff --git a/services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs b/services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs index 8221ae6c7e..fe092d1ebd 100644 --- a/services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs @@ -1,19 +1,22 @@ -const { expect } = require('chai') -const sinon = require('sinon') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/Project/ProjectEntityHandler' -const SandboxedModule = require('sandboxed-module') -const Errors = require('../../../../app/src/Features/Errors/Errors') + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) describe('ProjectEntityHandler', function () { const projectId = '4eecb1c1bffa66588e0000a1' const docId = '4eecb1c1bffa66588e0000a2' - beforeEach(function () { - this.TpdsUpdateSender = { + beforeEach(async function (ctx) { + ctx.TpdsUpdateSender = { addDoc: sinon.stub().callsArg(1), addFile: sinon.stub().callsArg(1), } - this.ProjectModel = class Project { + ctx.ProjectModel = class Project { constructor(options) { this._id = projectId this.name = 'project_name_here' @@ -21,59 +24,77 @@ describe('ProjectEntityHandler', function () { this.rootFolder = [this.rootFolder] } } - this.project = new this.ProjectModel() + ctx.project = new ctx.ProjectModel() - this.ProjectLocator = { findElement: sinon.stub() } - this.DocumentUpdaterHandler = { + ctx.ProjectLocator = { findElement: sinon.stub() } + ctx.DocumentUpdaterHandler = { updateProjectStructure: sinon.stub().yields(), } - this.callback = sinon.stub() + ctx.callback = sinon.stub() - this.ProjectEntityHandler = SandboxedModule.require(modulePath, { - requires: { - '../Docstore/DocstoreManager': (this.DocstoreManager = { - promises: {}, - }), - '../../Features/DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../../models/Project': { - Project: this.ProjectModel, - }, - './ProjectLocator': this.ProjectLocator, - './ProjectGetter': (this.ProjectGetter = { promises: {} }), - '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender, - }, - }) + vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({ + default: (ctx.DocstoreManager = { + promises: {}, + }), + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: ctx.ProjectModel, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = { promises: {} }), + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender', + () => ({ + default: ctx.TpdsUpdateSender, + }) + ) + + ctx.ProjectEntityHandler = (await import(modulePath)).default }) describe('getting folders, docs and files', function () { - beforeEach(function () { - this.project.rootFolder = [ + beforeEach(function (ctx) { + ctx.project.rootFolder = [ { docs: [ - (this.doc1 = { + (ctx.doc1 = { name: 'doc1', _id: 'doc1_id', }), ], fileRefs: [ - (this.file1 = { + (ctx.file1 = { rev: 1, _id: 'file1_id', name: 'file1', }), ], folders: [ - (this.folder1 = { + (ctx.folder1 = { name: 'folder1', docs: [ - (this.doc2 = { + (ctx.doc2 = { name: 'doc2', _id: 'doc2_id', }), ], fileRefs: [ - (this.file2 = { + (ctx.file2 = { rev: 2, name: 'file2', _id: 'file2_id', @@ -84,54 +105,54 @@ describe('ProjectEntityHandler', function () { ], }, ] - this.ProjectGetter.promises.getProjectWithoutDocLines = sinon + ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon .stub() - .resolves(this.project) + .resolves(ctx.project) }) describe('getAllDocs', function () { let fetchedDocs - beforeEach(async function () { - this.docs = [ + beforeEach(async function (ctx) { + ctx.docs = [ { - _id: this.doc1._id, - lines: (this.lines1 = ['one']), - rev: (this.rev1 = 1), + _id: ctx.doc1._id, + lines: (ctx.lines1 = ['one']), + rev: (ctx.rev1 = 1), }, { - _id: this.doc2._id, - lines: (this.lines2 = ['two']), - rev: (this.rev2 = 2), + _id: ctx.doc2._id, + lines: (ctx.lines2 = ['two']), + rev: (ctx.rev2 = 2), }, ] - this.DocstoreManager.promises.getAllDocs = sinon + ctx.DocstoreManager.promises.getAllDocs = sinon .stub() - .resolves(this.docs) + .resolves(ctx.docs) fetchedDocs = - await this.ProjectEntityHandler.promises.getAllDocs(projectId) + await ctx.ProjectEntityHandler.promises.getAllDocs(projectId) }) - it('should get the doc lines and rev from the docstore', function () { - this.DocstoreManager.promises.getAllDocs + it('should get the doc lines and rev from the docstore', function (ctx) { + ctx.DocstoreManager.promises.getAllDocs .calledWith(projectId) .should.equal(true) }) - it('should call the callback with the docs with the lines and rev included', function () { + it('should call the callback with the docs with the lines and rev included', function (ctx) { expect(fetchedDocs).to.deep.equal({ '/doc1': { - _id: this.doc1._id, - lines: this.lines1, - name: this.doc1.name, - rev: this.rev1, - folder: this.project.rootFolder[0], + _id: ctx.doc1._id, + lines: ctx.lines1, + name: ctx.doc1.name, + rev: ctx.rev1, + folder: ctx.project.rootFolder[0], }, '/folder1/doc2': { - _id: this.doc2._id, - lines: this.lines2, - name: this.doc2.name, - rev: this.rev2, - folder: this.folder1, + _id: ctx.doc2._id, + lines: ctx.lines2, + name: ctx.doc2.name, + rev: ctx.rev2, + folder: ctx.folder1, }, }) }) @@ -139,85 +160,85 @@ describe('ProjectEntityHandler', function () { describe('getAllFiles', function () { let allFiles - beforeEach(async function () { - this.callback = sinon.stub() - allFiles = await this.ProjectEntityHandler.promises.getAllFiles( + beforeEach(async function (ctx) { + ctx.callback = sinon.stub() + allFiles = await ctx.ProjectEntityHandler.promises.getAllFiles( projectId, - this.callback + ctx.callback ) }) - it('should call the callback with the files', function () { + it('should call the callback with the files', function (ctx) { expect(allFiles).to.deep.equal({ - '/file1': { ...this.file1, folder: this.project.rootFolder[0] }, - '/folder1/file2': { ...this.file2, folder: this.folder1 }, + '/file1': { ...ctx.file1, folder: ctx.project.rootFolder[0] }, + '/folder1/file2': { ...ctx.file2, folder: ctx.folder1 }, }) }) }) describe('getAllDocPathsFromProject', function () { - beforeEach(function () { - this.docs = [ + beforeEach(function (ctx) { + ctx.docs = [ { - _id: this.doc1._id, - lines: (this.lines1 = ['one']), - rev: (this.rev1 = 1), + _id: ctx.doc1._id, + lines: (ctx.lines1 = ['one']), + rev: (ctx.rev1 = 1), }, { - _id: this.doc2._id, - lines: (this.lines2 = ['two']), - rev: (this.rev2 = 2), + _id: ctx.doc2._id, + lines: (ctx.lines2 = ['two']), + rev: (ctx.rev2 = 2), }, ] }) - it('should call the callback with the path for each docId', function () { + it('should call the callback with the path for each docId', function (ctx) { const expected = { - [this.doc1._id]: `/${this.doc1.name}`, - [this.doc2._id]: `/folder1/${this.doc2.name}`, + [ctx.doc1._id]: `/${ctx.doc1.name}`, + [ctx.doc2._id]: `/folder1/${ctx.doc2.name}`, } expect( - this.ProjectEntityHandler.getAllDocPathsFromProject( - this.project, - this.callback + ctx.ProjectEntityHandler.getAllDocPathsFromProject( + ctx.project, + ctx.callback ) ).to.deep.equal(expected) }) }) describe('getDocPathByProjectIdAndDocId', function () { - it('should call the callback with the path for an existing doc id at the root level', async function () { + it('should call the callback with the path for an existing doc id at the root level', async function (ctx) { const path = - await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.doc1._id + ctx.doc1._id ) - expect(path).to.deep.equal(`/${this.doc1.name}`) + expect(path).to.deep.equal(`/${ctx.doc1.name}`) }) - it('should call the callback with the path for an existing doc id nested within a folder', async function () { + it('should call the callback with the path for an existing doc id nested within a folder', async function (ctx) { const path = - await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.doc2._id + ctx.doc2._id ) - expect(path).to.deep.equal(`/folder1/${this.doc2.name}`) + expect(path).to.deep.equal(`/folder1/${ctx.doc2.name}`) }) - it('should call the callback with a NotFoundError for a non-existing doc', async function () { + it('should call the callback with a NotFoundError for a non-existing doc', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, 'non-existing-id' ) ).to.be.rejectedWith(Errors.NotFoundError) }) - it('should call the callback with a NotFoundError for an existing file', async function () { + it('should call the callback with a NotFoundError for an existing file', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.file1._id + ctx.file1._id ) ).to.be.rejectedWith(Errors.NotFoundError) }) @@ -225,66 +246,66 @@ describe('ProjectEntityHandler', function () { describe('_getAllFolders', async function () { let folders - beforeEach(async function () { - this.callback = sinon.stub() + beforeEach(async function (ctx) { + ctx.callback = sinon.stub() folders = - await this.ProjectEntityHandler.promises._getAllFolders(projectId) + await ctx.ProjectEntityHandler.promises._getAllFolders(projectId) }) - it('should get the project without the docs lines', function () { - this.ProjectGetter.promises.getProjectWithoutDocLines + it('should get the project without the docs lines', function (ctx) { + ctx.ProjectGetter.promises.getProjectWithoutDocLines .calledWith(projectId) .should.equal(true) }) - it('should call the callback with the folders', function () { + it('should call the callback with the folders', function (ctx) { expect(folders).to.deep.equal([ - { path: '/', folder: this.project.rootFolder[0] }, - { path: '/folder1', folder: this.folder1 }, + { path: '/', folder: ctx.project.rootFolder[0] }, + { path: '/folder1', folder: ctx.folder1 }, ]) }) }) describe('_getAllFoldersFromProject', function () { - it('should return the folders', function () { + it('should return the folders', function (ctx) { expect( - this.ProjectEntityHandler._getAllFoldersFromProject(this.project) + ctx.ProjectEntityHandler._getAllFoldersFromProject(ctx.project) ).to.deep.equal([ - { path: '/', folder: this.project.rootFolder[0] }, - { path: '/folder1', folder: this.folder1 }, + { path: '/', folder: ctx.project.rootFolder[0] }, + { path: '/folder1', folder: ctx.folder1 }, ]) }) }) }) describe('with an invalid file tree', function () { - beforeEach(function () { - this.project.rootFolder = [ + beforeEach(function (ctx) { + ctx.project.rootFolder = [ { docs: [ - (this.doc1 = { + (ctx.doc1 = { name: null, // invalid doc name _id: 'doc1_id', }), ], fileRefs: [ - (this.file1 = { + (ctx.file1 = { rev: 1, _id: 'file1_id', name: null, // invalid file name }), ], folders: [ - (this.folder1 = { + (ctx.folder1 = { name: null, // invalid folder name docs: [ - (this.doc2 = { + (ctx.doc2 = { name: 'doc2', _id: 'doc2_id', }), ], fileRefs: [ - (this.file2 = { + (ctx.file2 = { rev: 2, name: 'file2', _id: 'file2_id', @@ -296,107 +317,107 @@ describe('ProjectEntityHandler', function () { ], }, ] - this.ProjectGetter.promises.getProjectWithoutDocLines = sinon + ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon .stub() - .resolves(this.project) + .resolves(ctx.project) }) describe('getAllDocs', function () { - beforeEach(async function () { - this.docs = [ + beforeEach(async function (ctx) { + ctx.docs = [ { - _id: this.doc1._id, - lines: (this.lines1 = ['one']), - rev: (this.rev1 = 1), + _id: ctx.doc1._id, + lines: (ctx.lines1 = ['one']), + rev: (ctx.rev1 = 1), }, { - _id: this.doc2._id, - lines: (this.lines2 = ['two']), - rev: (this.rev2 = 2), + _id: ctx.doc2._id, + lines: (ctx.lines2 = ['two']), + rev: (ctx.rev2 = 2), }, ] - this.DocstoreManager.promises.getAllDocs = sinon + ctx.DocstoreManager.promises.getAllDocs = sinon .stub() - .resolves(this.docs) + .resolves(ctx.docs) }) - it('should call the callback with an error', async function () { - await expect(this.ProjectEntityHandler.promises.getAllDocs(projectId)) - .to.be.rejected + it('should call the callback with an error', async function (ctx) { + await expect(ctx.ProjectEntityHandler.promises.getAllDocs(projectId)).to + .be.rejected }) }) describe('getAllFiles', function () { - it('should call the callback with and error', async function () { - await expect(this.ProjectEntityHandler.promises.getAllFiles(projectId)) + it('should call the callback with and error', async function (ctx) { + await expect(ctx.ProjectEntityHandler.promises.getAllFiles(projectId)) .to.be.rejected }) }) describe('getDocPathByProjectIdAndDocId', function () { - it('should call the callback with an error for an existing doc id at the root level', async function () { + it('should call the callback with an error for an existing doc id at the root level', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.doc1._id + ctx.doc1._id ) ).to.be.rejectedWith(Error) }) - it('should call the callback with an error for an existing doc id nested within a folder', async function () { + it('should call the callback with an error for an existing doc id nested within a folder', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.doc2._id + ctx.doc2._id ) ).to.be.rejectedWith(Error) }) - it('should call the callback with an error for a non-existing doc', async function () { + it('should call the callback with an error for a non-existing doc', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, 'non-existing-id' ) ).to.be.rejectedWith(Error) }) - it('should call the callback with an error for an existing file', async function () { + it('should call the callback with an error for an existing file', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId( projectId, - this.file1._id + ctx.file1._id ) ).to.be.rejectedWith(Error) }) }) describe('_getAllFolders', function () { - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { await expect( - this.ProjectEntityHandler.promises._getAllFolders(projectId) + ctx.ProjectEntityHandler.promises._getAllFolders(projectId) ).to.be.rejected }) }) describe('getAllEntities', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject = sinon + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject = sinon .stub() - .resolves(this.project) + .resolves(ctx.project) }) - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getAllEntities(projectId) + ctx.ProjectEntityHandler.promises.getAllEntities(projectId) ).to.be.rejected }) }) describe('getAllDocPathsFromProjectById', function () { - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getAllDocPathsFromProjectById( + ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById( projectId ) ).to.be.rejected @@ -404,11 +425,11 @@ describe('ProjectEntityHandler', function () { }) describe('getDocPathFromProjectByDocId', function () { - it('should call the callback with an error', async function () { + it('should call the callback with an error', async function (ctx) { await expect( - this.ProjectEntityHandler.promises.getDocPathFromProjectByDocId( + ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId( projectId, - this.doc1._id + ctx.doc1._id ) ).to.be.rejected }) @@ -416,26 +437,26 @@ describe('ProjectEntityHandler', function () { }) describe('getDoc', function () { - beforeEach(function () { - this.lines = ['mock', 'doc', 'lines'] - this.rev = 5 - this.version = 42 - this.ranges = { mock: 'ranges' } - this.callback = sinon.stub() - this.DocstoreManager.promises.getDoc = sinon.stub().resolves({ - lines: this.lines, - rev: this.rev, - version: this.version, - ranges: this.ranges, + beforeEach(function (ctx) { + ctx.lines = ['mock', 'doc', 'lines'] + ctx.rev = 5 + ctx.version = 42 + ctx.ranges = { mock: 'ranges' } + ctx.callback = sinon.stub() + ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({ + lines: ctx.lines, + rev: ctx.rev, + version: ctx.version, + ranges: ctx.ranges, }) }) - it('should call the callback with the lines, version and rev', async function () { - const doc = await this.ProjectEntityHandler.promises.getDoc( + it('should call the callback with the lines, version and rev', async function (ctx) { + const doc = await ctx.ProjectEntityHandler.promises.getDoc( projectId, docId ) - this.DocstoreManager.promises.getDoc + ctx.DocstoreManager.promises.getDoc .calledWith(projectId, docId) .should.equal(true) expect(doc).to.exist @@ -445,32 +466,32 @@ describe('ProjectEntityHandler', function () { describe('promises.getDoc', function () { let result - beforeEach(async function () { - this.lines = ['mock', 'doc', 'lines'] - this.rev = 5 - this.version = 42 - this.ranges = { mock: 'ranges' } + beforeEach(async function (ctx) { + ctx.lines = ['mock', 'doc', 'lines'] + ctx.rev = 5 + ctx.version = 42 + ctx.ranges = { mock: 'ranges' } - this.DocstoreManager.promises.getDoc = sinon.stub().resolves({ - lines: this.lines, - rev: this.rev, - version: this.version, - ranges: this.ranges, + ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({ + lines: ctx.lines, + rev: ctx.rev, + version: ctx.version, + ranges: ctx.ranges, }) - result = await this.ProjectEntityHandler.promises.getDoc(projectId, docId) + result = await ctx.ProjectEntityHandler.promises.getDoc(projectId, docId) }) - it('should call the docstore', function () { - this.DocstoreManager.promises.getDoc + it('should call the docstore', function (ctx) { + ctx.DocstoreManager.promises.getDoc .calledWith(projectId, docId) .should.equal(true) }) - it('should return the lines, rev, version and ranges', function () { - expect(result.lines).to.equal(this.lines) - expect(result.rev).to.equal(this.rev) - expect(result.version).to.equal(this.version) - expect(result.ranges).to.equal(this.ranges) + it('should return the lines, rev, version and ranges', function (ctx) { + expect(result.lines).to.equal(ctx.lines) + expect(result.rev).to.equal(ctx.rev) + expect(result.version).to.equal(ctx.version) + expect(result.ranges).to.equal(ctx.ranges) }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs b/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs index ce6fa4ccc6..4e8f9e86d1 100644 --- a/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs @@ -1,222 +1,261 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const tk = require('timekeeper') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const { ObjectId } = require('mongodb-legacy') -const SandboxedModule = require('sandboxed-module') -const { Project } = require('../helpers/models/Project') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import tk from 'timekeeper' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import mongodb from 'mongodb-legacy' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' + +const { Project } = indirectlyImportModels(['Project']) +const { ObjectId } = mongodb + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) const MODULE_PATH = '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler' describe('ProjectEntityMongoUpdateHandler', function () { - beforeEach(function () { + beforeEach(async function (ctx) { tk.freeze(new Date()) - this.doc = { + ctx.doc = { _id: new ObjectId(), name: 'test-doc.txt', lines: ['hello', 'world'], rev: 1234, } - this.docPath = { + ctx.docPath = { mongo: 'rootFolder.0.docs.0', fileSystem: '/test-doc.txt', } - this.file = { + ctx.file = { _id: new ObjectId(), name: 'something.jpg', linkedFileData: { provider: 'url' }, hash: 'some-hash', } - this.filePath = { + ctx.filePath = { fileSystem: '/something.png', mongo: 'rootFolder.0.fileRefs.0', } - this.subfolder = { _id: new ObjectId(), name: 'test-subfolder' } - this.subfolderPath = { + ctx.subfolder = { _id: new ObjectId(), name: 'test-subfolder' } + ctx.subfolderPath = { fileSystem: '/test-folder/test-subfolder', mongo: 'rootFolder.0.folders.0.folders.0', } - this.notSubfolder = { _id: new ObjectId(), name: 'test-folder-2' } - this.notSubfolderPath = { + ctx.notSubfolder = { _id: new ObjectId(), name: 'test-folder-2' } + ctx.notSubfolderPath = { fileSystem: '/test-folder-2/test-subfolder', mongo: 'rootFolder.0.folders.0.folders.0', } - this.folder = { + ctx.folder = { _id: new ObjectId(), name: 'test-folder', - folders: [this.subfolder], + folders: [ctx.subfolder], } - this.folderPath = { + ctx.folderPath = { fileSystem: '/test-folder', mongo: 'rootFolder.0.folders.0', } - this.rootFolder = { + ctx.rootFolder = { _id: new ObjectId(), - folders: [this.folder], - docs: [this.doc], - fileRefs: [this.file], + folders: [ctx.folder], + docs: [ctx.doc], + fileRefs: [ctx.file], } - this.rootFolderPath = { + ctx.rootFolderPath = { fileSystem: '/', mongo: 'rootFolder.0', } - this.project = { + ctx.project = { _id: new ObjectId(), name: 'project name', - rootFolder: [this.rootFolder], + rootFolder: [ctx.rootFolder], } - this.Settings = { maxEntitiesPerProject: 100 } - this.CooldownManager = {} - this.LockManager = { + ctx.Settings = { maxEntitiesPerProject: 100 } + ctx.CooldownManager = {} + ctx.LockManager = { promises: { runWithLock: sinon.spy((namespace, id, runner) => runner()), }, } - this.FolderModel = sinon.stub() - this.ProjectMock = sinon.mock(Project) - this.ProjectEntityHandler = { + ctx.FolderModel = sinon.stub() + ctx.ProjectMock = sinon.mock(Project) + ctx.ProjectEntityHandler = { getAllEntitiesFromProject: sinon.stub(), } - this.ProjectLocator = { + ctx.ProjectLocator = { findElementByMongoPath: sinon.stub().throws(new Error('not found')), promises: { findElement: sinon.stub().rejects(new Error('not found')), findElementByPath: sinon.stub().rejects(new Error('not found')), }, } - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.rootFolder._id, + project: ctx.project, + element_id: ctx.rootFolder._id, type: 'folder', }) .resolves({ - element: this.rootFolder, - path: this.rootFolderPath, + element: ctx.rootFolder, + path: ctx.rootFolderPath, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.folder._id, + project: ctx.project, + element_id: ctx.folder._id, type: 'folder', }) .resolves({ - element: this.folder, - path: this.folderPath, - folder: this.rootFolder, + element: ctx.folder, + path: ctx.folderPath, + folder: ctx.rootFolder, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.subfolder._id, + project: ctx.project, + element_id: ctx.subfolder._id, type: 'folder', }) .resolves({ - element: this.subfolder, - path: this.subfolderPath, - folder: this.folder, + element: ctx.subfolder, + path: ctx.subfolderPath, + folder: ctx.folder, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.file._id, + project: ctx.project, + element_id: ctx.file._id, type: 'file', }) .resolves({ - element: this.file, - path: this.filePath, - folder: this.rootFolder, + element: ctx.file, + path: ctx.filePath, + folder: ctx.rootFolder, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.doc._id, + project: ctx.project, + element_id: ctx.doc._id, type: 'doc', }) .resolves({ - element: this.doc, - path: this.docPath, - folder: this.rootFolder, + element: ctx.doc, + path: ctx.docPath, + folder: ctx.rootFolder, }) - this.ProjectLocator.promises.findElementByPath + ctx.ProjectLocator.promises.findElementByPath .withArgs( sinon.match({ - project: this.project, + project: ctx.project, path: '/', }) ) - .resolves({ element: this.rootFolder, type: 'folder', folder: null }) - this.ProjectLocator.promises.findElementByPath + .resolves({ element: ctx.rootFolder, type: 'folder', folder: null }) + ctx.ProjectLocator.promises.findElementByPath .withArgs( sinon.match({ - project: this.project, + project: ctx.project, path: '/test-folder', }) ) .resolves({ - element: this.folder, + element: ctx.folder, type: 'folder', - folder: this.rootFolder, + folder: ctx.rootFolder, }) - this.ProjectLocator.promises.findElementByPath + ctx.ProjectLocator.promises.findElementByPath .withArgs( sinon.match({ - project: this.project, + project: ctx.project, path: '/test-folder/test-subfolder', }) ) .resolves({ - element: this.subfolder, + element: ctx.subfolder, type: 'folder', - folder: this.folder, + folder: ctx.folder, }) - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProjectWithoutLock: sinon .stub() - .withArgs(this.project._id) - .resolves(this.project), - getProjectWithOnlyFolders: sinon.stub().resolves(this.project), + .withArgs(ctx.project._id) + .resolves(ctx.project), + getProjectWithOnlyFolders: sinon.stub().resolves(ctx.project), }, } - this.FolderStructureBuilder = { + ctx.FolderStructureBuilder = { buildFolderStructure: sinon.stub(), } - this.subject = SandboxedModule.require(MODULE_PATH, { - requires: { - 'mongodb-legacy': { ObjectId }, - '@overleaf/settings': this.Settings, - '../Cooldown/CooldownManager': this.CooldownManager, - '../../models/Folder': { Folder: this.FolderModel }, - '../../infrastructure/LockManager': this.LockManager, - '../../models/Project': { Project }, - './ProjectEntityHandler': this.ProjectEntityHandler, - './ProjectLocator': this.ProjectLocator, - './ProjectGetter': this.ProjectGetter, - './FolderStructureBuilder': this.FolderStructureBuilder, - }, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({ + default: ctx.CooldownManager, + })) + + vi.doMock('../../../../app/src/models/Folder', () => ({ + Folder: ctx.FolderModel, + })) + + vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({ + default: ctx.LockManager, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Project/FolderStructureBuilder', + () => ({ + default: ctx.FolderStructureBuilder, + }) + ) + + ctx.subject = (await import(MODULE_PATH)).default }) - afterEach(function () { - this.ProjectMock.restore() + afterEach(function (ctx) { + ctx.ProjectMock.restore() tk.reset() }) describe('addDoc', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const doc = { _id: new ObjectId(), name: 'other.txt' } const userId = new ObjectId().toString() - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { @@ -226,96 +265,96 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - this.result = await this.subject.promises.addDoc( - this.project._id, - this.folder._id, + .resolves(ctx.project) + ctx.result = await ctx.subject.promises.addDoc( + ctx.project._id, + ctx.folder._id, doc, userId ) }) - it('adds the document in Mongo', function () { - this.ProjectMock.verify() + it('adds the document in Mongo', function (ctx) { + ctx.ProjectMock.verify() }) - it('returns path info and the project', function () { - expect(this.result).to.deep.equal({ + it('returns path info and the project', function (ctx) { + expect(ctx.result).to.deep.equal({ result: { path: { mongo: 'rootFolder.0.folders.0', fileSystem: '/test-folder/other.txt', }, }, - project: this.project, + project: ctx.project, }) }) }) describe('addFile', function () { let userId - beforeEach(function () { + beforeEach(function (ctx) { userId = new ObjectId().toString() - this.newFile = { _id: new ObjectId(), name: 'picture.jpg' } - this.ProjectMock.expects('findOneAndUpdate') + ctx.newFile = { _id: new ObjectId(), name: 'picture.jpg' } + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { - $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile }, + $push: { 'rootFolder.0.folders.0.fileRefs': ctx.newFile }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) + .resolves(ctx.project) }) describe('happy path', function () { - beforeEach(async function () { - this.result = await this.subject.promises.addFile( - this.project._id, - this.folder._id, - this.newFile, + beforeEach(async function (ctx) { + ctx.result = await ctx.subject.promises.addFile( + ctx.project._id, + ctx.folder._id, + ctx.newFile, userId ) }) - it('adds the file in Mongo', function () { - this.ProjectMock.verify() + it('adds the file in Mongo', function (ctx) { + ctx.ProjectMock.verify() }) - it('returns path info and the project', function () { - expect(this.result).to.deep.equal({ + it('returns path info and the project', function (ctx) { + expect(ctx.result).to.deep.equal({ result: { path: { mongo: 'rootFolder.0.folders.0', fileSystem: '/test-folder/picture.jpg', }, }, - project: this.project, + project: ctx.project, }) }) }) describe('when entity limit is reached', function () { - beforeEach(function () { - this.savedMaxEntities = this.Settings.maxEntitiesPerProject - this.Settings.maxEntitiesPerProject = 3 + beforeEach(function (ctx) { + ctx.savedMaxEntities = ctx.Settings.maxEntitiesPerProject + ctx.Settings.maxEntitiesPerProject = 3 }) - afterEach(function () { - this.Settings.maxEntitiesPerProject = this.savedMaxEntities + afterEach(function (ctx) { + ctx.Settings.maxEntitiesPerProject = ctx.savedMaxEntities }) - it('should throw an error', async function () { + it('should throw an error', async function (ctx) { await expect( - this.subject.promises.addFile( - this.project._id, - this.folder._id, - this.newFile, + ctx.subject.promises.addFile( + ctx.project._id, + ctx.folder._id, + ctx.newFile, userId ) ).to.be.rejected @@ -324,17 +363,17 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('addFolder', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() const folderName = 'New folder' - this.FolderModel.withArgs({ name: folderName }).returns({ + ctx.FolderModel.withArgs({ name: folderName }).returns({ _id: new ObjectId(), name: folderName, }) - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { @@ -348,22 +387,22 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - await this.subject.promises.addFolder( - this.project._id, - this.folder._id, + .resolves(ctx.project) + await ctx.subject.promises.addFolder( + ctx.project._id, + ctx.folder._id, folderName, userId ) }) - it('adds the folder in Mongo', function () { - this.ProjectMock.verify() + it('adds the folder in Mongo', function (ctx) { + ctx.ProjectMock.verify() }) }) describe('replaceFileWithNew', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const newFile = { _id: new ObjectId(), name: 'some-other-file.png', @@ -371,10 +410,10 @@ describe('ProjectEntityMongoUpdateHandler', function () { hash: 'some-hash', } // Update the file in place - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.fileRefs.0': { $exists: true }, }, { @@ -393,119 +432,119 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - this.ProjectLocator.findElementByMongoPath - .withArgs(this.project, 'rootFolder.0.fileRefs.0') + .resolves(ctx.project) + ctx.ProjectLocator.findElementByMongoPath + .withArgs(ctx.project, 'rootFolder.0.fileRefs.0') .returns(newFile) - await this.subject.promises.replaceFileWithNew( - this.project._id, - this.file._id, + await ctx.subject.promises.replaceFileWithNew( + ctx.project._id, + ctx.file._id, newFile, 'userId' ) }) - it('updates the database', function () { - this.ProjectMock.verify() + it('updates the database', function (ctx) { + ctx.ProjectMock.verify() }) }) describe('mkdirp', function () { describe('when the path is just a slash', function () { - beforeEach(async function () { - this.result = await this.subject.promises.mkdirp(this.project._id, '/') + beforeEach(async function (ctx) { + ctx.result = await ctx.subject.promises.mkdirp(ctx.project._id, '/') }) - it('should return the root folder', function () { - expect(this.result.folder).to.deep.equal(this.rootFolder) + it('should return the root folder', function (ctx) { + expect(ctx.result.folder).to.deep.equal(ctx.rootFolder) }) - it('should not report a parent folder', function () { - expect(this.result.folder.parentFolder_id).not.to.exist + it('should not report a parent folder', function (ctx) { + expect(ctx.result.folder.parentFolder_id).not.to.exist }) - it('should not return new folders', function () { - expect(this.result.newFolders).to.have.length(0) + it('should not return new folders', function (ctx) { + expect(ctx.result.newFolders).to.have.length(0) }) }) describe('when the folder already exists', function () { - beforeEach(async function () { - this.result = await this.subject.promises.mkdirp( - this.project._id, + beforeEach(async function (ctx) { + ctx.result = await ctx.subject.promises.mkdirp( + ctx.project._id, '/test-folder' ) }) - it('should return the existing folder', function () { - expect(this.result.folder).to.deep.equal(this.folder) + it('should return the existing folder', function (ctx) { + expect(ctx.result.folder).to.deep.equal(ctx.folder) }) - it('should report the parent folder', function () { - expect(this.result.folder.parentFolder_id).to.equal(this.rootFolder._id) + it('should report the parent folder', function (ctx) { + expect(ctx.result.folder.parentFolder_id).to.equal(ctx.rootFolder._id) }) - it('should not return new folders', function () { - expect(this.result.newFolders).to.have.length(0) + it('should not return new folders', function (ctx) { + expect(ctx.result.newFolders).to.have.length(0) }) }) describe('when the path is a new folder at the top level', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() - this.newFolder = { _id: new ObjectId(), name: 'new-folder' } - this.FolderModel.returns(this.newFolder) - this.exactCaseMatch = false - this.ProjectMock.expects('findOneAndUpdate') + ctx.newFolder = { _id: new ObjectId(), name: 'new-folder' } + ctx.FolderModel.returns(ctx.newFolder) + ctx.exactCaseMatch = false + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id, 'rootFolder.0': { $exists: true } }, + { _id: ctx.project._id, 'rootFolder.0': { $exists: true } }, { - $push: { 'rootFolder.0.folders': this.newFolder }, + $push: { 'rootFolder.0.folders': ctx.newFolder }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) - this.result = await this.subject.promises.mkdirp( - this.project._id, + .resolves(ctx.project) + ctx.result = await ctx.subject.promises.mkdirp( + ctx.project._id, '/new-folder/', userId, - { exactCaseMatch: this.exactCaseMatch } + { exactCaseMatch: ctx.exactCaseMatch } ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) - it('should make just one folder', function () { - expect(this.result.newFolders).to.have.length(1) + it('should make just one folder', function (ctx) { + expect(ctx.result.newFolders).to.have.length(1) }) - it('should return the new folder', function () { - expect(this.result.folder.name).to.equal('new-folder') + it('should return the new folder', function (ctx) { + expect(ctx.result.folder.name).to.equal('new-folder') }) - it('should return the parent folder', function () { - expect(this.result.folder.parentFolder_id).to.equal(this.rootFolder._id) + it('should return the parent folder', function (ctx) { + expect(ctx.result.folder.parentFolder_id).to.equal(ctx.rootFolder._id) }) - it('should pass the exactCaseMatch option to ProjectLocator', function () { + it('should pass the exactCaseMatch option to ProjectLocator', function (ctx) { expect( - this.ProjectLocator.promises.findElementByPath - ).to.have.been.calledWithMatch({ exactCaseMatch: this.exactCaseMatch }) + ctx.ProjectLocator.promises.findElementByPath + ).to.have.been.calledWithMatch({ exactCaseMatch: ctx.exactCaseMatch }) }) }) describe('adding a subfolder', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() - this.newFolder = { _id: new ObjectId(), name: 'new-folder' } - this.FolderModel.returns(this.newFolder) - this.ProjectMock.expects('findOneAndUpdate') + ctx.newFolder = { _id: new ObjectId(), name: 'new-folder' } + ctx.FolderModel.returns(ctx.newFolder) + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { @@ -519,71 +558,71 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - this.result = await this.subject.promises.mkdirp( - this.project._id, + .resolves(ctx.project) + ctx.result = await ctx.subject.promises.mkdirp( + ctx.project._id, '/test-folder/new-folder', userId ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) - it('should create one folder', function () { - expect(this.result.newFolders).to.have.length(1) + it('should create one folder', function (ctx) { + expect(ctx.result.newFolders).to.have.length(1) }) - it('should return the new folder', function () { - expect(this.result.folder.name).to.equal('new-folder') + it('should return the new folder', function (ctx) { + expect(ctx.result.folder.name).to.equal('new-folder') }) - it('should return the parent folder', function () { - expect(this.result.folder.parentFolder_id).to.equal(this.folder._id) + it('should return the parent folder', function (ctx) { + expect(ctx.result.folder.parentFolder_id).to.equal(ctx.folder._id) }) }) describe('when mutliple folders are missing', async function () { let userId - beforeEach(function () { + beforeEach(function (ctx) { userId = new ObjectId().toString() - this.folder1 = { _id: new ObjectId(), name: 'folder1' } - this.folder1Path = { + ctx.folder1 = { _id: new ObjectId(), name: 'folder1' } + ctx.folder1Path = { fileSystem: '/test-folder/folder1', mongo: 'rootFolder.0.folders.0.folders.0', } - this.folder2 = { _id: new ObjectId(), name: 'folder2' } - this.folder2Path = { + ctx.folder2 = { _id: new ObjectId(), name: 'folder2' } + ctx.folder2Path = { fileSystem: '/test-folder/folder1/folder2', mongo: 'rootFolder.0.folders.0.folders.0.folders.0', } - this.FolderModel.onFirstCall().returns(this.folder1) - this.FolderModel.onSecondCall().returns(this.folder2) - this.ProjectLocator.promises.findElement + ctx.FolderModel.onFirstCall().returns(ctx.folder1) + ctx.FolderModel.onSecondCall().returns(ctx.folder2) + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.folder1._id, + project: ctx.project, + element_id: ctx.folder1._id, type: 'folder', }) .resolves({ - element: this.folder1, - path: this.folder1Path, + element: ctx.folder1, + path: ctx.folder1Path, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.folder2._id, + project: ctx.project, + element_id: ctx.folder2._id, type: 'folder', }) .resolves({ - element: this.folder2, - path: this.folder2Path, + element: ctx.folder2, + path: ctx.folder2Path, }) - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { @@ -597,11 +636,11 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - this.ProjectMock.expects('findOneAndUpdate') + .resolves(ctx.project) + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0.folders.0': { $exists: true }, }, { @@ -615,7 +654,7 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) + .resolves(ctx.project) }) ;[ { @@ -628,33 +667,31 @@ describe('ProjectEntityMongoUpdateHandler', function () { }, ].forEach(({ description, path }) => { describe(description, function () { - beforeEach(async function () { - this.result = await this.subject.promises.mkdirp( - this.project._id, + beforeEach(async function (ctx) { + ctx.result = await ctx.subject.promises.mkdirp( + ctx.project._id, path, userId ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) - it('should add multiple folders', function () { - const newFolders = this.result.newFolders + it('should add multiple folders', function (ctx) { + const newFolders = ctx.result.newFolders expect(newFolders).to.have.length(2) expect(newFolders[0].name).to.equal('folder1') expect(newFolders[1].name).to.equal('folder2') }) - it('should return the last folder', function () { - expect(this.result.folder.name).to.equal('folder2') + it('should return the last folder', function (ctx) { + expect(ctx.result.folder.name).to.equal('folder2') }) - it('should return the parent folder', function () { - expect(this.result.folder.parentFolder_id).to.equal( - this.folder1._id - ) + it('should return the parent folder', function (ctx) { + expect(ctx.result.folder.parentFolder_id).to.equal(ctx.folder1._id) }) }) }) @@ -663,85 +700,85 @@ describe('ProjectEntityMongoUpdateHandler', function () { describe('moveEntity', function () { describe('moving a doc into a different folder', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() - this.pathAfterMove = { + ctx.pathAfterMove = { fileSystem: '/somewhere/else.txt', } - this.oldDocs = ['old-doc'] - this.oldFiles = ['old-file'] - this.newDocs = ['new-doc'] - this.newFiles = ['new-file'] + ctx.oldDocs = ['old-doc'] + ctx.oldFiles = ['old-file'] + ctx.newDocs = ['new-doc'] + ctx.newFiles = ['new-file'] - this.ProjectEntityHandler.getAllEntitiesFromProject + ctx.ProjectEntityHandler.getAllEntitiesFromProject .onFirstCall() - .returns({ docs: this.oldDocs, files: this.oldFiles }) - this.ProjectEntityHandler.getAllEntitiesFromProject + .returns({ docs: ctx.oldDocs, files: ctx.oldFiles }) + ctx.ProjectEntityHandler.getAllEntitiesFromProject .onSecondCall() - .returns({ docs: this.newDocs, files: this.newFiles }) + .returns({ docs: ctx.newDocs, files: ctx.newFiles }) - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { - $push: { 'rootFolder.0.folders.0.docs': this.doc }, + $push: { 'rootFolder.0.folders.0.docs': ctx.doc }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) - this.ProjectMock.expects('findOneAndUpdate') + .resolves(ctx.project) + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { - $pull: { 'rootFolder.0.docs': { _id: this.doc._id } }, + $pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) - this.result = await this.subject.promises.moveEntity( - this.project._id, - this.doc._id, - this.folder._id, + .resolves(ctx.project) + ctx.result = await ctx.subject.promises.moveEntity( + ctx.project._id, + ctx.doc._id, + ctx.folder._id, 'doc', userId ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) - it('should report what changed', function () { - expect(this.result).to.deep.equal({ - project: this.project, + it('should report what changed', function (ctx) { + expect(ctx.result).to.deep.equal({ + project: ctx.project, startPath: '/test-doc.txt', endPath: '/test-folder/test-doc.txt', - rev: this.doc.rev, + rev: ctx.doc.rev, changes: { - oldDocs: this.oldDocs, - newDocs: this.newDocs, - oldFiles: this.oldFiles, - newFiles: this.newFiles, - newProject: this.project, + oldDocs: ctx.oldDocs, + newDocs: ctx.newDocs, + oldFiles: ctx.oldFiles, + newFiles: ctx.newFiles, + newProject: ctx.project, }, }) }) }) describe('when moving a folder inside itself', function () { - it('throws an error', async function () { + it('throws an error', async function (ctx) { await expect( - this.subject.promises.moveEntity( - this.project._id, - this.folder._id, - this.folder._id, + ctx.subject.promises.moveEntity( + ctx.project._id, + ctx.folder._id, + ctx.folder._id, 'folder' ) ).to.be.rejectedWith(Errors.InvalidNameError) @@ -749,12 +786,12 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('when moving a folder to a subfolder of itself', function () { - it('throws an error', async function () { + it('throws an error', async function (ctx) { await expect( - this.subject.promises.moveEntity( - this.project._id, - this.folder._id, - this.subfolder._id, + ctx.subject.promises.moveEntity( + ctx.project._id, + ctx.folder._id, + ctx.subfolder._id, 'folder' ) ).to.be.rejectedWith(Errors.InvalidNameError) @@ -762,12 +799,12 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('when moving a folder to a subfolder which starts with the same characters', function () { - it('does not throw an error', async function () { + it('does not throw an error', async function (ctx) { await expect( - this.subject.promises.moveEntity( - this.project._id, - this.folder._id, - this.notSubfolder._id, + ctx.subject.promises.moveEntity( + ctx.project._id, + ctx.folder._id, + ctx.notSubfolder._id, 'folder' ) ).not.to.be.rejectedWith(Errors.InvalidNameError) @@ -776,55 +813,55 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('deleteEntity', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id }, + { _id: ctx.project._id }, { - $pull: { 'rootFolder.0.docs': { _id: this.doc._id } }, + $pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) - await this.subject.promises.deleteEntity( - this.project._id, - this.doc._id, + .resolves(ctx.project) + await ctx.subject.promises.deleteEntity( + ctx.project._id, + ctx.doc._id, 'doc', userId ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) }) describe('renameEntity', function () { describe('happy path', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const userId = new ObjectId().toString() - this.newName = 'new.tex' - this.oldDocs = ['old-doc'] - this.oldFiles = ['old-file'] - this.newDocs = ['new-doc'] - this.newFiles = ['new-file'] + ctx.newName = 'new.tex' + ctx.oldDocs = ['old-doc'] + ctx.oldFiles = ['old-file'] + ctx.newDocs = ['new-doc'] + ctx.newFiles = ['new-file'] - this.ProjectEntityHandler.getAllEntitiesFromProject + ctx.ProjectEntityHandler.getAllEntitiesFromProject .onFirstCall() - .returns({ docs: this.oldDocs, files: this.oldFiles }) - this.ProjectEntityHandler.getAllEntitiesFromProject + .returns({ docs: ctx.oldDocs, files: ctx.oldFiles }) + ctx.ProjectEntityHandler.getAllEntitiesFromProject .onSecondCall() - .returns({ docs: this.newDocs, files: this.newFiles }) + .returns({ docs: ctx.newDocs, files: ctx.newFiles }) - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id, 'rootFolder.0.docs.0': { $exists: true } }, + { _id: ctx.project._id, 'rootFolder.0.docs.0': { $exists: true } }, { $set: { - 'rootFolder.0.docs.0.name': this.newName, + 'rootFolder.0.docs.0.name': ctx.newName, lastUpdated: new Date(), lastUpdatedBy: userId, }, @@ -832,45 +869,45 @@ describe('ProjectEntityMongoUpdateHandler', function () { } ) .chain('exec') - .resolves(this.project) - this.result = await this.subject.promises.renameEntity( - this.project._id, - this.doc._id, + .resolves(ctx.project) + ctx.result = await ctx.subject.promises.renameEntity( + ctx.project._id, + ctx.doc._id, 'doc', - this.newName, + ctx.newName, userId ) }) - it('should update the database', function () { - this.ProjectMock.verify() + it('should update the database', function (ctx) { + ctx.ProjectMock.verify() }) - it('returns info', function () { - expect(this.result).to.deep.equal({ - project: this.project, + it('returns info', function (ctx) { + expect(ctx.result).to.deep.equal({ + project: ctx.project, startPath: '/test-doc.txt', endPath: '/new.tex', - rev: this.doc.rev, + rev: ctx.doc.rev, changes: { - oldDocs: this.oldDocs, - newDocs: this.newDocs, - oldFiles: this.oldFiles, - newFiles: this.newFiles, - newProject: this.project, + oldDocs: ctx.oldDocs, + newDocs: ctx.newDocs, + oldFiles: ctx.oldFiles, + newFiles: ctx.newFiles, + newProject: ctx.project, }, }) }) }) describe('name already exists', function () { - it('should throw an error', async function () { + it('should throw an error', async function (ctx) { await expect( - this.subject.promises.renameEntity( - this.project._id, - this.doc._id, + ctx.subject.promises.renameEntity( + ctx.project._id, + ctx.doc._id, 'doc', - this.folder.name + ctx.folder.name ) ).to.be.rejectedWith(Errors.DuplicateNameError) }) @@ -881,136 +918,131 @@ describe('ProjectEntityMongoUpdateHandler', function () { describe('updating the project', function () { describe('when the parent folder is given', function () { let userId - beforeEach(function () { + beforeEach(function (ctx) { userId = new ObjectId().toString() - this.newFile = { _id: new ObjectId(), name: 'new file.png' } - this.ProjectMock.expects('findOneAndUpdate') + ctx.newFile = { _id: new ObjectId(), name: 'new file.png' } + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: true }, }, { - $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile }, + $push: { 'rootFolder.0.folders.0.fileRefs': ctx.newFile }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) + .resolves(ctx.project) }) - it('should update the database', async function () { - await this.subject.promises._putElement( - this.project, - this.folder._id, - this.newFile, + it('should update the database', async function (ctx) { + await ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, + ctx.newFile, 'files', userId ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) - it('should add an s onto the type if not included', async function () { - await this.subject.promises._putElement( - this.project, - this.folder._id, - this.newFile, + it('should add an s onto the type if not included', async function (ctx) { + await ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, + ctx.newFile, 'file', userId ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('error cases', function () { - it('should throw an error if element is null', async function () { + it('should throw an error if element is null', async function (ctx) { await expect( - this.subject.promises._putElement( - this.project, - this.folder._id, + ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, null, 'file' ) ).to.be.rejected }) - it('should error if the element has no _id', async function () { + it('should error if the element has no _id', async function (ctx) { const file = { name: 'something' } await expect( - this.subject.promises._putElement( - this.project, - this.folder._id, + ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, file, 'file' ) ).to.be.rejected }) - it('should error if element name contains invalid characters', async function () { + it('should error if element name contains invalid characters', async function (ctx) { const file = { _id: new ObjectId(), name: 'something*bad' } await expect( - this.subject.promises._putElement( - this.project, - this.folder._id, + ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, file, 'file' ) ).to.be.rejected }) - it('should error if element name is too long', async function () { + it('should error if element name is too long', async function (ctx) { const file = { _id: new ObjectId(), name: 'long-'.repeat(1000) + 'something', } await expect( - this.subject.promises._putElement( - this.project, - this.folder._id, + ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, file, 'file' ) ).to.be.rejectedWith(Errors.InvalidNameError) }) - it('should error if the folder name is too long', async function () { + it('should error if the folder name is too long', async function (ctx) { const file = { _id: new ObjectId(), name: 'something', } - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project: this.project, - element_id: this.folder._id, + project: ctx.project, + element_id: ctx.folder._id, type: 'folder', }) .resolves({ - element: this.folder, + element: ctx.folder, path: { fileSystem: 'subdir/'.repeat(1000) + 'foo' }, }) await expect( - this.subject.promises._putElement( - this.project, - this.folder._id, + ctx.subject.promises._putElement( + ctx.project, + ctx.folder._id, file, 'file' ) ).to.be.rejectedWith(Errors.InvalidNameError) }) ;['file', 'doc', 'folder'].forEach(entityType => { - it(`should error if a ${entityType} already exists with the same name`, async function () { + it(`should error if a ${entityType} already exists with the same name`, async function (ctx) { const file = { _id: new ObjectId(), - name: this[entityType].name, + name: ctx[entityType].name, } await expect( - this.subject.promises._putElement( - this.project, - null, - file, - 'file' - ) + ctx.subject.promises._putElement(ctx.project, null, file, 'file') ).to.be.rejectedWith(Errors.DuplicateNameError) }) }) @@ -1018,24 +1050,24 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('when the parent folder is not given', function () { - it('should default to root folder insert', async function () { + it('should default to root folder insert', async function (ctx) { const userId = new ObjectId().toString() - this.newFile = { _id: new ObjectId(), name: 'new file.png' } - this.ProjectMock.expects('findOneAndUpdate') + ctx.newFile = { _id: new ObjectId(), name: 'new file.png' } + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id, 'rootFolder.0': { $exists: true } }, + { _id: ctx.project._id, 'rootFolder.0': { $exists: true } }, { - $push: { 'rootFolder.0.fileRefs': this.newFile }, + $push: { 'rootFolder.0.fileRefs': ctx.newFile }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, } ) .chain('exec') - .resolves(this.project) - await this.subject.promises._putElement( - this.project, - this.rootFolder._id, - this.newFile, + .resolves(ctx.project) + await ctx.subject.promises._putElement( + ctx.project, + ctx.rootFolder._id, + ctx.newFile, 'file', userId ) @@ -1044,53 +1076,53 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('createNewFolderStructure', function () { - beforeEach(function () { - this.mockRootFolder = 'MOCK_ROOT_FOLDER' - this.docUploads = ['MOCK_DOC_UPLOAD'] - this.fileUploads = ['MOCK_FILE_UPLOAD'] - this.FolderStructureBuilder.buildFolderStructure - .withArgs(this.docUploads, this.fileUploads) - .returns(this.mockRootFolder) - this.updateExpectation = this.ProjectMock.expects('findOneAndUpdate') + beforeEach(function (ctx) { + ctx.mockRootFolder = 'MOCK_ROOT_FOLDER' + ctx.docUploads = ['MOCK_DOC_UPLOAD'] + ctx.fileUploads = ['MOCK_FILE_UPLOAD'] + ctx.FolderStructureBuilder.buildFolderStructure + .withArgs(ctx.docUploads, ctx.fileUploads) + .returns(ctx.mockRootFolder) + ctx.updateExpectation = ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, 'rootFolder.0.folders.0': { $exists: false }, 'rootFolder.0.docs.0': { $exists: false }, 'rootFolder.0.files.0': { $exists: false }, }, - { $set: { rootFolder: [this.mockRootFolder] }, $inc: { version: 1 } }, + { $set: { rootFolder: [ctx.mockRootFolder] }, $inc: { version: 1 } }, { new: true, lean: true, fields: { version: 1 } } ) .chain('exec') }) describe('happy path', function () { - beforeEach(async function () { - this.updateExpectation.resolves({ version: 1 }) - await this.subject.promises.createNewFolderStructure( - this.project._id, - this.docUploads, - this.fileUploads + beforeEach(async function (ctx) { + ctx.updateExpectation.resolves({ version: 1 }) + await ctx.subject.promises.createNewFolderStructure( + ctx.project._id, + ctx.docUploads, + ctx.fileUploads ) }) - it('updates the database', function () { - this.ProjectMock.verify() + it('updates the database', function (ctx) { + ctx.ProjectMock.verify() }) }) describe("when the update doesn't find a matching document", function () { - beforeEach(async function () { - this.updateExpectation.resolves(null) + beforeEach(async function (ctx) { + ctx.updateExpectation.resolves(null) }) - it('throws an error', async function () { + it('throws an error', async function (ctx) { await expect( - this.subject.promises.createNewFolderStructure( - this.project._id, - this.docUploads, - this.fileUploads + ctx.subject.promises.createNewFolderStructure( + ctx.project._id, + ctx.docUploads, + ctx.fileUploads ) ).to.be.rejected }) @@ -1098,54 +1130,54 @@ describe('ProjectEntityMongoUpdateHandler', function () { }) describe('replaceDocWithFile', function () { - it('should simultaneously remove the doc and add the file', async function () { + it('should simultaneously remove the doc and add the file', async function (ctx) { const userId = new ObjectId().toString() - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id, 'rootFolder.0': { $exists: true } }, + { _id: ctx.project._id, 'rootFolder.0': { $exists: true } }, { - $pull: { 'rootFolder.0.docs': { _id: this.doc._id } }, - $push: { 'rootFolder.0.fileRefs': this.file }, + $pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } }, + $push: { 'rootFolder.0.fileRefs': ctx.file }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, }, { new: true } ) .chain('exec') - .resolves(this.project) - await this.subject.promises.replaceDocWithFile( - this.project._id, - this.doc._id, - this.file, + .resolves(ctx.project) + await ctx.subject.promises.replaceDocWithFile( + ctx.project._id, + ctx.doc._id, + ctx.file, userId ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) describe('replaceFileWithDoc', function () { - it('should simultaneously remove the file and add the doc', async function () { + it('should simultaneously remove the file and add the doc', async function (ctx) { const userId = new ObjectId().toString() - this.ProjectMock.expects('findOneAndUpdate') + ctx.ProjectMock.expects('findOneAndUpdate') .withArgs( - { _id: this.project._id, 'rootFolder.0': { $exists: true } }, + { _id: ctx.project._id, 'rootFolder.0': { $exists: true } }, { - $pull: { 'rootFolder.0.fileRefs': { _id: this.file._id } }, - $push: { 'rootFolder.0.docs': this.doc }, + $pull: { 'rootFolder.0.fileRefs': { _id: ctx.file._id } }, + $push: { 'rootFolder.0.docs': ctx.doc }, $inc: { version: 1 }, $set: { lastUpdated: new Date(), lastUpdatedBy: userId }, }, { new: true } ) .chain('exec') - .resolves(this.project) - await this.subject.promises.replaceFileWithDoc( - this.project._id, - this.file._id, - this.doc, + .resolves(ctx.project) + await ctx.subject.promises.replaceFileWithDoc( + ctx.project._id, + ctx.file._id, + ctx.doc, userId ) - this.ProjectMock.verify() + ctx.ProjectMock.verify() }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs b/services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs index a81a0c8e71..386aa347a1 100644 --- a/services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs @@ -1,8 +1,13 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import mongodb from 'mongodb-legacy' + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + +const { ObjectId } = mongodb const MODULE_PATH = '../../../../app/src/Features/Project/ProjectEntityUpdateHandler' @@ -16,8 +21,8 @@ describe('ProjectEntityUpdateHandler', function () { const newFileId = '4eecaffcbffa66588e000099' const userId = 1234 - beforeEach(function () { - this.project = { + beforeEach(async function (ctx) { + ctx.project = { _id: projectId, name: 'project name', overleaf: { @@ -26,9 +31,9 @@ describe('ProjectEntityUpdateHandler', function () { }, }, } - this.user = { _id: new ObjectId() } + ctx.user = { _id: new ObjectId() } - this.DocModel = class Doc { + ctx.DocModel = class Doc { constructor(options) { this.name = options.name this.lines = options.lines @@ -36,7 +41,7 @@ describe('ProjectEntityUpdateHandler', function () { this.rev = options.rev ?? 0 } } - this.FileModel = class File { + ctx.FileModel = class File { constructor(options) { this.name = options.name // use a new id for replacement files @@ -54,20 +59,20 @@ describe('ProjectEntityUpdateHandler', function () { } } } - this.docName = 'doc-name' - this.docLines = ['1234', 'abc'] - this.doc = { _id: new ObjectId(), name: this.docName } + ctx.docName = 'doc-name' + ctx.docLines = ['1234', 'abc'] + ctx.doc = { _id: new ObjectId(), name: ctx.docName } - this.fileName = 'something.jpg' - this.fileSystemPath = 'somehintg' - this.file = { _id: new ObjectId(), name: this.fileName, rev: 2 } + ctx.fileName = 'something.jpg' + ctx.fileSystemPath = 'somehintg' + ctx.file = { _id: new ObjectId(), name: ctx.fileName, rev: 2 } - this.linkedFileData = { provider: 'url' } + ctx.linkedFileData = { provider: 'url' } - this.source = 'editor' - this.callback = sinon.stub() + ctx.source = 'editor' + ctx.callback = sinon.stub() - this.DocstoreManager = { + ctx.DocstoreManager = { promises: { getDoc: sinon.stub(), isDocDeleted: sinon.stub(), @@ -75,7 +80,7 @@ describe('ProjectEntityUpdateHandler', function () { deleteDoc: sinon.stub(), }, } - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { flushDocToMongo: sinon.stub().resolves(), flushProjectToMongo: sinon.stub().resolves(), @@ -85,47 +90,47 @@ describe('ProjectEntityUpdateHandler', function () { deleteDoc: sinon.stub().resolves(), }, } - this.fs = { + ctx.fs = { promises: { unlink: sinon.stub().resolves(), }, } - this.LockManager = { + ctx.LockManager = { promises: { runWithLock: sinon.spy((namespace, id, runner, callback) => runner(callback) ), }, - withTimeout: sinon.stub().returns(this.LockManager), + withTimeout: sinon.stub().returns(ctx.LockManager), } - this.ProjectModel = { + ctx.ProjectModel = { updateOne: sinon.stub().returns({ exec: sinon.stub().resolves() }), } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProject: sinon.stub(), getProjectWithoutDocLines: sinon.stub(), }, } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: { findElement: sinon.stub(), findElementByPath: sinon.stub(), }, } - this.ProjectUpdater = { + ctx.ProjectUpdater = { promises: { markAsUpdated: sinon.stub().resolves(), }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { getAllEntitiesFromProject: sinon.stub(), promises: { getDoc: sinon.stub(), getDocPathByProjectIdAndDocId: sinon.stub(), }, } - this.ProjectEntityMongoUpdateHandler = { + ctx.ProjectEntityMongoUpdateHandler = { promises: { addDoc: sinon.stub(), addFile: sinon.stub(), @@ -141,7 +146,7 @@ describe('ProjectEntityUpdateHandler', function () { replaceFileWithDoc: sinon.stub(), }, } - this.TpdsUpdateSender = { + ctx.TpdsUpdateSender = { promises: { addFile: sinon.stub().resolves(), addDoc: sinon.stub(), @@ -149,91 +154,165 @@ describe('ProjectEntityUpdateHandler', function () { moveEntity: sinon.stub().resolves(), }, } - this.FileStoreHandler = { + ctx.FileStoreHandler = { promises: { uploadFileFromDisk: sinon.stub(), }, } - this.FileWriter = { + ctx.FileWriter = { promises: { writeLinesToDisk: sinon.stub(), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.ProjectOptionsHandler = { + ctx.ProjectOptionsHandler = { setHistoryRangesSupport: sinon.stub().resolves(), } - this.ProjectEntityUpdateHandler = SandboxedModule.require(MODULE_PATH, { - requires: { - '@overleaf/settings': { validRootDocExtensions: ['tex'] }, - fs: this.fs, - '../../models/Doc': { Doc: this.DocModel }, - '../Docstore/DocstoreManager': this.DocstoreManager, - '../../Features/DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../../models/File': { File: this.FileModel }, - '../FileStore/FileStoreHandler': this.FileStoreHandler, - '../../infrastructure/LockManager': this.LockManager, - '../../models/Project': { Project: this.ProjectModel }, - './ProjectGetter': this.ProjectGetter, - './ProjectLocator': this.ProjectLocator, - './ProjectUpdateHandler': this.ProjectUpdater, - './ProjectEntityHandler': this.ProjectEntityHandler, - './ProjectEntityMongoUpdateHandler': - this.ProjectEntityMongoUpdateHandler, - './ProjectOptionsHandler': this.ProjectOptionsHandler, - '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender, - '../Editor/EditorRealTimeController': this.EditorRealTimeController, - '../../infrastructure/FileWriter': this.FileWriter, - }, - }) + + vi.doMock('@overleaf/settings', () => ({ + default: { validRootDocExtensions: ['tex'] }, + })) + + vi.doMock('fs', () => ({ + default: ctx.fs, + })) + + vi.doMock('../../../../app/src/models/Doc', () => ({ + Doc: ctx.DocModel, + })) + + vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({ + default: ctx.DocstoreManager, + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/models/File', () => ({ + File: ctx.FileModel, + })) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: ctx.FileStoreHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({ + default: ctx.LockManager, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: ctx.ProjectModel, + })) + + 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/ProjectUpdateHandler', + () => ({ + default: ctx.ProjectUpdater, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler', + () => ({ + default: ctx.ProjectEntityMongoUpdateHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectOptionsHandler', + () => ({ + default: ctx.ProjectOptionsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender', + () => ({ + default: ctx.TpdsUpdateSender, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/FileWriter', () => ({ + default: ctx.FileWriter, + })) + + ctx.ProjectEntityUpdateHandler = (await import(MODULE_PATH)).default }) describe('updateDocLines', function () { - beforeEach(function () { - this.path = '/somewhere/something.tex' - this.doc = { + beforeEach(function (ctx) { + ctx.path = '/somewhere/something.tex' + ctx.doc = { _id: docId, } - this.version = 42 - this.ranges = { mock: 'ranges' } - this.lastUpdatedAt = new Date().getTime() - this.lastUpdatedBy = 'fake-last-updater-id' - this.parentFolder = { _id: new ObjectId() } - this.DocstoreManager.promises.isDocDeleted.resolves(false) - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectLocator.promises.findElement.resolves({ - element: this.doc, + ctx.version = 42 + ctx.ranges = { mock: 'ranges' } + ctx.lastUpdatedAt = new Date().getTime() + ctx.lastUpdatedBy = 'fake-last-updater-id' + ctx.parentFolder = { _id: new ObjectId() } + ctx.DocstoreManager.promises.isDocDeleted.resolves(false) + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.doc, path: { - fileSystem: this.path, + fileSystem: ctx.path, }, - folder: this.parentFolder, + folder: ctx.parentFolder, }) - this.TpdsUpdateSender.promises.addDoc.resolves() + ctx.TpdsUpdateSender.promises.addDoc.resolves() }) describe('when the doc has been modified', function () { - beforeEach(async function () { - this.DocstoreManager.promises.updateDoc.resolves({ + beforeEach(async function (ctx) { + ctx.DocstoreManager.promises.updateDoc.resolves({ modified: true, - rev: (this.rev = 5), + rev: (ctx.rev = 5), }) - await this.ProjectEntityUpdateHandler.promises.updateDocLines( + await ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should get the project with very few fields', function () { - this.ProjectGetter.promises.getProject + it('should get the project with very few fields', function (ctx) { + ctx.ProjectGetter.promises.getProject .calledWith(projectId, { name: true, rootFolder: true, @@ -241,182 +320,164 @@ describe('ProjectEntityUpdateHandler', function () { .should.equal(true) }) - it('should find the doc', function () { - this.ProjectLocator.promises.findElement + it('should find the doc', function (ctx) { + ctx.ProjectLocator.promises.findElement .calledWith({ - project: this.project, + project: ctx.project, type: 'docs', element_id: docId, }) .should.equal(true) }) - it('should update the doc in the docstore', function () { - this.DocstoreManager.promises.updateDoc - .calledWith( - projectId, - docId, - this.docLines, - this.version, - this.ranges - ) + it('should update the doc in the docstore', function (ctx) { + ctx.DocstoreManager.promises.updateDoc + .calledWith(projectId, docId, ctx.docLines, ctx.version, ctx.ranges) .should.equal(true) }) - it('should mark the project as updated', function () { + it('should mark the project as updated', function (ctx) { sinon.assert.calledWith( - this.ProjectUpdater.promises.markAsUpdated, + ctx.ProjectUpdater.promises.markAsUpdated, projectId, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should send the doc to the TPDS', function () { - this.TpdsUpdateSender.promises.addDoc.should.have.been.calledWith({ + it('should send the doc to the TPDS', function (ctx) { + ctx.TpdsUpdateSender.promises.addDoc.should.have.been.calledWith({ projectId, - projectName: this.project.name, + projectName: ctx.project.name, docId, - rev: this.rev, - path: this.path, - folderId: this.parentFolder._id, + rev: ctx.rev, + path: ctx.path, + folderId: ctx.parentFolder._id, }) }) }) describe('when the doc has not been modified', function () { - beforeEach(async function () { - this.DocstoreManager.promises.updateDoc.resolves({ + beforeEach(async function (ctx) { + ctx.DocstoreManager.promises.updateDoc.resolves({ modified: false, - rev: (this.rev = 5), + rev: (ctx.rev = 5), }) - await this.ProjectEntityUpdateHandler.promises.updateDocLines( + await ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should not mark the project as updated', function () { - this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) + it('should not mark the project as updated', function (ctx) { + ctx.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) }) - it('should not send the doc the to the TPDS', function () { - this.TpdsUpdateSender.promises.addDoc.called.should.equal(false) + it('should not send the doc the to the TPDS', function (ctx) { + ctx.TpdsUpdateSender.promises.addDoc.called.should.equal(false) }) }) describe('when the doc has been deleted', function () { - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectLocator.promises.findElement.rejects( + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectLocator.promises.findElement.rejects( new Errors.NotFoundError() ) - this.DocstoreManager.promises.isDocDeleted.resolves(true) - this.DocstoreManager.promises.updateDoc.resolves({}) + ctx.DocstoreManager.promises.isDocDeleted.resolves(true) + ctx.DocstoreManager.promises.updateDoc.resolves({}) - await this.ProjectEntityUpdateHandler.promises.updateDocLines( + await ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should update the doc in the docstore', function () { - this.DocstoreManager.promises.updateDoc - .calledWith( - projectId, - docId, - this.docLines, - this.version, - this.ranges - ) + it('should update the doc in the docstore', function (ctx) { + ctx.DocstoreManager.promises.updateDoc + .calledWith(projectId, docId, ctx.docLines, ctx.version, ctx.ranges) .should.equal(true) }) - it('should not mark the project as updated', function () { - this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) + it('should not mark the project as updated', function (ctx) { + ctx.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) }) - it('should not send the doc the to the TPDS', function () { - this.TpdsUpdateSender.promises.addDoc.called.should.equal(false) + it('should not send the doc the to the TPDS', function (ctx) { + ctx.TpdsUpdateSender.promises.addDoc.called.should.equal(false) }) }) describe('when projects and docs collection are de-synced', function () { - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) // The doc is not in the file-tree, but also not marked as deleted. // This should not happen, but web should handle it. - this.ProjectLocator.promises.findElement.rejects( + ctx.ProjectLocator.promises.findElement.rejects( new Errors.NotFoundError() ) - this.DocstoreManager.promises.isDocDeleted.resolves(false) + ctx.DocstoreManager.promises.isDocDeleted.resolves(false) - this.DocstoreManager.promises.updateDoc.resolves({}) + ctx.DocstoreManager.promises.updateDoc.resolves({}) - await this.ProjectEntityUpdateHandler.promises.updateDocLines( + await ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should update the doc in the docstore', function () { - this.DocstoreManager.promises.updateDoc - .calledWith( - projectId, - docId, - this.docLines, - this.version, - this.ranges - ) + it('should update the doc in the docstore', function (ctx) { + ctx.DocstoreManager.promises.updateDoc + .calledWith(projectId, docId, ctx.docLines, ctx.version, ctx.ranges) .should.equal(true) }) - it('should not mark the project as updated', function () { - this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) + it('should not mark the project as updated', function (ctx) { + ctx.ProjectUpdater.promises.markAsUpdated.called.should.equal(false) }) - it('should not send the doc the to the TPDS', function () { - this.TpdsUpdateSender.promises.addDoc.called.should.equal(false) + it('should not send the doc the to the TPDS', function (ctx) { + ctx.TpdsUpdateSender.promises.addDoc.called.should.equal(false) }) }) describe('when the doc is not related to the project', function () { let updateDocLinesPromise - beforeEach(function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectLocator.promises.findElement.rejects( + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectLocator.promises.findElement.rejects( new Errors.NotFoundError() ) - this.DocstoreManager.promises.isDocDeleted.rejects( + ctx.DocstoreManager.promises.isDocDeleted.rejects( new Errors.NotFoundError() ) updateDocLinesPromise = - this.ProjectEntityUpdateHandler.promises.updateDocLines( + ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) @@ -432,7 +493,7 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.be.instanceOf(Errors.NotFoundError) }) - it('should not update the doc', async function () { + it('should not update the doc', async function (ctx) { let error try { @@ -442,10 +503,10 @@ describe('ProjectEntityUpdateHandler', function () { } expect(error).to.exist - this.DocstoreManager.promises.updateDoc.called.should.equal(false) + ctx.DocstoreManager.promises.updateDoc.called.should.equal(false) }) - it('should not send the doc the to the TPDS', async function () { + it('should not send the doc the to the TPDS', async function (ctx) { let error try { @@ -455,25 +516,25 @@ describe('ProjectEntityUpdateHandler', function () { } expect(error).to.exist - this.TpdsUpdateSender.promises.addDoc.called.should.equal(false) + ctx.TpdsUpdateSender.promises.addDoc.called.should.equal(false) }) }) describe('when the project is not found', function () { let error - beforeEach(async function () { - this.ProjectGetter.promises.getProject.rejects( + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.rejects( new Errors.NotFoundError() ) try { - await this.ProjectEntityUpdateHandler.promises.updateDocLines( + await ctx.ProjectEntityUpdateHandler.promises.updateDocLines( projectId, docId, - this.docLines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.docLines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) } catch (err) { error = err @@ -484,47 +545,47 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.be.instanceOf(Errors.NotFoundError) }) - it('should not update the doc', async function () { - this.DocstoreManager.promises.updateDoc.called.should.equal(false) + it('should not update the doc', async function (ctx) { + ctx.DocstoreManager.promises.updateDoc.called.should.equal(false) }) - it('should not send the doc the to the TPDS', function () { - this.TpdsUpdateSender.promises.addDoc.called.should.equal(false) + it('should not send the doc the to the TPDS', function (ctx) { + ctx.TpdsUpdateSender.promises.addDoc.called.should.equal(false) }) }) }) describe('setRootDoc', function () { - beforeEach(function () { - this.rootDocId = 'root-doc-id-123123' + beforeEach(function (ctx) { + ctx.rootDocId = 'root-doc-id-123123' }) - it('should call Project.updateOne when the doc exists and has a valid extension', async function () { - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves( + it('should call Project.updateOne when the doc exists and has a valid extension', async function (ctx) { + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves( `/main.tex` ) - await this.ProjectEntityUpdateHandler.promises.setRootDoc( + await ctx.ProjectEntityUpdateHandler.promises.setRootDoc( projectId, - this.rootDocId + ctx.rootDocId ) - this.ProjectModel.updateOne - .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId }) + ctx.ProjectModel.updateOne + .calledWith({ _id: projectId }, { rootDoc_id: ctx.rootDocId }) .should.equal(true) }) - it("should not call Project.updateOne when the doc doesn't exist", async function () { - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.rejects( + it("should not call Project.updateOne when the doc doesn't exist", async function (ctx) { + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.rejects( Errors.NotFoundError ) let error try { - await this.ProjectEntityUpdateHandler.promises.setRootDoc( + await ctx.ProjectEntityUpdateHandler.promises.setRootDoc( projectId, - this.rootDocId + ctx.rootDocId ) } catch (err) { error = err @@ -532,19 +593,19 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.exist - this.ProjectModel.updateOne - .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId }) + ctx.ProjectModel.updateOne + .calledWith({ _id: projectId }, { rootDoc_id: ctx.rootDocId }) .should.equal(false) }) - it('should call the callback with an UnsupportedFileTypeError when the doc has an unaccepted file extension', function () { - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves( + it('should call the callback with an UnsupportedFileTypeError when the doc has an unaccepted file extension', function (ctx) { + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves( `/foo/bar.baz` ) - this.ProjectEntityUpdateHandler.setRootDoc( + ctx.ProjectEntityUpdateHandler.setRootDoc( projectId, - this.rootDocId, + ctx.rootDocId, error => { expect(error).to.be.an.instanceof(Errors.UnsupportedFileTypeError) } @@ -553,9 +614,9 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('unsetRootDoc', function () { - it('should call Project.updateOne', async function () { - await this.ProjectEntityUpdateHandler.promises.unsetRootDoc(projectId) - this.ProjectModel.updateOne + it('should call Project.updateOne', async function (ctx) { + await ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc(projectId) + ctx.ProjectModel.updateOne .calledWith({ _id: projectId }, { $unset: { rootDoc_id: true } }) .should.equal(true) }) @@ -563,81 +624,81 @@ describe('ProjectEntityUpdateHandler', function () { describe('addDoc', function () { describe('adding a doc', function () { - beforeEach(async function () { - this.path = '/path/to/doc' - this.rev = 5 + beforeEach(async function (ctx) { + ctx.path = '/path/to/doc' + ctx.rev = 5 - this.newDoc = new this.DocModel({ - name: this.docName, + ctx.newDoc = new ctx.DocModel({ + name: ctx.docName, lines: undefined, _id: docId, - rev: this.rev, + rev: ctx.rev, }) - this.DocstoreManager.promises.updateDoc.resolves({ + ctx.DocstoreManager.promises.updateDoc.resolves({ lines: false, - rev: this.rev, + rev: ctx.rev, }) - this.TpdsUpdateSender.promises.addDoc.resolves() - this.ProjectEntityMongoUpdateHandler.promises.addDoc.resolves({ - result: { path: { fileSystem: this.path } }, - project: this.project, + ctx.TpdsUpdateSender.promises.addDoc.resolves() + ctx.ProjectEntityMongoUpdateHandler.promises.addDoc.resolves({ + result: { path: { fileSystem: ctx.path } }, + project: ctx.project, }) - await this.ProjectEntityUpdateHandler.promises.addDoc( + await ctx.ProjectEntityUpdateHandler.promises.addDoc( projectId, docId, - this.docName, - this.docLines, + ctx.docName, + ctx.docLines, userId, - this.source + ctx.source ) }) - it('creates the doc without history', function () { - this.DocstoreManager.promises.updateDoc - .calledWith(projectId, docId, this.docLines, 0, {}) + it('creates the doc without history', function (ctx) { + ctx.DocstoreManager.promises.updateDoc + .calledWith(projectId, docId, ctx.docLines, 0, {}) .should.equal(true) }) - it('sends the change in project structure to the doc updater', function () { + it('sends the change in project structure to the doc updater', function (ctx) { const newDocs = [ { - doc: this.newDoc, - path: this.path, - docLines: this.docLines.join('\n'), + doc: ctx.newDoc, + path: ctx.path, + docLines: ctx.docLines.join('\n'), ranges: {}, }, ] - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( projectId, projectHistoryId, userId, { newDocs, - newProject: this.project, + newProject: ctx.project, }, - this.source + ctx.source ) }) }) describe('adding a doc with an invalid name', function () { - beforeEach(function () { - this.path = '/path/to/doc' + beforeEach(function (ctx) { + ctx.path = '/path/to/doc' - this.newDoc = { _id: docId } + ctx.newDoc = { _id: docId } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.addDoc( + await ctx.ProjectEntityUpdateHandler.promises.addDoc( projectId, folderId, - `*${this.docName}`, - this.docLines, + `*${ctx.docName}`, + ctx.docLines, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -650,122 +711,122 @@ describe('ProjectEntityUpdateHandler', function () { describe('addFile', function () { describe('adding a file', function () { - beforeEach(async function () { - this.path = '/path/to/file' + beforeEach(async function (ctx) { + ctx.path = '/path/to/file' - this.newFile = { + ctx.newFile = { _id: fileId, hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', rev: 0, - name: this.fileName, - linkedFileData: this.linkedFileData, + name: ctx.fileName, + linkedFileData: ctx.linkedFileData, } - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.newFile, + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.newFile, createdBlob: true, }) - this.TpdsUpdateSender.promises.addFile.resolves() - this.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({ - result: { path: { fileSystem: this.path } }, - project: this.project, + ctx.TpdsUpdateSender.promises.addFile.resolves() + ctx.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({ + result: { path: { fileSystem: ctx.path } }, + project: ctx.project, }) - await this.ProjectEntityUpdateHandler.promises.addFile( + await ctx.ProjectEntityUpdateHandler.promises.addFile( projectId, folderId, - this.fileName, - this.fileSystemPath, - this.linkedFileData, + ctx.fileName, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) }) - it('updates the file in the filestore', function () { - this.FileStoreHandler.promises.uploadFileFromDisk + it('updates the file in the filestore', function (ctx) { + ctx.FileStoreHandler.promises.uploadFileFromDisk .calledWith( projectId, - { name: this.fileName, linkedFileData: this.linkedFileData }, - this.fileSystemPath + { name: ctx.fileName, linkedFileData: ctx.linkedFileData }, + ctx.fileSystemPath ) .should.equal(true) }) - it('updates the file in mongo', function () { + it('updates the file in mongo', function (ctx) { const fileMatcher = sinon.match(file => { - return file.name === this.fileName + return file.name === ctx.fileName }) - this.ProjectEntityMongoUpdateHandler.promises.addFile + ctx.ProjectEntityMongoUpdateHandler.promises.addFile .calledWithMatch(projectId, folderId, fileMatcher) .should.equal(true) }) - it('notifies the tpds', function () { - this.TpdsUpdateSender.promises.addFile + it('notifies the tpds', function (ctx) { + ctx.TpdsUpdateSender.promises.addFile .calledWith({ projectId, - historyId: this.project.overleaf.history.id, - projectName: this.project.name, + historyId: ctx.project.overleaf.history.id, + projectName: ctx.project.name, fileId, - hash: this.newFile.hash, + hash: ctx.newFile.hash, rev: 0, - path: this.path, + path: ctx.path, folderId, }) .should.equal(true) }) - it('sends the change in project structure to the doc updater', function () { + it('sends the change in project structure to the doc updater', function (ctx) { const newFiles = [ { - file: this.newFile, - path: this.path, + file: ctx.newFile, + path: ctx.path, createdBlob: true, }, ] - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( projectId, projectHistoryId, userId, { newFiles, - newProject: this.project, + newProject: ctx.project, }, - this.source + ctx.source ) }) }) describe('adding a file with an invalid name', function () { - beforeEach(function () { - this.path = '/path/to/file' + beforeEach(function (ctx) { + ctx.path = '/path/to/file' - this.newFile = { + ctx.newFile = { _id: fileId, hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', rev: 0, - name: this.fileName, - linkedFileData: this.linkedFileData, + name: ctx.fileName, + linkedFileData: ctx.linkedFileData, } - this.TpdsUpdateSender.promises.addFile.resolves() - this.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({ - result: { path: { fileSystem: this.path } }, - project: this.project, + ctx.TpdsUpdateSender.promises.addFile.resolves() + ctx.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({ + result: { path: { fileSystem: ctx.path } }, + project: ctx.project, }) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.addFile( + await ctx.ProjectEntityUpdateHandler.promises.addFile( projectId, folderId, - `*${this.fileName}`, - this.fileSystemPath, - this.linkedFileData, + `*${ctx.fileName}`, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -778,20 +839,20 @@ describe('ProjectEntityUpdateHandler', function () { describe('upsertDoc', function () { describe('upserting into an invalid folder', function () { - beforeEach(function () { - this.ProjectLocator.promises.findElement.resolves({ element: null }) + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findElement.resolves({ element: null }) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertDoc( + await ctx.ProjectEntityUpdateHandler.promises.upsertDoc( projectId, folderId, - this.docName, - this.docLines, - this.source, + ctx.docName, + ctx.docLines, + ctx.source, userId ) } catch (err) { @@ -804,36 +865,36 @@ describe('ProjectEntityUpdateHandler', function () { describe('updating an existing doc', function () { let upsertDocResponse - beforeEach(async function () { - this.existingDoc = { _id: docId, name: this.docName } - this.existingFile = { + beforeEach(async function (ctx) { + ctx.existingDoc = { _id: docId, name: ctx.docName } + ctx.existingFile = { _id: fileId, - name: this.fileName, + name: ctx.fileName, hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', } - this.folder = { + ctx.folder = { _id: folderId, - docs: [this.existingDoc], - fileRefs: [this.existingFile], + docs: [ctx.existingDoc], + fileRefs: [ctx.existingFile], } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.DocumentUpdaterHandler.promises.setDocument.resolves() + ctx.DocumentUpdaterHandler.promises.setDocument.resolves() upsertDocResponse = - await this.ProjectEntityUpdateHandler.promises.upsertDoc( + await ctx.ProjectEntityUpdateHandler.promises.upsertDoc( projectId, folderId, - this.docName, - this.docLines, - this.source, + ctx.docName, + ctx.docLines, + ctx.source, userId ) }) - it('tries to find the folder', function () { - this.ProjectLocator.promises.findElement + it('tries to find the folder', function (ctx) { + ctx.ProjectLocator.promises.findElement .calledWith({ project_id: projectId, element_id: folderId, @@ -842,50 +903,50 @@ describe('ProjectEntityUpdateHandler', function () { .should.equal(true) }) - it('updates the doc contents', function () { - this.DocumentUpdaterHandler.promises.setDocument + it('updates the doc contents', function (ctx) { + ctx.DocumentUpdaterHandler.promises.setDocument .calledWith( projectId, - this.existingDoc._id, + ctx.existingDoc._id, userId, - this.docLines, - this.source + ctx.docLines, + ctx.source ) .should.equal(true) }) - it('returns the doc', function () { + it('returns the doc', function (ctx) { expect(upsertDocResponse.isNew).to.equal(false) - expect(upsertDocResponse.doc).to.eql(this.existingDoc) + expect(upsertDocResponse.doc).to.eql(ctx.existingDoc) }) }) describe('creating a new doc', function () { let upsertDocResponse - beforeEach(async function () { - this.folder = { _id: folderId, docs: [], fileRefs: [] } - this.newDoc = { _id: docId } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + beforeEach(async function (ctx) { + ctx.folder = { _id: folderId, docs: [], fileRefs: [] } + ctx.newDoc = { _id: docId } + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.ProjectEntityUpdateHandler.promises.addDocWithRanges = { - withoutLock: sinon.stub().resolves({ doc: this.newDoc }), + ctx.ProjectEntityUpdateHandler.promises.addDocWithRanges = { + withoutLock: sinon.stub().resolves({ doc: ctx.newDoc }), } upsertDocResponse = - await this.ProjectEntityUpdateHandler.promises.upsertDoc( + await ctx.ProjectEntityUpdateHandler.promises.upsertDoc( projectId, folderId, - this.docName, - this.docLines, - this.source, + ctx.docName, + ctx.docLines, + ctx.source, userId ) }) - it('tries to find the folder', function () { - this.ProjectLocator.promises.findElement + it('tries to find the folder', function (ctx) { + ctx.ProjectLocator.promises.findElement .calledWith({ project_id: projectId, element_id: folderId, @@ -894,46 +955,46 @@ describe('ProjectEntityUpdateHandler', function () { .should.equal(true) }) - it('adds the doc', function () { - this.ProjectEntityUpdateHandler.promises.addDocWithRanges.withoutLock.should.have.been.calledWith( + it('adds the doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.addDocWithRanges.withoutLock.should.have.been.calledWith( projectId, folderId, - this.docName, - this.docLines, + ctx.docName, + ctx.docLines, {}, userId, - this.source + ctx.source ) }) - it('returns the doc', function () { + it('returns the doc', function (ctx) { expect(upsertDocResponse.isNew).to.equal(true) - expect(upsertDocResponse.doc).to.equal(this.newDoc) + expect(upsertDocResponse.doc).to.equal(ctx.newDoc) }) }) describe('upserting a new doc with an invalid name', function () { - beforeEach(function () { - this.folder = { _id: folderId, docs: [], fileRefs: [] } - this.newDoc = { _id: docId } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + beforeEach(function (ctx) { + ctx.folder = { _id: folderId, docs: [], fileRefs: [] } + ctx.newDoc = { _id: docId } + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.ProjectEntityUpdateHandler.promises.addDocWithRanges = { - withoutLock: sinon.stub().resolves({ doc: this.newDoc }), + ctx.ProjectEntityUpdateHandler.promises.addDocWithRanges = { + withoutLock: sinon.stub().resolves({ doc: ctx.newDoc }), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertDoc( + await ctx.ProjectEntityUpdateHandler.promises.upsertDoc( projectId, folderId, - `*${this.docName}`, - this.docLines, - this.source, + `*${ctx.docName}`, + ctx.docLines, + ctx.source, userId ) } catch (err) { @@ -945,90 +1006,90 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('upserting a doc on top of a file', function () { - beforeEach(async function () { - this.newProject = { + beforeEach(async function (ctx) { + ctx.newProject = { name: 'new project', overleaf: { history: { id: projectHistoryId } }, } - this.existingFile = { _id: fileId, name: 'foo.tex', rev: 12 } - this.folder = { _id: folderId, docs: [], fileRefs: [this.existingFile] } - this.newDoc = { _id: docId } - this.docLines = ['line one', 'line two'] - this.folderPath = '/path/to/folder' - this.filePath = '/path/to/folder/foo.tex' - this.ProjectLocator.promises.findElement + ctx.existingFile = { _id: fileId, name: 'foo.tex', rev: 12 } + ctx.folder = { _id: folderId, docs: [], fileRefs: [ctx.existingFile] } + ctx.newDoc = { _id: docId } + ctx.docLines = ['line one', 'line two'] + ctx.folderPath = '/path/to/folder' + ctx.filePath = '/path/to/folder/foo.tex' + ctx.ProjectLocator.promises.findElement .withArgs({ project_id: projectId, - element_id: this.folder._id, + element_id: ctx.folder._id, type: 'folder', }) .resolves({ - element: this.folder, + element: ctx.folder, path: { - fileSystem: this.folderPath, + fileSystem: ctx.folderPath, }, }) - this.DocstoreManager.promises.updateDoc.resolves({ rev: null }) - this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc.resolves( - this.newProject + ctx.DocstoreManager.promises.updateDoc.resolves({ rev: null }) + ctx.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc.resolves( + ctx.newProject ) - this.TpdsUpdateSender.promises.addDoc.resolves() + ctx.TpdsUpdateSender.promises.addDoc.resolves() - await this.ProjectEntityUpdateHandler.promises.upsertDoc( + await ctx.ProjectEntityUpdateHandler.promises.upsertDoc( projectId, folderId, 'foo.tex', - this.docLines, - this.source, + ctx.docLines, + ctx.source, userId ) }) - it('notifies docstore of the new doc', function () { - expect(this.DocstoreManager.promises.updateDoc).to.have.been.calledWith( + it('notifies docstore of the new doc', function (ctx) { + expect(ctx.DocstoreManager.promises.updateDoc).to.have.been.calledWith( projectId, - this.newDoc._id, - this.docLines + ctx.newDoc._id, + ctx.docLines ) }) - it('adds the new doc and removes the file in one go', function () { + it('adds the new doc and removes the file in one go', function (ctx) { expect( - this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc + ctx.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc ).to.have.been.calledWithMatch( projectId, - this.existingFile._id, - this.newDoc + ctx.existingFile._id, + ctx.newDoc ) }) - it('sends the doc to TPDS', function () { - expect(this.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({ + it('sends the doc to TPDS', function (ctx) { + expect(ctx.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({ projectId, - docId: this.newDoc._id, - path: this.filePath, - projectName: this.newProject.name, - rev: this.existingFile.rev + 1, + docId: ctx.newDoc._id, + path: ctx.filePath, + projectName: ctx.newProject.name, + rev: ctx.existingFile.rev + 1, folderId, }) }) - it('sends the updates to the doc updater', function () { + it('sends the updates to the doc updater', function (ctx) { const oldFiles = [ { - file: this.existingFile, - path: this.filePath, + file: ctx.existingFile, + path: ctx.filePath, }, ] const newDocs = [ { - doc: sinon.match(this.newDoc), - path: this.filePath, - docLines: this.docLines.join('\n'), + doc: sinon.match(ctx.newDoc), + path: ctx.filePath, + docLines: ctx.docLines.join('\n'), }, ] expect( - this.DocumentUpdaterHandler.promises.updateProjectStructure + ctx.DocumentUpdaterHandler.promises.updateProjectStructure ).to.have.been.calledWith( projectId, projectHistoryId, @@ -1036,19 +1097,17 @@ describe('ProjectEntityUpdateHandler', function () { { oldFiles, newDocs, - newProject: this.newProject, + newProject: ctx.newProject, }, - this.source + ctx.source ) }) - it('should notify everyone of the file deletion', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( + it('should notify everyone of the file deletion', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( projectId, 'removeEntity', - this.existingFile._id, + ctx.existingFile._id, 'convertFileToDoc' ) }) @@ -1056,30 +1115,30 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('upsertFile', function () { - beforeEach(function () { - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.file, + beforeEach(function (ctx) { + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.file, createdBlob: true, }) }) describe('upserting into an invalid folder', function () { - beforeEach(function () { - this.ProjectLocator.promises.findElement.resolves({ element: null }) + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findElement.resolves({ element: null }) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertFile( + await ctx.ProjectEntityUpdateHandler.promises.upsertFile( projectId, folderId, - this.fileName, - this.fileSystemPath, - this.linkedFileData, + ctx.fileName, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -1091,190 +1150,190 @@ describe('ProjectEntityUpdateHandler', function () { describe('updating an existing file', function () { let upsertFileResult - beforeEach(async function () { - this.existingFile = { _id: fileId, name: this.fileName, rev: 1 } - this.newFile = { + beforeEach(async function (ctx) { + ctx.existingFile = { _id: fileId, name: ctx.fileName, rev: 1 } + ctx.newFile = { _id: new ObjectId(), - name: this.fileName, + name: ctx.fileName, rev: 3, hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', } - this.folder = { _id: folderId, fileRefs: [this.existingFile], docs: [] } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + ctx.folder = { _id: folderId, fileRefs: [ctx.existingFile], docs: [] } + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.newProject = 'new-project-stub' - this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.resolves( + ctx.newProject = 'new-project-stub' + ctx.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.resolves( { - oldFileRef: this.existingFile, - project: this.project, - path: { fileSystem: this.fileSystemPath }, - newProject: this.newProject, - newFileRef: this.newFile, + oldFileRef: ctx.existingFile, + project: ctx.project, + path: { fileSystem: ctx.fileSystemPath }, + newProject: ctx.newProject, + newFileRef: ctx.newFile, } ) upsertFileResult = - await this.ProjectEntityUpdateHandler.promises.upsertFile( + await ctx.ProjectEntityUpdateHandler.promises.upsertFile( projectId, folderId, - this.fileName, - this.fileSystemPath, - this.linkedFileData, + ctx.fileName, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) }) - it('uploads a new version of the file', function () { - this.FileStoreHandler.promises.uploadFileFromDisk.should.have.been.calledWith( + it('uploads a new version of the file', function (ctx) { + ctx.FileStoreHandler.promises.uploadFileFromDisk.should.have.been.calledWith( projectId, { - name: this.fileName, - linkedFileData: this.linkedFileData, + name: ctx.fileName, + linkedFileData: ctx.linkedFileData, }, - this.fileSystemPath + ctx.fileSystemPath ) }) - it('replaces the file in mongo', function () { - this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.should.have.been.calledWith( + it('replaces the file in mongo', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.should.have.been.calledWith( projectId, - this.existingFile._id, - this.file, + ctx.existingFile._id, + ctx.file, userId ) }) - it('notifies the tpds', function () { - this.TpdsUpdateSender.promises.addFile.should.have.been.calledWith({ + it('notifies the tpds', function (ctx) { + ctx.TpdsUpdateSender.promises.addFile.should.have.been.calledWith({ projectId, - historyId: this.project.overleaf.history.id, - projectName: this.project.name, - fileId: this.newFile._id, - hash: this.newFile.hash, - rev: this.newFile.rev, - path: this.fileSystemPath, + historyId: ctx.project.overleaf.history.id, + projectName: ctx.project.name, + fileId: ctx.newFile._id, + hash: ctx.newFile.hash, + rev: ctx.newFile.rev, + path: ctx.fileSystemPath, folderId, }) }) - it('updates the project structure in the doc updater', function () { + it('updates the project structure in the doc updater', function (ctx) { const oldFiles = [ { - file: this.existingFile, - path: this.fileSystemPath, + file: ctx.existingFile, + path: ctx.fileSystemPath, }, ] const newFiles = [ { - file: this.newFile, - path: this.fileSystemPath, + file: ctx.newFile, + path: ctx.fileSystemPath, createdBlob: true, }, ] - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( projectId, projectHistoryId, userId, { oldFiles, newFiles, - newProject: this.newProject, + newProject: ctx.newProject, }, - this.source + ctx.source ) }) - it('returns the file', function () { + it('returns the file', function (ctx) { expect(upsertFileResult.isNew).to.be.false expect(upsertFileResult.fileRef.toString()).to.eql( - this.existingFile.toString() + ctx.existingFile.toString() ) }) }) describe('creating a new file', function () { let upsertFileResult - beforeEach(async function () { - this.folder = { _id: folderId, fileRefs: [], docs: [] } - this.newFile = { + beforeEach(async function (ctx) { + ctx.folder = { _id: folderId, fileRefs: [], docs: [] } + ctx.newFile = { _id: fileId, hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.newFile, + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.newFile, createdBlob: true, }) - this.ProjectEntityUpdateHandler.promises.addFile = { - mainTask: sinon.stub().resolves(this.newFile), + ctx.ProjectEntityUpdateHandler.promises.addFile = { + mainTask: sinon.stub().resolves(ctx.newFile), } upsertFileResult = - await this.ProjectEntityUpdateHandler.promises.upsertFile( + await ctx.ProjectEntityUpdateHandler.promises.upsertFile( projectId, folderId, - this.fileName, - this.fileSystemPath, - this.linkedFileData, + ctx.fileName, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) }) - it('tries to find the folder', function () { - this.ProjectLocator.promises.findElement.should.have.been.calledWith({ + it('tries to find the folder', function (ctx) { + ctx.ProjectLocator.promises.findElement.should.have.been.calledWith({ project_id: projectId, element_id: folderId, type: 'folder', }) }) - it('adds the file', function () { + it('adds the file', function (ctx) { expect( - this.ProjectEntityUpdateHandler.promises.addFile.mainTask + ctx.ProjectEntityUpdateHandler.promises.addFile.mainTask ).to.have.been.calledWith({ projectId, folderId, userId, - fileRef: this.newFile, - source: this.source, + fileRef: ctx.newFile, + source: ctx.source, createdBlob: true, }) }) - it('returns the file', function () { - expect(upsertFileResult.fileRef).to.eql(this.newFile) + it('returns the file', function (ctx) { + expect(upsertFileResult.fileRef).to.eql(ctx.newFile) expect(upsertFileResult.isNew).to.be.true }) }) describe('upserting a new file with an invalid name', function () { - beforeEach(function () { - this.folder = { _id: folderId, fileRefs: [] } - this.newFile = { _id: fileId } - this.ProjectLocator.promises.findElement.resolves({ - element: this.folder, + beforeEach(function (ctx) { + ctx.folder = { _id: folderId, fileRefs: [] } + ctx.newFile = { _id: fileId } + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.folder, }) - this.ProjectEntityUpdateHandler.promises.addFile = { - mainTask: sinon.stub().resolves(this.newFile), + ctx.ProjectEntityUpdateHandler.promises.addFile = { + mainTask: sinon.stub().resolves(ctx.newFile), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertFile( + await ctx.ProjectEntityUpdateHandler.promises.upsertFile( projectId, folderId, - `*${this.fileName}`, - this.fileSystemPath, - this.linkedFileData, + `*${ctx.fileName}`, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -1285,110 +1344,108 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('upserting file on top of a doc', function () { - beforeEach(async function () { - this.path = '/path/to/doc' - this.existingDoc = { _id: new ObjectId(), name: this.fileName } - this.folder = { + beforeEach(async function (ctx) { + ctx.path = '/path/to/doc' + ctx.existingDoc = { _id: new ObjectId(), name: ctx.fileName } + ctx.folder = { _id: folderId, fileRefs: [], - docs: [this.existingDoc], + docs: [ctx.existingDoc], } - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project_id: this.project._id.toString(), + project_id: ctx.project._id.toString(), element_id: folderId, type: 'folder', }) - .resolves({ element: this.folder }) - this.ProjectLocator.promises.findElement + .resolves({ element: ctx.folder }) + ctx.ProjectLocator.promises.findElement .withArgs({ - project_id: this.project._id.toString(), - element_id: this.existingDoc._id, + project_id: ctx.project._id.toString(), + element_id: ctx.existingDoc._id, type: 'doc', }) .resolves({ - element: this.existingDoc, - path: { fileSystem: this.path }, - folder: this.folder, + element: ctx.existingDoc, + path: { fileSystem: ctx.path }, + folder: ctx.folder, }) - this.newFile = { + ctx.newFile = { _id: newFileId, name: 'dummy-upload-filename', rev: 0, - linkedFileData: this.linkedFileData, + linkedFileData: ctx.linkedFileData, } - this.newProject = { + ctx.newProject = { name: 'new project', overleaf: { history: { id: projectHistoryId } }, } - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.newFile, + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.newFile, createdBlob: true, }) - this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves( - this.newProject + ctx.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves( + ctx.newProject ) - await this.ProjectEntityUpdateHandler.promises.upsertFile( + await ctx.ProjectEntityUpdateHandler.promises.upsertFile( projectId, folderId, - this.fileName, - this.fileSystemPath, - this.linkedFileData, + ctx.fileName, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) }) - it('replaces the existing doc with a file', function () { + it('replaces the existing doc with a file', function (ctx) { expect( - this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile + ctx.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile ).to.have.been.calledWith( projectId, - this.existingDoc._id, - this.newFile, + ctx.existingDoc._id, + ctx.newFile, userId ) }) - it('updates the doc structure', function () { + it('updates the doc structure', function (ctx) { const oldDocs = [ { - doc: this.existingDoc, - path: this.path, + doc: ctx.existingDoc, + path: ctx.path, }, ] const newFiles = [ { - file: this.newFile, - path: this.path, + file: ctx.newFile, + path: ctx.path, createdBlob: true, }, ] const updates = { oldDocs, newFiles, - newProject: this.newProject, + newProject: ctx.newProject, } expect( - this.DocumentUpdaterHandler.promises.updateProjectStructure + ctx.DocumentUpdaterHandler.promises.updateProjectStructure ).to.have.been.calledWith( projectId, projectHistoryId, userId, updates, - this.source + ctx.source ) }) - it('tells everyone in the room the doc is removed', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( + it('tells everyone in the room the doc is removed', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( projectId, 'removeEntity', - this.existingDoc._id, + ctx.existingDoc._id, 'convertDocToFile' ) }) @@ -1398,90 +1455,90 @@ describe('ProjectEntityUpdateHandler', function () { describe('upsertDocWithPath', function () { describe('upserting a doc', function () { let upsertDocWithPathResult - beforeEach(async function () { - this.path = '/folder/doc.tex' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.doc = { _id: docId } - this.isNewDoc = true - this.ProjectEntityUpdateHandler.promises.mkdirp = { + beforeEach(async function (ctx) { + ctx.path = '/folder/doc.tex' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.doc = { _id: docId } + ctx.isNewDoc = true + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertDoc = { + ctx.ProjectEntityUpdateHandler.promises.upsertDoc = { withoutLock: sinon .stub() - .resolves({ doc: this.doc, isNew: this.isNewDoc }), + .resolves({ doc: ctx.doc, isNew: ctx.isNewDoc }), } upsertDocWithPathResult = - await this.ProjectEntityUpdateHandler.promises.upsertDocWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertDocWithPath( projectId, - this.path, - this.docLines, - this.source, + ctx.path, + ctx.docLines, + ctx.source, userId ) }) - it('creates any necessary folders', function () { - this.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock + it('creates any necessary folders', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock .calledWith(projectId, '/folder', userId) .should.equal(true) }) - it('upserts the doc', function () { - this.ProjectEntityUpdateHandler.promises.upsertDoc.withoutLock + it('upserts the doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.upsertDoc.withoutLock .calledWith( projectId, - this.folder._id, + ctx.folder._id, 'doc.tex', - this.docLines, - this.source, + ctx.docLines, + ctx.source, userId ) .should.equal(true) }) - it('returns a doc, the isNewDoc flag, newFolders and a folder', function () { + it('returns a doc, the isNewDoc flag, newFolders and a folder', function (ctx) { expect(upsertDocWithPathResult).to.eql({ - doc: this.doc, - isNew: this.isNewDoc, - newFolders: this.newFolders, - folder: this.folder, + doc: ctx.doc, + isNew: ctx.isNewDoc, + newFolders: ctx.newFolders, + folder: ctx.folder, }) }) }) describe('upserting a doc with an invalid path', function () { - beforeEach(function () { - this.path = '/*folder/doc.tex' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.doc = { _id: docId } - this.isNewDoc = true - this.ProjectEntityUpdateHandler.promises.mkdirp = { + beforeEach(function (ctx) { + ctx.path = '/*folder/doc.tex' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.doc = { _id: docId } + ctx.isNewDoc = true + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertDoc = { + ctx.ProjectEntityUpdateHandler.promises.upsertDoc = { withoutLock: sinon .stub() - .resolves({ doc: this.doc, isNew: this.isNewDoc }), + .resolves({ doc: ctx.doc, isNew: ctx.isNewDoc }), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertDocWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertDocWithPath( projectId, - this.path, - this.docLines, - this.source, + ctx.path, + ctx.docLines, + ctx.source, userId ) } catch (err) { @@ -1493,33 +1550,33 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('upserting a doc with an invalid name', function () { - beforeEach(function () { - this.path = '/folder/*doc.tex' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.doc = { _id: docId } - this.isNewDoc = true - this.ProjectEntityUpdateHandler.promises.mkdirp = { + beforeEach(function (ctx) { + ctx.path = '/folder/*doc.tex' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.doc = { _id: docId } + ctx.isNewDoc = true + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertDoc = { + ctx.ProjectEntityUpdateHandler.promises.upsertDoc = { withoutLock: sinon .stub() - .resolves({ doc: this.doc, isNew: this.isNewDoc }), + .resolves({ doc: ctx.doc, isNew: ctx.isNewDoc }), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertDocWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertDocWithPath( projectId, - this.path, - this.docLines, - this.source, + ctx.path, + ctx.docLines, + ctx.source, userId ) } catch (err) { @@ -1534,101 +1591,101 @@ describe('ProjectEntityUpdateHandler', function () { describe('upsertFileWithPath', function () { describe('upserting a file', function () { let upsertFileWithPathResult - beforeEach(async function () { - this.path = '/folder/file.png' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.file = { _id: fileId } - this.isNewFile = true - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.newFile, + beforeEach(async function (ctx) { + ctx.path = '/folder/file.png' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.file = { _id: fileId } + ctx.isNewFile = true + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.newFile, createdBlob: true, }) - this.ProjectEntityUpdateHandler.promises.mkdirp = { + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertFile = { + ctx.ProjectEntityUpdateHandler.promises.upsertFile = { mainTask: sinon .stub() - .resolves({ fileRef: this.file, isNew: this.isNewFile }), + .resolves({ fileRef: ctx.file, isNew: ctx.isNewFile }), } upsertFileWithPathResult = - await this.ProjectEntityUpdateHandler.promises.upsertFileWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertFileWithPath( projectId, - this.path, - this.fileSystemPath, - this.linkedFileData, + ctx.path, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) }) - it('creates any necessary folders', function () { - this.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock + it('creates any necessary folders', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock .calledWith(projectId, '/folder', userId) .should.equal(true) }) - it('upserts the file', function () { - this.ProjectEntityUpdateHandler.promises.upsertFile.mainTask.should.have.been.calledWith( + it('upserts the file', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.upsertFile.mainTask.should.have.been.calledWith( { projectId, - folderId: this.folder._id, + folderId: ctx.folder._id, fileName: 'file.png', - fsPath: this.fileSystemPath, - linkedFileData: this.linkedFileData, + fsPath: ctx.fileSystemPath, + linkedFileData: ctx.linkedFileData, userId, - fileRef: this.newFile, - source: this.source, + fileRef: ctx.newFile, + source: ctx.source, createdBlob: true, } ) }) - it('returns an object with the fileRef, isNew flag, undefined oldFileRef, newFolders, and folder', function () { + it('returns an object with the fileRef, isNew flag, undefined oldFileRef, newFolders, and folder', function (ctx) { expect(upsertFileWithPathResult).to.eql({ - fileRef: this.file, - isNew: this.isNewFile, - newFolders: this.newFolders, - folder: this.folder, + fileRef: ctx.file, + isNew: ctx.isNewFile, + newFolders: ctx.newFolders, + folder: ctx.folder, oldFileRef: undefined, }) }) }) describe('upserting a file with an invalid path', function () { - beforeEach(function () { - this.path = '/*folder/file.png' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.file = { _id: fileId } - this.isNewFile = true - this.ProjectEntityUpdateHandler.promises.mkdirp = { + beforeEach(function (ctx) { + ctx.path = '/*folder/file.png' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.file = { _id: fileId } + ctx.isNewFile = true + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertFile = { + ctx.ProjectEntityUpdateHandler.promises.upsertFile = { mainTask: sinon .stub() - .resolves({ doc: this.file, isNew: this.isNewFile }), + .resolves({ doc: ctx.file, isNew: ctx.isNewFile }), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertFileWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertFileWithPath( projectId, - this.path, - this.fileSystemPath, - this.linkedFileData, + ctx.path, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -1639,35 +1696,35 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('upserting a file with an invalid name', function () { - beforeEach(function () { - this.path = '/folder/*file.png' - this.newFolders = ['mock-a', 'mock-b'] - this.folder = { _id: folderId } - this.file = { _id: fileId } - this.isNewFile = true - this.ProjectEntityUpdateHandler.promises.mkdirp = { + beforeEach(function (ctx) { + ctx.path = '/folder/*file.png' + ctx.newFolders = ['mock-a', 'mock-b'] + ctx.folder = { _id: folderId } + ctx.file = { _id: fileId } + ctx.isNewFile = true + ctx.ProjectEntityUpdateHandler.promises.mkdirp = { withoutLock: sinon .stub() - .resolves({ newFolders: this.newFolders, folder: this.folder }), + .resolves({ newFolders: ctx.newFolders, folder: ctx.folder }), } - this.ProjectEntityUpdateHandler.promises.upsertFile = { + ctx.ProjectEntityUpdateHandler.promises.upsertFile = { mainTask: sinon .stub() - .resolves({ doc: this.file, isNew: this.isNewFile }), + .resolves({ doc: ctx.file, isNew: ctx.isNewFile }), } }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.upsertFileWithPath( + await ctx.ProjectEntityUpdateHandler.promises.upsertFileWithPath( projectId, - this.path, - this.fileSystemPath, - this.linkedFileData, + ctx.path, + ctx.fileSystemPath, + ctx.linkedFileData, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -1680,65 +1737,65 @@ describe('ProjectEntityUpdateHandler', function () { describe('deleteEntity', function () { let deleteEntityResult - beforeEach(async function () { - this.path = '/path/to/doc.tex' - this.doc = { _id: docId } - this.projectBeforeDeletion = { _id: projectId, name: 'project' } - this.newProject = 'new-project' - this.ProjectEntityMongoUpdateHandler.promises.deleteEntity.resolves({ - entity: this.doc, - path: { fileSystem: this.path }, - projectBeforeDeletion: this.projectBeforeDeletion, - newProject: this.newProject, + beforeEach(async function (ctx) { + ctx.path = '/path/to/doc.tex' + ctx.doc = { _id: docId } + ctx.projectBeforeDeletion = { _id: projectId, name: 'project' } + ctx.newProject = 'new-project' + ctx.ProjectEntityMongoUpdateHandler.promises.deleteEntity.resolves({ + entity: ctx.doc, + path: { fileSystem: ctx.path }, + projectBeforeDeletion: ctx.projectBeforeDeletion, + newProject: ctx.newProject, }) - this.ProjectEntityUpdateHandler._cleanUpEntity = sinon + ctx.ProjectEntityUpdateHandler._cleanUpEntity = sinon .stub() - .resolves([{ type: 'doc', entity: this.doc, path: this.path }]) + .resolves([{ type: 'doc', entity: ctx.doc, path: ctx.path }]) deleteEntityResult = - await this.ProjectEntityUpdateHandler.promises.deleteEntity( + await ctx.ProjectEntityUpdateHandler.promises.deleteEntity( projectId, docId, 'doc', userId, - this.source + ctx.source ) }) - it('flushes the project to mongo', function () { - this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith( + it('flushes the project to mongo', function (ctx) { + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith( projectId ) }) - it('deletes the entity in mongo', function () { - this.ProjectEntityMongoUpdateHandler.promises.deleteEntity + it('deletes the entity in mongo', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.deleteEntity .calledWith(projectId, docId, 'doc', userId) .should.equal(true) }) - it('cleans up the doc in the docstore', function () { - this.ProjectEntityUpdateHandler._cleanUpEntity + it('cleans up the doc in the docstore', function (ctx) { + ctx.ProjectEntityUpdateHandler._cleanUpEntity .calledWith( - this.projectBeforeDeletion, - this.newProject, - this.doc, + ctx.projectBeforeDeletion, + ctx.newProject, + ctx.doc, 'doc', - this.path, + ctx.path, userId, - this.source + ctx.source ) .should.equal(true) }) - it('it notifies the tpds', function () { - this.TpdsUpdateSender.promises.deleteEntity.should.have.been.calledWith({ + it('it notifies the tpds', function (ctx) { + ctx.TpdsUpdateSender.promises.deleteEntity.should.have.been.calledWith({ projectId, - path: this.path, - projectName: this.projectBeforeDeletion.name, + path: ctx.path, + projectName: ctx.projectBeforeDeletion.name, entityId: docId, entityType: 'doc', - subtreeEntityIds: [this.doc._id], + subtreeEntityIds: [ctx.doc._id], }) }) @@ -1749,62 +1806,62 @@ describe('ProjectEntityUpdateHandler', function () { describe('deleteEntityWithPath', function () { describe('when the entity exists', function () { - beforeEach(async function () { - this.doc = { _id: docId } - this.ProjectLocator.promises.findElementByPath.resolves({ - element: this.doc, + beforeEach(async function (ctx) { + ctx.doc = { _id: docId } + ctx.ProjectLocator.promises.findElementByPath.resolves({ + element: ctx.doc, type: 'doc', }) - this.ProjectEntityUpdateHandler.promises.deleteEntity = { + ctx.ProjectEntityUpdateHandler.promises.deleteEntity = { withoutLock: sinon.stub().resolves(), } - this.path = '/path/to/doc.tex' - await this.ProjectEntityUpdateHandler.promises.deleteEntityWithPath( + ctx.path = '/path/to/doc.tex' + await ctx.ProjectEntityUpdateHandler.promises.deleteEntityWithPath( projectId, - this.path, + ctx.path, userId, - this.source + ctx.source ) }) - it('finds the entity', function () { - this.ProjectLocator.promises.findElementByPath + it('finds the entity', function (ctx) { + ctx.ProjectLocator.promises.findElementByPath .calledWith({ project_id: projectId, - path: this.path, + path: ctx.path, exactCaseMatch: true, }) .should.equal(true) }) - it('deletes the entity', function () { - this.ProjectEntityUpdateHandler.promises.deleteEntity.withoutLock.should.have.been.calledWith( + it('deletes the entity', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.deleteEntity.withoutLock.should.have.been.calledWith( projectId, - this.doc._id, + ctx.doc._id, 'doc', userId, - this.source + ctx.source ) }) }) describe('when the entity does not exist', function () { - beforeEach(function () { - this.ProjectLocator.promises.findElementByPath.resolves({ + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findElementByPath.resolves({ element: null, }) - this.path = '/doc.tex' + ctx.path = '/doc.tex' }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.deleteEntityWithPath( + await ctx.ProjectEntityUpdateHandler.promises.deleteEntityWithPath( projectId, - this.path, + ctx.path, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -1816,77 +1873,77 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('mkdirp', function () { - beforeEach(async function () { - this.docPath = '/folder/doc.tex' - this.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({}) - await this.ProjectEntityUpdateHandler.promises.mkdirp( + beforeEach(async function (ctx) { + ctx.docPath = '/folder/doc.tex' + ctx.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({}) + await ctx.ProjectEntityUpdateHandler.promises.mkdirp( projectId, - this.docPath, + ctx.docPath, userId ) }) - it('calls ProjectEntityMongoUpdateHandler', function () { - this.ProjectEntityMongoUpdateHandler.promises.mkdirp - .calledWith(projectId, this.docPath, userId) + it('calls ProjectEntityMongoUpdateHandler', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.mkdirp + .calledWith(projectId, ctx.docPath, userId) .should.equal(true) }) }) describe('mkdirpWithExactCase', function () { - beforeEach(async function () { - this.docPath = '/folder/doc.tex' - this.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({}) - await this.ProjectEntityUpdateHandler.promises.mkdirpWithExactCase( + beforeEach(async function (ctx) { + ctx.docPath = '/folder/doc.tex' + ctx.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({}) + await ctx.ProjectEntityUpdateHandler.promises.mkdirpWithExactCase( projectId, - this.docPath, + ctx.docPath, userId ) }) - it('calls ProjectEntityMongoUpdateHandler', function () { - this.ProjectEntityMongoUpdateHandler.promises.mkdirp - .calledWith(projectId, this.docPath, userId, { exactCaseMatch: true }) + it('calls ProjectEntityMongoUpdateHandler', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.mkdirp + .calledWith(projectId, ctx.docPath, userId, { exactCaseMatch: true }) .should.equal(true) }) }) describe('addFolder', function () { describe('adding a folder', function () { - beforeEach(async function () { - this.parentFolderId = '123asdf' - this.folderName = 'new-folder' - this.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({}) - await this.ProjectEntityUpdateHandler.promises.addFolder( + beforeEach(async function (ctx) { + ctx.parentFolderId = '123asdf' + ctx.folderName = 'new-folder' + ctx.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({}) + await ctx.ProjectEntityUpdateHandler.promises.addFolder( projectId, - this.parentFolderId, - this.folderName, + ctx.parentFolderId, + ctx.folderName, userId ) }) - it('calls ProjectEntityMongoUpdateHandler', function () { - this.ProjectEntityMongoUpdateHandler.promises.addFolder - .calledWith(projectId, this.parentFolderId, this.folderName, userId) + it('calls ProjectEntityMongoUpdateHandler', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.addFolder + .calledWith(projectId, ctx.parentFolderId, ctx.folderName, userId) .should.equal(true) }) }) describe('adding a folder with an invalid name', function () { - beforeEach(function () { - this.parentFolderId = '123asdf' - this.folderName = '*new-folder' - this.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({}) + beforeEach(function (ctx) { + ctx.parentFolderId = '123asdf' + ctx.folderName = '*new-folder' + ctx.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({}) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.addFolder( + await ctx.ProjectEntityUpdateHandler.promises.addFolder( projectId, - this.parentFolderId, - this.folderName + ctx.parentFolderId, + ctx.folderName ) } catch (err) { error = err @@ -1898,44 +1955,44 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('moveEntity', function () { - beforeEach(async function () { - this.project_name = 'project name' - this.startPath = '/a.tex' - this.endPath = '/folder/b.tex' - this.rev = 2 - this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } - this.ProjectEntityMongoUpdateHandler.promises.moveEntity.resolves({ - project: this.project, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, - changes: this.changes, + beforeEach(async function (ctx) { + ctx.project_name = 'project name' + ctx.startPath = '/a.tex' + ctx.endPath = '/folder/b.tex' + ctx.rev = 2 + ctx.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + ctx.ProjectEntityMongoUpdateHandler.promises.moveEntity.resolves({ + project: ctx.project, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, + changes: ctx.changes, }) - await this.ProjectEntityUpdateHandler.promises.moveEntity( + await ctx.ProjectEntityUpdateHandler.promises.moveEntity( projectId, docId, folderId, 'doc', userId, - this.source + ctx.source ) }) - it('moves the entity in mongo', function () { - this.ProjectEntityMongoUpdateHandler.promises.moveEntity + it('moves the entity in mongo', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.moveEntity .calledWith(projectId, docId, folderId, 'doc', userId) .should.equal(true) }) - it('notifies tpds', function () { - this.TpdsUpdateSender.promises.moveEntity + it('notifies tpds', function (ctx) { + ctx.TpdsUpdateSender.promises.moveEntity .calledWith({ projectId, - projectName: this.project_name, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, + projectName: ctx.project_name, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, entityId: docId, entityType: 'doc', folderId, @@ -1943,14 +2000,14 @@ describe('ProjectEntityUpdateHandler', function () { .should.equal(true) }) - it('sends the changes in project structure to the doc updater', function () { - this.DocumentUpdaterHandler.promises.updateProjectStructure + it('sends the changes in project structure to the doc updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.updateProjectStructure .calledWith( projectId, projectHistoryId, userId, - this.changes, - this.source + ctx.changes, + ctx.source ) .should.equal(true) }) @@ -1958,45 +2015,45 @@ describe('ProjectEntityUpdateHandler', function () { describe('renameEntity', function () { describe('renaming an entity', function () { - beforeEach(async function () { - this.project_name = 'project name' - this.startPath = '/folder/a.tex' - this.endPath = '/folder/b.tex' - this.rev = 2 - this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } - this.newDocName = 'b.tex' - this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ - project: this.project, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, - changes: this.changes, + beforeEach(async function (ctx) { + ctx.project_name = 'project name' + ctx.startPath = '/folder/a.tex' + ctx.endPath = '/folder/b.tex' + ctx.rev = 2 + ctx.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + ctx.newDocName = 'b.tex' + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ + project: ctx.project, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, + changes: ctx.changes, }) - await this.ProjectEntityUpdateHandler.promises.renameEntity( + await ctx.ProjectEntityUpdateHandler.promises.renameEntity( projectId, docId, 'doc', - this.newDocName, + ctx.newDocName, userId, - this.source + ctx.source ) }) - it('moves the entity in mongo', function () { - this.ProjectEntityMongoUpdateHandler.promises.renameEntity - .calledWith(projectId, docId, 'doc', this.newDocName, userId) + it('moves the entity in mongo', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity + .calledWith(projectId, docId, 'doc', ctx.newDocName, userId) .should.equal(true) }) - it('notifies tpds', function () { - this.TpdsUpdateSender.promises.moveEntity + it('notifies tpds', function (ctx) { + ctx.TpdsUpdateSender.promises.moveEntity .calledWith({ projectId, - projectName: this.project_name, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, + projectName: ctx.project_name, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, entityId: docId, entityType: 'doc', folderId: null, @@ -2004,53 +2061,53 @@ describe('ProjectEntityUpdateHandler', function () { .should.equal(true) }) - it('flushes the project in doc updater', function () { - this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith( + it('flushes the project in doc updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith( projectId ) }) - it('sends the changes in project structure to the doc updater', function () { - this.DocumentUpdaterHandler.promises.updateProjectStructure + it('sends the changes in project structure to the doc updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.updateProjectStructure .calledWith( projectId, projectHistoryId, userId, - this.changes, - this.source + ctx.changes, + ctx.source ) .should.equal(true) }) }) describe('renaming an entity to an invalid name', function () { - beforeEach(function () { - this.project_name = 'project name' - this.startPath = '/folder/a.tex' - this.endPath = '/folder/b.tex' - this.rev = 2 - this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } - this.newDocName = '*b.tex' - this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ - project: this.project, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, - changes: this.changes, + beforeEach(function (ctx) { + ctx.project_name = 'project name' + ctx.startPath = '/folder/a.tex' + ctx.endPath = '/folder/b.tex' + ctx.rev = 2 + ctx.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + ctx.newDocName = '*b.tex' + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ + project: ctx.project, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, + changes: ctx.changes, }) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.renameEntity( + await ctx.ProjectEntityUpdateHandler.promises.renameEntity( projectId, docId, 'doc', - this.newDocName, + ctx.newDocName, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -2061,33 +2118,33 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('renaming an entity with a non-string value', function () { - beforeEach(function () { - this.project_name = 'project name' - this.startPath = '/folder/a.tex' - this.endPath = '/folder/b.tex' - this.rev = 2 - this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } - this.newDocName = ['hello'] - this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ - project: this.project, - startPath: this.startPath, - endPath: this.endPath, - rev: this.rev, - changes: this.changes, + beforeEach(function (ctx) { + ctx.project_name = 'project name' + ctx.startPath = '/folder/a.tex' + ctx.endPath = '/folder/b.tex' + ctx.rev = 2 + ctx.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + ctx.newDocName = ['hello'] + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({ + project: ctx.project, + startPath: ctx.startPath, + endPath: ctx.endPath, + rev: ctx.rev, + changes: ctx.changes, }) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.renameEntity( + await ctx.ProjectEntityUpdateHandler.promises.renameEntity( projectId, docId, 'doc', - this.newDocName, + ctx.newDocName, userId, - this.source + ctx.source ) } catch (err) { error = err @@ -2095,7 +2152,7 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.be.instanceOf(Error) expect( - this.ProjectEntityMongoUpdateHandler.promises.renameEntity.called + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity.called ).to.equal(false) }) }) @@ -2103,15 +2160,15 @@ describe('ProjectEntityUpdateHandler', function () { describe('resyncProjectHistory', function () { describe('a deleted project', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject.resolves({}) + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves({}) }) - it('should return an error', async function () { + it('should return an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) @@ -2128,16 +2185,16 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('a project without project-history enabled', function () { - beforeEach(function () { - this.project.overleaf = {} - this.ProjectGetter.promises.getProject.resolves(this.project) + beforeEach(function (ctx) { + ctx.project.overleaf = {} + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) }) - it('should return an error', async function () { + it('should return an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) @@ -2161,48 +2218,48 @@ describe('ProjectEntityUpdateHandler', function () { path: 'universe.png', }, ] - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) const folders = [] - this.ProjectEntityHandler.getAllEntitiesFromProject.returns({ + ctx.ProjectEntityHandler.getAllEntitiesFromProject.returns({ docs, files, folders, }) - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) }) - it('gets the project', function () { - this.ProjectGetter.promises.getProject.should.have.been.calledWith( + it('gets the project', function (ctx) { + ctx.ProjectGetter.promises.getProject.should.have.been.calledWith( projectId ) }) - it('gets the entities for the project', function () { - this.ProjectEntityHandler.getAllEntitiesFromProject.should.have.been.calledWith( - this.project + it('gets the entities for the project', function (ctx) { + ctx.ProjectEntityHandler.getAllEntitiesFromProject.should.have.been.calledWith( + ctx.project ) }) - it('uses an extended timeout', function () { - this.LockManager.withTimeout.should.have.been.calledWith(6 * 60) + it('uses an extended timeout', function (ctx) { + ctx.LockManager.withTimeout.should.have.been.calledWith(6 * 60) }) - it('tells the doc updater to sync the project', function () { - this.DocumentUpdaterHandler.promises.resyncProjectHistory + it('tells the doc updater to sync the project', function (ctx) { + ctx.DocumentUpdaterHandler.promises.resyncProjectHistory .calledWith(projectId, projectHistoryId, docs, files) .should.equal(true) }) }) describe('a project with duplicate filenames', function () { - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.docs = [ + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.docs = [ { doc: { _id: 'doc1', name: 'main.tex' }, path: 'main.tex' }, { doc: { _id: 'doc2', name: 'duplicate.tex' }, @@ -2221,7 +2278,7 @@ describe('ProjectEntityUpdateHandler', function () { path: 'a/b/c/duplicate.tex', }, ] - this.files = [ + ctx.files = [ { file: { _id: 'file1', name: 'image.jpg', hash: 'hash1' }, path: 'image.jpg', @@ -2239,20 +2296,20 @@ describe('ProjectEntityUpdateHandler', function () { path: 'another dupe (22)', }, ] - this.ProjectEntityHandler.getAllEntitiesFromProject.returns({ - docs: this.docs, - files: this.files, + ctx.ProjectEntityHandler.getAllEntitiesFromProject.returns({ + docs: ctx.docs, + files: ctx.files, folders: [], }) - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) }) - it('renames the duplicate files', function () { + it('renames the duplicate files', function (ctx) { const renameEntity = - this.ProjectEntityMongoUpdateHandler.promises.renameEntity + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity expect(renameEntity).to.have.callCount(4) expect(renameEntity).to.have.been.calledWith( projectId, @@ -2284,8 +2341,8 @@ describe('ProjectEntityUpdateHandler', function () { ) }) - it('tells the doc updater to resync the project', function () { - const docs = this.docs.map(d => { + it('tells the doc updater to resync the project', function (ctx) { + const docs = ctx.docs.map(d => { if (d.doc._id === 'doc3') { return Object.assign({}, d, { path: 'a/b/c/duplicate.tex (1)' }) } @@ -2294,7 +2351,7 @@ describe('ProjectEntityUpdateHandler', function () { } return d }) - const files = this.files.map(f => { + const files = ctx.files.map(f => { if (f.file._id === 'file3') { return Object.assign({}, f, { path: 'duplicate.jpg (1)' }) } @@ -2304,15 +2361,15 @@ describe('ProjectEntityUpdateHandler', function () { return f }) expect( - this.DocumentUpdaterHandler.promises.resyncProjectHistory + ctx.DocumentUpdaterHandler.promises.resyncProjectHistory ).to.have.been.calledWith(projectId, projectHistoryId, docs, files) }) }) describe('a project with bad filenames', function () { - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.docs = [ + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.docs = [ { doc: { _id: 'doc1', name: '/d/e/f/test.tex' }, path: 'a/b/c/d/e/f/test.tex', @@ -2322,7 +2379,7 @@ describe('ProjectEntityUpdateHandler', function () { path: 'a', }, ] - this.files = [ + ctx.files = [ { file: { _id: 'file1', name: 'A*.png', hash: 'hash1' }, path: 'A*.png', @@ -2332,20 +2389,20 @@ describe('ProjectEntityUpdateHandler', function () { path: 'A_.png', }, ] - this.ProjectEntityHandler.getAllEntitiesFromProject.returns({ - docs: this.docs, - files: this.files, + ctx.ProjectEntityHandler.getAllEntitiesFromProject.returns({ + docs: ctx.docs, + files: ctx.files, folders: [], }) - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) }) - it('renames the files', function () { + it('renames the files', function (ctx) { const renameEntity = - this.ProjectEntityMongoUpdateHandler.promises.renameEntity + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity expect(renameEntity).to.have.callCount(4) expect(renameEntity).to.have.been.calledWith( projectId, @@ -2377,8 +2434,8 @@ describe('ProjectEntityUpdateHandler', function () { ) }) - it('tells the doc updater to resync the project', function () { - const docs = this.docs.map(d => { + it('tells the doc updater to resync the project', function (ctx) { + const docs = ctx.docs.map(d => { if (d.doc._id === 'doc1') { return Object.assign({}, d, { path: 'a/b/c/_d_e_f_test.tex' }) } @@ -2387,7 +2444,7 @@ describe('ProjectEntityUpdateHandler', function () { } return d }) - const files = this.files.map(f => { + const files = ctx.files.map(f => { if (f.file._id === 'file1') { return Object.assign({}, f, { path: 'A_.png' }) } @@ -2397,7 +2454,7 @@ describe('ProjectEntityUpdateHandler', function () { return f }) expect( - this.DocumentUpdaterHandler.promises.resyncProjectHistory + ctx.DocumentUpdaterHandler.promises.resyncProjectHistory ).to.have.been.calledWith(projectId, projectHistoryId, docs, files) }) }) @@ -2424,22 +2481,22 @@ describe('ProjectEntityUpdateHandler', function () { }, ] const files = [] - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectEntityHandler.getAllEntitiesFromProject.returns({ + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectEntityHandler.getAllEntitiesFromProject.returns({ docs, files, folders, }) - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) }) - it('renames the folder', function () { + it('renames the folder', function (ctx) { const renameEntity = - this.ProjectEntityMongoUpdateHandler.promises.renameEntity + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity expect(renameEntity).to.have.callCount(1) expect(renameEntity).to.have.been.calledWith( projectId, @@ -2450,7 +2507,7 @@ describe('ProjectEntityUpdateHandler', function () { ) }) - it('tells the doc updater to resync the project', function () { + it('tells the doc updater to resync the project', function (ctx) { const fixedDocs = docs.map(d => { if (d.doc._id === 'doc2') { return Object.assign({}, d, { path: 'bad_/doc2.tex' }) @@ -2458,7 +2515,7 @@ describe('ProjectEntityUpdateHandler', function () { return d }) expect( - this.DocumentUpdaterHandler.promises.resyncProjectHistory + ctx.DocumentUpdaterHandler.promises.resyncProjectHistory ).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files) }) }) @@ -2481,22 +2538,22 @@ describe('ProjectEntityUpdateHandler', function () { }, ] const files = [] - beforeEach(async function () { - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectEntityHandler.getAllEntitiesFromProject.returns({ + beforeEach(async function (ctx) { + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectEntityHandler.getAllEntitiesFromProject.returns({ docs, files, folders, }) - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) }) - it('renames the doc', function () { + it('renames the doc', function (ctx) { const renameEntity = - this.ProjectEntityMongoUpdateHandler.promises.renameEntity + ctx.ProjectEntityMongoUpdateHandler.promises.renameEntity expect(renameEntity).to.have.callCount(1) expect(renameEntity).to.have.been.calledWith( projectId, @@ -2507,7 +2564,7 @@ describe('ProjectEntityUpdateHandler', function () { ) }) - it('tells the doc updater to resync the project', function () { + it('tells the doc updater to resync the project', function (ctx) { const fixedDocs = docs.map(d => { if (d.doc._id === 'doc1') { return Object.assign({}, d, { path: 'chapters (1)' }) @@ -2515,23 +2572,23 @@ describe('ProjectEntityUpdateHandler', function () { return d }) expect( - this.DocumentUpdaterHandler.promises.resyncProjectHistory + ctx.DocumentUpdaterHandler.promises.resyncProjectHistory ).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files) }) }) describe('a project with an invalid file tree', function () { - beforeEach(function () { - this.callback = sinon.stub() - this.ProjectGetter.promises.getProject.resolves(this.project) - this.ProjectEntityHandler.getAllEntitiesFromProject.throws() + beforeEach(function (ctx) { + ctx.callback = sinon.stub() + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.ProjectEntityHandler.getAllEntitiesFromProject.throws() }) - it('calls the callback with an error', async function () { + it('calls the callback with an error', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.resyncProjectHistory( + await ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory( projectId, {} ) @@ -2545,160 +2602,158 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('_cleanUpEntity', function () { - beforeEach(function () { - this.entityId = '4eecaffcbffa66588e000009' - this.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon + beforeEach(function (ctx) { + ctx.entityId = '4eecaffcbffa66588e000009' + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon .stub() .resolves() }) describe('a file', function () { - beforeEach(async function () { - this.path = '/file/system/path.png' - this.entity = { _id: this.entityId } - this.newProject = 'new-project' - this.subtreeListing = - await this.ProjectEntityUpdateHandler._cleanUpEntity( - this.project, - this.newProject, - this.entity, + beforeEach(async function (ctx) { + ctx.path = '/file/system/path.png' + ctx.entity = { _id: ctx.entityId } + ctx.newProject = 'new-project' + ctx.subtreeListing = + await ctx.ProjectEntityUpdateHandler._cleanUpEntity( + ctx.project, + ctx.newProject, + ctx.entity, 'file', - this.path, + ctx.path, userId, - this.source + ctx.source ) }) - it('should not attempt to delete from the document updater', function () { - this.DocumentUpdaterHandler.promises.deleteDoc.called.should.equal( - false - ) + it('should not attempt to delete from the document updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.deleteDoc.called.should.equal(false) }) - it('should send the update to the doc updater', function () { - const oldFiles = [{ file: this.entity, path: this.path }] - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + it('should send the update to the doc updater', function (ctx) { + const oldFiles = [{ file: ctx.entity, path: ctx.path }] + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( projectId, projectHistoryId, userId, { oldFiles, oldDocs: [], - newProject: this.newProject, + newProject: ctx.newProject, }, - this.source + ctx.source ) }) - it('should return a subtree listing containing only the file', function () { - expect(this.subtreeListing).to.deep.equal([ - { type: 'file', entity: this.entity, path: this.path }, + it('should return a subtree listing containing only the file', function (ctx) { + expect(ctx.subtreeListing).to.deep.equal([ + { type: 'file', entity: ctx.entity, path: ctx.path }, ]) }) }) describe('a doc', function () { - beforeEach(async function () { - this.path = '/file/system/path.tex' - this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves() - this.entity = { _id: this.entityId } - this.newProject = 'new-project' - this.subtreeListing = - await this.ProjectEntityUpdateHandler._cleanUpEntity( - this.project, - this.newProject, - this.entity, + beforeEach(async function (ctx) { + ctx.path = '/file/system/path.tex' + ctx.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves() + ctx.entity = { _id: ctx.entityId } + ctx.newProject = 'new-project' + ctx.subtreeListing = + await ctx.ProjectEntityUpdateHandler._cleanUpEntity( + ctx.project, + ctx.newProject, + ctx.entity, 'doc', - this.path, + ctx.path, userId, - this.source + ctx.source ) }) - it('should clean up the doc', function () { - this.ProjectEntityUpdateHandler._cleanUpDoc - .calledWith(this.project, this.entity, this.path, userId) + it('should clean up the doc', function (ctx) { + ctx.ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(ctx.project, ctx.entity, ctx.path, userId) .should.equal(true) }) - it('should send the update to the doc updater', function () { - const oldDocs = [{ doc: this.entity, path: this.path }] - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + it('should send the update to the doc updater', function (ctx) { + const oldDocs = [{ doc: ctx.entity, path: ctx.path }] + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( projectId, projectHistoryId, userId, { oldDocs, oldFiles: [], - newProject: this.newProject, + newProject: ctx.newProject, }, - this.source + ctx.source ) }) - it('should return a subtree listing containing only the doc', function () { - expect(this.subtreeListing).to.deep.equal([ - { type: 'doc', entity: this.entity, path: this.path }, + it('should return a subtree listing containing only the doc', function (ctx) { + expect(ctx.subtreeListing).to.deep.equal([ + { type: 'doc', entity: ctx.entity, path: ctx.path }, ]) }) }) describe('a folder', function () { - beforeEach(async function () { - this.folder = { + beforeEach(async function (ctx) { + ctx.folder = { folders: [ { name: 'subfolder', fileRefs: [ - (this.file1 = { _id: 'file-id-1', name: 'file-name-1' }), + (ctx.file1 = { _id: 'file-id-1', name: 'file-name-1' }), ], - docs: [(this.doc1 = { _id: 'doc-id-1', name: 'doc-name-1' })], + docs: [(ctx.doc1 = { _id: 'doc-id-1', name: 'doc-name-1' })], folders: [], }, ], - fileRefs: [(this.file2 = { _id: 'file-id-2', name: 'file-name-2' })], - docs: [(this.doc2 = { _id: 'doc-id-2', name: 'doc-name-2' })], + fileRefs: [(ctx.file2 = { _id: 'file-id-2', name: 'file-name-2' })], + docs: [(ctx.doc2 = { _id: 'doc-id-2', name: 'doc-name-2' })], } - this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves() + ctx.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves() const path = '/folder' - this.newProject = 'new-project' - this.subtreeListing = - await this.ProjectEntityUpdateHandler._cleanUpEntity( - this.project, - this.newProject, - this.folder, + ctx.newProject = 'new-project' + ctx.subtreeListing = + await ctx.ProjectEntityUpdateHandler._cleanUpEntity( + ctx.project, + ctx.newProject, + ctx.folder, 'folder', path, userId, - this.source + ctx.source ) }) - it('should clean up all sub docs', function () { - this.ProjectEntityUpdateHandler._cleanUpDoc + it('should clean up all sub docs', function (ctx) { + ctx.ProjectEntityUpdateHandler._cleanUpDoc .calledWith( - this.project, - this.doc1, + ctx.project, + ctx.doc1, '/folder/subfolder/doc-name-1', userId ) .should.equal(true) - this.ProjectEntityUpdateHandler._cleanUpDoc - .calledWith(this.project, this.doc2, '/folder/doc-name-2', userId) + ctx.ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(ctx.project, ctx.doc2, '/folder/doc-name-2', userId) .should.equal(true) }) - it('should should send one update to the doc updater for all docs and files', function () { + it('should should send one update to the doc updater for all docs and files', function (ctx) { const oldFiles = [ - { file: this.file2, path: '/folder/file-name-2' }, - { file: this.file1, path: '/folder/subfolder/file-name-1' }, + { file: ctx.file2, path: '/folder/file-name-2' }, + { file: ctx.file1, path: '/folder/subfolder/file-name-1' }, ] const oldDocs = [ - { doc: this.doc2, path: '/folder/doc-name-2' }, - { doc: this.doc1, path: '/folder/subfolder/doc-name-1' }, + { doc: ctx.doc2, path: '/folder/doc-name-2' }, + { doc: ctx.doc1, path: '/folder/subfolder/doc-name-1' }, ] - this.DocumentUpdaterHandler.promises.updateProjectStructure + ctx.DocumentUpdaterHandler.promises.updateProjectStructure .calledWith( projectId, projectHistoryId, @@ -2706,94 +2761,94 @@ describe('ProjectEntityUpdateHandler', function () { { oldFiles, oldDocs, - newProject: this.newProject, + newProject: ctx.newProject, }, - this.source + ctx.source ) .should.equal(true) }) - it('should return a subtree listing containing all sub-entities', function () { - expect(this.subtreeListing).to.have.deep.members([ - { type: 'folder', entity: this.folder, path: '/folder' }, + it('should return a subtree listing containing all sub-entities', function (ctx) { + expect(ctx.subtreeListing).to.have.deep.members([ + { type: 'folder', entity: ctx.folder, path: '/folder' }, { type: 'folder', - entity: this.folder.folders[0], + entity: ctx.folder.folders[0], path: '/folder/subfolder', }, { type: 'file', - entity: this.file1, + entity: ctx.file1, path: '/folder/subfolder/file-name-1', }, { type: 'doc', - entity: this.doc1, + entity: ctx.doc1, path: '/folder/subfolder/doc-name-1', }, - { type: 'file', entity: this.file2, path: '/folder/file-name-2' }, - { type: 'doc', entity: this.doc2, path: '/folder/doc-name-2' }, + { type: 'file', entity: ctx.file2, path: '/folder/file-name-2' }, + { type: 'doc', entity: ctx.doc2, path: '/folder/doc-name-2' }, ]) }) }) }) describe('_cleanUpDoc', function () { - beforeEach(function () { - this.doc = { + beforeEach(function (ctx) { + ctx.doc = { _id: new ObjectId(), name: 'test.tex', } - this.path = '/path/to/doc' - this.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon + ctx.path = '/path/to/doc' + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon .stub() .resolves() - this.DocstoreManager.promises.deleteDoc.resolves() + ctx.DocstoreManager.promises.deleteDoc.resolves() }) describe('when the doc is the root doc', function () { - beforeEach(async function () { - this.project.rootDoc_id = this.doc._id - await this.ProjectEntityUpdateHandler._cleanUpDoc( - this.project, - this.doc, - this.path, + beforeEach(async function (ctx) { + ctx.project.rootDoc_id = ctx.doc._id + await ctx.ProjectEntityUpdateHandler._cleanUpDoc( + ctx.project, + ctx.doc, + ctx.path, userId ) }) - it('should unset the root doc', function () { - this.ProjectEntityUpdateHandler.promises.unsetRootDoc.should.have.been.calledWith( + it('should unset the root doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc.should.have.been.calledWith( projectId ) }) - it('should delete the doc in the doc updater', function () { - this.DocumentUpdaterHandler.promises.deleteDoc - .calledWith(projectId, this.doc._id.toString()) + it('should delete the doc in the doc updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.deleteDoc + .calledWith(projectId, ctx.doc._id.toString()) .should.equal(true) }) - it('should delete the doc in the doc store', function () { - this.DocstoreManager.promises.deleteDoc - .calledWith(projectId, this.doc._id.toString(), 'test.tex') + it('should delete the doc in the doc store', function (ctx) { + ctx.DocstoreManager.promises.deleteDoc + .calledWith(projectId, ctx.doc._id.toString(), 'test.tex') .should.equal(true) }) }) describe('when the doc is not the root doc', function () { - beforeEach(async function () { - this.project.rootDoc_id = new ObjectId() - await this.ProjectEntityUpdateHandler._cleanUpDoc( - this.project, - this.doc, - this.path, + beforeEach(async function (ctx) { + ctx.project.rootDoc_id = new ObjectId() + await ctx.ProjectEntityUpdateHandler._cleanUpDoc( + ctx.project, + ctx.doc, + ctx.path, userId ) }) - it('should not unset the root doc', function () { - this.ProjectEntityUpdateHandler.promises.unsetRootDoc.called.should.equal( + it('should not unset the root doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc.called.should.equal( false ) }) @@ -2801,132 +2856,126 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('convertDocToFile', function () { - beforeEach(function () { - this.docPath = '/folder/doc.tex' - this.docLines = ['line one', 'line two'] - this.tmpFilePath = '/tmp/file' - this.fileStoreUrl = 'http://filestore/file' - this.folder = { _id: new ObjectId() } - this.rev = 3 - this.ProjectLocator.promises.findElement + beforeEach(function (ctx) { + ctx.docPath = '/folder/doc.tex' + ctx.docLines = ['line one', 'line two'] + ctx.tmpFilePath = '/tmp/file' + ctx.fileStoreUrl = 'http://filestore/file' + ctx.folder = { _id: new ObjectId() } + ctx.rev = 3 + ctx.ProjectLocator.promises.findElement .withArgs({ - project_id: this.project._id, - element_id: this.doc._id, + project_id: ctx.project._id, + element_id: ctx.doc._id, type: 'doc', }) .resolves({ - element: this.doc, - path: { fileSystem: this.path }, - folder: this.folder, + element: ctx.doc, + path: { fileSystem: ctx.path }, + folder: ctx.folder, }) - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement .withArgs({ - project_id: this.project._id.toString(), - element_id: this.file._id, + project_id: ctx.project._id.toString(), + element_id: ctx.file._id, type: 'file', }) .resolves({ - element: this.file, - path: this.docPath, - folder: this.folder, + element: ctx.file, + path: ctx.docPath, + folder: ctx.folder, }) - this.DocstoreManager.promises.getDoc - .withArgs(this.project._id, this.doc._id) - .resolves({ lines: this.docLines, rev: this.rev }) - this.FileWriter.promises.writeLinesToDisk.resolves(this.tmpFilePath) - this.FileStoreHandler.promises.uploadFileFromDisk.resolves({ - fileRef: this.file, + ctx.DocstoreManager.promises.getDoc + .withArgs(ctx.project._id, ctx.doc._id) + .resolves({ lines: ctx.docLines, rev: ctx.rev }) + ctx.FileWriter.promises.writeLinesToDisk.resolves(ctx.tmpFilePath) + ctx.FileStoreHandler.promises.uploadFileFromDisk.resolves({ + fileRef: ctx.file, createdBlob: true, }) - this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves( - this.project + ctx.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves( + ctx.project ) }) describe('successfully', function () { - beforeEach(async function () { - await this.ProjectEntityUpdateHandler.promises.convertDocToFile( - this.project._id, - this.doc._id, + beforeEach(async function (ctx) { + await ctx.ProjectEntityUpdateHandler.promises.convertDocToFile( + ctx.project._id, + ctx.doc._id, userId, - this.source + ctx.source ) }) - it('deletes the document in doc updater', function () { + it('deletes the document in doc updater', function (ctx) { expect( - this.DocumentUpdaterHandler.promises.deleteDoc - ).to.have.been.calledWith(this.project._id, this.doc._id) + ctx.DocumentUpdaterHandler.promises.deleteDoc + ).to.have.been.calledWith(ctx.project._id, ctx.doc._id) }) - it('uploads the file to filestore', function () { + it('uploads the file to filestore', function (ctx) { expect( - this.FileStoreHandler.promises.uploadFileFromDisk + ctx.FileStoreHandler.promises.uploadFileFromDisk ).to.have.been.calledWith( - this.project._id, - { name: this.doc.name, rev: this.rev + 1 }, - this.tmpFilePath + ctx.project._id, + { name: ctx.doc.name, rev: ctx.rev + 1 }, + ctx.tmpFilePath ) }) - it('cleans up the temporary file', function () { - expect(this.fs.promises.unlink).to.have.been.calledWith( - this.tmpFilePath - ) + it('cleans up the temporary file', function (ctx) { + expect(ctx.fs.promises.unlink).to.have.been.calledWith(ctx.tmpFilePath) }) - it('replaces the doc with the file', function () { + it('replaces the doc with the file', function (ctx) { expect( - this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile + ctx.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile ).to.have.been.calledWith( - this.project._id, - this.doc._id, - this.file, + ctx.project._id, + ctx.doc._id, + ctx.file, userId ) }) - it('notifies document updater of changes', function () { + it('notifies document updater of changes', function (ctx) { expect( - this.DocumentUpdaterHandler.promises.updateProjectStructure + ctx.DocumentUpdaterHandler.promises.updateProjectStructure ).to.have.been.calledWith( - this.project._id, - this.project.overleaf.history.id, + ctx.project._id, + ctx.project.overleaf.history.id, userId, { - oldDocs: [{ doc: this.doc, path: this.path }], + oldDocs: [{ doc: ctx.doc, path: ctx.path }], newFiles: [ { - file: this.file, - path: this.path, + file: ctx.file, + path: ctx.path, createdBlob: true, }, ], - newProject: this.project, + newProject: ctx.project, }, - this.source + ctx.source ) }) - it('should notify real-time of the doc deletion', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('should notify real-time of the doc deletion', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'removeEntity', - this.doc._id, + ctx.doc._id, 'convertDocToFile' ) }) - it('should notify real-time of the file creation', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('should notify real-time of the file creation', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'reciveNewFile', - this.folder._id, - this.file, + ctx.folder._id, + ctx.file, 'convertDocToFile', null ) @@ -2934,24 +2983,24 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('when the doc has ranges', function () { - it('should throw a DocHasRangesError', async function () { - this.ranges = { comments: [{ id: 123 }] } - this.DocstoreManager.promises.getDoc - .withArgs(this.project._id, this.doc._id) + it('should throw a DocHasRangesError', async function (ctx) { + ctx.ranges = { comments: [{ id: 123 }] } + ctx.DocstoreManager.promises.getDoc + .withArgs(ctx.project._id, ctx.doc._id) .resolves({ - lines: this.docLines, + lines: ctx.docLines, rev: 'rev', version: 'version', - ranges: this.ranges, + ranges: ctx.ranges, }) let error try { - await this.ProjectEntityUpdateHandler.promises.convertDocToFile( - this.project._id, - this.doc._id, - this.user._id, - this.source + await ctx.ProjectEntityUpdateHandler.promises.convertDocToFile( + ctx.project._id, + ctx.doc._id, + ctx.user._id, + ctx.source ) } catch (err) { error = err @@ -2963,65 +3012,65 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('isPathValidForMainBibliographyDoc', function () { - it('should not allow other endings than .bib', function () { + it('should not allow other endings than .bib', function (ctx) { const endings = ['.tex', '.png', '.jpg', '.pdf', '.docx', '.doc'] endings.forEach(ending => { expect( - this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( + ctx.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( `/foo/bar/baz${ending}` ) ).to.be.false }) }) - it('should allow a mix of lower and uppercase letters', function () { + it('should allow a mix of lower and uppercase letters', function (ctx) { const endings = ['.bib', '.BiB', '.BIB', '.bIB'] endings.forEach(ending => { expect( - this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( + ctx.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( `/foo/bar/baz.${ending}` ) ).to.be.true }) }) - it('should not allow a path without an extension', function () { + it('should not allow a path without an extension', function (ctx) { expect( - this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( + ctx.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc( '/foo/bar/baz' ) ).to.be.false }) - it('should not allow the empty path', function () { + it('should not allow the empty path', function (ctx) { expect( - this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc('') + ctx.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc('') ).to.be.false }) }) describe('setMainBibliographyDoc', function () { describe('on success', function () { - beforeEach(async function () { - this.doc = { + beforeEach(async function (ctx) { + ctx.doc = { _id: new ObjectId(), name: 'test.bib', } - this.path = '/path/to/test.bib' - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId - .withArgs(this.project._id, this.doc._id) - .resolves(this.path) + ctx.path = '/path/to/test.bib' + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId + .withArgs(ctx.project._id, ctx.doc._id) + .resolves(ctx.path) - await this.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( - this.project._id, - this.doc._id + await ctx.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( + ctx.project._id, + ctx.doc._id ) }) - it('should update the project with the new main bibliography doc', function () { - expect(this.ProjectModel.updateOne).to.have.been.calledWith( - { _id: this.project._id }, - { mainBibliographyDoc_id: this.doc._id } + it('should update the project with the new main bibliography doc', function (ctx) { + expect(ctx.ProjectModel.updateOne).to.have.been.calledWith( + { _id: ctx.project._id }, + { mainBibliographyDoc_id: ctx.doc._id } ) }) }) @@ -3029,18 +3078,18 @@ describe('ProjectEntityUpdateHandler', function () { describe('on failure', function () { describe("when document can't be found", function () { let setMainBibliographyDocPromise - beforeEach(function () { - this.doc = { + beforeEach(function (ctx) { + ctx.doc = { _id: new ObjectId(), name: 'test.bib', } - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId - .withArgs(this.project._id, this.doc._id) + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId + .withArgs(ctx.project._id, ctx.doc._id) .rejects(new Error('error')) setMainBibliographyDocPromise = - this.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( - this.project._id, - this.doc._id + ctx.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( + ctx.project._id, + ctx.doc._id ) }) @@ -3056,7 +3105,7 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.be.instanceOf(Error) }) - it('should not update the project with the new main bibliography doc', async function () { + it('should not update the project with the new main bibliography doc', async function (ctx) { let error try { @@ -3066,26 +3115,26 @@ describe('ProjectEntityUpdateHandler', function () { } expect(error).to.exist - expect(this.ProjectModel.updateOne).to.not.have.been.called + expect(ctx.ProjectModel.updateOne).to.not.have.been.called }) }) describe("when path is not a bib file can't be found", function () { let setMainBibliographyDocPromise - beforeEach(function () { - this.doc = { + beforeEach(function (ctx) { + ctx.doc = { _id: new ObjectId(), name: 'test.bib', } - this.path = '/path/to/test.tex' - this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId - .withArgs(this.project._id, this.doc._id) - .resolves(this.path) + ctx.path = '/path/to/test.tex' + ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId + .withArgs(ctx.project._id, ctx.doc._id) + .resolves(ctx.path) setMainBibliographyDocPromise = - this.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( - this.project._id, - this.doc._id + ctx.ProjectEntityUpdateHandler.promises.setMainBibliographyDoc( + ctx.project._id, + ctx.doc._id ) }) @@ -3101,7 +3150,7 @@ describe('ProjectEntityUpdateHandler', function () { expect(error).to.be.instanceOf(Error) }) - it('should not update the project with the new main bibliography doc', async function () { + it('should not update the project with the new main bibliography doc', async function (ctx) { let error try { @@ -3111,7 +3160,7 @@ describe('ProjectEntityUpdateHandler', function () { } expect(error).to.exist - expect(this.ProjectModel.updateOne).to.not.have.been.called + expect(ctx.ProjectModel.updateOne).to.not.have.been.called }) }) }) @@ -3120,24 +3169,24 @@ describe('ProjectEntityUpdateHandler', function () { describe('appendToDoc', function () { describe('when document cannot be found', function () { let appendToDocPromise - beforeEach(function () { - this.appendedLines = ['5678', 'def'] - this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() - this.ProjectLocator.promises.findElement = sinon.stub() - this.ProjectLocator.promises.findElement + beforeEach(function (ctx) { + ctx.appendedLines = ['5678', 'def'] + ctx.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + ctx.ProjectLocator.promises.findElement = sinon.stub() + ctx.ProjectLocator.promises.findElement .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) .rejects(new Errors.NotFoundError()) appendToDocPromise = - this.ProjectEntityUpdateHandler.promises.appendToDocWithPath( + ctx.ProjectEntityUpdateHandler.promises.appendToDocWithPath( projectId, docId, - this.appendedLines, - this.source, + ctx.appendedLines, + ctx.source, userId ) }) - it('should not talk to DocumentUpdaterHandler', async function () { + it('should not talk to DocumentUpdaterHandler', async function (ctx) { let error try { @@ -3147,7 +3196,7 @@ describe('ProjectEntityUpdateHandler', function () { } expect(error).to.exist - this.DocumentUpdaterHandler.promises.appendToDocument.should.not.have + ctx.DocumentUpdaterHandler.promises.appendToDocument.should.not.have .been.called }) @@ -3166,33 +3215,33 @@ describe('ProjectEntityUpdateHandler', function () { describe('when document is found', function () { let appendToDocResult - beforeEach(async function () { - this.appendedLines = ['5678', 'def'] - this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() - this.DocumentUpdaterHandler.promises.appendToDocument - .withArgs(projectId, docId, userId, this.appendedLines, this.source) + beforeEach(async function (ctx) { + ctx.appendedLines = ['5678', 'def'] + ctx.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + ctx.DocumentUpdaterHandler.promises.appendToDocument + .withArgs(projectId, docId, userId, ctx.appendedLines, ctx.source) .resolves({ rev: 1 }) - this.ProjectLocator.promises.findElement = sinon.stub() - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement = sinon.stub() + ctx.ProjectLocator.promises.findElement .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) .resolves({ element: { _id: docId } }) appendToDocResult = - await this.ProjectEntityUpdateHandler.promises.appendToDocWithPath( + await ctx.ProjectEntityUpdateHandler.promises.appendToDocWithPath( projectId, docId, - this.appendedLines, - this.source, + ctx.appendedLines, + ctx.source, userId ) }) - it('should forward call to DocumentUpdaterHandler.appendToDocument', function () { - this.DocumentUpdaterHandler.promises.appendToDocument.should.have.been.calledWith( + it('should forward call to DocumentUpdaterHandler.appendToDocument', function (ctx) { + ctx.DocumentUpdaterHandler.promises.appendToDocument.should.have.been.calledWith( projectId, docId, userId, - this.appendedLines, - this.source + ctx.appendedLines, + ctx.source ) }) @@ -3202,27 +3251,27 @@ describe('ProjectEntityUpdateHandler', function () { }) describe('when DocumentUpdater throws an error', function () { - beforeEach(function () { - this.appendedLines = ['5678', 'def'] - this.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() - this.DocumentUpdaterHandler.promises.appendToDocument.rejects( + beforeEach(function (ctx) { + ctx.appendedLines = ['5678', 'def'] + ctx.DocumentUpdaterHandler.promises.appendToDocument = sinon.stub() + ctx.DocumentUpdaterHandler.promises.appendToDocument.rejects( new Error() ) - this.ProjectLocator.promises.findElement = sinon.stub() - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement = sinon.stub() + ctx.ProjectLocator.promises.findElement .withArgs({ project_id: projectId, element_id: docId, type: 'doc' }) .resolves({ element: { _id: docId } }) }) - it('should return the response from DocumentUpdaterHandler', async function () { + it('should return the response from DocumentUpdaterHandler', async function (ctx) { let error try { - await this.ProjectEntityUpdateHandler.promises.appendToDocWithPath( + await ctx.ProjectEntityUpdateHandler.promises.appendToDocWithPath( projectId, docId, - this.appendedLines, - this.source, + ctx.appendedLines, + ctx.source, userId ) } catch (err) { diff --git a/services/web/test/unit/src/Project/ProjectGetter.test.mjs b/services/web/test/unit/src/Project/ProjectGetter.test.mjs index 8f58b8a10b..d8c27a0c50 100644 --- a/services/web/test/unit/src/Project/ProjectGetter.test.mjs +++ b/services/web/test/unit/src/Project/ProjectGetter.test.mjs @@ -1,30 +1,31 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/Project/ProjectGetter.js' -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +const modulePath = '../../../../app/src/Features/Project/ProjectGetter.mjs' + +const { ObjectId } = mongodb describe('ProjectGetter', function () { - beforeEach(function () { - this.project = { _id: new ObjectId() } - this.projectIdStr = this.project._id.toString() - this.deletedProject = { deleterData: { wombat: 'potato' } } - this.userId = new ObjectId() + beforeEach(async function (ctx) { + ctx.project = { _id: new ObjectId() } + ctx.projectIdStr = ctx.project._id.toString() + ctx.deletedProject = { deleterData: { wombat: 'potato' } } + ctx.userId = new ObjectId() - this.DeletedProject = { + ctx.DeletedProject = { find: sinon.stub().returns({ - exec: sinon.stub().resolves([this.deletedProject]), + exec: sinon.stub().resolves([ctx.deletedProject]), }), } - this.Project = { + ctx.Project = { find: sinon.stub().returns({ exec: sinon.stub().resolves(), }), findOne: sinon.stub().returns({ - exec: sinon.stub().resolves(this.project), + exec: sinon.stub().resolves(ctx.project), }), } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getProjectsUserIsMemberOf: sinon.stub().resolves({ readAndWrite: [], @@ -34,58 +35,76 @@ describe('ProjectGetter', function () { }), }, } - this.LockManager = { + ctx.LockManager = { promises: { runWithLock: sinon .stub() .callsFake((namespace, id, runner) => runner()), }, } - this.db = { + ctx.db = { projects: { - findOne: sinon.stub().resolves(this.project), + findOne: sinon.stub().resolves(ctx.project), }, users: {}, } - this.ProjectEntityMongoUpdateHandler = { + ctx.ProjectEntityMongoUpdateHandler = { lockKey: sinon.stub().returnsArg(0), } - this.ProjectGetter = SandboxedModule.require(modulePath, { - requires: { - '../../infrastructure/mongodb': { db: this.db, ObjectId }, - '../../models/Project': { - Project: this.Project, - }, - '../../models/DeletedProject': { - DeletedProject: this.DeletedProject, - }, - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - '../../infrastructure/LockManager': this.LockManager, - './ProjectEntityMongoUpdateHandler': - this.ProjectEntityMongoUpdateHandler, - }, - }) + + vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({ + db: ctx.db, + ObjectId, + })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: ctx.Project, + })) + + vi.doMock('../../../../app/src/models/DeletedProject', () => ({ + DeletedProject: ctx.DeletedProject, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({ + default: ctx.LockManager, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler', + () => ({ + default: ctx.ProjectEntityMongoUpdateHandler, + }) + ) + + ctx.ProjectGetter = (await import(modulePath)).default }) describe('getProjectWithoutDocLines', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject = sinon.stub().resolves() + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves() }) describe('passing an id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProjectWithoutDocLines( - this.project._id + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProjectWithoutDocLines( + ctx.project._id ) }) - it('should call find with the project id', function () { - this.ProjectGetter.promises.getProject - .calledWith(this.project._id) + it('should call find with the project id', function (ctx) { + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project._id) .should.equal(true) }) - it('should exclude the doc lines', function () { + it('should exclude the doc lines', function (ctx) { const excludes = { 'rootFolder.docs.lines': 0, 'rootFolder.folders.docs.lines': 0, @@ -97,32 +116,32 @@ describe('ProjectGetter', function () { 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0, } - this.ProjectGetter.promises.getProject - .calledWith(this.project._id, excludes) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project._id, excludes) .should.equal(true) }) }) }) describe('getProjectWithOnlyFolders', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject = sinon.stub().resolves() + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves() }) describe('passing an id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProjectWithOnlyFolders( - this.project._id + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProjectWithOnlyFolders( + ctx.project._id ) }) - it('should call find with the project id', function () { - this.ProjectGetter.promises.getProject - .calledWith(this.project._id) + it('should call find with the project id', function (ctx) { + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project._id) .should.equal(true) }) - it('should exclude the docs and files lines', function () { + it('should exclude the docs and files lines', function (ctx) { const excludes = { 'rootFolder.docs': 0, 'rootFolder.fileRefs': 0, @@ -141,8 +160,8 @@ describe('ProjectGetter', function () { 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0, 'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0, } - this.ProjectGetter.promises.getProject - .calledWith(this.project._id, excludes) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project._id, excludes) .should.equal(true) }) }) @@ -151,58 +170,58 @@ describe('ProjectGetter', function () { describe('getProject', function () { describe('without projection', function () { describe('with project id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProject(this.projectIdStr) + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProject(ctx.projectIdStr) }) - it('should call findOne with the project id', function () { - expect(this.db.projects.findOne.callCount).to.equal(1) + it('should call findOne with the project id', function (ctx) { + expect(ctx.db.projects.findOne.callCount).to.equal(1) expect( - this.db.projects.findOne.lastCall.args[0]._id.toString() - ).to.equal(this.projectIdStr) + ctx.db.projects.findOne.lastCall.args[0]._id.toString() + ).to.equal(ctx.projectIdStr) }) }) describe('without project id', function () { - it('should be rejected', function () { + it('should be rejected', function (ctx) { expect( - this.ProjectGetter.promises.getProject(null) + ctx.ProjectGetter.promises.getProject(null) ).to.be.rejectedWith('no project id provided') - expect(this.db.projects.findOne.callCount).to.equal(0) + expect(ctx.db.projects.findOne.callCount).to.equal(0) }) }) }) describe('with projection', function () { - beforeEach(function () { - this.projection = { _id: 1 } + beforeEach(function (ctx) { + ctx.projection = { _id: 1 } }) describe('with project id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProject( - this.projectIdStr, - this.projection + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProject( + ctx.projectIdStr, + ctx.projection ) }) - it('should call findOne with the project id', function () { - expect(this.db.projects.findOne.callCount).to.equal(1) + it('should call findOne with the project id', function (ctx) { + expect(ctx.db.projects.findOne.callCount).to.equal(1) expect( - this.db.projects.findOne.lastCall.args[0]._id.toString() - ).to.equal(this.projectIdStr) - expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({ - projection: this.projection, + ctx.db.projects.findOne.lastCall.args[0]._id.toString() + ).to.equal(ctx.projectIdStr) + expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({ + projection: ctx.projection, }) }) }) describe('without project id', function () { - it('should be rejected', function () { + it('should be rejected', function (ctx) { expect( - this.ProjectGetter.promises.getProject(null) + ctx.ProjectGetter.promises.getProject(null) ).to.be.rejectedWith('no project id provided') - expect(this.db.projects.findOne.callCount).to.equal(0) + expect(ctx.db.projects.findOne.callCount).to.equal(0) }) }) }) @@ -211,155 +230,151 @@ describe('ProjectGetter', function () { describe('getProjectWithoutLock', function () { describe('without projection', function () { describe('with project id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProjectWithoutLock( - this.projectIdStr + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProjectWithoutLock( + ctx.projectIdStr ) }) - it('should call findOne with the project id', function () { - expect(this.db.projects.findOne.callCount).to.equal(1) + it('should call findOne with the project id', function (ctx) { + expect(ctx.db.projects.findOne.callCount).to.equal(1) expect( - this.db.projects.findOne.lastCall.args[0]._id.toString() - ).to.equal(this.projectIdStr) + ctx.db.projects.findOne.lastCall.args[0]._id.toString() + ).to.equal(ctx.projectIdStr) }) }) describe('without project id', function () { - it('should be rejected', function () { + it('should be rejected', function (ctx) { expect( - this.ProjectGetter.promises.getProjectWithoutLock(null) + ctx.ProjectGetter.promises.getProjectWithoutLock(null) ).to.be.rejectedWith('no project id provided') - expect(this.db.projects.findOne.callCount).to.equal(0) + expect(ctx.db.projects.findOne.callCount).to.equal(0) }) }) }) describe('with projection', function () { - beforeEach(function () { - this.projection = { _id: 1 } + beforeEach(function (ctx) { + ctx.projection = { _id: 1 } }) describe('with project id', function () { - beforeEach(async function () { - await this.ProjectGetter.promises.getProjectWithoutLock( - this.project._id, - this.projection + beforeEach(async function (ctx) { + await ctx.ProjectGetter.promises.getProjectWithoutLock( + ctx.project._id, + ctx.projection ) }) - it('should call findOne with the project id', function () { - expect(this.db.projects.findOne.callCount).to.equal(1) + it('should call findOne with the project id', function (ctx) { + expect(ctx.db.projects.findOne.callCount).to.equal(1) expect( - this.db.projects.findOne.lastCall.args[0]._id.toString() - ).to.equal(this.projectIdStr) - expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({ - projection: this.projection, + ctx.db.projects.findOne.lastCall.args[0]._id.toString() + ).to.equal(ctx.projectIdStr) + expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({ + projection: ctx.projection, }) }) }) describe('without project id', function () { - it('should be rejected', function () { + it('should be rejected', function (ctx) { expect( - this.ProjectGetter.promises.getProjectWithoutLock(null) + ctx.ProjectGetter.promises.getProjectWithoutLock(null) ).to.be.rejectedWith('no project id provided') - expect(this.db.projects.findOne.callCount).to.equal(0) + expect(ctx.db.projects.findOne.callCount).to.equal(0) }) }) }) }) describe('findAllUsersProjects', function () { - beforeEach(function () { - this.fields = { mock: 'fields' } - this.projectOwned = { _id: 'mock-owned-projects' } - this.projectRW = { _id: 'mock-rw-projects' } - this.projectReview = { _id: 'mock-review-projects' } - this.projectRO = { _id: 'mock-ro-projects' } - this.projectTokenRW = { _id: 'mock-token-rw-projects' } - this.projectTokenRO = { _id: 'mock-token-ro-projects' } - this.Project.find - .withArgs({ owner_ref: this.userId }, this.fields) - .returns({ exec: sinon.stub().resolves([this.projectOwned]) }) + beforeEach(function (ctx) { + ctx.fields = { mock: 'fields' } + ctx.projectOwned = { _id: 'mock-owned-projects' } + ctx.projectRW = { _id: 'mock-rw-projects' } + ctx.projectReview = { _id: 'mock-review-projects' } + ctx.projectRO = { _id: 'mock-ro-projects' } + ctx.projectTokenRW = { _id: 'mock-token-rw-projects' } + ctx.projectTokenRO = { _id: 'mock-token-ro-projects' } + ctx.Project.find + .withArgs({ owner_ref: ctx.userId }, ctx.fields) + .returns({ exec: sinon.stub().resolves([ctx.projectOwned]) }) }) - it('should return a promise with all the projects', async function () { - this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ - readAndWrite: [this.projectRW], - readOnly: [this.projectRO], - tokenReadAndWrite: [this.projectTokenRW], - tokenReadOnly: [this.projectTokenRO], - review: [this.projectReview], + it('should return a promise with all the projects', async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ + readAndWrite: [ctx.projectRW], + readOnly: [ctx.projectRO], + tokenReadAndWrite: [ctx.projectTokenRW], + tokenReadOnly: [ctx.projectTokenRO], + review: [ctx.projectReview], }) - const projects = await this.ProjectGetter.promises.findAllUsersProjects( - this.userId, - this.fields + const projects = await ctx.ProjectGetter.promises.findAllUsersProjects( + ctx.userId, + ctx.fields ) expect(projects).to.deep.equal({ - owned: [this.projectOwned], - readAndWrite: [this.projectRW], - readOnly: [this.projectRO], - tokenReadAndWrite: [this.projectTokenRW], - tokenReadOnly: [this.projectTokenRO], - review: [this.projectReview], + owned: [ctx.projectOwned], + readAndWrite: [ctx.projectRW], + readOnly: [ctx.projectRO], + tokenReadAndWrite: [ctx.projectTokenRW], + tokenReadOnly: [ctx.projectTokenRO], + review: [ctx.projectReview], }) }) - it('should remove duplicate projects', async function () { - this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ - readAndWrite: [this.projectRW, this.projectOwned], - readOnly: [this.projectRO, this.projectRW], - tokenReadAndWrite: [this.projectTokenRW, this.projectRO], - tokenReadOnly: [ - this.projectTokenRW, - this.projectTokenRO, - this.projectRO, - ], - review: [this.projectReview], + it('should remove duplicate projects', async function (ctx) { + ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ + readAndWrite: [ctx.projectRW, ctx.projectOwned], + readOnly: [ctx.projectRO, ctx.projectRW], + tokenReadAndWrite: [ctx.projectTokenRW, ctx.projectRO], + tokenReadOnly: [ctx.projectTokenRW, ctx.projectTokenRO, ctx.projectRO], + review: [ctx.projectReview], }) - const projects = await this.ProjectGetter.promises.findAllUsersProjects( - this.userId, - this.fields + const projects = await ctx.ProjectGetter.promises.findAllUsersProjects( + ctx.userId, + ctx.fields ) expect(projects).to.deep.equal({ - owned: [this.projectOwned], - readAndWrite: [this.projectRW], - readOnly: [this.projectRO], - tokenReadAndWrite: [this.projectTokenRW], - tokenReadOnly: [this.projectTokenRO], - review: [this.projectReview], + owned: [ctx.projectOwned], + readAndWrite: [ctx.projectRW], + readOnly: [ctx.projectRO], + tokenReadAndWrite: [ctx.projectTokenRW], + tokenReadOnly: [ctx.projectTokenRO], + review: [ctx.projectReview], }) }) }) describe('getProjectIdByReadAndWriteToken', function () { describe('when project find returns project', function () { - this.beforeEach(async function () { - this.projectIdFound = - await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken( + beforeEach(async function (ctx) { + ctx.projectIdFound = + await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken( 'token' ) }) - it('should find project with token', function () { - this.Project.findOne + it('should find project with token', function (ctx) { + ctx.Project.findOne .calledWithMatch({ 'tokens.readAndWrite': 'token' }) .should.equal(true) }) - it('should return the project id', function () { - expect(this.projectIdFound).to.equal(this.project._id) + it('should return the project id', function (ctx) { + expect(ctx.projectIdFound).to.equal(ctx.project._id) }) }) describe('when project not found', function () { - it('should return undefined', async function () { - this.Project.findOne.returns({ exec: sinon.stub().resolves(null) }) + it('should return undefined', async function (ctx) { + ctx.Project.findOne.returns({ exec: sinon.stub().resolves(null) }) const projectId = - await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken( + await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken( 'token' ) @@ -368,86 +383,79 @@ describe('ProjectGetter', function () { }) describe('when project find returns error', function () { - this.beforeEach(async function () { - this.Project.findOne.returns({ exec: sinon.stub().rejects() }) + beforeEach(async function (ctx) { + ctx.Project.findOne.returns({ exec: sinon.stub().rejects() }) }) - it('should be rejected', function () { + it('should be rejected', function (ctx) { expect( - this.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token') + ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token') ).to.be.rejected }) }) }) describe('findUsersProjectsByName', function () { - it('should perform a case-insensitive search', async function () { - this.project1 = { _id: 1, name: 'find me!' } - this.project2 = { _id: 2, name: 'not me!' } - this.project3 = { _id: 3, name: 'FIND ME!' } - this.project4 = { _id: 4, name: 'Find Me!' } - this.Project.find.withArgs({ owner_ref: this.userId }).returns({ + it('should perform a case-insensitive search', async function (ctx) { + ctx.project1 = { _id: 1, name: 'find me!' } + ctx.project2 = { _id: 2, name: 'not me!' } + ctx.project3 = { _id: 3, name: 'FIND ME!' } + ctx.project4 = { _id: 4, name: 'Find Me!' } + ctx.Project.find.withArgs({ owner_ref: ctx.userId }).returns({ exec: sinon .stub() - .resolves([ - this.project1, - this.project2, - this.project3, - this.project4, - ]), + .resolves([ctx.project1, ctx.project2, ctx.project3, ctx.project4]), }) - const projects = - await this.ProjectGetter.promises.findUsersProjectsByName( - this.userId, - this.project1.name - ) + const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName( + ctx.userId, + ctx.project1.name + ) const projectNames = projects.map(project => project.name) expect(projectNames).to.have.members([ - this.project1.name, - this.project3.name, - this.project4.name, + ctx.project1.name, + ctx.project3.name, + ctx.project4.name, ]) }) - it('should search collaborations as well', async function () { - this.project1 = { _id: 1, name: 'find me!' } - this.project2 = { _id: 2, name: 'FIND ME!' } - this.project3 = { _id: 3, name: 'Find Me!' } - this.project4 = { _id: 4, name: 'find ME!' } - this.project5 = { _id: 5, name: 'FIND me!' } - this.Project.find - .withArgs({ owner_ref: this.userId }) - .returns({ exec: sinon.stub().resolves([this.project1]) }) - this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ - readAndWrite: [this.project2], - readOnly: [this.project3], - tokenReadAndWrite: [this.project4], - tokenReadOnly: [this.project5], + it('should search collaborations as well', async function (ctx) { + ctx.project1 = { _id: 1, name: 'find me!' } + ctx.project2 = { _id: 2, name: 'FIND ME!' } + ctx.project3 = { _id: 3, name: 'Find Me!' } + ctx.project4 = { _id: 4, name: 'find ME!' } + ctx.project5 = { _id: 5, name: 'FIND me!' } + ctx.Project.find + .withArgs({ owner_ref: ctx.userId }) + .returns({ exec: sinon.stub().resolves([ctx.project1]) }) + ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({ + readAndWrite: [ctx.project2], + readOnly: [ctx.project3], + tokenReadAndWrite: [ctx.project4], + tokenReadOnly: [ctx.project5], }) - const projects = - await this.ProjectGetter.promises.findUsersProjectsByName( - this.userId, - this.project1.name - ) + const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName( + ctx.userId, + ctx.project1.name + ) expect(projects.map(project => project.name)).to.have.members([ - this.project1.name, - this.project2.name, + ctx.project1.name, + ctx.project2.name, ]) }) }) describe('getUsersDeletedProjects', function () { - it('should look up the deleted projects by deletedProjectOwnerId', async function () { - await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe') - sinon.assert.calledWith(this.DeletedProject.find, { + it('should look up the deleted projects by deletedProjectOwnerId', async function (ctx) { + await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe') + sinon.assert.calledWith(ctx.DeletedProject.find, { 'deleterData.deletedProjectOwnerId': 'giraffe', }) }) - it('should pass the found projects to the callback', async function () { + it('should pass the found projects to the callback', async function (ctx) { const docs = - await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe') - expect(docs).to.deep.equal([this.deletedProject]) + await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe') + expect(docs).to.deep.equal([ctx.deletedProject]) }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs b/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs index 00b6ccf38b..e3b94e09b5 100644 --- a/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs +++ b/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs @@ -46,11 +46,14 @@ describe('ProjectHistoryHandler', function () { }) ) - vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({ - default: (ctx.HistoryManager = { - promises: {}, - }), - })) + vi.doMock( + '../../../../app/src/Features/History/HistoryManager.mjs', + () => ({ + default: (ctx.HistoryManager = { + promises: {}, + }), + }) + ) vi.doMock( '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index f060fc6eca..aa13023245 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -5,10 +5,13 @@ import Errors from '../../../../app/src/Features/Errors/Errors.js' const ObjectId = mongodb.ObjectId -const MODULE_PATH = new URL( - '../../../../app/src/Features/Project/ProjectListController', - import.meta.url -).pathname +const MODULE_PATH = `${import.meta.dirname}/../../../../app/src/Features/Project/ProjectListController` + +// Mock AnalyticsManager as it isn't used in these tests but causes the User model to be imported +// TODO: remove this once all models are ESM and this kind of mocking is no longer necessary +vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.js', () => { + return {} +}) describe('ProjectListController', function () { beforeEach(async function (ctx) { diff --git a/services/web/test/unit/src/Project/ProjectLocator.test.mjs b/services/web/test/unit/src/Project/ProjectLocator.test.mjs index 6cea98a024..60092b17a4 100644 --- a/services/web/test/unit/src/Project/ProjectLocator.test.mjs +++ b/services/web/test/unit/src/Project/ProjectLocator.test.mjs @@ -1,8 +1,11 @@ -const { expect } = require('chai') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/Project/ProjectLocator' -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const Errors = require('../../../../app/src/Features/Errors/Errors') + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) const project = { _id: '1234566', rootFolder: [] } const rootDoc = { name: 'rootDoc', _id: 'das239djd' } @@ -38,47 +41,50 @@ project.rootFolder[0] = rootFolder project.rootDoc_id = rootDoc._id describe('ProjectLocator', function () { - beforeEach(function () { - this.ProjectGetter = { + beforeEach(async function (ctx) { + ctx.ProjectGetter = { getProject: sinon.stub().callsArgWith(2, null, project), } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchived: sinon.stub(), isTrashed: sinon.stub(), isArchivedOrTrashed: sinon.stub(), } - this.locator = SandboxedModule.require(modulePath, { - requires: { - '../../models/User': { User: this.User }, - './ProjectGetter': this.ProjectGetter, - './ProjectHelper': this.ProjectHelper, - }, - }) + + vi.doMock('../../../../app/src/models/User', () => ({ + default: { User: ctx.User }, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + ctx.locator = (await import(modulePath)).default }) describe('finding a doc', function () { - it('finds one at the root level', async function () { - const { element, path, folder } = await this.locator.promises.findElement( - { - project_id: project._id, - element_id: doc2._id, - type: 'docs', - } - ) + it('finds one at the root level', async function (ctx) { + const { element, path, folder } = await ctx.locator.promises.findElement({ + project_id: project._id, + element_id: doc2._id, + type: 'docs', + }) element._id.should.equal(doc2._id) path.fileSystem.should.equal(`/${doc2.name}`) folder._id.should.equal(project.rootFolder[0]._id) path.mongo.should.equal('rootFolder.0.docs.1') }) - it('when it is nested', async function () { - const { element, path, folder } = await this.locator.promises.findElement( - { - project_id: project._id, - element_id: subSubDoc._id, - type: 'doc', - } - ) + it('when it is nested', async function (ctx) { + const { element, path, folder } = await ctx.locator.promises.findElement({ + project_id: project._id, + element_id: subSubDoc._id, + type: 'doc', + }) expect(element._id).to.equal(subSubDoc._id) path.fileSystem.should.equal( `/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}` @@ -87,9 +93,9 @@ describe('ProjectLocator', function () { path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0') }) - it('should give error if element could not be found', async function () { + it('should give error if element could not be found', async function (ctx) { await expect( - this.locator.promises.findElement({ + ctx.locator.promises.findElement({ project_id: project._id, element_id: 'ddsd432nj42', type: 'docs', @@ -101,20 +107,18 @@ describe('ProjectLocator', function () { }) describe('finding a folder', function () { - it('should return root folder when looking for root folder', async function () { - const { element: foundElement } = await this.locator.promises.findElement( - { - project_id: project._id, - element_id: rootFolder._id, - type: 'folder', - } - ) + it('should return root folder when looking for root folder', async function (ctx) { + const { element: foundElement } = await ctx.locator.promises.findElement({ + project_id: project._id, + element_id: rootFolder._id, + type: 'folder', + }) foundElement._id.should.equal(rootFolder._id) }) - it('should not return root folder when searching for docs', async function () { + it('should not return root folder when searching for docs', async function (ctx) { await expect( - this.locator.promises.findElement({ + ctx.locator.promises.findElement({ project_id: project._id, element_id: rootFolder._id, type: 'docs', @@ -124,9 +128,9 @@ describe('ProjectLocator', function () { .and.eventually.have.property('message', 'entity not found') }) - it('should not return root folder when searching for files', async function () { + it('should not return root folder when searching for files', async function (ctx) { await expect( - this.locator.promises.findElement({ + ctx.locator.promises.findElement({ project_id: project._id, element_id: rootFolder._id, type: 'files', @@ -136,12 +140,12 @@ describe('ProjectLocator', function () { .and.eventually.have.property('message', 'entity not found') }) - it('when at root', async function () { + it('when at root', async function (ctx) { const { element: foundElement, path, folder: parentFolder, - } = await this.locator.promises.findElement({ + } = await ctx.locator.promises.findElement({ project_id: project._id, element_id: subFolder._id, type: 'folder', @@ -152,12 +156,12 @@ describe('ProjectLocator', function () { path.mongo.should.equal('rootFolder.0.folders.1') }) - it('when deeply nested', async function () { + it('when deeply nested', async function (ctx) { const { element: foundElement, path, folder: parentFolder, - } = await this.locator.promises.findElement({ + } = await ctx.locator.promises.findElement({ project_id: project._id, element_id: secondSubFolder._id, type: 'folder', @@ -171,12 +175,12 @@ describe('ProjectLocator', function () { }) describe('finding a file', function () { - it('when at root', async function () { + it('when at root', async function (ctx) { const { element: foundElement, path, folder: parentFolder, - } = await this.locator.promises.findElement({ + } = await ctx.locator.promises.findElement({ project_id: project._id, element_id: file1._id, type: 'fileRefs', @@ -187,12 +191,12 @@ describe('ProjectLocator', function () { path.mongo.should.equal('rootFolder.0.fileRefs.0') }) - it('when deeply nested', async function () { + it('when deeply nested', async function (ctx) { const { element: foundElement, path, folder: parentFolder, - } = await this.locator.promises.findElement({ + } = await ctx.locator.promises.findElement({ project_id: project._id, element_id: subSubFile._id, type: 'fileRefs', @@ -207,25 +211,21 @@ describe('ProjectLocator', function () { }) describe('finding an element with wrong element type', function () { - it('should add an s onto the element type', async function () { - const { element: foundElement } = await this.locator.promises.findElement( - { - project_id: project._id, - element_id: subSubDoc._id, - type: 'doc', - } - ) + it('should add an s onto the element type', async function (ctx) { + const { element: foundElement } = await ctx.locator.promises.findElement({ + project_id: project._id, + element_id: subSubDoc._id, + type: 'doc', + }) foundElement._id.should.equal(subSubDoc._id) }) - it('should convert file to fileRefs', async function () { - const { element: foundElement } = await this.locator.promises.findElement( - { - project_id: project._id, - element_id: file1._id, - type: 'fileRefs', - } - ) + it('should convert file to fileRefs', async function (ctx) { + const { element: foundElement } = await ctx.locator.promises.findElement({ + project_id: project._id, + element_id: file1._id, + type: 'fileRefs', + }) foundElement._id.should.equal(file1._id) }) }) @@ -243,12 +243,12 @@ describe('ProjectLocator', function () { _id: '1234566', rootFolder: [rootFolder2], } - it('should find doc in project', async function () { + it('should find doc in project', async function (ctx) { const { element: foundElement, path, folder: parentFolder, - } = await this.locator.promises.findElement({ + } = await ctx.locator.promises.findElement({ project: project2, element_id: doc3._id, type: 'docs', @@ -261,38 +261,38 @@ describe('ProjectLocator', function () { }) describe('finding root doc', function () { - it('should return root doc when passed project', async function () { - const { element: doc } = await this.locator.promises.findRootDoc(project) + it('should return root doc when passed project', async function (ctx) { + const { element: doc } = await ctx.locator.promises.findRootDoc(project) doc._id.should.equal(rootDoc._id) }) - it('should return root doc when passed project_id', async function () { - const { element: doc } = await this.locator.promises.findRootDoc( + it('should return root doc when passed project_id', async function (ctx) { + const { element: doc } = await ctx.locator.promises.findRootDoc( project._id ) doc._id.should.equal(rootDoc._id) }) - it('should return null when the project has no rootDoc', async function () { + it('should return null when the project has no rootDoc', async function (ctx) { project.rootDoc_id = null const { element: rootDoc } = - await this.locator.promises.findRootDoc(project) + await ctx.locator.promises.findRootDoc(project) expect(rootDoc).to.equal(null) }) - it('should return null when the rootDoc_id no longer exists', async function () { + it('should return null when the rootDoc_id no longer exists', async function (ctx) { project.rootDoc_id = 'doesntexist' const { element: rootDoc } = - await this.locator.promises.findRootDoc(project) + await ctx.locator.promises.findRootDoc(project) expect(rootDoc).to.equal(null) }) }) describe('findElementByPath', function () { - it('should take a doc path and return the element for a root level document', async function () { + it('should take a doc path and return the element for a root level document', async function (ctx) { const path = `${doc1.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -301,10 +301,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(rootFolder) }) - it('should take a doc path and return the element for a root level document with a starting slash', async function () { + it('should take a doc path and return the element for a root level document with a starting slash', async function (ctx) { const path = `/${doc1.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -313,10 +313,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(rootFolder) }) - it('should take a doc path and return the element for a nested document', async function () { + it('should take a doc path and return the element for a nested document', async function (ctx) { const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -325,10 +325,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(secondSubFolder) }) - it('should take a file path and return the element for a root level document', async function () { + it('should take a file path and return the element for a root level document', async function (ctx) { const path = `${file1.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -337,10 +337,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(rootFolder) }) - it('should take a file path and return the element for a nested document', async function () { + it('should take a file path and return the element for a nested document', async function (ctx) { const path = `${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -349,10 +349,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(secondSubFolder) }) - it('should take a file path and return the element for a nested document case insenstive', async function () { + it('should take a file path and return the element for a nested document case insenstive', async function (ctx) { const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -361,10 +361,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(secondSubFolder) }) - it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function () { + it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function (ctx) { const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}` await expect( - this.locator.promises.findElementByPath({ + ctx.locator.promises.findElementByPath({ project, path, exactCaseMatch: true, @@ -372,10 +372,10 @@ describe('ProjectLocator', function () { ).to.eventually.be.rejected }) - it('should take a file path and return the element for a nested folder', async function () { + it('should take a file path and return the element for a nested folder', async function (ctx) { const path = `${subFolder.name}/${secondSubFolder.name}` const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -384,10 +384,10 @@ describe('ProjectLocator', function () { expect(folder).to.equal(subFolder) }) - it('should take a file path and return the root folder', async function () { + it('should take a file path and return the root folder', async function (ctx) { const path = '/' const { element, type, folder } = - await this.locator.promises.findElementByPath({ + await ctx.locator.promises.findElementByPath({ project, path, }) @@ -396,16 +396,16 @@ describe('ProjectLocator', function () { expect(folder).to.equal(null) }) - it('should return an error if the file can not be found inside know folder', async function () { + it('should return an error if the file can not be found inside know folder', async function (ctx) { const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt` - await expect(this.locator.promises.findElementByPath({ project, path })) - .to.eventually.be.rejected + await expect(ctx.locator.promises.findElementByPath({ project, path })).to + .eventually.be.rejected }) - it('should return an error if the file can not be found inside unknown folder', async function () { + it('should return an error if the file can not be found inside unknown folder', async function (ctx) { const path = 'this/does/not/exist.txt' await expect( - this.locator.promises.findElementByPath({ + ctx.locator.promises.findElementByPath({ project, path, }) @@ -413,8 +413,8 @@ describe('ProjectLocator', function () { }) describe('where duplicate folder exists', function () { - beforeEach(function () { - this.duplicateFolder = { + beforeEach(function (ctx) { + ctx.duplicateFolder = { name: 'duplicate1', _id: '1234', folders: [ @@ -425,13 +425,13 @@ describe('ProjectLocator', function () { fileRefs: [], }, ], - docs: [(this.doc = { name: 'main.tex', _id: '456' })], + docs: [(ctx.doc = { name: 'main.tex', _id: '456' })], fileRefs: [], } - this.project = { + ctx.project = { rootFolder: [ { - folders: [this.duplicateFolder, this.duplicateFolder], + folders: [ctx.duplicateFolder, ctx.duplicateFolder], fileRefs: [], docs: [], }, @@ -439,26 +439,26 @@ describe('ProjectLocator', function () { } }) - it('should not call the callback more than once', async function () { - const path = `${this.duplicateFolder.name}/${this.doc.name}` - await this.locator.promises.findElementByPath({ - project: this.project, + it('should not call the callback more than once', async function (ctx) { + const path = `${ctx.duplicateFolder.name}/${ctx.doc.name}` + await ctx.locator.promises.findElementByPath({ + project: ctx.project, path, }) }) // mocha will throw exception if done called multiple times - it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function () { - const path = `${this.duplicateFolder.name}/1/main.tex` - await this.locator.promises.findElementByPath({ - project: this.project, + it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function (ctx) { + const path = `${ctx.duplicateFolder.name}/1/main.tex` + await ctx.locator.promises.findElementByPath({ + project: ctx.project, path, }) }) }) // mocha will throw exception if done called multiple times describe('with a null doc', function () { - beforeEach(function () { - this.project = { + beforeEach(function (ctx) { + ctx.project = { rootFolder: [ { folders: [], @@ -469,10 +469,10 @@ describe('ProjectLocator', function () { } }) - it('should not crash with a null', async function () { + it('should not crash with a null', async function (ctx) { const path = '/other.tex' - const { element } = await this.locator.promises.findElementByPath({ - project: this.project, + const { element } = await ctx.locator.promises.findElementByPath({ + project: ctx.project, path, }) element.name.should.equal('other.tex') @@ -480,14 +480,14 @@ describe('ProjectLocator', function () { }) describe('with a null project', function () { - beforeEach(function () { - this.ProjectGetter = { getProject: sinon.stub().callsArg(2) } + beforeEach(function (ctx) { + ctx.ProjectGetter = { getProject: sinon.stub().callsArg(2) } }) - it('should not crash with a null', async function () { + it('should not crash with a null', async function (ctx) { const path = '/other.tex' await expect( - this.locator.promises.findElementByPath({ + ctx.locator.promises.findElementByPath({ project_id: project._id, path, }) @@ -496,15 +496,13 @@ describe('ProjectLocator', function () { }) describe('with a project_id', function () { - it('should take a doc path and return the element for a root level document', async function () { + it('should take a doc path and return the element for a root level document', async function (ctx) { const path = `${doc1.name}` - const { element, type } = await this.locator.promises.findElementByPath( - { - project_id: project._id, - path, - } - ) - this.ProjectGetter.getProject + const { element, type } = await ctx.locator.promises.findElementByPath({ + project_id: project._id, + path, + }) + ctx.ProjectGetter.getProject .calledWith(project._id, { rootFolder: true, rootDoc_id: true }) .should.equal(true) element.should.deep.equal(doc1) @@ -514,17 +512,17 @@ describe('ProjectLocator', function () { }) describe('findElementByMongoPath', function () { - it('traverses the file tree like Mongo would do', function () { - const element = this.locator.findElementByMongoPath( + it('traverses the file tree like Mongo would do', function (ctx) { + const element = ctx.locator.findElementByMongoPath( project, 'rootFolder.0.folders.1.folders.0.fileRefs.0' ) expect(element).to.equal(subSubFile) }) - it('throws an error if no element is found', function () { + it('throws an error if no element is found', function (ctx) { expect(() => - this.locator.findElementByMongoPath( + ctx.locator.findElementByMongoPath( project, 'rootolder.0.folders.0.folders.0.fileRefs.0' ) diff --git a/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs b/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs index ec4ea57778..4c56407603 100644 --- a/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs +++ b/services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs @@ -1,60 +1,81 @@ -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') -const sinon = require('sinon') +import { vi, expect } from 'vitest' +import mongodb from 'mongodb-legacy' +import sinon from 'sinon' const modulePath = - '../../../../app/src/Features/Project/ProjectRootDocManager.js' -const SandboxedModule = require('sandboxed-module') + '../../../../app/src/Features/Project/ProjectRootDocManager.mjs' + +const { ObjectId } = mongodb describe('ProjectRootDocManager', function () { - beforeEach(function () { - this.project_id = 'project-123' - this.docPaths = {} - this.docId1 = new ObjectId() - this.docId2 = new ObjectId() - this.docId3 = new ObjectId() - this.docId4 = new ObjectId() - this.docPaths[this.docId1] = '/chapter1.tex' - this.docPaths[this.docId2] = '/main.tex' - this.docPaths[this.docId3] = '/nested/chapter1a.tex' - this.docPaths[this.docId4] = '/nested/chapter1b.tex' - this.sl_req_id = 'sl-req-id-123' - this.callback = sinon.stub() - this.globbyFiles = ['a.tex', 'b.tex', 'main.tex'] - this.globby = sinon.stub().resolves(this.globbyFiles) + beforeEach(async function (ctx) { + ctx.project_id = 'project-123' + ctx.docPaths = {} + ctx.docId1 = new ObjectId() + ctx.docId2 = new ObjectId() + ctx.docId3 = new ObjectId() + ctx.docId4 = new ObjectId() + ctx.docPaths[ctx.docId1] = '/chapter1.tex' + ctx.docPaths[ctx.docId2] = '/main.tex' + ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex' + ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex' + ctx.sl_req_id = 'sl-req-id-123' + ctx.callback = sinon.stub() + ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex'] + ctx.globby = sinon.stub().resolves(ctx.globbyFiles) - this.fs = { + ctx.fs = { readFile: sinon.stub().callsArgWith(2, new Error('file not found')), stat: sinon.stub().callsArgWith(1, null, { size: 100 }), } - this.ProjectRootDocManager = SandboxedModule.require(modulePath, { - requires: { - './ProjectEntityHandler': (this.ProjectEntityHandler = {}), - './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), - './ProjectGetter': (this.ProjectGetter = {}), - '../../infrastructure/GracefulShutdown': { - BackgroundTaskTracker: class { - add() {} - done() {} - }, - }, - globby: this.globby, - fs: this.fs, + + 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/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = {}), + })) + + vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({ + BackgroundTaskTracker: class { + add() {} + done() {} }, - }) + })) + + vi.doMock('globby', () => ({ + default: ctx.globby, + })) + + vi.doMock('fs', () => ({ + default: ctx.fs, + })) + + ctx.ProjectRootDocManager = (await import(modulePath)).default }) describe('setRootDocAutomatically', function () { - beforeEach(function () { - this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - this.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon + beforeEach(function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon .stub() .returns(true) }) describe('when there is a suitable root doc', function () { - beforeEach(async function () { - this.docs = { + beforeEach(async function (ctx) { + ctx.docs = { '/chapter1.tex': { - _id: this.docId1, + _id: ctx.docId1, lines: [ 'something else', '\\begin{document}', @@ -63,7 +84,7 @@ describe('ProjectRootDocManager', function () { ], }, '/main.tex': { - _id: this.docId2, + _id: ctx.docId2, lines: [ 'different line', '\\documentclass{article}', @@ -71,114 +92,114 @@ describe('ProjectRootDocManager', function () { ], }, '/nested/chapter1a.tex': { - _id: this.docId3, + _id: ctx.docId3, lines: ['Hello world'], }, '/nested/chapter1b.tex': { - _id: this.docId4, + _id: ctx.docId4, lines: ['Hello world'], }, } - this.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.getAllDocs = sinon .stub() - .callsArgWith(1, null, this.docs) - await this.ProjectRootDocManager.promises.setRootDocAutomatically( - this.project_id + .callsArgWith(1, null, ctx.docs) + await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( + ctx.project_id ) }) - it('should check the docs of the project', function () { - this.ProjectEntityHandler.getAllDocs - .calledWith(this.project_id) + it('should check the docs of the project', function (ctx) { + ctx.ProjectEntityHandler.getAllDocs + .calledWith(ctx.project_id) .should.equal(true) }) - it('should set the root doc to the doc containing a documentclass', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId2) + it('should set the root doc to the doc containing a documentclass', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId2) .should.equal(true) }) }) describe('when the root doc is an Rtex file', function () { - beforeEach(async function () { - this.docs = { + beforeEach(async function (ctx) { + ctx.docs = { '/chapter1.tex': { - _id: this.docId1, + _id: ctx.docId1, lines: ['\\begin{document}', 'Hello world', '\\end{document}'], }, '/main.Rtex': { - _id: this.docId2, + _id: ctx.docId2, lines: ['\\documentclass{article}', '\\input{chapter1}'], }, } - this.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.getAllDocs = sinon .stub() - .callsArgWith(1, null, this.docs) - await this.ProjectRootDocManager.promises.setRootDocAutomatically( - this.project_id + .callsArgWith(1, null, ctx.docs) + await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( + ctx.project_id ) }) - it('should set the root doc to the doc containing a documentclass', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId2) + it('should set the root doc to the doc containing a documentclass', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId2) .should.equal(true) }) }) describe('when there is no suitable root doc', function () { - beforeEach(async function () { - this.docs = { + beforeEach(async function (ctx) { + ctx.docs = { '/chapter1.tex': { - _id: this.docId1, + _id: ctx.docId1, lines: ['\\begin{document}', 'Hello world', '\\end{document}'], }, '/style.bst': { - _id: this.docId2, + _id: ctx.docId2, lines: ['%Example: \\documentclass{article}'], }, } - this.ProjectEntityHandler.getAllDocs = sinon + ctx.ProjectEntityHandler.getAllDocs = sinon .stub() - .callsArgWith(1, null, this.docs) - await this.ProjectRootDocManager.promises.setRootDocAutomatically( - this.project_id + .callsArgWith(1, null, ctx.docs) + await ctx.ProjectRootDocManager.promises.setRootDocAutomatically( + ctx.project_id ) }) - it('should not set the root doc to the doc containing a documentclass', function () { - this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) + it('should not set the root doc to the doc containing a documentclass', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) }) }) }) describe('findRootDocFileFromDirectory', function () { - beforeEach(function () { - this.fs.readFile + beforeEach(function (ctx) { + ctx.fs.readFile .withArgs('/foo/a.tex') .callsArgWith(2, null, 'Hello World!') - this.fs.readFile + ctx.fs.readFile .withArgs('/foo/b.tex') .callsArgWith(2, null, "I'm a little teapot, get me out of here.") - this.fs.readFile + ctx.fs.readFile .withArgs('/foo/main.tex') .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory") - this.fs.readFile + ctx.fs.readFile .withArgs('/foo/c.tex') .callsArgWith(2, null, 'Tomato, tomahto.') - this.fs.readFile + ctx.fs.readFile .withArgs('/foo/a/a.tex') .callsArgWith(2, null, 'Potato? Potahto. Potootee!') - this.documentclassContent = '% test\n\\documentclass\n% test' + ctx.documentclassContent = '% test\n\\documentclass\n% test' }) describe('when there is a file in a subfolder', function () { - beforeEach(function () { + beforeEach(function (ctx) { // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references - this.globbyFiles.splice( + ctx.globbyFiles.splice( 0, - this.globbyFiles.length, + ctx.globbyFiles.length, 'c.tex', 'a.tex', 'a/a.tex', @@ -186,90 +207,90 @@ describe('ProjectRootDocManager', function () { ) }) - it('processes the root folder files first, and then the subfolder, in alphabetical order', async function () { + it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) { const { path } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('a.tex') sinon.assert.callOrder( - this.fs.readFile.withArgs('/foo/a.tex'), - this.fs.readFile.withArgs('/foo/b.tex'), - this.fs.readFile.withArgs('/foo/c.tex'), - this.fs.readFile.withArgs('/foo/a/a.tex') + ctx.fs.readFile.withArgs('/foo/a.tex'), + ctx.fs.readFile.withArgs('/foo/b.tex'), + ctx.fs.readFile.withArgs('/foo/c.tex'), + ctx.fs.readFile.withArgs('/foo/a/a.tex') ) }) - it('processes smaller files first', async function () { - this.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 }) + it('processes smaller files first', async function (ctx) { + ctx.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 }) const { path } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('c.tex') sinon.assert.callOrder( - this.fs.readFile.withArgs('/foo/c.tex'), - this.fs.readFile.withArgs('/foo/a.tex'), - this.fs.readFile.withArgs('/foo/b.tex'), - this.fs.readFile.withArgs('/foo/a/a.tex') + ctx.fs.readFile.withArgs('/foo/c.tex'), + ctx.fs.readFile.withArgs('/foo/a.tex'), + ctx.fs.readFile.withArgs('/foo/b.tex'), + ctx.fs.readFile.withArgs('/foo/a/a.tex') ) }) }) describe('when main.tex contains a documentclass', function () { - beforeEach(function () { - this.fs.readFile + beforeEach(function (ctx) { + ctx.fs.readFile .withArgs('/foo/main.tex') - .callsArgWith(2, null, this.documentclassContent) + .callsArgWith(2, null, ctx.documentclassContent) }) - it('returns main.tex', async function () { + it('returns main.tex', async function (ctx) { const { path, content } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('main.tex') - expect(content).to.equal(this.documentclassContent) + expect(content).to.equal(ctx.documentclassContent) }) - it('processes main.text first and stops processing when it finds the content', async function () { - await this.ProjectRootDocManager.findRootDocFileFromDirectory('/foo') - expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(this.fs.readFile).not.to.be.calledWith('/foo/a.tex') + it('processes main.text first and stops processing when it finds the content', async function (ctx) { + await ctx.ProjectRootDocManager.findRootDocFileFromDirectory('/foo') + expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.readFile).not.to.be.calledWith('/foo/a.tex') }) }) describe('when main.tex does not contain a line starting with \\documentclass', function () { - beforeEach(function () { - this.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo') - this.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo') - this.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo') - this.fs.readFile + beforeEach(function (ctx) { + ctx.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo') + ctx.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo') + ctx.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo') + ctx.fs.readFile .withArgs('/foo/nested/chapter1a.tex') .callsArgWith(2, null, 'foo') }) - it('returns the first .tex file from the root folder', async function () { - this.globbyFiles.splice( + it('returns the first .tex file from the root folder', async function (ctx) { + ctx.globbyFiles.splice( 0, - this.globbyFiles.length, + ctx.globbyFiles.length, 'a.tex', 'z.tex', 'nested/chapter1a.tex' ) const { path, content } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('a.tex') expect(content).to.equal('foo') }) - it('returns main.tex file from the root folder', async function () { - this.globbyFiles.splice( + it('returns main.tex file from the root folder', async function (ctx) { + ctx.globbyFiles.splice( 0, - this.globbyFiles.length, + ctx.globbyFiles.length, 'a.tex', 'z.tex', 'main.tex', @@ -277,7 +298,7 @@ describe('ProjectRootDocManager', function () { ) const { path, content } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('main.tex') @@ -286,60 +307,60 @@ describe('ProjectRootDocManager', function () { }) describe('when a.tex contains a documentclass', function () { - beforeEach(function () { - this.fs.readFile + beforeEach(function (ctx) { + ctx.fs.readFile .withArgs('/foo/a.tex') - .callsArgWith(2, null, this.documentclassContent) + .callsArgWith(2, null, ctx.documentclassContent) }) - it('returns a.tex', async function () { + it('returns a.tex', async function (ctx) { const { path, content } = - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) expect(path).to.equal('a.tex') - expect(content).to.equal(this.documentclassContent) + expect(content).to.equal(ctx.documentclassContent) }) - it('processes main.text first and stops processing when it finds the content', async function () { - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + it('processes main.text first and stops processing when it finds the content', async function (ctx) { + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) - expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(this.fs.readFile).to.be.calledWith('/foo/a.tex') - expect(this.fs.readFile).not.to.be.calledWith('/foo/b.tex') + expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex') + expect(ctx.fs.readFile).not.to.be.calledWith('/foo/b.tex') }) }) describe('when there is no documentclass', function () { - it('returns with no error', async function () { - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + it('returns with no error', async function (ctx) { + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) }) - it('processes all the files', async function () { - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + it('processes all the files', async function (ctx) { + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) - expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') - expect(this.fs.readFile).to.be.calledWith('/foo/a.tex') - expect(this.fs.readFile).to.be.calledWith('/foo/b.tex') + expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex') + expect(ctx.fs.readFile).to.be.calledWith('/foo/b.tex') }) }) describe('when there is an error reading a file', function () { - beforeEach(function () { - this.fs.readFile + beforeEach(function (ctx) { + ctx.fs.readFile .withArgs('/foo/a.tex') .callsArgWith(2, new Error('something went wrong')) }) - it('returns an error', async function () { + it('returns an error', async function (ctx) { let error try { - await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory( + await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory( '/foo' ) } catch (err) { @@ -353,206 +374,196 @@ describe('ProjectRootDocManager', function () { describe('setRootDocFromName', function () { describe('when there is a suitable root doc', function () { - beforeEach(async function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + beforeEach(async function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, this.docPaths) - this.ProjectEntityUpdateHandler.setRootDoc = sinon - .stub() - .callsArgWith(2) - await this.ProjectRootDocManager.promises.setRootDocFromName( - this.project_id, + .callsArgWith(1, null, ctx.docPaths) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + await ctx.ProjectRootDocManager.promises.setRootDocFromName( + ctx.project_id, '/main.tex' ) }) - it('should check the docs of the project', function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById - .calledWith(this.project_id) + it('should check the docs of the project', function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(ctx.project_id) .should.equal(true) }) - it('should set the root doc to main.tex', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId2.toString()) + it('should set the root doc to main.tex', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) }) describe('when there is a suitable root doc but the leading slash is missing', function () { - beforeEach(async function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + beforeEach(async function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, this.docPaths) - this.ProjectEntityUpdateHandler.setRootDoc = sinon - .stub() - .callsArgWith(2) - await this.ProjectRootDocManager.promises.setRootDocFromName( - this.project_id, + .callsArgWith(1, null, ctx.docPaths) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + await ctx.ProjectRootDocManager.promises.setRootDocFromName( + ctx.project_id, 'main.tex' ) }) - it('should check the docs of the project', function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById - .calledWith(this.project_id) + it('should check the docs of the project', function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(ctx.project_id) .should.equal(true) }) - it('should set the root doc to main.tex', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId2.toString()) + it('should set the root doc to main.tex', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) }) describe('when there is a suitable root doc with a basename match', function () { - beforeEach(async function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + beforeEach(async function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, this.docPaths) - this.ProjectEntityUpdateHandler.setRootDoc = sinon - .stub() - .callsArgWith(2) - await this.ProjectRootDocManager.promises.setRootDocFromName( - this.project_id, + .callsArgWith(1, null, ctx.docPaths) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + await ctx.ProjectRootDocManager.promises.setRootDocFromName( + ctx.project_id, 'chapter1a.tex' ) }) - it('should check the docs of the project', function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById - .calledWith(this.project_id) + it('should check the docs of the project', function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(ctx.project_id) .should.equal(true) }) - it('should set the root doc using the basename', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId3.toString()) + it('should set the root doc using the basename', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId3.toString()) .should.equal(true) }) }) describe('when there is a suitable root doc but the filename is in quotes', function () { - beforeEach(async function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + beforeEach(async function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, this.docPaths) - this.ProjectEntityUpdateHandler.setRootDoc = sinon - .stub() - .callsArgWith(2) - await this.ProjectRootDocManager.promises.setRootDocFromName( - this.project_id, + .callsArgWith(1, null, ctx.docPaths) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + await ctx.ProjectRootDocManager.promises.setRootDocFromName( + ctx.project_id, "'main.tex'" ) }) - it('should check the docs of the project', function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById - .calledWith(this.project_id) + it('should check the docs of the project', function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(ctx.project_id) .should.equal(true) }) - it('should set the root doc to main.tex', function () { - this.ProjectEntityUpdateHandler.setRootDoc - .calledWith(this.project_id, this.docId2.toString()) + it('should set the root doc to main.tex', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc + .calledWith(ctx.project_id, ctx.docId2.toString()) .should.equal(true) }) }) describe('when there is no suitable root doc', function () { - beforeEach(async function () { - this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + beforeEach(async function (ctx) { + ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon .stub() - .callsArgWith(1, null, this.docPaths) - this.ProjectEntityUpdateHandler.setRootDoc = sinon - .stub() - .callsArgWith(2) - await this.ProjectRootDocManager.promises.setRootDocFromName( - this.project_id, + .callsArgWith(1, null, ctx.docPaths) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + await ctx.ProjectRootDocManager.promises.setRootDocFromName( + ctx.project_id, 'other.tex' ) }) - it('should not set the root doc', function () { - this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) + it('should not set the root doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false) }) }) }) describe('ensureRootDocumentIsSet', function () { - beforeEach(function () { - this.project = {} - this.ProjectGetter.getProject = sinon + beforeEach(function (ctx) { + ctx.project = {} + ctx.ProjectGetter.getProject = sinon .stub() - .callsArgWith(2, null, this.project) - this.ProjectRootDocManager.setRootDocAutomatically = sinon + .callsArgWith(2, null, ctx.project) + ctx.ProjectRootDocManager.setRootDocAutomatically = sinon .stub() .callsArgWith(1, null) }) describe('when the root doc is set', function () { - beforeEach(function () { - this.project.rootDoc_id = this.docId2 - this.ProjectRootDocManager.ensureRootDocumentIsSet( - this.project_id, - this.callback + beforeEach(function (ctx) { + ctx.project.rootDoc_id = ctx.docId2 + ctx.ProjectRootDocManager.ensureRootDocumentIsSet( + ctx.project_id, + ctx.callback ) }) - it('should find the project fetching only the rootDoc_id field', function () { - this.ProjectGetter.getProject - .calledWith(this.project_id, { rootDoc_id: 1 }) + it('should find the project fetching only the rootDoc_id field', function (ctx) { + ctx.ProjectGetter.getProject + .calledWith(ctx.project_id, { rootDoc_id: 1 }) .should.equal(true) }) - it('should not try to update the project rootDoc_id', function () { - this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + it('should not try to update the project rootDoc_id', function (ctx) { + ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( false ) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('when the root doc is not set', function () { - beforeEach(function () { - this.ProjectRootDocManager.ensureRootDocumentIsSet( - this.project_id, - this.callback + beforeEach(function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsSet( + ctx.project_id, + ctx.callback ) }) - it('should find the project with only the rootDoc_id field', function () { - this.ProjectGetter.getProject - .calledWith(this.project_id, { rootDoc_id: 1 }) + it('should find the project with only the rootDoc_id field', function (ctx) { + ctx.ProjectGetter.getProject + .calledWith(ctx.project_id, { rootDoc_id: 1 }) .should.equal(true) }) - it('should update the project rootDoc_id', function () { - this.ProjectRootDocManager.setRootDocAutomatically - .calledWith(this.project_id) + it('should update the project rootDoc_id', function (ctx) { + ctx.ProjectRootDocManager.setRootDocAutomatically + .calledWith(ctx.project_id) .should.equal(true) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('when the project does not exist', function () { - beforeEach(function () { - this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - this.ProjectRootDocManager.ensureRootDocumentIsSet( - this.project_id, - this.callback + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) + ctx.ProjectRootDocManager.ensureRootDocumentIsSet( + ctx.project_id, + ctx.callback ) }) - it('should call the callback with an error', function () { - this.callback + it('should call the callback with an error', function (ctx) { + ctx.callback .calledWith( sinon.match .instanceOf(Error) @@ -564,125 +575,125 @@ describe('ProjectRootDocManager', function () { }) describe('ensureRootDocumentIsValid', function () { - beforeEach(function () { - this.project = {} - this.ProjectGetter.getProject = sinon + beforeEach(function (ctx) { + ctx.project = {} + ctx.ProjectGetter.getProject = sinon .stub() - .callsArgWith(2, null, this.project) - this.ProjectGetter.getProjectWithoutDocLines = sinon + .callsArgWith(2, null, ctx.project) + ctx.ProjectGetter.getProjectWithoutDocLines = sinon .stub() - .callsArgWith(1, null, this.project) - this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() - this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() - this.ProjectRootDocManager.setRootDocAutomatically = sinon + .callsArgWith(1, null, ctx.project) + ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() + ctx.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + ctx.ProjectRootDocManager.setRootDocAutomatically = sinon .stub() .callsArgWith(1, null) }) describe('when the root doc is set', function () { describe('when the root doc is valid', function () { - beforeEach(function () { - this.project.rootDoc_id = this.docId2 - this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon + beforeEach(function (ctx) { + ctx.project.rootDoc_id = ctx.docId2 + ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon .stub() - .callsArgWith(2, null, this.docPaths[this.docId2]) - this.ProjectRootDocManager.ensureRootDocumentIsValid( - this.project_id, - this.callback + .callsArgWith(2, null, ctx.docPaths[ctx.docId2]) + ctx.ProjectRootDocManager.ensureRootDocumentIsValid( + ctx.project_id, + ctx.callback ) }) - it('should find the project without doc lines', function () { - this.ProjectGetter.getProjectWithoutDocLines - .calledWith(this.project_id) + it('should find the project without doc lines', function (ctx) { + ctx.ProjectGetter.getProjectWithoutDocLines + .calledWith(ctx.project_id) .should.equal(true) }) - it('should not try to update the project rootDoc_id', function () { - this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + it('should not try to update the project rootDoc_id', function (ctx) { + ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( false ) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('when the root doc is not valid', function () { - beforeEach(function () { - this.project.rootDoc_id = new ObjectId() - this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon + beforeEach(function (ctx) { + ctx.project.rootDoc_id = new ObjectId() + ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon .stub() .callsArgWith(2, null, null) - this.ProjectRootDocManager.ensureRootDocumentIsValid( - this.project_id, - this.callback + ctx.ProjectRootDocManager.ensureRootDocumentIsValid( + ctx.project_id, + ctx.callback ) }) - it('should find the project without doc lines', function () { - this.ProjectGetter.getProjectWithoutDocLines - .calledWith(this.project_id) + it('should find the project without doc lines', function (ctx) { + ctx.ProjectGetter.getProjectWithoutDocLines + .calledWith(ctx.project_id) .should.equal(true) }) - it('should unset the root doc', function () { - this.ProjectEntityUpdateHandler.unsetRootDoc - .calledWith(this.project_id) + it('should unset the root doc', function (ctx) { + ctx.ProjectEntityUpdateHandler.unsetRootDoc + .calledWith(ctx.project_id) .should.equal(true) }) - it('should try to find a new rootDoc', function () { - this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + it('should try to find a new rootDoc', function (ctx) { + ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( true ) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) }) describe('when the root doc is not set', function () { - beforeEach(function () { - this.ProjectRootDocManager.ensureRootDocumentIsValid( - this.project_id, - this.callback + beforeEach(function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsValid( + ctx.project_id, + ctx.callback ) }) - it('should find the project without doc lines', function () { - this.ProjectGetter.getProjectWithoutDocLines - .calledWith(this.project_id) + it('should find the project without doc lines', function (ctx) { + ctx.ProjectGetter.getProjectWithoutDocLines + .calledWith(ctx.project_id) .should.equal(true) }) - it('should update the project rootDoc_id', function () { - this.ProjectRootDocManager.setRootDocAutomatically - .calledWith(this.project_id) + it('should update the project rootDoc_id', function (ctx) { + ctx.ProjectRootDocManager.setRootDocAutomatically + .calledWith(ctx.project_id) .should.equal(true) }) - it('should call the callback', function () { - this.callback.called.should.equal(true) + it('should call the callback', function (ctx) { + ctx.callback.called.should.equal(true) }) }) describe('when the project does not exist', function () { - beforeEach(function () { - this.ProjectGetter.getProjectWithoutDocLines = sinon + beforeEach(function (ctx) { + ctx.ProjectGetter.getProjectWithoutDocLines = sinon .stub() .callsArgWith(1, null, null) - this.ProjectRootDocManager.ensureRootDocumentIsValid( - this.project_id, - this.callback + ctx.ProjectRootDocManager.ensureRootDocumentIsValid( + ctx.project_id, + ctx.callback ) }) - it('should call the callback with an error', function () { - this.callback + it('should call the callback with an error', function (ctx) { + ctx.callback .calledWith( sinon.match .instanceOf(Error) diff --git a/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs b/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs index 96de680e82..4c98509782 100644 --- a/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs +++ b/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs @@ -1,37 +1,34 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = require('path').join( - __dirname, +import { vi, expect } from 'vitest' +import sinon from 'sinon' +const modulePath = '../../../../app/src/Features/Subscription/LimitationsManager' -) describe('LimitationsManager', function () { - beforeEach(function () { - this.user = { - _id: (this.userId = 'user-id'), + beforeEach(async function (ctx) { + ctx.user = { + _id: (ctx.userId = 'user-id'), features: { collaborators: 1 }, } - this.project = { - _id: (this.projectId = 'project-id'), - owner_ref: this.userId, + ctx.project = { + _id: (ctx.projectId = 'project-id'), + owner_ref: ctx.userId, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProject: sinon.stub().callsFake(async (projectId, fields) => { - if (projectId === this.projectId) { - return this.project + if (projectId === ctx.projectId) { + return ctx.project } else { return null } }), }, } - 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 } @@ -39,7 +36,7 @@ describe('LimitationsManager', function () { }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves(), getSubscription: sinon.stub().resolves(), @@ -47,161 +44,194 @@ describe('LimitationsManager', function () { }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getInvitedEditCollaboratorCount: sinon.stub().resolves(0), getMemberIdPrivilegeLevel: sinon.stub(), }, } - this.CollaboratorsInviteGetter = { + ctx.CollaboratorsInviteGetter = { promises: { getEditInviteCount: sinon.stub().resolves(0), }, } - this.LimitationsManager = SandboxedModule.require(modulePath, { - requires: { - '../Project/ProjectGetter': this.ProjectGetter, - '../User/UserGetter': this.UserGetter, - './SubscriptionLocator': this.SubscriptionLocator, - '@overleaf/settings': (this.Settings = {}), - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - '../Collaborators/CollaboratorsInviteGetter': - this.CollaboratorsInviteGetter, - './V1SubscriptionManager': this.V1SubscriptionManager, - }, - }) + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.Settings = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter', + () => ({ + default: ctx.CollaboratorsInviteGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/V1SubscriptionManager', + () => ({ + default: ctx.V1SubscriptionManager, + }) + ) + + ctx.LimitationsManager = (await import(modulePath)).default }) describe('allowedNumberOfCollaboratorsInProject', function () { describe('when the project is owned by a user without a subscription', function () { - beforeEach(function () { - this.Settings.defaultFeatures = { collaborators: 23 } - this.project.owner_ref = this.userId - delete this.user.features + beforeEach(function (ctx) { + ctx.Settings.defaultFeatures = { collaborators: 23 } + ctx.project.owner_ref = ctx.userId + delete ctx.user.features }) - it('should return the default number', async function () { + it('should return the default number', async function (ctx) { const result = - await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject( - this.projectId + await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject( + ctx.projectId ) - expect(result).to.equal(this.Settings.defaultFeatures.collaborators) + expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators) }) }) describe('when the project is owned by a user with a subscription', function () { - beforeEach(function () { - this.project.owner_ref = this.userId - this.user.features = { collaborators: 21 } + beforeEach(function (ctx) { + ctx.project.owner_ref = ctx.userId + ctx.user.features = { collaborators: 21 } }) - it('should return the number of collaborators the user is allowed', async function () { + it('should return the number of collaborators the user is allowed', async function (ctx) { const result = - await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject( - this.projectId + await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject( + ctx.projectId ) - expect(result).to.equal(this.user.features.collaborators) + expect(result).to.equal(ctx.user.features.collaborators) }) }) }) describe('allowedNumberOfCollaboratorsForUser', function () { describe('when the user has no features', function () { - beforeEach(function () { - this.Settings.defaultFeatures = { collaborators: 23 } - delete this.user.features + beforeEach(function (ctx) { + ctx.Settings.defaultFeatures = { collaborators: 23 } + delete ctx.user.features }) - it('should return the default number', async function () { + it('should return the default number', async function (ctx) { const result = - await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser( - this.userId + await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser( + ctx.userId ) - expect(result).to.equal(this.Settings.defaultFeatures.collaborators) + expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators) }) }) describe('when the user has features', function () { - beforeEach(async function () { - this.user.features = { collaborators: 21 } - this.result = - await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser( - this.userId + beforeEach(async function (ctx) { + ctx.user.features = { collaborators: 21 } + ctx.result = + await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser( + ctx.userId ) }) - it('should return the number of collaborators the user is allowed', function () { - expect(this.result).to.equal(this.user.features.collaborators) + it('should return the number of collaborators the user is allowed', function (ctx) { + expect(ctx.result).to.equal(ctx.user.features.collaborators) }) }) }) describe('canAcceptEditCollaboratorInvite', function () { describe('when the project has fewer collaborators than allowed', function () { - beforeEach(function () { - this.current_number = 1 - this.user.features.collaborators = 2 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) + beforeEach(function (ctx) { + ctx.current_number = 1 + ctx.user.features.collaborators = 2 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon + .stub() + .resolves(ctx.current_number) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const result = - await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite( - this.projectId + await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite( + ctx.projectId ) expect(result).to.be.true }) }) describe('when accepting the invite would exceed the collaborator limit', function () { - beforeEach(function () { - this.current_number = 2 - this.user.features.collaborators = 2 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) + beforeEach(function (ctx) { + ctx.current_number = 2 + ctx.user.features.collaborators = 2 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon + .stub() + .resolves(ctx.current_number) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite( - this.projectId + await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite( + ctx.projectId ) expect(result).to.be.false }) }) describe('when the project has more collaborators than allowed', function () { - beforeEach(function () { - this.current_number = 3 - this.user.features.collaborators = 2 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) + beforeEach(function (ctx) { + ctx.current_number = 3 + ctx.user.features.collaborators = 2 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon + .stub() + .resolves(ctx.current_number) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite( - this.projectId + await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite( + ctx.projectId ) expect(result).to.be.false }) }) describe('when the project has infinite collaborators', function () { - beforeEach(function () { - this.current_number = 100 - this.user.features.collaborators = -1 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) + beforeEach(function (ctx) { + ctx.current_number = 100 + ctx.user.features.collaborators = -1 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon + .stub() + .resolves(ctx.current_number) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const result = - await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite( - this.projectId + await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite( + ctx.projectId ) expect(result).to.be.true }) @@ -210,21 +240,22 @@ describe('LimitationsManager', function () { describe('canAddXEditCollaborators', function () { describe('when the project has fewer collaborators than allowed', function () { - beforeEach(function () { - this.current_number = 1 - this.user.features.collaborators = 2 - this.invite_count = 0 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 1 + ctx.user.features.collaborators = 2 + ctx.invite_count = 0 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.true @@ -232,21 +263,22 @@ describe('LimitationsManager', function () { }) describe('when the project has fewer collaborators and invites than allowed', function () { - beforeEach(function () { - this.current_number = 1 - this.user.features.collaborators = 4 - this.invite_count = 1 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 1 + ctx.user.features.collaborators = 4 + ctx.invite_count = 1 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.true @@ -254,21 +286,22 @@ describe('LimitationsManager', function () { }) describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () { - beforeEach(function () { - this.current_number = 1 - this.user.features.collaborators = 2 - this.invite_count = 0 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 1 + ctx.user.features.collaborators = 2 + ctx.invite_count = 0 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 2 ) expect(result).to.be.false @@ -276,21 +309,22 @@ describe('LimitationsManager', function () { }) describe('when the project has more collaborators than allowed', function () { - beforeEach(function () { - this.current_number = 3 - this.user.features.collaborators = 2 - this.invite_count = 0 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 3 + ctx.user.features.collaborators = 2 + ctx.invite_count = 0 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.false @@ -298,21 +332,22 @@ describe('LimitationsManager', function () { }) describe('when the project has infinite collaborators', function () { - beforeEach(function () { - this.current_number = 100 - this.user.features.collaborators = -1 - this.invite_count = 0 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 100 + ctx.user.features.collaborators = -1 + ctx.invite_count = 0 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.true @@ -320,21 +355,22 @@ describe('LimitationsManager', function () { }) describe('when the project has more invites than allowed', function () { - beforeEach(function () { - this.current_number = 0 - this.user.features.collaborators = 2 - this.invite_count = 2 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 0 + ctx.user.features.collaborators = 2 + ctx.invite_count = 2 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.false @@ -342,21 +378,22 @@ describe('LimitationsManager', function () { }) describe('when the project has more invites and collaborators than allowed', function () { - beforeEach(function () { - this.current_number = 1 - this.user.features.collaborators = 2 - this.invite_count = 1 - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = - sinon.stub().resolves(this.current_number) - this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + beforeEach(function (ctx) { + ctx.current_number = 1 + ctx.user.features.collaborators = 2 + ctx.invite_count = 1 + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon .stub() - .resolves(this.invite_count) + .resolves(ctx.current_number) + ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon + .stub() + .resolves(ctx.invite_count) }) - it('should return false', async function () { + it('should return false', async function (ctx) { const result = - await this.LimitationsManager.promises.canAddXEditCollaborators( - this.projectId, + await ctx.LimitationsManager.promises.canAddXEditCollaborators( + ctx.projectId, 1 ) expect(result).to.be.false @@ -365,19 +402,19 @@ describe('LimitationsManager', function () { }) describe('canChangeCollaboratorPrivilegeLevel', function () { - beforeEach(function () { - this.collaboratorId = 'collaborator-id' - this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves( + beforeEach(function (ctx) { + ctx.collaboratorId = 'collaborator-id' + ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves( 'readOnly' ) }) describe("when the limit hasn't been reached", function () { - it('accepts changing a viewer to an editor', async function () { + it('accepts changing a viewer to an editor', async function (ctx) { const result = - await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( - this.projectId, - this.collaboratorId, + await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( + ctx.projectId, + ctx.collaboratorId, 'readAndWrite' ) expect(result).to.be.true @@ -385,30 +422,30 @@ describe('LimitationsManager', function () { }) describe('when the limit has been reached', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves( + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves( 1 ) }) - it('accepts changing a reviewer to an editor', async function () { - this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves( + it('accepts changing a reviewer to an editor', async function (ctx) { + ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves( 'review' ) const result = - await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( - this.projectId, - this.collaboratorId, + await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( + ctx.projectId, + ctx.collaboratorId, 'readAndWrite' ) expect(result).to.be.true }) - it('rejects changing a viewer to a reviewer', async function () { + it('rejects changing a viewer to a reviewer', async function (ctx) { const result = - await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( - this.projectId, - this.collaboratorId, + await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel( + ctx.projectId, + ctx.collaboratorId, 'review' ) expect(result).to.be.false @@ -417,25 +454,25 @@ describe('LimitationsManager', function () { }) describe('userHasSubscription', function () { - beforeEach(function () { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + beforeEach(function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves() }) - it('should return true if the recurly token is set', async function () { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + it('should return true if the recurly token is set', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves({ recurlySubscription_id: '1234', }) const { hasSubscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(hasSubscription).to.be.true }) - it('should return true if the paymentProvider field is set', async function () { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + it('should return true if the paymentProvider field is set', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves({ paymentProvider: { @@ -443,80 +480,80 @@ describe('LimitationsManager', function () { }, }) const { hasSubscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(hasSubscription).to.be.true }) - it('should return false if the recurly token is not set', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({}) + it('should return false if the recurly token is not set', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({}) const { hasSubscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(hasSubscription).to.be.false }) - it('should return false if the subscription is undefined', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.resolves() + it('should return false if the subscription is undefined', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves() const { hasSubscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(hasSubscription).to.be.false }) - it('should return the subscription', async function () { + it('should return the subscription', async function (ctx) { const stubbedSubscription = { freeTrial: {}, token: '' } - this.SubscriptionLocator.promises.getUsersSubscription.resolves( + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves( stubbedSubscription ) const { subscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(subscription).to.deep.equal(stubbedSubscription) }) describe('when user has a custom account', function () { - beforeEach(function () { - this.fakeSubscription = { customAccount: true } - this.SubscriptionLocator.promises.getUsersSubscription.resolves( - this.fakeSubscription + beforeEach(function (ctx) { + ctx.fakeSubscription = { customAccount: true } + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves( + ctx.fakeSubscription ) }) - it('should return true', async function () { + it('should return true', async function (ctx) { const { hasSubscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) expect(hasSubscription).to.be.true }) - it('should return the subscription', async function () { + it('should return the subscription', async function (ctx) { const { subscription } = - await this.LimitationsManager.promises.userHasSubscription(this.user) - expect(subscription).to.deep.equal(this.fakeSubscription) + await ctx.LimitationsManager.promises.userHasSubscription(ctx.user) + expect(subscription).to.deep.equal(ctx.fakeSubscription) }) }) }) describe('userIsMemberOfGroupSubscription', function () { - beforeEach(function () { - this.SubscriptionLocator.promises.getMemberSubscriptions = sinon + beforeEach(function (ctx) { + ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon .stub() .resolves() }) - it('should return false if there are no groups subcriptions', async function () { - this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([]) + it('should return false if there are no groups subcriptions', async function (ctx) { + ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([]) const { isMember } = - await this.LimitationsManager.promises.userIsMemberOfGroupSubscription( - this.user + await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription( + ctx.user ) expect(isMember).to.be.false }) - it('should return true if there are no groups subcriptions', async function () { + it('should return true if there are no groups subcriptions', async function (ctx) { const subscriptions = ['mock-subscription'] - this.SubscriptionLocator.promises.getMemberSubscriptions.resolves( + ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves( subscriptions ) const { isMember, subscriptions: retSubscriptions } = - await this.LimitationsManager.promises.userIsMemberOfGroupSubscription( - this.user + await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription( + ctx.user ) expect(isMember).to.be.true expect(retSubscriptions).to.deep.equal(subscriptions) @@ -524,52 +561,52 @@ describe('LimitationsManager', function () { }) describe('hasPaidSubscription', function () { - beforeEach(function () { - this.SubscriptionLocator.promises.getMemberSubscriptions = sinon + beforeEach(function (ctx) { + ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon .stub() .resolves([]) - this.SubscriptionLocator.promises.getUsersSubscription = sinon + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves(null) }) - it('should return true if userIsMemberOfGroupSubscription', async function () { - this.SubscriptionLocator.promises.getMemberSubscriptions = sinon + it('should return true if userIsMemberOfGroupSubscription', async function (ctx) { + ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon .stub() .resolves([{ _id: '123' }]) const { hasPaidSubscription } = - await this.LimitationsManager.promises.hasPaidSubscription(this.user) + await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user) expect(hasPaidSubscription).to.be.true }) - it('should return true if userHasSubscription', async function () { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + it('should return true if userHasSubscription', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves({ recurlySubscription_id: '123' }) const { hasPaidSubscription } = - await this.LimitationsManager.promises.hasPaidSubscription(this.user) + await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user) expect(hasPaidSubscription).to.be.true }) - it('should return false if none are true', async function () { + it('should return false if none are true', async function (ctx) { const { hasPaidSubscription } = - await this.LimitationsManager.promises.hasPaidSubscription(this.user) + await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user) expect(hasPaidSubscription).to.be.false }) - it('should have userHasSubscriptionOrIsGroupMember alias', async function () { + it('should have userHasSubscriptionOrIsGroupMember alias', async function (ctx) { const { hasPaidSubscription } = - await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember( - this.user + await ctx.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember( + ctx.user ) expect(hasPaidSubscription).to.be.false }) }) describe('hasGroupMembersLimitReached', function () { - beforeEach(function () { - this.subscriptionId = '12312' - this.subscription = { + beforeEach(function (ctx) { + ctx.subscriptionId = '12312' + ctx.subscription = { membersLimit: 3, member_ids: ['', ''], teamInvites: [ @@ -578,37 +615,37 @@ describe('LimitationsManager', function () { } }) - it('should return true if the limit is hit (including members and invites)', async function () { - this.SubscriptionLocator.promises.getSubscription.resolves( - this.subscription + it('should return true if the limit is hit (including members and invites)', async function (ctx) { + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription ) const { limitReached } = - await this.LimitationsManager.promises.hasGroupMembersLimitReached( - this.subscriptionId + await ctx.LimitationsManager.promises.hasGroupMembersLimitReached( + ctx.subscriptionId ) expect(limitReached).to.be.true }) - it('should return false if the limit is not hit (including members and invites)', async function () { - this.subscription.membersLimit = 4 - this.SubscriptionLocator.promises.getSubscription.resolves( - this.subscription + it('should return false if the limit is not hit (including members and invites)', async function (ctx) { + ctx.subscription.membersLimit = 4 + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription ) const { limitReached } = - await this.LimitationsManager.promises.hasGroupMembersLimitReached( - this.subscriptionId + await ctx.LimitationsManager.promises.hasGroupMembersLimitReached( + ctx.subscriptionId ) expect(limitReached).to.be.false }) - it('should return true if the limit has been exceded (including members and invites)', async function () { - this.subscription.membersLimit = 2 - this.SubscriptionLocator.promises.getSubscription.resolves( - this.subscription + it('should return true if the limit has been exceded (including members and invites)', async function (ctx) { + ctx.subscription.membersLimit = 2 + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription ) const { limitReached } = - await this.LimitationsManager.promises.hasGroupMembersLimitReached( - this.subscriptionId + await ctx.LimitationsManager.promises.hasGroupMembersLimitReached( + ctx.subscriptionId ) expect(limitReached).to.be.true }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs index c7b6b3f0a0..1dbe825783 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs @@ -1,11 +1,7 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const chai = require('chai') -const { expect } = chai -const { - PaymentProviderSubscription, -} = require('../../../../app/src/Features/Subscription/PaymentProviderEntities') -const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper') +import { expect, vi } from 'vitest' +import sinon from 'sinon' +import { PaymentProviderSubscription } from '../../../../app/src/Features/Subscription/PaymentProviderEntities.js' +import SubscriptionHelper from '../../../../app/src/Features/Subscription/SubscriptionHelper.js' const MODULE_PATH = '../../../../app/src/Features/Subscription/SubscriptionHandler' @@ -47,8 +43,8 @@ const mockSubscriptionChanges = { } describe('SubscriptionHandler', function () { - beforeEach(function () { - this.Settings = { + beforeEach(async function (ctx) { + ctx.Settings = { plans: [ { planCode: 'collaborator', @@ -69,49 +65,49 @@ describe('SubscriptionHandler', function () { versioning: false, }, } - this.activeRecurlySubscription = + ctx.activeRecurlySubscription = mockRecurlySubscriptions['subscription-123-active'] - this.activeRecurlyClientSubscription = + ctx.activeRecurlyClientSubscription = mockRecurlyClientSubscriptions['subscription-123-active'] - this.activeRecurlySubscriptionChange = + ctx.activeRecurlySubscriptionChange = mockSubscriptionChanges['subscription-123-active'] - this.User = {} - this.user = { _id: (this.user_id = 'user_id_here_') } - this.subscription = { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + ctx.User = {} + ctx.user = { _id: (ctx.user_id = 'user_id_here_') } + ctx.subscription = { + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, } - this.RecurlyWrapper = { + ctx.RecurlyWrapper = { promises: { - getSubscription: sinon.stub().resolves(this.activeRecurlySubscription), + getSubscription: sinon.stub().resolves(ctx.activeRecurlySubscription), redeemCoupon: sinon.stub().resolves(), createSubscription: sinon .stub() - .resolves(this.activeRecurlySubscription), + .resolves(ctx.activeRecurlySubscription), getBillingInfo: sinon.stub().resolves(), getAccountPastDueInvoices: sinon.stub().resolves(), attemptInvoiceCollection: sinon.stub().resolves(), listAccountActiveSubscriptions: sinon.stub().resolves([]), }, } - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { reactivateSubscriptionByUuid: sinon .stub() - .resolves(this.activeRecurlyClientSubscription), + .resolves(ctx.activeRecurlyClientSubscription), cancelSubscriptionByUuid: sinon.stub().resolves(), applySubscriptionChangeRequest: sinon .stub() - .resolves(this.activeRecurlySubscriptionChange), + .resolves(ctx.activeRecurlySubscriptionChange), getSubscription: sinon .stub() - .resolves(this.activeRecurlyClientSubscription), + .resolves(ctx.activeRecurlyClientSubscription), pauseSubscriptionByUuid: sinon.stub().resolves(), resumeSubscriptionByUuid: sinon.stub().resolves(), failInvoice: sinon.stub(), getPastDueInvoices: sinon.stub(), }, } - this.SubscriptionUpdater = { + ctx.SubscriptionUpdater = { promises: { updateSubscriptionFromRecurly: sinon.stub().resolves(), syncSubscription: sinon.stub().resolves(), @@ -121,139 +117,189 @@ describe('SubscriptionHandler', function () { }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { userHasSubscription: sinon.stub().resolves(), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { - getUsersSubscription: sinon.stub().resolves(this.subscription), + getUsersSubscription: sinon.stub().resolves(ctx.subscription), }, } - this.EmailHandler = { + ctx.EmailHandler = { sendEmail: sinon.stub(), sendDeferredEmail: sinon.stub(), } - this.UserUpdater = { + ctx.UserUpdater = { promises: { updateUser: sinon.stub().resolves(), }, } - this.SubscriptionHandler = SandboxedModule.require(MODULE_PATH, { - requires: { - './RecurlyWrapper': this.RecurlyWrapper, - './RecurlyClient': this.RecurlyClient, - '@overleaf/settings': this.Settings, - '../../models/User': { - User: this.User, - }, - './SubscriptionHelper': SubscriptionHelper, - './SubscriptionUpdater': this.SubscriptionUpdater, - './SubscriptionLocator': this.SubscriptionLocator, - './LimitationsManager': this.LimitationsManager, - '../Email/EmailHandler': this.EmailHandler, - '../Analytics/AnalyticsManager': this.AnalyticsManager, - '../User/UserUpdater': this.UserUpdater, - '../../infrastructure/Modules': (this.Modules = { - promises: { - hooks: { - fire: sinon.stub(), - }, + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyWrapper', + () => ({ + default: ctx.RecurlyWrapper, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionHelper', + () => ({ + default: SubscriptionHelper, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionUpdater', + () => ({ + default: ctx.SubscriptionUpdater, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: ctx.UserUpdater, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { + promises: { + hooks: { + fire: sinon.stub(), }, - }), - }, - }) + }, + }), + })) + + ctx.SubscriptionHandler = (await import(MODULE_PATH)).default }) describe('createSubscription', function () { - beforeEach(function () { - this.subscriptionDetails = { + beforeEach(function (ctx) { + ctx.subscriptionDetails = { cvv: '123', number: '12345', } - this.recurlyTokenIds = { billing: '45555666' } + ctx.recurlyTokenIds = { billing: '45555666' } }) describe('successfully', function () { - beforeEach(async function () { - await this.SubscriptionHandler.promises.createSubscription( - this.user, - this.subscriptionDetails, - this.recurlyTokenIds + beforeEach(async function (ctx) { + await ctx.SubscriptionHandler.promises.createSubscription( + ctx.user, + ctx.subscriptionDetails, + ctx.recurlyTokenIds ) }) - it('should create the subscription with the wrapper', function () { - this.RecurlyWrapper.promises.createSubscription - .calledWith(this.user, this.subscriptionDetails, this.recurlyTokenIds) + it('should create the subscription with the wrapper', function (ctx) { + ctx.RecurlyWrapper.promises.createSubscription + .calledWith(ctx.user, ctx.subscriptionDetails, ctx.recurlyTokenIds) .should.equal(true) }) - it('should sync the subscription to the user', function () { - this.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( + it('should sync the subscription to the user', function (ctx) { + ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( true ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( - this.activeRecurlySubscription + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( + ctx.activeRecurlySubscription ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( - this.user._id + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( + ctx.user._id ) }) - it('should not set last trial date if not a trial/the trial_started_at is not set', function () { - this.UserUpdater.promises.updateUser.should.not.have.been.called + it('should not set last trial date if not a trial/the trial_started_at is not set', function (ctx) { + ctx.UserUpdater.promises.updateUser.should.not.have.been.called }) }) describe('when the subscription is a trial and has a trial_started_at date', function () { - beforeEach(async function () { - this.activeRecurlySubscription.trial_started_at = + beforeEach(async function (ctx) { + ctx.activeRecurlySubscription.trial_started_at = '2024-01-01T09:58:35.531+00:00' - await this.SubscriptionHandler.promises.createSubscription( - this.user, - this.subscriptionDetails, - this.recurlyTokenIds + await ctx.SubscriptionHandler.promises.createSubscription( + ctx.user, + ctx.subscriptionDetails, + ctx.recurlyTokenIds ) }) - it('should set the users lastTrial date', function () { - this.UserUpdater.promises.updateUser.should.have.been.calledOnce - expect(this.UserUpdater.promises.updateUser.args[0][0]).to.deep.equal({ - _id: this.user_id, + it('should set the users lastTrial date', function (ctx) { + ctx.UserUpdater.promises.updateUser.should.have.been.calledOnce + expect(ctx.UserUpdater.promises.updateUser.args[0][0]).to.deep.equal({ + _id: ctx.user_id, lastTrial: { $not: { - $gt: new Date(this.activeRecurlySubscription.trial_started_at), + $gt: new Date(ctx.activeRecurlySubscription.trial_started_at), }, }, }) - expect(this.UserUpdater.promises.updateUser.args[0][1]).to.deep.equal({ + expect(ctx.UserUpdater.promises.updateUser.args[0][1]).to.deep.equal({ $set: { - lastTrial: new Date( - this.activeRecurlySubscription.trial_started_at - ), + lastTrial: new Date(ctx.activeRecurlySubscription.trial_started_at), }, }) }) }) describe('when there is already a subscription in Recurly', function () { - beforeEach(function () { - this.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ - this.subscription, + beforeEach(function (ctx) { + ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ + ctx.subscription, ]) }) - it('should an error', function () { + it('should an error', function (ctx) { expect( - this.SubscriptionHandler.promises.createSubscription( - this.user, - this.subscriptionDetails, - this.recurlyTokenIds + ctx.SubscriptionHandler.promises.createSubscription( + ctx.user, + ctx.subscriptionDetails, + ctx.recurlyTokenIds ) ).to.be.rejectedWith('user already has subscription in recurly') }) @@ -261,26 +307,26 @@ describe('SubscriptionHandler', function () { }) describe('updateSubscription', function () { - beforeEach(function () { - this.user.id = this.activeRecurlySubscription.account.account_code - this.User.findById = (userId, projection) => ({ + beforeEach(function (ctx) { + ctx.user.id = ctx.activeRecurlySubscription.account.account_code + ctx.User.findById = (userId, projection) => ({ exec: () => { - userId.should.equal(this.user.id) - return Promise.resolve(this.user) + userId.should.equal(ctx.user.id) + return Promise.resolve(ctx.user) }, }) }) - it('should not fire updatePaidSubscription hook if user has no subscription', async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + it('should not fire updatePaidSubscription hook if user has no subscription', async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: null, }) - await this.SubscriptionHandler.promises.updateSubscription( - this.user, - this.plan_code + await ctx.SubscriptionHandler.promises.updateSubscription( + ctx.user, + ctx.plan_code ) - expect(this.Modules.promises.hooks.fire).to.not.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'updatePaidSubscription', sinon.match.any, sinon.match.any, @@ -288,16 +334,16 @@ describe('SubscriptionHandler', function () { ) }) - it('should not fire updatePaidSubscription hook if user has custom subscription', async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + it('should not fire updatePaidSubscription hook if user has custom subscription', async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { customAccount: true }, }) - await this.SubscriptionHandler.promises.updateSubscription( - this.user, - this.plan_code + await ctx.SubscriptionHandler.promises.updateSubscription( + ctx.user, + ctx.plan_code ) - expect(this.Modules.promises.hooks.fire).to.not.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'updatePaidSubscription', sinon.match.any, sinon.match.any, @@ -305,104 +351,104 @@ describe('SubscriptionHandler', function () { ) }) - it('should fire updatePaidSubscription to update a valid subscription', async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + it('should fire updatePaidSubscription to update a valid subscription', async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.updateSubscription( - this.user, - this.plan_code + await ctx.SubscriptionHandler.promises.updateSubscription( + ctx.user, + ctx.plan_code ) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'updatePaidSubscription', - this.subscription, - this.plan_code, - this.user._id + ctx.subscription, + ctx.plan_code, + ctx.user._id ) }) }) describe('cancelPendingSubscriptionChange', function () { - beforeEach(function () { - this.user.id = this.activeRecurlySubscription.account.account_code - this.User.findById = (userId, projection) => ({ + beforeEach(function (ctx) { + ctx.user.id = ctx.activeRecurlySubscription.account.account_code + ctx.User.findById = (userId, projection) => ({ exec: () => { - userId.should.equal(this.user.id) - return Promise.resolve(this.user) + userId.should.equal(ctx.user.id) + return Promise.resolve(ctx.user) }, }) }) - it('should not fire cancelPendingPaidSubscriptionChange hook if user has no subscription', async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + it('should not fire cancelPendingPaidSubscriptionChange hook if user has no subscription', async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: null, }) - await this.SubscriptionHandler.promises.cancelPendingSubscriptionChange( - this.user, - this.plan_code + await ctx.SubscriptionHandler.promises.cancelPendingSubscriptionChange( + ctx.user, + ctx.plan_code ) - expect(this.Modules.promises.hooks.fire).to.not.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'cancelPendingPaidSubscriptionChange', sinon.match.any ) }) - it('should fire cancelPendingPaidSubscriptionChange to update a valid subscription', async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + it('should fire cancelPendingPaidSubscriptionChange to update a valid subscription', async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.cancelPendingSubscriptionChange( - this.user, - this.plan_code + await ctx.SubscriptionHandler.promises.cancelPendingSubscriptionChange( + ctx.user, + ctx.plan_code ) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cancelPendingPaidSubscriptionChange', - this.subscription + ctx.subscription ) }) }) describe('cancelSubscription', function () { describe('with a user without a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.cancelSubscription(this.user) + await ctx.SubscriptionHandler.promises.cancelSubscription(ctx.user) }) - it('should redirect to the subscription dashboard', function () { - this.RecurlyClient.promises.cancelSubscriptionByUuid.called.should.equal( + it('should redirect to the subscription dashboard', function (ctx) { + ctx.RecurlyClient.promises.cancelSubscriptionByUuid.called.should.equal( false ) }) }) describe('with a user with a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.cancelSubscription(this.user) + await ctx.SubscriptionHandler.promises.cancelSubscription(ctx.user) }) - it('should cancel the subscription', function () { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + it('should cancel the subscription', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cancelPaidSubscription', - this.subscription + ctx.subscription ) }) - it('should send the email after 1 hour', function () { + it('should send the email after 1 hour', function (ctx) { const ONE_HOUR_IN_MS = 1000 * 60 * 60 - expect(this.EmailHandler.sendDeferredEmail).to.have.been.calledWith( + expect(ctx.EmailHandler.sendDeferredEmail).to.have.been.calledWith( 'canceledSubscription', - { to: this.user.email, first_name: this.user.first_name }, + { to: ctx.user.email, first_name: ctx.user.first_name }, ONE_HOUR_IN_MS ) }) @@ -411,40 +457,40 @@ describe('SubscriptionHandler', function () { describe('resumeSubscription', function () { describe('for a user without a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, - subscription: this.subscription, + subscription: ctx.subscription, }) }) - it('should not make a resume call to recurly', async function () { + it('should not make a resume call to recurly', async function (ctx) { expect( - this.SubscriptionHandler.promises.resumeSubscription(this.user) + ctx.SubscriptionHandler.promises.resumeSubscription(ctx.user) ).to.be.rejectedWith('No active subscription to resume') - this.RecurlyClient.promises.resumeSubscriptionByUuid.called.should.equal( + ctx.RecurlyClient.promises.resumeSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', }, }) }) - it('should call resume hook', async function () { - await this.SubscriptionHandler.promises.resumeSubscription(this.user) + it('should call resume hook', async function (ctx) { + await ctx.SubscriptionHandler.promises.resumeSubscription(ctx.user) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'resumePaidSubscription', { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', } @@ -455,62 +501,62 @@ describe('SubscriptionHandler', function () { describe('pauseSubscription', function () { describe('for a user without a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, - subscription: this.subscription, + subscription: ctx.subscription, }) }) - it('should not make a pause call to recurly', async function () { + it('should not make a pause call to recurly', async function (ctx) { expect( - this.SubscriptionHandler.promises.pauseSubscription(this.user, 3) + ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('No active subscription to pause') - this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( + ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with an annual subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator-annual', }, }) }) - it('should not make a pause call to recurly', async function () { + it('should not make a pause call to recurly', async function (ctx) { expect( - this.SubscriptionHandler.promises.pauseSubscription(this.user, 3) + ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Can only pause monthly individual plans') - this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( + ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: [], }, }) }) - it('should call pause hook', async function () { - await this.SubscriptionHandler.promises.pauseSubscription(this.user, 3) + it('should call pause hook', async function (ctx) { + await ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'pausePaidSubscription', { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: [], @@ -521,11 +567,11 @@ describe('SubscriptionHandler', function () { }) describe('for a user in a trial', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'trial', trialEndsAt: Date.now() + 1000000, @@ -534,33 +580,33 @@ describe('SubscriptionHandler', function () { }, }) }) - it('should not make a pause call to recurly', async function () { + it('should not make a pause call to recurly', async function (ctx) { expect( - this.SubscriptionHandler.promises.pauseSubscription(this.user, 3) + ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Cannot pause a subscription in a trial') - this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( + ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with addons', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { - recurlySubscription_id: this.activeRecurlySubscription.uuid, + recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: ['mock-addon'], }, }) }) - it('should not make a pause call to recurly', async function () { + it('should not make a pause call to recurly', async function (ctx) { expect( - this.SubscriptionHandler.promises.pauseSubscription(this.user, 3) + ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Cannot pause a subscription with addons') - this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( + ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) @@ -569,48 +615,44 @@ describe('SubscriptionHandler', function () { describe('reactivateSubscription', function () { describe('with a user without a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.reactivateSubscription( - this.user - ) + await ctx.SubscriptionHandler.promises.reactivateSubscription(ctx.user) }) - it('should redirect to the subscription dashboard', function () { - this.RecurlyClient.promises.reactivateSubscriptionByUuid.called.should.equal( + it('should redirect to the subscription dashboard', function (ctx) { + ctx.RecurlyClient.promises.reactivateSubscriptionByUuid.called.should.equal( false ) }) - it('should not send a notification email', function () { - sinon.assert.notCalled(this.EmailHandler.sendEmail) + it('should not send a notification email', function (ctx) { + sinon.assert.notCalled(ctx.EmailHandler.sendEmail) }) }) describe('with a user with a subscription', function () { - beforeEach(async function () { - this.LimitationsManager.promises.userHasSubscription.resolves({ + beforeEach(async function (ctx) { + ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, - subscription: this.subscription, + subscription: ctx.subscription, }) - await this.SubscriptionHandler.promises.reactivateSubscription( - this.user - ) + await ctx.SubscriptionHandler.promises.reactivateSubscription(ctx.user) }) - it('should reactivate the subscription', function () { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + it('should reactivate the subscription', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'reactivatePaidSubscription', - this.subscription + ctx.subscription ) }) - it('should send a notification email', function () { + it('should send a notification email', function (ctx) { sinon.assert.calledWith( - this.EmailHandler.sendEmail, + ctx.EmailHandler.sendEmail, 'reactivatedSubscription' ) }) @@ -619,42 +661,42 @@ describe('SubscriptionHandler', function () { describe('syncSubscription', function () { describe('with an actionable request', function () { - beforeEach(async function () { - this.user.id = this.activeRecurlySubscription.account.account_code + beforeEach(async function (ctx) { + ctx.user.id = ctx.activeRecurlySubscription.account.account_code - this.User.findById = (userId, projection) => ({ + ctx.User.findById = (userId, projection) => ({ exec: () => { - userId.should.equal(this.user.id) - return Promise.resolve(this.user) + userId.should.equal(ctx.user.id) + return Promise.resolve(ctx.user) }, }) - await this.SubscriptionHandler.promises.syncSubscription( - this.activeRecurlySubscription, + await ctx.SubscriptionHandler.promises.syncSubscription( + ctx.activeRecurlySubscription, {} ) }) - it('should request the affected subscription from the API', function () { - this.RecurlyWrapper.promises.getSubscription - .calledWith(this.activeRecurlySubscription.uuid) + it('should request the affected subscription from the API', function (ctx) { + ctx.RecurlyWrapper.promises.getSubscription + .calledWith(ctx.activeRecurlySubscription.uuid) .should.equal(true) }) - it('should request the account details of the subscription', function () { - const options = this.RecurlyWrapper.promises.getSubscription.args[0][1] + it('should request the account details of the subscription', function (ctx) { + const options = ctx.RecurlyWrapper.promises.getSubscription.args[0][1] options.includeAccount.should.equal(true) }) - it('should sync the subscription to the user', function () { - this.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( + it('should sync the subscription to the user', function (ctx) { + ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( true ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( - this.activeRecurlySubscription + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( + ctx.activeRecurlySubscription ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( - this.user._id + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( + ctx.user._id ) }) }) @@ -662,52 +704,52 @@ describe('SubscriptionHandler', function () { describe('attemptPaypalInvoiceCollection', function () { describe('for credit card users', function () { - beforeEach(async function () { - this.RecurlyWrapper.promises.getBillingInfo.resolves({ + beforeEach(async function (ctx) { + ctx.RecurlyWrapper.promises.getBillingInfo.resolves({ paypal_billing_agreement_id: null, }) - await this.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( - this.activeRecurlySubscription.account.account_code + await ctx.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( + ctx.activeRecurlySubscription.account.account_code ) }) - it('gets billing infos', function () { + it('gets billing infos', function (ctx) { sinon.assert.calledWith( - this.RecurlyWrapper.promises.getBillingInfo, - this.activeRecurlySubscription.account.account_code + ctx.RecurlyWrapper.promises.getBillingInfo, + ctx.activeRecurlySubscription.account.account_code ) }) - it('skips user', function () { + it('skips user', function (ctx) { sinon.assert.notCalled( - this.RecurlyWrapper.promises.getAccountPastDueInvoices + ctx.RecurlyWrapper.promises.getAccountPastDueInvoices ) }) }) describe('for paypal users', function () { - beforeEach(async function () { - this.RecurlyWrapper.promises.getBillingInfo.resolves({ + beforeEach(async function (ctx) { + ctx.RecurlyWrapper.promises.getBillingInfo.resolves({ paypal_billing_agreement_id: 'mock-billing-agreement', }) - this.RecurlyWrapper.promises.getAccountPastDueInvoices.resolves([ + ctx.RecurlyWrapper.promises.getAccountPastDueInvoices.resolves([ { invoice_number: 'mock-invoice-number' }, ]) - await this.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( - this.activeRecurlySubscription.account.account_code + await ctx.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( + ctx.activeRecurlySubscription.account.account_code ) }) - it('gets past due invoices', function () { + it('gets past due invoices', function (ctx) { sinon.assert.calledWith( - this.RecurlyWrapper.promises.getAccountPastDueInvoices, - this.activeRecurlySubscription.account.account_code + ctx.RecurlyWrapper.promises.getAccountPastDueInvoices, + ctx.activeRecurlySubscription.account.account_code ) }) - it('calls attemptInvoiceCollection', function () { + it('calls attemptInvoiceCollection', function (ctx) { sinon.assert.calledWith( - this.RecurlyWrapper.promises.attemptInvoiceCollection, + ctx.RecurlyWrapper.promises.attemptInvoiceCollection, 'mock-invoice-number' ) }) @@ -716,159 +758,159 @@ describe('SubscriptionHandler', function () { describe('validateNoSubscriptionInRecurly', function () { describe('with a subscription in recurly', function () { - beforeEach(async function () { - this.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ - this.subscription, + beforeEach(async function (ctx) { + ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ + ctx.subscription, ]) - this.isValid = - await this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( - this.user_id + ctx.isValid = + await ctx.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( + ctx.user_id ) }) - it('should call RecurlyWrapper.promises.listAccountActiveSubscriptions with the user id', function () { - this.RecurlyWrapper.promises.listAccountActiveSubscriptions - .calledWith(this.user_id) + it('should call RecurlyWrapper.promises.listAccountActiveSubscriptions with the user id', function (ctx) { + ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions + .calledWith(ctx.user_id) .should.equal(true) }) - it('should sync the subscription', function () { - this.SubscriptionUpdater.promises.syncSubscription - .calledWith(this.subscription, this.user_id) + it('should sync the subscription', function (ctx) { + ctx.SubscriptionUpdater.promises.syncSubscription + .calledWith(ctx.subscription, ctx.user_id) .should.equal(true) }) - it('should return false', function () { - expect(this.isValid).to.equal(false) + it('should return false', function (ctx) { + expect(ctx.isValid).to.equal(false) }) }) describe('with no subscription in recurly', function () { - beforeEach(async function () { - this.isValid = - await this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( - this.user_id + beforeEach(async function (ctx) { + ctx.isValid = + await ctx.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( + ctx.user_id ) }) - it('should be rejected and not sync the subscription', function () { - this.SubscriptionUpdater.promises.syncSubscription.called.should.equal( + it('should be rejected and not sync the subscription', function (ctx) { + ctx.SubscriptionUpdater.promises.syncSubscription.called.should.equal( false ) }) - it('should return true', function () { - expect(this.isValid).to.equal(true) + it('should return true', function (ctx) { + expect(ctx.isValid).to.equal(true) }) }) }) describe('revertPlanChange', function () { describe('with correct invoices', function () { - beforeEach(async function () { - this.subscriptionRestorePoint = { + beforeEach(async function (ctx) { + ctx.subscriptionRestorePoint = { planCode: 'collaborator', addOns: [ { addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 }, ], _id: 'restore-point-id', } - this.pastDueInvoice = { + ctx.pastDueInvoice = { id: 'invoice-123', dueAt: new Date(), collectionMethod: 'automatic', } - this.user.id = this.activeRecurlySubscription.account.account_code - this.User.findById = (userId, projection) => ({ + ctx.user.id = ctx.activeRecurlySubscription.account.account_code + ctx.User.findById = (userId, projection) => ({ exec: () => { - userId.should.equal(this.user.id) - return Promise.resolve(this.user) + userId.should.equal(ctx.user.id) + return Promise.resolve(ctx.user) }, }) - this.RecurlyClient.promises.getSubscription.resolves( - this.activeRecurlyClientSubscription + ctx.RecurlyClient.promises.getSubscription.resolves( + ctx.activeRecurlyClientSubscription ) - this.RecurlyClient.promises.getPastDueInvoices.resolves([ - this.pastDueInvoice, + ctx.RecurlyClient.promises.getPastDueInvoices.resolves([ + ctx.pastDueInvoice, ]) - this.RecurlyClient.promises.failInvoice.resolves() - this.SubscriptionUpdater.promises.setSubscriptionWasReverted.resolves() - this.RecurlyClient.promises.applySubscriptionChangeRequest.resolves() + ctx.RecurlyClient.promises.failInvoice.resolves() + ctx.SubscriptionUpdater.promises.setSubscriptionWasReverted.resolves() + ctx.RecurlyClient.promises.applySubscriptionChangeRequest.resolves() - await this.SubscriptionHandler.promises.revertPlanChange( - this.activeRecurlyClientSubscription.id, - this.subscriptionRestorePoint + await ctx.SubscriptionHandler.promises.revertPlanChange( + ctx.activeRecurlyClientSubscription.id, + ctx.subscriptionRestorePoint ) }) - it('should fetch the subscription from recurly', async function () { + it('should fetch the subscription from recurly', async function (ctx) { expect( - this.RecurlyClient.promises.getSubscription.calledWith( - this.activeRecurlyClientSubscription.id + ctx.RecurlyClient.promises.getSubscription.calledWith( + ctx.activeRecurlyClientSubscription.id ) ).to.be.true }) - it('should fail the invoice', async function () { + it('should fail the invoice', async function (ctx) { expect( - this.RecurlyClient.promises.failInvoice.calledWith( - this.pastDueInvoice.id + ctx.RecurlyClient.promises.failInvoice.calledWith( + ctx.pastDueInvoice.id ) ).to.be.true }) - it('should call setSubscriptionWasReverted', async function () { + it('should call setSubscriptionWasReverted', async function (ctx) { expect( - this.SubscriptionUpdater.promises.setSubscriptionWasReverted.calledWith( - this.subscriptionRestorePoint._id + ctx.SubscriptionUpdater.promises.setSubscriptionWasReverted.calledWith( + ctx.subscriptionRestorePoint._id ) ).to.be.true }) - it('should sync the subscription', async function () { - this.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( + it('should sync the subscription', async function (ctx) { + ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( true ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( - this.activeRecurlySubscription + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( + ctx.activeRecurlySubscription ) - this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( - this.user._id + ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( + ctx.user._id ) }) }) describe('should throw an IndeterminateInvoiceError when', function () { - beforeEach(function () { - this.subscriptionRestorePoint = { + beforeEach(function (ctx) { + ctx.subscriptionRestorePoint = { planCode: 'collaborator', addOns: [ { addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 }, ], _id: 'restore-point-id', } - this.RecurlyClient.promises.getSubscription.resolves( - this.activeRecurlyClientSubscription + ctx.RecurlyClient.promises.getSubscription.resolves( + ctx.activeRecurlyClientSubscription ) }) - it('finds a past due invoice older than 24 hours', async function () { + it('finds a past due invoice older than 24 hours', async function (ctx) { const oldInvoice = { id: 'invoice-123', dueAt: new Date(Date.now() - 25 * 60 * 60 * 1000), // 25 hours ago collectionMethod: 'automatic', } - this.RecurlyClient.promises.getPastDueInvoices.resolves([oldInvoice]) + ctx.RecurlyClient.promises.getPastDueInvoices.resolves([oldInvoice]) await expect( - this.SubscriptionHandler.promises.revertPlanChange( - this.activeRecurlyClientSubscription.id, - this.subscriptionRestorePoint + ctx.SubscriptionHandler.promises.revertPlanChange( + ctx.activeRecurlyClientSubscription.id, + ctx.subscriptionRestorePoint ) ).to.be.rejectedWith('cant determine invoice to fail for plan revert') }) - it('finds more than one past due invoice', async function () { + it('finds more than one past due invoice', async function (ctx) { const invoices = [ { id: 'invoice-123', @@ -881,28 +923,28 @@ describe('SubscriptionHandler', function () { collectionMethod: 'automatic', }, ] - this.RecurlyClient.promises.getPastDueInvoices.resolves(invoices) + ctx.RecurlyClient.promises.getPastDueInvoices.resolves(invoices) await expect( - this.SubscriptionHandler.promises.revertPlanChange( - this.activeRecurlyClientSubscription.id, - this.subscriptionRestorePoint + ctx.SubscriptionHandler.promises.revertPlanChange( + ctx.activeRecurlyClientSubscription.id, + ctx.subscriptionRestorePoint ) ).to.be.rejectedWith('cant determine invoice to fail for plan revert') }) - it('finds an invoice with a collectionMethod other than automatic', async function () { + it('finds an invoice with a collectionMethod other than automatic', async function (ctx) { const manualInvoice = { id: 'invoice-123', dueAt: new Date(), collectionMethod: 'manual', } - this.RecurlyClient.promises.getPastDueInvoices.resolves([manualInvoice]) + ctx.RecurlyClient.promises.getPastDueInvoices.resolves([manualInvoice]) await expect( - this.SubscriptionHandler.promises.revertPlanChange( - this.activeRecurlyClientSubscription.id, - this.subscriptionRestorePoint + ctx.SubscriptionHandler.promises.revertPlanChange( + ctx.activeRecurlyClientSubscription.id, + ctx.subscriptionRestorePoint ) ).to.be.rejectedWith('cant determine invoice to fail for plan revert') }) diff --git a/services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs b/services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs index eec4d0d7a1..5b29772134 100644 --- a/services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs +++ b/services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs @@ -1,15 +1,20 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') +import { vi, expect } from 'vitest' +import sinon from 'sinon' + +import mongodb from 'mongodb-legacy' +import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/Subscription/TeamInvitesHandler' -const { ObjectId } = require('mongodb-legacy') -const Errors = require('../../../../app/src/Features/Errors/Errors') +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + +const { ObjectId } = mongodb describe('TeamInvitesHandler', function () { - beforeEach(function () { - this.manager = { + beforeEach(async function (ctx) { + ctx.manager = { _id: '666666', first_name: 'Daenerys', last_name: 'Targaryen', @@ -17,34 +22,34 @@ describe('TeamInvitesHandler', function () { emails: [{ email: 'daenerys@example.com' }], } - this.token = 'aaaaaaaaaaaaaaaaaaaaaa' + ctx.token = 'aaaaaaaaaaaaaaaaaaaaaa' - this.teamInvite = { + ctx.teamInvite = { email: 'jorah@example.com', - token: this.token, + token: ctx.token, } // ensure teamInvite can be converted from Document to Object - this.teamInvite.toObject = () => this.teamInvite + ctx.teamInvite.toObject = () => ctx.teamInvite - this.subscription = { + ctx.subscription = { id: '55153a8014829a865bbf700d', _id: new ObjectId('55153a8014829a865bbf700d'), recurlySubscription_id: '1a2b3c4d5e6f7g', - admin_id: this.manager._id, + admin_id: ctx.manager._id, groupPlan: true, member_ids: [], - teamInvites: [this.teamInvite], + teamInvites: [ctx.teamInvite], save: sinon.stub().resolves(), } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub(), - getSubscription: sinon.stub().resolves(this.subscription), + getSubscription: sinon.stub().resolves(ctx.subscription), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(), getUserByAnyEmail: sinon.stub().resolves(), @@ -52,55 +57,55 @@ describe('TeamInvitesHandler', function () { }, } - this.SubscriptionUpdater = { + ctx.SubscriptionUpdater = { promises: { addUserToGroup: sinon.stub().resolves(), deleteSubscription: sinon.stub().resolves(), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { teamHasReachedMemberLimit: sinon.stub().returns(false), } - this.Subscription = { + ctx.Subscription = { findOne: sinon.stub().resolves(), updateOne: sinon.stub().resolves(), } - this.SSOConfig = { + ctx.SSOConfig = { findById: sinon.stub().resolves(), } - this.EmailHandler = { + ctx.EmailHandler = { promises: { sendEmail: sinon.stub().resolves(null), }, } - this.newToken = 'bbbbbbbbb' + ctx.newToken = 'bbbbbbbbb' - this.crypto = { + ctx.crypto = { randomBytes: () => { - return { toString: sinon.stub().returns(this.newToken) } + return { toString: sinon.stub().returns(ctx.newToken) } }, } - this.UserGetter.promises.getUser - .withArgs(this.manager._id) - .resolves(this.manager) - this.UserGetter.promises.getUserByAnyEmail - .withArgs(this.manager.email) - .resolves(this.manager) - this.UserGetter.promises.getUserByMainEmail - .withArgs(this.manager.email) - .resolves(this.manager) + ctx.UserGetter.promises.getUser + .withArgs(ctx.manager._id) + .resolves(ctx.manager) + ctx.UserGetter.promises.getUserByAnyEmail + .withArgs(ctx.manager.email) + .resolves(ctx.manager) + ctx.UserGetter.promises.getUserByMainEmail + .withArgs(ctx.manager.email) + .resolves(ctx.manager) - this.SubscriptionLocator.promises.getUsersSubscription.resolves( - this.subscription + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves( + ctx.subscription ) - this.NotificationsBuilder = { + ctx.NotificationsBuilder = { promises: { groupInvitation: sinon.stub().returns({ create: sinon.stub().resolves(), @@ -109,51 +114,105 @@ describe('TeamInvitesHandler', function () { }, } - this.Subscription.findOne.resolves(this.subscription) + ctx.Subscription.findOne.resolves(ctx.subscription) - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { terminateSubscriptionByUuid: sinon.stub().resolves(), }, } - this.TeamInvitesHandler = SandboxedModule.require(modulePath, { - requires: { - 'mongodb-legacy': { ObjectId }, - crypto: this.crypto, - '@overleaf/settings': { siteUrl: 'http://example.com' }, - '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) }, - '../../models/Subscription': { Subscription: this.Subscription }, - '../../models/SSOConfig': { SSOConfig: this.SSOConfig }, - '../User/UserGetter': this.UserGetter, - './SubscriptionLocator': this.SubscriptionLocator, - './SubscriptionUpdater': this.SubscriptionUpdater, - './LimitationsManager': this.LimitationsManager, - '../Email/EmailHandler': this.EmailHandler, - '../Notifications/NotificationsBuilder': this.NotificationsBuilder, - '../../infrastructure/Modules': (this.Modules = { - promises: { hooks: { fire: sinon.stub().resolves() } }, - }), - './RecurlyClient': this.RecurlyClient, - }, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('crypto', () => ({ + default: ctx.crypto, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: { siteUrl: 'http://example.com' }, + })) + + vi.doMock('../../../../app/src/models/TeamInvite', () => ({ + TeamInvite: (ctx.TeamInvite = {}), + })) + + vi.doMock('../../../../app/src/models/Subscription', () => ({ + Subscription: ctx.Subscription, + })) + + vi.doMock('../../../../app/src/models/SSOConfig', () => ({ + SSOConfig: ctx.SSOConfig, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionUpdater', + () => ({ + default: ctx.SubscriptionUpdater, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { + promises: { hooks: { fire: sinon.stub().resolves() } }, + }), + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + ctx.TeamInvitesHandler = (await import(modulePath)).default }) describe('getInvite', function () { - it("returns the invite if there's one", async function () { + it("returns the invite if there's one", async function (ctx) { const { invite, subscription } = - await this.TeamInvitesHandler.promises.getInvite(this.token) + await ctx.TeamInvitesHandler.promises.getInvite(ctx.token) - expect(invite).to.deep.eq(this.teamInvite) - expect(subscription).to.deep.eq(this.subscription) + expect(invite).to.deep.eq(ctx.teamInvite) + expect(subscription).to.deep.eq(ctx.subscription) }) - it("returns teamNotFound if there's none", async function () { - this.Subscription.findOne = sinon.stub().resolves(null) + it("returns teamNotFound if there's none", async function (ctx) { + ctx.Subscription.findOne = sinon.stub().resolves(null) let error try { - await this.TeamInvitesHandler.promises.getInvite(this.token) + await ctx.TeamInvitesHandler.promises.getInvite(ctx.token) } catch (err) { error = err } @@ -163,66 +222,66 @@ describe('TeamInvitesHandler', function () { }) describe('createInvite', function () { - it('adds the team invite to the subscription', async function () { - const invite = await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + it('adds the team invite to the subscription', async function (ctx) { + const invite = await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) - expect(invite.token).to.eq(this.newToken) + expect(invite.token).to.eq(ctx.newToken) expect(invite.email).to.eq('john.snow@example.com') expect(invite.inviterName).to.eq( 'Daenerys Targaryen (daenerys@example.com)' ) expect(invite.invite).to.be.true - expect(this.subscription.teamInvites).to.deep.include(invite) + expect(ctx.subscription.teamInvites).to.deep.include(invite) }) - it('sends an email', async function () { - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + it('sends an email', async function (ctx) { + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) - this.EmailHandler.promises.sendEmail + ctx.EmailHandler.promises.sendEmail .calledWith( 'verifyEmailToJoinTeam', sinon.match({ to: 'john.snow@example.com', - inviter: this.manager, - acceptInviteUrl: `http://example.com/subscription/invites/${this.newToken}/`, + inviter: ctx.manager, + acceptInviteUrl: `http://example.com/subscription/invites/${ctx.newToken}/`, }) ) .should.equal(true) }) - it('refreshes the existing invite if the email has already been invited', async function () { - const originalInvite = Object.assign({}, this.teamInvite) + it('refreshes the existing invite if the email has already been invited', async function (ctx) { + const originalInvite = Object.assign({}, ctx.teamInvite) - const invite = await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + const invite = await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, originalInvite.email ) expect(invite).to.exist - expect(this.subscription.teamInvites.length).to.eq(1) - expect(this.subscription.teamInvites).to.deep.include(invite) + expect(ctx.subscription.teamInvites.length).to.eq(1) + expect(ctx.subscription.teamInvites).to.deep.include(invite) expect(invite.email).to.eq(originalInvite.email) - this.subscription.save.calledOnce.should.eq(true) + ctx.subscription.save.calledOnce.should.eq(true) }) - it('removes any legacy invite from the subscription', async function () { - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + it('removes any legacy invite from the subscription', async function (ctx) { + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) - this.Subscription.updateOne + ctx.Subscription.updateOne .calledWith( { _id: new ObjectId('55153a8014829a865bbf700d') }, { $pull: { invited_emails: 'john.snow@example.com' } } @@ -230,77 +289,77 @@ describe('TeamInvitesHandler', function () { .should.eq(true) }) - it('add user to subscription if inviting self', async function () { - const invite = await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, - this.manager.email + it('add user to subscription if inviting self', async function (ctx) { + const invite = await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, + ctx.manager.email ) sinon.assert.calledWith( - this.SubscriptionUpdater.promises.addUserToGroup, - this.subscription._id, - this.manager._id + ctx.SubscriptionUpdater.promises.addUserToGroup, + ctx.subscription._id, + ctx.manager._id ) - sinon.assert.notCalled(this.subscription.save) + sinon.assert.notCalled(ctx.subscription.save) expect(invite.token).to.not.exist - expect(invite.email).to.eq(this.manager.email) - expect(invite.first_name).to.eq(this.manager.first_name) - expect(invite.last_name).to.eq(this.manager.last_name) + expect(invite.email).to.eq(ctx.manager.email) + expect(invite.first_name).to.eq(ctx.manager.first_name) + expect(invite.last_name).to.eq(ctx.manager.last_name) expect(invite.invite).to.be.false }) - it('sends an SSO invite if SSO is enabled and inviting self', async function () { - this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123') - this.SSOConfig.findById - .withArgs(this.subscription.ssoConfig) + it('sends an SSO invite if SSO is enabled and inviting self', async function (ctx) { + ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123') + ctx.SSOConfig.findById + .withArgs(ctx.subscription.ssoConfig) .resolves({ enabled: true }) - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, - this.manager.email + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, + ctx.manager.email ) sinon.assert.calledWith( - this.Modules.promises.hooks.fire, + ctx.Modules.promises.hooks.fire, 'sendGroupSSOReminder', - this.manager._id, - this.subscription._id + ctx.manager._id, + ctx.subscription._id ) }) - it('does not send an SSO invite if SSO is disabled and inviting self', async function () { - this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123') - this.SSOConfig.findById - .withArgs(this.subscription.ssoConfig) + it('does not send an SSO invite if SSO is disabled and inviting self', async function (ctx) { + ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123') + ctx.SSOConfig.findById + .withArgs(ctx.subscription.ssoConfig) .resolves({ enabled: false }) - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, - this.manager.email + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, + ctx.manager.email ) - sinon.assert.notCalled(this.Modules.promises.hooks.fire) + sinon.assert.notCalled(ctx.Modules.promises.hooks.fire) }) - it('sends a notification if inviting registered user', async function () { + it('sends a notification if inviting registered user', async function (ctx) { const id = new ObjectId('6a6b3a8014829a865bbf700d') const managedUsersEnabled = false - this.UserGetter.promises.getUserByMainEmail + ctx.UserGetter.promises.getUserByMainEmail .withArgs('john.snow@example.com') .resolves({ _id: id, }) - const invite = await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + const invite = await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) - this.NotificationsBuilder.promises + ctx.NotificationsBuilder.promises .groupInvitation( id.toString(), - this.subscription._id, + ctx.subscription._id, managedUsersEnabled ) .create.calledWith(invite) @@ -309,42 +368,42 @@ describe('TeamInvitesHandler', function () { }) describe('importInvite', function () { - beforeEach(function () { - this.sentAt = new Date() + beforeEach(function (ctx) { + ctx.sentAt = new Date() }) - it('can imports an invite from v1', function () { - this.TeamInvitesHandler.importInvite( - this.subscription, + it('can imports an invite from v1', function (ctx) { + ctx.TeamInvitesHandler.importInvite( + ctx.subscription, 'A-Team', 'hannibal@a-team.org', 'secret', - this.sentAt, + ctx.sentAt, error => { expect(error).not.to.exist - this.subscription.save.calledOnce.should.eq(true) + ctx.subscription.save.calledOnce.should.eq(true) - const invite = this.subscription.teamInvites.find( + const invite = ctx.subscription.teamInvites.find( i => i.email === 'hannibal@a-team.org' ) expect(invite.token).to.eq('secret') - expect(invite.sentAt).to.eq(this.sentAt) + expect(invite.sentAt).to.eq(ctx.sentAt) } ) }) }) describe('acceptInvite', function () { - beforeEach(function () { - this.user = { + beforeEach(function (ctx) { + ctx.user = { id: '123456789', first_name: 'Tyrion', last_name: 'Lannister', email: 'tyrion@example.com', } - this.user_subscription = { + ctx.user_subscription = { id: '66264b9125930b976cc0811e', _id: new ObjectId('66264b9125930b976cc0811e'), groupPlan: false, @@ -355,17 +414,17 @@ describe('TeamInvitesHandler', function () { save: sinon.stub().resolves(), } - this.ipAddress = '127.0.0.1' + ctx.ipAddress = '127.0.0.1' - this.UserGetter.promises.getUserByAnyEmail - .withArgs(this.user.email) - .resolves(this.user) + ctx.UserGetter.promises.getUserByAnyEmail + .withArgs(ctx.user.email) + .resolves(ctx.user) - this.SubscriptionLocator.promises.getUsersSubscription - .withArgs(this.user.id) - .resolves(this.user_subscription) + ctx.SubscriptionLocator.promises.getUsersSubscription + .withArgs(ctx.user.id) + .resolves(ctx.user_subscription) - this.subscription.teamInvites.push({ + ctx.subscription.teamInvites.push({ email: 'john.snow@example.com', token: 'dddddddd', inviterName: 'Daenerys Targaryen (daenerys@example.com)', @@ -373,24 +432,24 @@ describe('TeamInvitesHandler', function () { }) describe('with standard group', function () { - it('adds the user to the team', async function () { - await this.TeamInvitesHandler.promises.acceptInvite( + it('adds the user to the team', async function (ctx) { + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) - this.SubscriptionUpdater.promises.addUserToGroup - .calledWith(this.subscription._id, this.user.id) + ctx.SubscriptionUpdater.promises.addUserToGroup + .calledWith(ctx.subscription._id, ctx.user.id) .should.eq(true) }) - it('removes the invite from the subscription', async function () { - await this.TeamInvitesHandler.promises.acceptInvite( + it('removes the invite from the subscription', async function (ctx) { + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) - this.Subscription.updateOne + ctx.Subscription.updateOne .calledWith( { _id: new ObjectId('55153a8014829a865bbf700d') }, { $pull: { teamInvites: { email: 'john.snow@example.com' } } } @@ -398,114 +457,114 @@ describe('TeamInvitesHandler', function () { .should.eq(true) }) - it('removes dashboard notification after they accepted group invitation', async function () { + it('removes dashboard notification after they accepted group invitation', async function (ctx) { const managedUsersEnabled = false - await this.TeamInvitesHandler.promises.acceptInvite( + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) sinon.assert.called( - this.NotificationsBuilder.promises.groupInvitation( - this.user.id, - this.subscription._id, + ctx.NotificationsBuilder.promises.groupInvitation( + ctx.user.id, + ctx.subscription._id, managedUsersEnabled ).read ) }) - it('should not schedule an SSO invite reminder', async function () { - await this.TeamInvitesHandler.promises.acceptInvite( + it('should not schedule an SSO invite reminder', async function (ctx) { + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) - sinon.assert.notCalled(this.Modules.promises.hooks.fire) + sinon.assert.notCalled(ctx.Modules.promises.hooks.fire) }) }) describe('with managed group', function () { - it('should enroll the group member', async function () { - this.subscription.managedUsersEnabled = true + it('should enroll the group member', async function (ctx) { + ctx.subscription.managedUsersEnabled = true - await this.TeamInvitesHandler.promises.acceptInvite( + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) sinon.assert.calledWith( - this.SubscriptionUpdater.promises.deleteSubscription, - this.user_subscription, - { id: this.user.id, ip: this.ipAddress } + ctx.SubscriptionUpdater.promises.deleteSubscription, + ctx.user_subscription, + { id: ctx.user.id, ip: ctx.ipAddress } ) sinon.assert.calledWith( - this.RecurlyClient.promises.terminateSubscriptionByUuid, - this.user_subscription.recurlySubscription_id + ctx.RecurlyClient.promises.terminateSubscriptionByUuid, + ctx.user_subscription.recurlySubscription_id ) sinon.assert.calledWith( - this.Modules.promises.hooks.fire, + ctx.Modules.promises.hooks.fire, 'enrollInManagedSubscription', - this.user.id, - this.subscription + ctx.user.id, + ctx.subscription ) }) - it('should not delete the users subscription if that subscription is also the join target', async function () { - this.subscription.managedUsersEnabled = true - this.SubscriptionLocator.promises.getUsersSubscription - .withArgs(this.user.id) - .resolves(this.subscription) + it('should not delete the users subscription if that subscription is also the join target', async function (ctx) { + ctx.subscription.managedUsersEnabled = true + ctx.SubscriptionLocator.promises.getUsersSubscription + .withArgs(ctx.user.id) + .resolves(ctx.subscription) - await this.TeamInvitesHandler.promises.acceptInvite( + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) sinon.assert.notCalled( - this.SubscriptionUpdater.promises.deleteSubscription + ctx.SubscriptionUpdater.promises.deleteSubscription ) }) }) describe('with group SSO enabled', function () { - it('should schedule an SSO invite reminder', async function () { - this.subscription.ssoConfig = 'ssoconfig1' - this.SSOConfig.findById + it('should schedule an SSO invite reminder', async function (ctx) { + ctx.subscription.ssoConfig = 'ssoconfig1' + ctx.SSOConfig.findById .withArgs('ssoconfig1') .resolves({ enabled: true }) - await this.TeamInvitesHandler.promises.acceptInvite( + await ctx.TeamInvitesHandler.promises.acceptInvite( 'dddddddd', - this.user.id, - this.ipAddress + ctx.user.id, + ctx.ipAddress ) sinon.assert.calledWith( - this.Modules.promises.hooks.fire, + ctx.Modules.promises.hooks.fire, 'scheduleGroupSSOReminder', - this.user.id, - this.subscription._id + ctx.user.id, + ctx.subscription._id ) }) }) }) describe('revokeInvite', function () { - it('removes the team invite from the subscription', async function () { - await this.TeamInvitesHandler.promises.revokeInvite( - this.manager._id, - this.subscription, + it('removes the team invite from the subscription', async function (ctx) { + await ctx.TeamInvitesHandler.promises.revokeInvite( + ctx.manager._id, + ctx.subscription, 'jorah@example.com' ) - this.Subscription.updateOne + ctx.Subscription.updateOne .calledWith( { _id: new ObjectId('55153a8014829a865bbf700d') }, { $pull: { teamInvites: { email: 'jorah@example.com' } } } ) .should.eq(true) - this.Subscription.updateOne + ctx.Subscription.updateOne .calledWith( { _id: new ObjectId('55153a8014829a865bbf700d') }, { $pull: { invited_emails: 'jorah@example.com' } } @@ -513,7 +572,7 @@ describe('TeamInvitesHandler', function () { .should.eq(true) }) - it('removes dashboard notification for pending group invitation', async function () { + it('removes dashboard notification for pending group invitation', async function (ctx) { const managedUsersEnabled = false const pendingUser = { @@ -521,20 +580,20 @@ describe('TeamInvitesHandler', function () { email: 'tyrion@example.com', } - this.UserGetter.promises.getUserByAnyEmail + ctx.UserGetter.promises.getUserByAnyEmail .withArgs(pendingUser.email) .resolves(pendingUser) - await this.TeamInvitesHandler.promises.revokeInvite( - this.manager._id, - this.subscription, + await ctx.TeamInvitesHandler.promises.revokeInvite( + ctx.manager._id, + ctx.subscription, pendingUser.email ) sinon.assert.called( - this.NotificationsBuilder.promises.groupInvitation( + ctx.NotificationsBuilder.promises.groupInvitation( pendingUser.id, - this.subscription._id, + ctx.subscription._id, managedUsersEnabled ).read ) @@ -542,47 +601,47 @@ describe('TeamInvitesHandler', function () { }) describe('createTeamInvitesForLegacyInvitedEmail', function () { - beforeEach(function () { - this.subscription.invited_emails = [ + beforeEach(function (ctx) { + ctx.subscription.invited_emails = [ 'eddard@example.com', 'robert@example.com', ] - this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null) - this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon + ctx.TeamInvitesHandler.createInvite = sinon.stub().resolves(null) + ctx.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon .stub() - .resolves([this.subscription]) + .resolves([ctx.subscription]) }) - it('sends an invitation email to addresses in the legacy invited_emails field', async function () { + it('sends an invitation email to addresses in the legacy invited_emails field', async function (ctx) { const invites = - await this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail( + await ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail( 'eddard@example.com' ) expect(invites.length).to.eq(1) const [invite] = invites - expect(invite.token).to.eq(this.newToken) + expect(invite.token).to.eq(ctx.newToken) expect(invite.email).to.eq('eddard@example.com') expect(invite.inviterName).to.eq( 'Daenerys Targaryen (daenerys@example.com)' ) expect(invite.invite).to.be.true - expect(this.subscription.teamInvites).to.deep.include(invite) + expect(ctx.subscription.teamInvites).to.deep.include(invite) }) }) describe('validation', function () { - it("doesn't create an invite if the team limit has been reached", async function () { - this.LimitationsManager.teamHasReachedMemberLimit = sinon + it("doesn't create an invite if the team limit has been reached", async function (ctx) { + ctx.LimitationsManager.teamHasReachedMemberLimit = sinon .stub() .returns(true) let error try { - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) } catch (err) { @@ -596,14 +655,14 @@ describe('TeamInvitesHandler', function () { }) }) - it("doesn't create an invite if the subscription is not in a group plan", async function () { - this.subscription.groupPlan = false + it("doesn't create an invite if the subscription is not in a group plan", async function (ctx) { + ctx.subscription.groupPlan = false let error try { - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'John.Snow@example.com' ) } catch (err) { @@ -617,24 +676,24 @@ describe('TeamInvitesHandler', function () { }) }) - it("doesn't create an invite if the user is already part of the team", async function () { + it("doesn't create an invite if the user is already part of the team", async function (ctx) { const member = { id: '1a2b', _id: '1a2b', email: 'tyrion@example.com', } - this.subscription.member_ids = [member.id] - this.UserGetter.promises.getUserByAnyEmail + ctx.subscription.member_ids = [member.id] + ctx.UserGetter.promises.getUserByAnyEmail .withArgs(member.email) .resolves(member) let error try { - await this.TeamInvitesHandler.promises.createInvite( - this.manager._id, - this.subscription, + await ctx.TeamInvitesHandler.promises.createInvite( + ctx.manager._id, + ctx.subscription, 'tyrion@example.com' ) } catch (err) { diff --git a/services/web/test/unit/src/Templates/TemplatesManager.test.mjs b/services/web/test/unit/src/Templates/TemplatesManager.test.mjs index 2dcf821a05..83f4eee752 100644 --- a/services/web/test/unit/src/Templates/TemplatesManager.test.mjs +++ b/services/web/test/unit/src/Templates/TemplatesManager.test.mjs @@ -1,236 +1,251 @@ -/* eslint-disable - max-len, - no-return-assign, - no-unused-vars, -*/ -// 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 - */ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { RequestFailedError } = require('@overleaf/fetch-utils') -const { ReadableString } = require('@overleaf/stream-utils') +import { beforeEach, describe, it, vi } from 'vitest' +import sinon from 'sinon' +import { RequestFailedError } from '@overleaf/fetch-utils' +import { ReadableString } from '@overleaf/stream-utils' const modulePath = '../../../../app/src/Features/Templates/TemplatesManager' describe('TemplatesManager', function () { - beforeEach(function () { - this.project_id = 'project-id' - this.brandVariationId = 'brand-variation-id' - this.compiler = 'pdflatex' - this.imageName = 'TL2017' - this.mainFile = 'main.tex' - this.templateId = 'template-id' - this.templateName = 'template name' - this.templateVersionId = 'template-version-id' - this.user_id = 'user-id' - this.dumpPath = `${this.dumpFolder}/${this.uuid}` - this.callback = sinon.stub() - this.pipeline = sinon.stub().callsFake(async (stream, res) => { + beforeEach(async function (ctx) { + ctx.project_id = 'project-id' + ctx.brandVariationId = 'brand-variation-id' + ctx.compiler = 'pdflatex' + ctx.imageName = 'TL2017' + ctx.mainFile = 'main.tex' + ctx.templateId = 'template-id' + ctx.templateName = 'template name' + ctx.templateVersionId = 'template-version-id' + ctx.user_id = 'user-id' + ctx.dumpFolder = 'dump/path' + ctx.uuid = '1234' + ctx.dumpPath = `${ctx.dumpFolder}/${ctx.uuid}` + ctx.callback = sinon.stub() + ctx.pipeline = sinon.stub().callsFake(async (stream, res) => { if (res.callback) res.callback() }) - this.request = sinon.stub().returns({ + ctx.request = sinon.stub().returns({ pipe() {}, on() {}, response: { statusCode: 200, }, }) - this.fs = { + ctx.fs = { promises: { unlink: sinon.stub() }, unlink: sinon.stub(), createWriteStream: sinon.stub().returns({ on: sinon.stub().yields() }), } - this.ProjectUploadManager = { + ctx.ProjectUploadManager = { promises: { createProjectFromZipArchiveWithName: sinon .stub() - .resolves({ _id: this.project_id }), + .resolves({ _id: ctx.project_id }), }, } - this.dumpFolder = 'dump/path' - this.ProjectOptionsHandler = { + ctx.ProjectOptionsHandler = { promises: { setCompiler: sinon.stub().resolves(), setImageName: sinon.stub().resolves(), setBrandVariationId: sinon.stub().resolves(), }, } - this.uuid = '1234' - this.ProjectRootDocManager = { + ctx.ProjectRootDocManager = { promises: { setRootDocFromName: sinon.stub().resolves(), }, } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { getProjectDescription: sinon.stub(), - fixProjectName: sinon.stub().returns(this.templateName), + fixProjectName: sinon.stub().returns(ctx.templateName), } - this.Project = { updateOne: sinon.stub().resolves() } - this.mockStream = new ReadableString('{}') - this.mockResponse = { + ctx.Project = { updateOne: sinon.stub().resolves() } + ctx.mockStream = new ReadableString('{}') + ctx.mockResponse = { status: 200, headers: new Headers({ 'Content-Length': '2', 'Content-Type': 'application/json', }), } - this.FetchUtils = { + ctx.FetchUtils = { fetchJson: sinon.stub(), fetchStreamWithResponse: sinon.stub().resolves({ - stream: this.mockStream, - response: this.mockResponse, + stream: ctx.mockStream, + response: ctx.mockResponse, }), RequestFailedError, } - this.TemplatesManager = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/fetch-utils': this.FetchUtils, - '../Uploads/ProjectUploadManager': this.ProjectUploadManager, - '../Project/ProjectOptionsHandler': this.ProjectOptionsHandler, - '../Project/ProjectRootDocManager': this.ProjectRootDocManager, - '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler, - '../Authentication/SessionManager': (this.SessionManager = { - getLoggedInUserId: sinon.stub(), - }), - '@overleaf/settings': { - path: { - dumpFolder: this.dumpFolder, - }, - siteUrl: (this.siteUrl = 'http://127.0.0.1:3000'), - apis: { - v1: { - url: (this.v1Url = 'http://overleaf.com'), - user: 'overleaf', - pass: 'password', - timeout: 10, - }, - }, - overleaf: { - host: this.v1Url, + vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils) + vi.doMock( + '../../../../app/src/Features/Uploads/ProjectUploadManager', + () => ({ default: ctx.ProjectUploadManager }) + ) + vi.doMock( + '../../../../app/src/Features/Project/ProjectOptionsHandler', + () => ({ default: ctx.ProjectOptionsHandler }) + ) + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ default: ctx.ProjectRootDocManager }) + ) + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ default: ctx.ProjectDetailsHandler }) + ) + + ctx.SessionManager = { + getLoggedInUserId: sinon.stub(), + } + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ default: ctx.SessionManager }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: { + path: { + dumpFolder: ctx.dumpFolder, + }, + siteUrl: (ctx.siteUrl = 'http://127.0.0.1:3000'), + apis: { + v1: { + url: (ctx.v1Url = 'http://overleaf.com'), + user: 'overleaf', + pass: 'password', + timeout: 10, }, }, - crypto: { - randomUUID: () => this.uuid, - }, - request: this.request, - fs: this.fs, - '../../models/Project': { Project: this.Project }, - 'stream/promises': { pipeline: this.pipeline }, - '../Compile/ClsiCacheManager': { - prepareClsiCache: sinon.stub().rejects(new Error('ignore this')), + overleaf: { + host: ctx.v1Url, }, }, - }).promises - return (this.zipUrl = - '%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex') + })) + + vi.doMock('node:crypto', () => ({ + default: { + randomUUID: () => ctx.uuid, + }, + })) + + vi.doMock('node:fs', () => ({ default: ctx.fs })) + + vi.doMock('request', () => ({ default: ctx.request })) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project: ctx.Project, + })) + + vi.doMock('node:stream/promises', () => ({ pipeline: ctx.pipeline })) + + vi.doMock('../../../../app/src/Features/Compile/ClsiCacheManager', () => ({ + default: { + prepareClsiCache: sinon.stub().rejects(new Error('ignore this')), + }, + })) + + ctx.TemplatesManager = (await import(modulePath)).default.promises + ctx.zipUrl = + '%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex' }) describe('createProjectFromV1Template', function () { describe('when all options passed', function () { - beforeEach(function () { - return this.TemplatesManager.createProjectFromV1Template( - this.brandVariationId, - this.compiler, - this.mainFile, - this.templateId, - this.templateName, - this.templateVersionId, - this.user_id, - this.imageName + beforeEach(async function (ctx) { + await ctx.TemplatesManager.createProjectFromV1Template( + ctx.brandVariationId, + ctx.compiler, + ctx.mainFile, + ctx.templateId, + ctx.templateName, + ctx.templateVersionId, + ctx.user_id, + ctx.imageName ) }) - it('should fetch zip from v1 based on template id', function () { - return this.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith( - `${this.v1Url}/api/v1/overleaf/templates/${this.templateVersionId}` + it('should fetch zip from v1 based on template id', function (ctx) { + ctx.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith( + `${ctx.v1Url}/api/v1/overleaf/templates/${ctx.templateVersionId}` ) }) - it('should save temporary file', function () { - return this.fs.createWriteStream.should.have.been.calledWith( - this.dumpPath - ) + it('should save temporary file', function (ctx) { + ctx.fs.createWriteStream.should.have.been.calledWith(ctx.dumpPath) }) - it('should create project', function () { - return this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch( - this.user_id, - this.templateName, - this.dumpPath, + it('should create project', function (ctx) { + ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch( + ctx.user_id, + ctx.templateName, + ctx.dumpPath, { - fromV1TemplateId: this.templateId, - fromV1TemplateVersionId: this.templateVersionId, + fromV1TemplateId: ctx.templateId, + fromV1TemplateVersionId: ctx.templateVersionId, } ) }) - it('should unlink file', function () { - return this.fs.promises.unlink.should.have.been.calledWith( - this.dumpPath + it('should unlink file', function (ctx) { + ctx.fs.promises.unlink.should.have.been.calledWith(ctx.dumpPath) + }) + + it('should set project options when passed', function (ctx) { + ctx.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch( + ctx.project_id, + ctx.compiler + ) + ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch( + ctx.project_id, + ctx.imageName + ) + ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch( + ctx.project_id, + ctx.mainFile + ) + ctx.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch( + ctx.project_id, + ctx.brandVariationId ) }) - it('should set project options when passed', function () { - this.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch( - this.project_id, - this.compiler - ) - this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch( - this.project_id, - this.imageName - ) - this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch( - this.project_id, - this.mainFile - ) - return this.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch( - this.project_id, - this.brandVariationId - ) - }) - - it('should update project', function () { - return this.Project.updateOne.should.have.been.calledWithMatch( - { _id: this.project_id }, + it('should update project', function (ctx) { + ctx.Project.updateOne.should.have.been.calledWithMatch( + { _id: ctx.project_id }, { - fromV1TemplateId: this.templateId, - fromV1TemplateVersionId: this.templateVersionId, + fromV1TemplateId: ctx.templateId, + fromV1TemplateVersionId: ctx.templateVersionId, } ) }) }) describe('when some options not set', function () { - beforeEach(function () { - return this.TemplatesManager.createProjectFromV1Template( + beforeEach(async function (ctx) { + await ctx.TemplatesManager.createProjectFromV1Template( null, null, null, - this.templateId, - this.templateName, - this.templateVersionId, - this.user_id, + ctx.templateId, + ctx.templateName, + ctx.templateVersionId, + ctx.user_id, null ) }) - it('should not set missing project options', function () { - this.ProjectOptionsHandler.promises.setCompiler.called.should.equal( + it('should not set missing project options', function (ctx) { + ctx.ProjectOptionsHandler.promises.setCompiler.called.should.equal( false ) - this.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal( + ctx.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal( false ) - this.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal( + ctx.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal( false ) - return this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch( - this.project_id, + ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch( + ctx.project_id, 'wl_texlive:2018.1' ) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs index 95d26e71ff..64c7782c3f 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs @@ -1,176 +1,194 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') -const { Project } = require('../helpers/models/Project') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' + +const { Project } = indirectlyImportModels(['Project']) + +const { ObjectId } = mongodb const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher' describe('TpdsProjectFlusher', function () { - beforeEach(function () { - this.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } } - this.folder = { _id: new ObjectId() } - this.docs = { + beforeEach(async function (ctx) { + ctx.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } } + ctx.folder = { _id: new ObjectId() } + ctx.docs = { '/doc/one': { _id: 'mock-doc-1', lines: ['one'], rev: 5, - folder: this.folder, + folder: ctx.folder, }, '/doc/two': { _id: 'mock-doc-2', lines: ['two'], rev: 6, - folder: this.folder, + folder: ctx.folder, }, } - this.files = { + ctx.files = { '/file/one': { _id: 'mock-file-1', rev: 7, - folder: this.folder, + folder: ctx.folder, hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }, '/file/two': { _id: 'mock-file-2', rev: 8, - folder: this.folder, + folder: ctx.folder, hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', }, } - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { flushProjectToMongo: sinon.stub().resolves(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { promises: { - getAllDocs: sinon.stub().withArgs(this.project._id).resolves(this.docs), - getAllFiles: sinon - .stub() - .withArgs(this.project._id) - .resolves(this.files), + getAllDocs: sinon.stub().withArgs(ctx.project._id).resolves(ctx.docs), + getAllFiles: sinon.stub().withArgs(ctx.project._id).resolves(ctx.files), }, } - this.TpdsUpdateSender = { + ctx.TpdsUpdateSender = { promises: { addDoc: sinon.stub().resolves(), addFile: sinon.stub().resolves(), }, } - this.ProjectMock = sinon.mock(Project) + ctx.ProjectMock = sinon.mock(Project) - this.TpdsProjectFlusher = SandboxedModule.require(MODULE_PATH, { - requires: { - '../DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../Project/ProjectGetter': this.ProjectGetter, - '../Project/ProjectEntityHandler': this.ProjectEntityHandler, - '../../models/Project': { Project }, - './TpdsUpdateSender': this.TpdsUpdateSender, - }, - }) + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock('../../../../app/src/models/Project', () => ({ + Project, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender', + () => ({ + default: ctx.TpdsUpdateSender, + }) + ) + + ctx.TpdsProjectFlusher = (await import(MODULE_PATH)).default }) - afterEach(function () { - this.ProjectMock.restore() + afterEach(function (ctx) { + ctx.ProjectMock.restore() }) describe('flushProjectToTpds', function () { describe('usually', function () { - beforeEach(async function () { - await this.TpdsProjectFlusher.promises.flushProjectToTpds( - this.project._id + beforeEach(async function (ctx) { + await ctx.TpdsProjectFlusher.promises.flushProjectToTpds( + ctx.project._id ) }) - it('should flush the project from the doc updater', function () { + it('should flush the project from the doc updater', function (ctx) { expect( - this.DocumentUpdaterHandler.promises.flushProjectToMongo - ).to.have.been.calledWith(this.project._id) + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo + ).to.have.been.calledWith(ctx.project._id) }) - it('should flush each doc to the TPDS', function () { - for (const [path, doc] of Object.entries(this.docs)) { - expect(this.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith( - { - projectId: this.project._id, - docId: doc._id, - projectName: this.project.name, - rev: doc.rev, - path, - folderId: this.folder._id, - } - ) + it('should flush each doc to the TPDS', function (ctx) { + for (const [path, doc] of Object.entries(ctx.docs)) { + expect(ctx.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({ + projectId: ctx.project._id, + docId: doc._id, + projectName: ctx.project.name, + rev: doc.rev, + path, + folderId: ctx.folder._id, + }) } }) - it('should flush each file to the TPDS', function () { - for (const [path, file] of Object.entries(this.files)) { - expect( - this.TpdsUpdateSender.promises.addFile - ).to.have.been.calledWith({ - projectId: this.project._id, - historyId: this.project.overleaf.history.id, - fileId: file._id, - hash: file.hash, - projectName: this.project.name, - rev: file.rev, - path, - folderId: this.folder._id, - }) + it('should flush each file to the TPDS', function (ctx) { + for (const [path, file] of Object.entries(ctx.files)) { + expect(ctx.TpdsUpdateSender.promises.addFile).to.have.been.calledWith( + { + projectId: ctx.project._id, + historyId: ctx.project.overleaf.history.id, + fileId: file._id, + hash: file.hash, + projectName: ctx.project.name, + rev: file.rev, + path, + folderId: ctx.folder._id, + } + ) } }) }) describe('when a TPDS flush is pending', function () { - beforeEach(async function () { - this.project.deferredTpdsFlushCounter = 2 - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.project.deferredTpdsFlushCounter = 2 + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, deferredTpdsFlushCounter: { $lte: 2 }, }, { $set: { deferredTpdsFlushCounter: 0 } } ) .chain('exec') .resolves() - await this.TpdsProjectFlusher.promises.flushProjectToTpds( - this.project._id + await ctx.TpdsProjectFlusher.promises.flushProjectToTpds( + ctx.project._id ) }) - it('resets the deferred flush counter', function () { - this.ProjectMock.verify() + it('resets the deferred flush counter', function (ctx) { + ctx.ProjectMock.verify() }) }) }) describe('deferProjectFlushToTpds', function () { - beforeEach(async function () { - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, }, { $inc: { deferredTpdsFlushCounter: 1 } } ) .chain('exec') .resolves() - await this.TpdsProjectFlusher.promises.deferProjectFlushToTpds( - this.project._id + await ctx.TpdsProjectFlusher.promises.deferProjectFlushToTpds( + ctx.project._id ) }) - it('increments the deferred flush counter', function () { - this.ProjectMock.verify() + it('increments the deferred flush counter', function (ctx) { + ctx.ProjectMock.verify() }) }) @@ -178,24 +196,24 @@ describe('TpdsProjectFlusher', function () { let cases = [0, undefined] cases.forEach(counterValue => { describe(`when the deferred flush counter is ${counterValue}`, function () { - beforeEach(async function () { - this.project.deferredTpdsFlushCounter = counterValue - await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded( - this.project._id + beforeEach(async function (ctx) { + ctx.project.deferredTpdsFlushCounter = counterValue + await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded( + ctx.project._id ) }) - it("doesn't flush the project from the doc updater", function () { - expect(this.DocumentUpdaterHandler.promises.flushProjectToMongo).not - .to.have.been.called + it("doesn't flush the project from the doc updater", function (ctx) { + expect(ctx.DocumentUpdaterHandler.promises.flushProjectToMongo).not.to + .have.been.called }) - it("doesn't flush any doc", function () { - expect(this.TpdsUpdateSender.promises.addDoc).not.to.have.been.called + it("doesn't flush any doc", function (ctx) { + expect(ctx.TpdsUpdateSender.promises.addDoc).not.to.have.been.called }) - it("doesn't flush any file", function () { - expect(this.TpdsUpdateSender.promises.addFile).not.to.have.been.called + it("doesn't flush any file", function (ctx) { + expect(ctx.TpdsUpdateSender.promises.addFile).not.to.have.been.called }) }) }) @@ -203,63 +221,63 @@ describe('TpdsProjectFlusher', function () { cases = [1, 2] cases.forEach(counterValue => { describe(`when the deferred flush counter is ${counterValue}`, function () { - beforeEach(async function () { - this.project.deferredTpdsFlushCounter = counterValue - this.ProjectMock.expects('updateOne') + beforeEach(async function (ctx) { + ctx.project.deferredTpdsFlushCounter = counterValue + ctx.ProjectMock.expects('updateOne') .withArgs( { - _id: this.project._id, + _id: ctx.project._id, deferredTpdsFlushCounter: { $lte: counterValue }, }, { $set: { deferredTpdsFlushCounter: 0 } } ) .chain('exec') .resolves() - await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded( - this.project._id + await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded( + ctx.project._id ) }) - it('flushes the project from the doc updater', function () { + it('flushes the project from the doc updater', function (ctx) { expect( - this.DocumentUpdaterHandler.promises.flushProjectToMongo - ).to.have.been.calledWith(this.project._id) + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo + ).to.have.been.calledWith(ctx.project._id) }) - it('flushes each doc to the TPDS', function () { - for (const [path, doc] of Object.entries(this.docs)) { + it('flushes each doc to the TPDS', function (ctx) { + for (const [path, doc] of Object.entries(ctx.docs)) { expect( - this.TpdsUpdateSender.promises.addDoc + ctx.TpdsUpdateSender.promises.addDoc ).to.have.been.calledWith({ - projectId: this.project._id, + projectId: ctx.project._id, docId: doc._id, - projectName: this.project.name, + projectName: ctx.project.name, rev: doc.rev, path, - folderId: this.folder._id, + folderId: ctx.folder._id, }) } }) - it('flushes each file to the TPDS', function () { - for (const [path, file] of Object.entries(this.files)) { + it('flushes each file to the TPDS', function (ctx) { + for (const [path, file] of Object.entries(ctx.files)) { expect( - this.TpdsUpdateSender.promises.addFile + ctx.TpdsUpdateSender.promises.addFile ).to.have.been.calledWith({ - projectId: this.project._id, - historyId: this.project.overleaf.history.id, + projectId: ctx.project._id, + historyId: ctx.project.overleaf.history.id, fileId: file._id, hash: file.hash, - projectName: this.project.name, + projectName: ctx.project.name, rev: file.rev, path, - folderId: this.folder._id, + folderId: ctx.folder._id, }) } }) - it('resets the deferred flush counter', function () { - this.ProjectMock.verify() + it('resets the deferred flush counter', function (ctx) { + ctx.ProjectMock.verify() }) }) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs index 5a27a26c07..5505ab95b5 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs @@ -1,12 +1,13 @@ -const { ObjectId } = require('mongodb-legacy') -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const sinon = require('sinon') -const { expect } = require('chai') +import { beforeEach, describe, expect, it, vi } from 'vitest' +import mongodb from 'mongodb-legacy' +import path from 'path' +import sinon from 'sinon' + +const { ObjectId } = mongodb const modulePath = path.join( - __dirname, - '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js' + import.meta.dirname, + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.mjs' ) const projectId = 'project_id_here' @@ -21,29 +22,29 @@ const filestoreUrl = 'filestore.overleaf.com' const projectHistoryUrl = 'http://project-history:3054' describe('TpdsUpdateSender', function () { - beforeEach(function () { - this.fakeUser = { + beforeEach(async function (ctx) { + ctx.fakeUser = { _id: '12390i', } - this.memberIds = [userId, collaberatorRef, readOnlyRef] - this.enqueueUrl = new URL( + ctx.memberIds = [userId, collaberatorRef, readOnlyRef] + ctx.enqueueUrl = new URL( 'http://tpdsworker/enqueue/web_to_tpds_http_requests' ) - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { - getInvitedMemberIds: sinon.stub().resolves(this.memberIds), + getInvitedMemberIds: sinon.stub().resolves(ctx.memberIds), }, } - this.docstoreUrl = 'docstore.overleaf.env' - this.response = { + ctx.docstoreUrl = 'docstore.overleaf.env' + ctx.response = { ok: true, json: sinon.stub(), } - this.FetchUtils = { + ctx.FetchUtils = { fetchNothing: sinon.stub().resolves(), } - this.settings = { + ctx.settings = { siteUrl, apis: { thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl }, @@ -51,7 +52,7 @@ describe('TpdsUpdateSender', function () { url: filestoreUrl, }, docstore: { - pubUrl: this.docstoreUrl, + pubUrl: ctx.docstoreUrl, }, project_history: { url: projectHistoryUrl, @@ -62,46 +63,63 @@ describe('TpdsUpdateSender', function () { getUsers .withArgs({ _id: { - $in: this.memberIds, + $in: ctx.memberIds, }, 'dropbox.access_token.uid': { $ne: null }, }) .resolves( - this.memberIds.map(userId => { + ctx.memberIds.map(userId => { return { _id: userId } }) ) - this.UserGetter = { + ctx.UserGetter = { promises: { getUsers }, } - this.TpdsUpdateSender = SandboxedModule.require(modulePath, { - requires: { - 'mongodb-legacy': { ObjectId }, - '@overleaf/settings': this.settings, - '@overleaf/fetch-utils': this.FetchUtils, - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - '../User/UserGetter.js': this.UserGetter, - '@overleaf/metrics': { - inc() {}, - }, + + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: { + inc() {}, }, - }) + })) + + ctx.TpdsUpdateSender = (await import(modulePath)).default }) describe('enqueue', function () { - it('should not call request if there is no tpdsworker url', async function () { - await this.TpdsUpdateSender.promises.enqueue(null, null, null) - this.FetchUtils.fetchNothing.should.not.have.been.called + it('should not call request if there is no tpdsworker url', async function (ctx) { + await ctx.TpdsUpdateSender.promises.enqueue(null, null, null) + ctx.FetchUtils.fetchNothing.should.not.have.been.called }) - it('should post the message to the tpdsworker', async function () { - this.settings.apis.tpdsworker = { url: 'http://tpdsworker' } + it('should post the message to the tpdsworker', async function (ctx) { + ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' } const group0 = 'myproject' const method0 = 'somemethod0' const job0 = 'do something' - await this.TpdsUpdateSender.promises.enqueue(group0, method0, job0) - this.FetchUtils.fetchNothing.should.have.been.calledWithMatch( - this.enqueueUrl, + await ctx.TpdsUpdateSender.promises.enqueue(group0, method0, job0) + ctx.FetchUtils.fetchNothing.should.have.been.calledWithMatch( + ctx.enqueueUrl, { method: 'POST', json: { group: group0, job: job0, method: method0 }, @@ -111,17 +129,17 @@ describe('TpdsUpdateSender', function () { }) describe('sending updates', function () { - beforeEach(function () { - this.settings.apis.tpdsworker = { url: 'http://tpdsworker' } + beforeEach(function (ctx) { + ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' } }) - it('queues a post the file with user and file id and hash', async function () { + it('queues a post the file with user and file id and hash', async function (ctx) { const fileId = '4545345' const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const historyId = 91525 const path = '/some/path/here.jpg' - await this.TpdsUpdateSender.promises.addFile({ + await ctx.TpdsUpdateSender.promises.addFile({ projectId, historyId, fileId, @@ -130,8 +148,8 @@ describe('TpdsUpdateSender', function () { projectName, }) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -148,8 +166,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: collaberatorRef, @@ -157,8 +175,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: readOnlyRef, @@ -168,12 +186,12 @@ describe('TpdsUpdateSender', function () { ) }) - it('post doc with stream origin of docstore', async function () { + it('post doc with stream origin of docstore', async function (ctx) { const docId = '4545345' const path = '/some/path/here.tex' const lines = ['line1', 'line2', 'line3'] - await this.TpdsUpdateSender.promises.addDoc({ + await ctx.TpdsUpdateSender.promises.addDoc({ projectId, docId, path, @@ -181,8 +199,8 @@ describe('TpdsUpdateSender', function () { projectName, }) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -192,15 +210,15 @@ describe('TpdsUpdateSender', function () { uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent( projectName )}${encodeURIComponent(path)}`, - streamOrigin: `${this.docstoreUrl}/project/${projectId}/doc/${docId}/raw`, + streamOrigin: `${ctx.docstoreUrl}/project/${projectId}/doc/${docId}/raw`, headers: {}, }, }, } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: collaberatorRef, @@ -211,8 +229,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: readOnlyRef, @@ -224,19 +242,19 @@ describe('TpdsUpdateSender', function () { ) }) - it('deleting entity', async function () { + it('deleting entity', async function (ctx) { const path = '/path/here/t.tex' const subtreeEntityIds = ['id1', 'id2'] - await this.TpdsUpdateSender.promises.deleteEntity({ + await ctx.TpdsUpdateSender.promises.deleteEntity({ projectId, path, projectName, subtreeEntityIds, }) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -253,8 +271,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: collaberatorRef, @@ -265,8 +283,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: readOnlyRef, @@ -278,19 +296,19 @@ describe('TpdsUpdateSender', function () { ) }) - it('moving entity', async function () { + it('moving entity', async function (ctx) { const startPath = 'staring/here/file.tex' const endPath = 'ending/here/file.tex' - await this.TpdsUpdateSender.promises.moveEntity({ + await ctx.TpdsUpdateSender.promises.moveEntity({ projectId, startPath, endPath, projectName, }) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -308,8 +326,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: collaberatorRef, @@ -320,8 +338,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: readOnlyRef, @@ -333,18 +351,18 @@ describe('TpdsUpdateSender', function () { ) }) - it('should be able to rename a project using the move entity func', async function () { + it('should be able to rename a project using the move entity func', async function (ctx) { const oldProjectName = '/oldProjectName/' const newProjectName = '/newProjectName/' - await this.TpdsUpdateSender.promises.moveEntity({ + await ctx.TpdsUpdateSender.promises.moveEntity({ projectId, projectName: oldProjectName, newProjectName, }) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -362,8 +380,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: collaberatorRef, @@ -374,8 +392,8 @@ describe('TpdsUpdateSender', function () { } ) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: readOnlyRef, @@ -387,11 +405,11 @@ describe('TpdsUpdateSender', function () { ) }) - it('pollDropboxForUser', async function () { - await this.TpdsUpdateSender.promises.pollDropboxForUser(userId) + it('pollDropboxForUser', async function (ctx) { + await ctx.TpdsUpdateSender.promises.pollDropboxForUser(userId) - expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch( - this.enqueueUrl, + expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch( + ctx.enqueueUrl, { json: { group: userId, @@ -410,24 +428,24 @@ describe('TpdsUpdateSender', function () { }) describe('user not linked to dropbox', function () { - beforeEach(function () { - this.UserGetter.promises.getUsers + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUsers .withArgs({ _id: { - $in: this.memberIds, + $in: ctx.memberIds, }, 'dropbox.access_token.uid': { $ne: null }, }) .resolves([]) }) - it('does not make request to tpds', async function () { + it('does not make request to tpds', async function (ctx) { const fileId = '4545345' const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const historyId = 91525 const path = '/some/path/here.jpg' - await this.TpdsUpdateSender.promises.addFile({ + await ctx.TpdsUpdateSender.promises.addFile({ projectId, historyId, hash, @@ -435,7 +453,7 @@ describe('TpdsUpdateSender', function () { path, projectName, }) - this.FetchUtils.fetchNothing.should.not.have.been.called + ctx.FetchUtils.fetchNothing.should.not.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 7586757cf0..6da7a0570e 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs @@ -236,12 +236,13 @@ describe('TokenAccessController', function () { }), })) + ctx.AdminAuthorizationHelper = { + canRedirectToAdminDomain: sinon.stub(), + } + vi.doMock( '../../../../app/src/Features/Helpers/AdminAuthorizationHelper', - () => - (ctx.AdminAuthorizationHelper = { - canRedirectToAdminDomain: sinon.stub(), - }) + () => ({ default: ctx.AdminAuthorizationHelper }) ) vi.doMock( @@ -764,17 +765,12 @@ describe('TokenAccessController', function () { .stub() .resolves([{ email: 'test@not-overleaf.com' }]) - await 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 - ) + await ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res + ) + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `${ctx.Settings.adminUrl}/#prefix`, }) }) diff --git a/services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs b/services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs index ba219f3c68..7f9f90c4ae 100644 --- a/services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs +++ b/services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs @@ -1,48 +1,53 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const mockFs = require('mock-fs') -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') -const Settings = require('@overleaf/settings') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import mockFs from 'mock-fs' +import mongodb from 'mongodb-legacy' +import Settings from '@overleaf/settings' + +const { ObjectId } = mongodb const MODULE_PATH = - '../../../../app/src/Features/Uploads/FileSystemImportManager.js' + '../../../../app/src/Features/Uploads/FileSystemImportManager.mjs' describe('FileSystemImportManager', function () { - beforeEach(function () { - this.projectId = new ObjectId() - this.folderId = new ObjectId() - this.newFolderId = new ObjectId() - this.userId = new ObjectId() + beforeEach(async function (ctx) { + ctx.projectId = new ObjectId() + ctx.folderId = new ObjectId() + ctx.newFolderId = new ObjectId() + ctx.userId = new ObjectId() - this.EditorController = { + ctx.EditorController = { promises: { addDoc: sinon.stub().resolves(), addFile: sinon.stub().resolves(), upsertDoc: sinon.stub().resolves(), upsertFile: sinon.stub().resolves(), - addFolder: sinon.stub().resolves({ _id: this.newFolderId }), + addFolder: sinon.stub().resolves({ _id: ctx.newFolderId }), }, } - this.FileSystemImportManager = SandboxedModule.require(MODULE_PATH, { - requires: { - '@overleaf/settings': { - textExtensions: ['tex', 'txt'], - editableFilenames: [ - 'latexmkrc', - '.latexmkrc', - 'makefile', - 'gnumakefile', - ], - fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings - }, - '../Editor/EditorController': this.EditorController, + + vi.doMock('@overleaf/settings', () => ({ + default: { + textExtensions: ['tex', 'txt'], + editableFilenames: [ + 'latexmkrc', + '.latexmkrc', + 'makefile', + 'gnumakefile', + ], + fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings }, - }) + })) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + ctx.FileSystemImportManager = (await import(MODULE_PATH)).default }) describe('importDir', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { mockFs({ 'import-test': { 'main.tex': 'My thesis', @@ -64,87 +69,87 @@ describe('FileSystemImportManager', function () { }, symlink: mockFs.symlink({ path: 'import-test' }), }) - this.entries = - await this.FileSystemImportManager.promises.importDir('import-test') - this.projectPaths = this.entries.map(x => x.projectPath) + ctx.entries = + await ctx.FileSystemImportManager.promises.importDir('import-test') + ctx.projectPaths = ctx.entries.map(x => x.projectPath) }) afterEach(function () { mockFs.restore() }) - it('should import regular docs', function () { - expect(this.entries).to.deep.include({ + it('should import regular docs', function (ctx) { + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/main.tex', lines: ['My thesis'], }) }) - it('should skip symlinks inside the import folder', function () { - expect(this.projectPaths).not.to.include('/link-to-main.tex') + it('should skip symlinks inside the import folder', function (ctx) { + expect(ctx.projectPaths).not.to.include('/link-to-main.tex') }) - it('should skip ignored files', function () { - expect(this.projectPaths).not.to.include('/.DS_Store') + it('should skip ignored files', function (ctx) { + expect(ctx.projectPaths).not.to.include('/.DS_Store') }) - it('should import binary files', function () { - expect(this.entries).to.deep.include({ + it('should import binary files', function (ctx) { + expect(ctx.entries).to.deep.include({ type: 'file', projectPath: '/images/cat.jpg', fsPath: 'import-test/images/cat.jpg', }) }) - it('should deal with Mac/Windows/Unix line endings', function () { - expect(this.entries).to.deep.include({ + it('should deal with Mac/Windows/Unix line endings', function (ctx) { + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/line-endings/unix.txt', lines: ['one', 'two', 'three'], }) - expect(this.entries).to.deep.include({ + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/line-endings/mac.txt', lines: ['uno', 'dos', 'tres'], }) - expect(this.entries).to.deep.include({ + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/line-endings/windows.txt', lines: ['ein', 'zwei', 'drei'], }) - expect(this.entries).to.deep.include({ + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/line-endings/mixed.txt', lines: ['uno', 'due', 'tre', 'quattro'], }) }) - it('should import documents with latin1 encoding', function () { - expect(this.entries).to.deep.include({ + it('should import documents with latin1 encoding', function (ctx) { + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/encodings/latin1.txt', lines: ['tétanisant!'], }) }) - it('should import documents with utf16-le encoding', function () { - expect(this.entries).to.deep.include({ + it('should import documents with utf16-le encoding', function (ctx) { + expect(ctx.entries).to.deep.include({ type: 'doc', projectPath: '/encodings/utf16le.txt', lines: ['\ufeffétonnant!'], }) }) - it('should error when the root folder is a symlink', async function () { - await expect(this.FileSystemImportManager.promises.importDir('symlink')) - .to.be.rejected + it('should error when the root folder is a symlink', async function (ctx) { + await expect(ctx.FileSystemImportManager.promises.importDir('symlink')).to + .be.rejected }) }) describe('addEntity', function () { describe('with directory', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { mockFs({ path: { to: { @@ -156,10 +161,10 @@ describe('FileSystemImportManager', function () { }, }) - await this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + await ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'folder', 'path/to/folder', false @@ -170,32 +175,32 @@ describe('FileSystemImportManager', function () { mockFs.restore() }) - it('should add a folder to the project', function () { - this.EditorController.promises.addFolder.should.have.been.calledWith( - this.projectId, - this.folderId, + it('should add a folder to the project', function (ctx) { + ctx.EditorController.promises.addFolder.should.have.been.calledWith( + ctx.projectId, + ctx.folderId, 'folder', 'upload' ) }) - it("should add the folder's contents", function () { - this.EditorController.promises.addDoc.should.have.been.calledWith( - this.projectId, - this.newFolderId, + it("should add the folder's contents", function (ctx) { + ctx.EditorController.promises.addDoc.should.have.been.calledWith( + ctx.projectId, + ctx.newFolderId, 'doc.tex', ['one', 'two', 'three'], 'upload', - this.userId + ctx.userId ) - this.EditorController.promises.addFile.should.have.been.calledWith( - this.projectId, - this.newFolderId, + ctx.EditorController.promises.addFile.should.have.been.calledWith( + ctx.projectId, + ctx.newFolderId, 'image.jpg', 'path/to/folder/image.jpg', null, 'upload', - this.userId + ctx.userId ) }) }) @@ -210,51 +215,51 @@ describe('FileSystemImportManager', function () { }) describe('with replace set to false', function () { - beforeEach(async function () { - await this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + beforeEach(async function (ctx) { + await ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'image.jpg', 'uploaded-file', false ) }) - it('should add the file', function () { - this.EditorController.promises.addFile.should.have.been.calledWith( - this.projectId, - this.folderId, + it('should add the file', function (ctx) { + ctx.EditorController.promises.addFile.should.have.been.calledWith( + ctx.projectId, + ctx.folderId, 'image.jpg', 'uploaded-file', null, 'upload', - this.userId + ctx.userId ) }) }) describe('with replace set to true', function () { - beforeEach(async function () { - await this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + beforeEach(async function (ctx) { + await ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'image.jpg', 'uploaded-file', true ) }) - it('should add the file', function () { - this.EditorController.promises.upsertFile.should.have.been.calledWith( - this.projectId, - this.folderId, + it('should add the file', function (ctx) { + ctx.EditorController.promises.upsertFile.should.have.been.calledWith( + ctx.projectId, + ctx.folderId, 'image.jpg', 'uploaded-file', null, 'upload', - this.userId + ctx.userId ) }) }) @@ -279,49 +284,49 @@ describe('FileSystemImportManager', function () { }) describe('with replace set to false', function () { - beforeEach(async function () { - await this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + beforeEach(async function (ctx) { + await ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'doc.tex', 'path/to/uploaded-file', false ) }) - it('should insert the doc', function () { - this.EditorController.promises.addDoc.should.have.been.calledWith( - this.projectId, - this.folderId, + it('should insert the doc', function (ctx) { + ctx.EditorController.promises.addDoc.should.have.been.calledWith( + ctx.projectId, + ctx.folderId, 'doc.tex', ['one', 'two', 'three'], 'upload', - this.userId + ctx.userId ) }) }) describe('with replace set to true', function () { - beforeEach(async function () { - await this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + beforeEach(async function (ctx) { + await ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'doc.tex', 'path/to/uploaded-file', true ) }) - it('should upsert the doc', function () { - this.EditorController.promises.upsertDoc.should.have.been.calledWith( - this.projectId, - this.folderId, + it('should upsert the doc', function (ctx) { + ctx.EditorController.promises.upsertDoc.should.have.been.calledWith( + ctx.projectId, + ctx.folderId, 'doc.tex', ['one', 'two', 'three'], 'upload', - this.userId + ctx.userId ) }) }) @@ -339,20 +344,20 @@ describe('FileSystemImportManager', function () { mockFs.restore() }) - it('should stop with an error', async function () { + it('should stop with an error', async function (ctx) { await expect( - this.FileSystemImportManager.promises.addEntity( - this.userId, - this.projectId, - this.folderId, + ctx.FileSystemImportManager.promises.addEntity( + ctx.userId, + ctx.projectId, + ctx.folderId, 'main.tex', 'path/to/symlink', false ) ).to.be.rejectedWith('path is symlink') - this.EditorController.promises.addFolder.should.not.have.been.called - this.EditorController.promises.addDoc.should.not.have.been.called - this.EditorController.promises.addFile.should.not.have.been.called + ctx.EditorController.promises.addFolder.should.not.have.been.called + ctx.EditorController.promises.addDoc.should.not.have.been.called + ctx.EditorController.promises.addFile.should.not.have.been.called }) }) }) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs index c7693a4725..8bc28e798b 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs @@ -1,176 +1,238 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const timekeeper = require('timekeeper') -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +import timekeeper from 'timekeeper' +import mongodb from 'mongodb-legacy' + +const { ObjectId } = mongodb const MODULE_PATH = - '../../../../app/src/Features/Uploads/ProjectUploadManager.js' + '../../../../app/src/Features/Uploads/ProjectUploadManager.mjs' describe('ProjectUploadManager', function () { - beforeEach(function () { - this.now = Date.now() - timekeeper.freeze(this.now) - this.rootFolderId = new ObjectId() - this.ownerId = new ObjectId() - this.zipPath = '/path/to/zip/file-name.zip' - this.extractedZipPath = `/path/to/zip/file-name-${this.now}` - this.mainContent = 'Contents of main.tex' - this.projectName = 'My project*' - this.fixedProjectName = 'My project' - this.uniqueProjectName = 'My project (1)' - this.project = { + beforeEach(async function (ctx) { + ctx.now = Date.now() + timekeeper.freeze(ctx.now) + ctx.rootFolderId = new ObjectId() + ctx.ownerId = new ObjectId() + ctx.zipPath = '/path/to/zip/file-name.zip' + ctx.extractedZipPath = `/path/to/zip/file-name-${ctx.now}` + ctx.mainContent = 'Contents of main.tex' + ctx.projectName = 'My project*' + ctx.fixedProjectName = 'My project' + ctx.uniqueProjectName = 'My project (1)' + ctx.project = { _id: new ObjectId(), - rootFolder: [{ _id: this.rootFolderId }], + rootFolder: [{ _id: ctx.rootFolderId }], overleaf: { history: { id: 12345 } }, } - this.doc = { + ctx.doc = { _id: new ObjectId(), name: 'main.tex', } - this.docFsPath = '/path/to/doc' - this.docLines = ['My thesis', 'by A. U. Thor'] - this.file = { + ctx.docFsPath = '/path/to/doc' + ctx.docLines = ['My thesis', 'by A. U. Thor'] + ctx.file = { _id: new ObjectId(), name: 'image.png', } - this.fileFsPath = '/path/to/file' + ctx.fileFsPath = '/path/to/file' - this.topLevelDestination = '/path/to/zip/file-extracted/nested' - this.newProjectVersion = 123 - this.importEntries = [ + ctx.topLevelDestination = '/path/to/zip/file-extracted/nested' + ctx.newProjectVersion = 123 + ctx.importEntries = [ { type: 'doc', projectPath: '/main.tex', - lines: this.docLines, + lines: ctx.docLines, }, { type: 'file', - projectPath: `/${this.file.name}`, - fsPath: this.fileFsPath, + projectPath: `/${ctx.file.name}`, + fsPath: ctx.fileFsPath, }, ] - this.docEntries = [ + ctx.docEntries = [ { - doc: this.doc, - path: `/${this.doc.name}`, - docLines: this.docLines.join('\n'), + doc: ctx.doc, + path: `/${ctx.doc.name}`, + docLines: ctx.docLines.join('\n'), }, ] - this.fileEntries = [ + ctx.fileEntries = [ { - file: this.file, - path: `/${this.file.name}`, + file: ctx.file, + path: `/${ctx.file.name}`, createdBlob: true, }, ] - this.fs = { + ctx.fs = { promises: { rm: sinon.stub().resolves(), }, } - this.ArchiveManager = { + ctx.ArchiveManager = { promises: { extractZipArchive: sinon.stub().resolves(), findTopLevelDirectory: sinon .stub() - .withArgs(this.extractedZipPath) - .resolves(this.topLevelDestination), + .withArgs(ctx.extractedZipPath) + .resolves(ctx.topLevelDestination), }, } - this.Doc = sinon.stub().returns(this.doc) - this.DocstoreManager = { + ctx.Doc = sinon.stub().returns(ctx.doc) + ctx.DocstoreManager = { promises: { updateDoc: sinon.stub().resolves(), }, } - this.DocumentHelper = { + ctx.DocumentHelper = { getTitleFromTexContent: sinon .stub() - .withArgs(this.mainContent) - .returns(this.projectName), + .withArgs(ctx.mainContent) + .returns(ctx.projectName), } - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { updateProjectStructure: sinon.stub().resolves(), }, } - this.FileStoreHandler = { + ctx.FileStoreHandler = { promises: { uploadFileFromDiskWithHistoryId: sinon.stub().resolves({ - fileRef: this.file, + fileRef: ctx.file, createdBlob: true, }), }, } - this.FileSystemImportManager = { + ctx.FileSystemImportManager = { promises: { importDir: sinon .stub() - .withArgs(this.topLevelDestination) - .resolves(this.importEntries), + .withArgs(ctx.topLevelDestination) + .resolves(ctx.importEntries), }, } - this.ProjectCreationHandler = { + ctx.ProjectCreationHandler = { promises: { - createBlankProject: sinon.stub().resolves(this.project), + createBlankProject: sinon.stub().resolves(ctx.project), }, } - this.ProjectEntityMongoUpdateHandler = { + ctx.ProjectEntityMongoUpdateHandler = { promises: { - createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion), + createNewFolderStructure: sinon.stub().resolves(ctx.newProjectVersion), }, } - this.ProjectRootDocManager = { + ctx.ProjectRootDocManager = { promises: { setRootDocAutomatically: sinon.stub().resolves(), findRootDocFileFromDirectory: sinon .stub() - .resolves({ path: 'main.tex', content: this.mainContent }), + .resolves({ path: 'main.tex', content: ctx.mainContent }), setRootDocFromName: sinon.stub().resolves(), }, } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { fixProjectName: sinon .stub() - .withArgs(this.projectName) - .returns(this.fixedProjectName), + .withArgs(ctx.projectName) + .returns(ctx.fixedProjectName), promises: { - generateUniqueName: sinon.stub().resolves(this.uniqueProjectName), + generateUniqueName: sinon.stub().resolves(ctx.uniqueProjectName), }, } - this.ProjectDeleter = { + ctx.ProjectDeleter = { promises: { deleteProject: sinon.stub().resolves(), }, } - this.TpdsProjectFlusher = { + ctx.TpdsProjectFlusher = { promises: { flushProjectToTpds: sinon.stub().resolves(), }, } - this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, { - requires: { - fs: this.fs, - './ArchiveManager': this.ArchiveManager, - '../../models/Doc': { Doc: this.Doc }, - '../Docstore/DocstoreManager': this.DocstoreManager, - '../Documents/DocumentHelper': this.DocumentHelper, - '../DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../FileStore/FileStoreHandler': this.FileStoreHandler, - './FileSystemImportManager': this.FileSystemImportManager, - '../Project/ProjectCreationHandler': this.ProjectCreationHandler, - '../Project/ProjectEntityMongoUpdateHandler': - this.ProjectEntityMongoUpdateHandler, - '../Project/ProjectRootDocManager': this.ProjectRootDocManager, - '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler, - '../Project/ProjectDeleter': this.ProjectDeleter, - '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher, - }, - }) + vi.doMock('fs', () => ({ + default: ctx.fs, + })) + + vi.doMock('../../../../app/src/Features/Uploads/ArchiveManager', () => ({ + default: ctx.ArchiveManager, + })) + + vi.doMock('../../../../app/src/models/Doc', () => ({ + Doc: ctx.Doc, + })) + + vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({ + default: ctx.DocstoreManager, + })) + + vi.doMock('../../../../app/src/Features/Documents/DocumentHelper', () => ({ + default: ctx.DocumentHelper, + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: ctx.FileStoreHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Uploads/FileSystemImportManager', + () => ({ + default: ctx.FileSystemImportManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler', + () => ({ + default: ctx.ProjectEntityMongoUpdateHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ + default: ctx.ProjectRootDocManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: ctx.ProjectDeleter, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher', + () => ({ + default: ctx.TpdsProjectFlusher, + }) + ) + + ctx.ProjectUploadManager = (await import(MODULE_PATH)).default }) afterEach(function () { @@ -179,65 +241,65 @@ describe('ProjectUploadManager', function () { describe('createProjectFromZipArchive', function () { describe('when the title can be read from the root document', function () { - beforeEach(async function () { - await this.ProjectUploadManager.promises.createProjectFromZipArchive( - this.ownerId, - this.projectName, - this.zipPath + beforeEach(async function (ctx) { + await ctx.ProjectUploadManager.promises.createProjectFromZipArchive( + ctx.ownerId, + ctx.projectName, + ctx.zipPath ) }) - it('should extract the archive', function () { - this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith( - this.zipPath, - this.extractedZipPath + it('should extract the archive', function (ctx) { + ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith( + ctx.zipPath, + ctx.extractedZipPath ) }) - it('should create a project', function () { - this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith( - this.ownerId, - this.uniqueProjectName + it('should create a project', function (ctx) { + ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith( + ctx.ownerId, + ctx.uniqueProjectName ) }) - it('should initialize the file tree', function () { - this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith( - this.project._id, - this.docEntries, - this.fileEntries + it('should initialize the file tree', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith( + ctx.project._id, + ctx.docEntries, + ctx.fileEntries ) }) - it('should notify document updater', function () { - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( - this.project._id, - this.project.overleaf.history.id, - this.ownerId, + it('should notify document updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + ctx.project._id, + ctx.project.overleaf.history.id, + ctx.ownerId, { - newDocs: this.docEntries, - newFiles: this.fileEntries, - newProject: { version: this.newProjectVersion }, + newDocs: ctx.docEntries, + newFiles: ctx.fileEntries, + newProject: { version: ctx.newProjectVersion }, }, null ) }) - it('should flush the project to TPDS', function () { - this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith( - this.project._id + it('should flush the project to TPDS', function (ctx) { + ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith( + ctx.project._id ) }) - it('should set the root document', function () { - this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith( - this.project._id, + it('should set the root document', function (ctx) { + ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith( + ctx.project._id, 'main.tex' ) }) - it('should remove the destination directory afterwards', function () { - this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, { + it('should remove the destination directory afterwards', function (ctx) { + ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, { recursive: true, force: true, }) @@ -245,122 +307,122 @@ describe('ProjectUploadManager', function () { }) describe("when the root document can't be determined", function () { - beforeEach(async function () { - this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves( + beforeEach(async function (ctx) { + ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves( {} ) - await this.ProjectUploadManager.promises.createProjectFromZipArchive( - this.ownerId, - this.projectName, - this.zipPath + await ctx.ProjectUploadManager.promises.createProjectFromZipArchive( + ctx.ownerId, + ctx.projectName, + ctx.zipPath ) }) - it('should not try to set the root doc', function () { - this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have + it('should not try to set the root doc', function (ctx) { + ctx.ProjectRootDocManager.promises.setRootDocFromName.should.not.have .been.called }) }) }) describe('createProjectFromZipArchiveWithName', function () { - beforeEach(async function () { - await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( - this.ownerId, - this.projectName, - this.zipPath + beforeEach(async function (ctx) { + await ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( + ctx.ownerId, + ctx.projectName, + ctx.zipPath ) }) - it('should extract the archive', function () { - this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith( - this.zipPath, - this.extractedZipPath + it('should extract the archive', function (ctx) { + ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith( + ctx.zipPath, + ctx.extractedZipPath ) }) - it('should create a project owned by the owner_id', function () { - this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith( - this.ownerId, - this.uniqueProjectName + it('should create a project owned by the owner_id', function (ctx) { + ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith( + ctx.ownerId, + ctx.uniqueProjectName ) }) - it('should automatically set the root doc', function () { - this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith( - this.project._id + it('should automatically set the root doc', function (ctx) { + ctx.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith( + ctx.project._id ) }) - it('should initialize the file tree', function () { - this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith( - this.project._id, - this.docEntries, - this.fileEntries + it('should initialize the file tree', function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith( + ctx.project._id, + ctx.docEntries, + ctx.fileEntries ) }) - it('should notify document updater', function () { - this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( - this.project._id, - this.project.overleaf.history.id, - this.ownerId, + it('should notify document updater', function (ctx) { + ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith( + ctx.project._id, + ctx.project.overleaf.history.id, + ctx.ownerId, { - newDocs: this.docEntries, - newFiles: this.fileEntries, - newProject: { version: this.newProjectVersion }, + newDocs: ctx.docEntries, + newFiles: ctx.fileEntries, + newProject: { version: ctx.newProjectVersion }, }, null ) }) - it('should flush the project to TPDS', function () { - this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith( - this.project._id + it('should flush the project to TPDS', function (ctx) { + ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith( + ctx.project._id ) }) - it('should remove the destination directory afterwards', function () { - this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, { + it('should remove the destination directory afterwards', function (ctx) { + ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, { recursive: true, force: true, }) }) describe('when initializing the folder structure fails', function () { - beforeEach(async function () { - this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects() + beforeEach(async function (ctx) { + ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects() await expect( - this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( - this.ownerId, - this.projectName, - this.zipPath + ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( + ctx.ownerId, + ctx.projectName, + ctx.zipPath ) ).to.be.rejected }) - it('should cleanup the blank project created', async function () { - this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith( - this.project._id + it('should cleanup the blank project created', async function (ctx) { + ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith( + ctx.project._id ) }) }) describe('when setting automatically the root doc fails', function () { - beforeEach(async function () { - this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects() + beforeEach(async function (ctx) { + ctx.ProjectRootDocManager.promises.setRootDocAutomatically.rejects() await expect( - this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( - this.ownerId, - this.projectName, - this.zipPath + ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName( + ctx.ownerId, + ctx.projectName, + ctx.zipPath ) ).to.be.rejected }) - it('should cleanup the blank project created', function () { - this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith( - this.project._id + it('should cleanup the blank project created', function (ctx) { + ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith( + ctx.project._id ) }) }) diff --git a/services/web/test/unit/src/User/UserDeleter.test.mjs b/services/web/test/unit/src/User/UserDeleter.test.mjs index 0c5e00c0f5..3baf70edd3 100644 --- a/services/web/test/unit/src/User/UserDeleter.test.mjs +++ b/services/web/test/unit/src/User/UserDeleter.test.mjs @@ -1,28 +1,34 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const tk = require('timekeeper') -const moment = require('moment') -const SandboxedModule = require('sandboxed-module') -const Errors = require('../../../../app/src/Features/Errors/Errors') -const ObjectId = require('mongoose').Types.ObjectId -const { User } = require('../helpers/models/User') -const { DeletedUser } = require('../helpers/models/DeletedUser') +import { beforeEach, describe, expect, vi, it } from 'vitest' +import sinon from 'sinon' +import tk from 'timekeeper' +import moment from 'moment' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +import mongoose from 'mongoose' +import indirectlyImportModels from '../helpers/indirectlyImportModels.js' -const modulePath = '../../../../app/src/Features/User/UserDeleter.js' +const { User, DeletedUser } = indirectlyImportModels(['User', 'DeletedUser']) + +const ObjectId = mongoose.Types.ObjectId + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + +const modulePath = '../../../../app/src/Features/User/UserDeleter.mjs' describe('UserDeleter', function () { - beforeEach(function () { + beforeEach(async function (ctx) { + ctx.userId = new ObjectId() + ctx.ipAddress = '1.2.3.4' + + ctx.UserMock = sinon.mock(User) + ctx.DeletedUserMock = sinon.mock(DeletedUser) + tk.freeze(Date.now()) - this.userId = new ObjectId() - this.ipAddress = '1.2.3.4' - - this.UserMock = sinon.mock(User) - this.DeletedUserMock = sinon.mock(DeletedUser) - - this.mockedUser = sinon.mock( + ctx.mockedUser = sinon.mock( new User({ - _id: this.userId, + _id: ctx.userId, email: 'bob@bob.com', lastLoggedIn: Date.now() + 1000, signUpDate: Date.now() + 2000, @@ -35,150 +41,213 @@ describe('UserDeleter', function () { referal_id: ['giraffe'], }) ) - this.user = this.mockedUser.object + ctx.user = ctx.mockedUser.object - this.NewsletterManager = { + ctx.NewsletterManager = { promises: { unsubscribe: sinon.stub().resolves(), }, } - this.ProjectDeleter = { + ctx.ProjectDeleter = { promises: { deleteUsersProjects: sinon.stub().resolves(), }, } - this.SubscriptionHandler = { + ctx.SubscriptionHandler = { promises: { cancelSubscription: sinon.stub().resolves(), }, } - this.SubscriptionUpdater = { + ctx.SubscriptionUpdater = { promises: { removeUserFromAllGroups: sinon.stub().resolves(), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves(), }, } - this.UserMembershipsHandler = { + ctx.UserMembershipsHandler = { promises: { removeUserFromAllEntities: sinon.stub().resolves(), }, } - this.UserSessionsManager = { + ctx.UserSessionsManager = { promises: { removeSessionsFromRedis: sinon.stub().resolves(), }, } - this.InstitutionsApi = { + ctx.InstitutionsApi = { promises: { deleteAffiliations: sinon.stub().resolves(), }, } - this.UserAuditLogEntry = { + ctx.UserAuditLogEntry = { deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }), } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves() } }, } - this.Feedback = { + ctx.Feedback = { deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }), } - this.OnboardingDataCollectionManager = { + ctx.OnboardingDataCollectionManager = { deleteOnboardingDataCollection: sinon.stub().resolves(), } - this.EmailHandler = { + ctx.EmailHandler = { promises: { sendEmail: sinon.stub().resolves(), }, } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.UserDeleter = SandboxedModule.require(modulePath, { - requires: { - '../../models/User': { User }, - '../../models/DeletedUser': { DeletedUser }, - '../../models/Feedback': { Feedback: this.Feedback }, - '../Newsletter/NewsletterManager': this.NewsletterManager, - './UserSessionsManager': this.UserSessionsManager, - '../Subscription/SubscriptionHandler': this.SubscriptionHandler, - '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater, - '../Subscription/SubscriptionLocator': this.SubscriptionLocator, - '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler, - '../Project/ProjectDeleter': this.ProjectDeleter, - '../Institutions/InstitutionsAPI': this.InstitutionsApi, - '../../models/UserAuditLogEntry': { - UserAuditLogEntry: this.UserAuditLogEntry, - }, - './UserAuditLogHandler': this.UserAuditLogHandler, - '../../infrastructure/Modules': this.Modules, - '../OnboardingDataCollection/OnboardingDataCollectionManager': - this.OnboardingDataCollectionManager, - '../Email/EmailHandler': this.EmailHandler, - }, - }) + vi.doMock('../../../../app/src/models/User', () => ({ + User, + })) + + vi.doMock('../../../../app/src/models/DeletedUser', () => ({ + DeletedUser, + })) + + vi.doMock('../../../../app/src/models/Feedback', () => ({ + Feedback: ctx.Feedback, + })) + + vi.doMock( + '../../../../app/src/Features/Newsletter/NewsletterManager', + () => ({ + default: ctx.NewsletterManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionHandler', + () => ({ + default: ctx.SubscriptionHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionUpdater', + () => ({ + default: ctx.SubscriptionUpdater, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipsHandler', + () => ({ + default: ctx.UserMembershipsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: ctx.ProjectDeleter, + })) + + vi.doMock( + '../../../../app/src/Features/Institutions/InstitutionsAPI', + () => ({ + default: ctx.InstitutionsApi, + }) + ) + + vi.doMock('../../../../app/src/models/UserAuditLogEntry', () => ({ + UserAuditLogEntry: ctx.UserAuditLogEntry, + })) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock( + '../../../../app/src/Features/OnboardingDataCollection/OnboardingDataCollectionManager', + () => ({ + default: ctx.OnboardingDataCollectionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + ctx.UserDeleter = (await import(modulePath)).default }) - afterEach(function () { - this.DeletedUserMock.restore() - this.UserMock.restore() - this.mockedUser.restore() + afterEach(function (ctx) { + ctx.DeletedUserMock.restore() + ctx.UserMock.restore() + ctx.mockedUser.restore() tk.reset() }) describe('deleteUser', function () { - beforeEach(function () { - this.UserMock.expects('findById') - .withArgs(this.userId) + beforeEach(function (ctx) { + ctx.UserMock.expects('findById') + .withArgs(ctx.userId) .chain('exec') - .resolves(this.user) + .resolves(ctx.user) }) describe('when the user can be deleted', function () { - beforeEach(function () { - this.deletedUser = { - user: this.user, + beforeEach(function (ctx) { + ctx.deletedUser = { + user: ctx.user, deleterData: { deletedAt: new Date(), - deletedUserId: this.userId, - deleterIpAddress: this.ipAddress, + deletedUserId: ctx.userId, + deleterIpAddress: ctx.ipAddress, deleterId: undefined, - deletedUserLastLoggedIn: this.user.lastLoggedIn, - deletedUserSignUpDate: this.user.signUpDate, - deletedUserLoginCount: this.user.loginCount, - deletedUserReferralId: this.user.referal_id, - deletedUserReferredUsers: this.user.refered_users, - deletedUserReferredUserCount: this.user.refered_user_count, - deletedUserOverleafId: this.user.overleaf.id, + deletedUserLastLoggedIn: ctx.user.lastLoggedIn, + deletedUserSignUpDate: ctx.user.signUpDate, + deletedUserLoginCount: ctx.user.loginCount, + deletedUserReferralId: ctx.user.referal_id, + deletedUserReferredUsers: ctx.user.refered_users, + deletedUserReferredUserCount: ctx.user.refered_user_count, + deletedUserOverleafId: ctx.user.overleaf.id, }, } }) describe('when only the ip address is passed', function () { - beforeEach(function () { - this.DeletedUserMock.expects('updateOne') + beforeEach(function (ctx) { + ctx.DeletedUserMock.expects('updateOne') .withArgs( - { 'deleterData.deletedUserId': this.userId }, - this.deletedUser, + { 'deleterData.deletedUserId': ctx.userId }, + ctx.deletedUser, { upsert: true } ) .chain('exec') @@ -186,123 +255,123 @@ describe('UserDeleter', function () { }) describe('when unsubscribing in Mailchimp succeeds', function () { - beforeEach(function () { - this.UserMock.expects('deleteOne') - .withArgs({ _id: this.userId }) + beforeEach(function (ctx) { + ctx.UserMock.expects('deleteOne') + .withArgs({ _id: ctx.userId }) .chain('exec') .resolves() }) - it('should find and the user in mongo by its id', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should find and the user in mongo by its id', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) - this.UserMock.verify() + ctx.UserMock.verify() }) - it('should delete the user from mailchimp', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should delete the user from mailchimp', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.NewsletterManager.promises.unsubscribe - ).to.have.been.calledWith(this.user, { delete: true }) + ctx.NewsletterManager.promises.unsubscribe + ).to.have.been.calledWith(ctx.user, { delete: true }) }) - it('should delete all the projects of a user', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should delete all the projects of a user', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.ProjectDeleter.promises.deleteUsersProjects - ).to.have.been.calledWith(this.userId) + ctx.ProjectDeleter.promises.deleteUsersProjects + ).to.have.been.calledWith(ctx.userId) }) - it("should cancel the user's subscription", async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it("should cancel the user's subscription", async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.SubscriptionHandler.promises.cancelSubscription - ).to.have.been.calledWith(this.user) + ctx.SubscriptionHandler.promises.cancelSubscription + ).to.have.been.calledWith(ctx.user) }) - it('should delete user affiliations', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should delete user affiliations', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.InstitutionsApi.promises.deleteAffiliations - ).to.have.been.calledWith(this.userId) + ctx.InstitutionsApi.promises.deleteAffiliations + ).to.have.been.calledWith(ctx.userId) }) - it('should cleanup collabratec access tokens', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should cleanup collabratec access tokens', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cleanupPersonalAccessTokens', - this.userId, + ctx.userId, ['collabratec', 'git_bridge'] ) }) - it('should fire the deleteUser hook for modules', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should fire the deleteUser hook for modules', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'deleteUser', - this.userId + ctx.userId ) }) - it('should stop the user sessions', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should stop the user sessions', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.UserSessionsManager.promises.removeSessionsFromRedis - ).to.have.been.calledWith(this.user) + ctx.UserSessionsManager.promises.removeSessionsFromRedis + ).to.have.been.calledWith(ctx.user) }) - it('should remove user from group subscriptions', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should remove user from group subscriptions', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.SubscriptionUpdater.promises.removeUserFromAllGroups - ).to.have.been.calledWith(this.userId) + ctx.SubscriptionUpdater.promises.removeUserFromAllGroups + ).to.have.been.calledWith(ctx.userId) }) - it('should remove user memberships', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should remove user memberships', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.UserMembershipsHandler.promises.removeUserFromAllEntities - ).to.have.been.calledWith(this.userId) + ctx.UserMembershipsHandler.promises.removeUserFromAllEntities + ).to.have.been.calledWith(ctx.userId) }) - it('rejects if the user is a subscription admin', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.rejects({ + it('rejects if the user is a subscription admin', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.rejects({ _id: 'some-subscription', }) - await expect(this.UserDeleter.promises.deleteUser(this.userId, {})) - .to.be.rejected + await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId, {})).to + .be.rejected }) - it('should create a deletedUser', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should create a deletedUser', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) - this.DeletedUserMock.verify() + ctx.DeletedUserMock.verify() }) describe('email notifications', function () { - it('should email the user', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should email the user', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) const emailOptions = { to: 'bob@bob.com', @@ -310,38 +379,38 @@ describe('UserDeleter', function () { actionDescribed: 'your Overleaf account was deleted', } expect( - this.EmailHandler.promises.sendEmail + ctx.EmailHandler.promises.sendEmail ).to.have.been.calledWith('securityAlert', emailOptions) }) - it('should not email the user with skipEmail === true', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should not email the user with skipEmail === true', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, skipEmail: true, }) - expect(this.EmailHandler.promises.sendEmail).not.to.have.been + expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been .called }) - it('should fail when the email service fails', async function () { - this.EmailHandler.promises.sendEmail = sinon + it('should fail when the email service fails', async function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon .stub() .rejects(new Error('email failed')) await expect( - this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) ).to.be.rejectedWith('email failed') }) describe('with "force: true" option', function () { - it('should succeed when the email service fails', async function () { - this.EmailHandler.promises.sendEmail = sinon + it('should succeed when the email service fails', async function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon .stub() .rejects(new Error('email failed')) await expect( - this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, force: true, }) ).to.be.fulfilled @@ -349,159 +418,148 @@ describe('UserDeleter', function () { }) }) - it('should add an audit log entry', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + it('should add an audit log entry', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) expect( - this.UserAuditLogHandler.promises.addEntry + ctx.UserAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.userId, + ctx.userId, 'delete-account', - this.userId, - this.ipAddress + ctx.userId, + ctx.ipAddress ) }) }) describe('when unsubscribing from mailchimp fails', function () { - beforeEach(function () { - this.NewsletterManager.promises.unsubscribe.rejects( + beforeEach(function (ctx) { + ctx.NewsletterManager.promises.unsubscribe.rejects( new Error('something went wrong') ) }) - it('should return an error and not delete the user', async function () { + it('should return an error and not delete the user', async function (ctx) { await expect( - this.UserDeleter.promises.deleteUser(this.userId, { - ipAddress: this.ipAddress, + ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, }) ).to.be.rejected - this.UserMock.verify() + ctx.UserMock.verify() }) }) describe('when called as a callback', function () { - beforeEach(function () { - this.UserMock.expects('deleteOne') - .withArgs({ _id: this.userId }) + beforeEach(function (ctx) { + ctx.UserMock.expects('deleteOne') + .withArgs({ _id: ctx.userId }) .chain('exec') .resolves() }) - it('should delete the user', function (done) { - this.UserDeleter.deleteUser( - this.userId, - { ipAddress: this.ipAddress }, - err => { - expect(err).not.to.exist - this.UserMock.verify() - this.DeletedUserMock.verify() - done() - } - ) + it('should delete the user', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + ipAddress: ctx.ipAddress, + }) + ctx.UserMock.verify() + ctx.DeletedUserMock.verify() }) }) }) describe('when a user and IP address are specified', function () { - beforeEach(function () { - this.ipAddress = '1.2.3.4' - this.deleterId = new ObjectId() + beforeEach(function (ctx) { + ctx.ipAddress = '1.2.3.4' + ctx.deleterId = new ObjectId() - this.deletedUser.deleterData.deleterIpAddress = this.ipAddress - this.deletedUser.deleterData.deleterId = this.deleterId + ctx.deletedUser.deleterData.deleterIpAddress = ctx.ipAddress + ctx.deletedUser.deleterData.deleterId = ctx.deleterId - this.DeletedUserMock.expects('updateOne') + ctx.DeletedUserMock.expects('updateOne') .withArgs( - { 'deleterData.deletedUserId': this.userId }, - this.deletedUser, + { 'deleterData.deletedUserId': ctx.userId }, + ctx.deletedUser, { upsert: true } ) .chain('exec') .resolves() - this.UserMock.expects('deleteOne') - .withArgs({ _id: this.userId }) + ctx.UserMock.expects('deleteOne') + .withArgs({ _id: ctx.userId }) .chain('exec') .resolves() }) - it('should add the deleted user id and ip address to the deletedUser', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - deleterUser: { _id: this.deleterId }, - ipAddress: this.ipAddress, + it('should add the deleted user id and ip address to the deletedUser', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + deleterUser: { _id: ctx.deleterId }, + ipAddress: ctx.ipAddress, }) - this.DeletedUserMock.verify() + ctx.DeletedUserMock.verify() }) - it('should add an audit log entry', async function () { - await this.UserDeleter.promises.deleteUser(this.userId, { - deleterUser: { _id: this.deleterId }, - ipAddress: this.ipAddress, + it('should add an audit log entry', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + deleterUser: { _id: ctx.deleterId }, + ipAddress: ctx.ipAddress, }) expect( - this.UserAuditLogHandler.promises.addEntry + ctx.UserAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.userId, + ctx.userId, 'delete-account', - this.deleterId, - this.ipAddress + ctx.deleterId, + ctx.ipAddress ) }) describe('when called as a callback', function () { - it('should delete the user', function (done) { - this.UserDeleter.deleteUser( - this.userId, - { - deleterUser: { _id: this.deleterId }, - ipAddress: this.ipAddress, - }, - err => { - expect(err).not.to.exist - this.UserMock.verify() - this.DeletedUserMock.verify() - done() - } - ) + it('should delete the user', async function (ctx) { + await ctx.UserDeleter.promises.deleteUser(ctx.userId, { + deleterUser: { _id: ctx.deleterId }, + ipAddress: ctx.ipAddress, + }) + + ctx.UserMock.verify() + ctx.DeletedUserMock.verify() }) }) }) }) describe('when the user cannot be deleted because they are a subscription admin', function () { - beforeEach(function () { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ + beforeEach(function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ _id: 'some-subscription', }) }) - it('fails with a SubscriptionAdminDeletionError', async function () { + it('fails with a SubscriptionAdminDeletionError', async function (ctx) { await expect( - this.UserDeleter.promises.deleteUser(this.userId) + ctx.UserDeleter.promises.deleteUser(ctx.userId) ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError) }) - it('should not create a deletedUser', async function () { - await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be + it('should not create a deletedUser', async function (ctx) { + await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId)).to.be .rejected - this.DeletedUserMock.verify() + ctx.DeletedUserMock.verify() }) - it('should not remove the user from mongo', async function () { - await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be + it('should not remove the user from mongo', async function (ctx) { + await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId)).to.be .rejected - this.UserMock.verify() + ctx.UserMock.verify() }) }) }) describe('ensureCanDeleteUser', function () { - it('should not return error when user can be deleted', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.resolves(null) + it('should not return error when user can be deleted', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(null) let error try { - await this.UserDeleter.promises.ensureCanDeleteUser(this.user) + await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user) } catch (e) { error = e } finally { @@ -509,13 +567,13 @@ describe('UserDeleter', function () { } }) - it('should return custom error when user is group admin', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ + it('should return custom error when user is group admin', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ _id: '123abc', }) let error try { - await this.UserDeleter.promises.ensureCanDeleteUser(this.user) + await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user) } catch (e) { error = e } finally { @@ -523,13 +581,13 @@ describe('UserDeleter', function () { } }) - it('propagates errors', async function () { - this.SubscriptionLocator.promises.getUsersSubscription.rejects( + it('propagates errors', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription.rejects( new Error('Some error') ) let error try { - await this.UserDeleter.promises.ensureCanDeleteUser(this.user) + await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user) } catch (e) { error = e } finally { @@ -542,8 +600,8 @@ describe('UserDeleter', function () { const userId1 = new ObjectId() const userId2 = new ObjectId() - beforeEach(function () { - this.deletedUsers = [ + beforeEach(function (ctx) { + ctx.deletedUsers = [ { user: { _id: userId1 }, deleterData: { deletedUserId: userId1 }, @@ -556,7 +614,7 @@ describe('UserDeleter', function () { }, ] - this.DeletedUserMock.expects('find') + ctx.DeletedUserMock.expects('find') .withArgs({ 'deleterData.deletedAt': { $lt: new Date(moment().subtract(90, 'days')), @@ -566,9 +624,9 @@ describe('UserDeleter', function () { }, }) .chain('exec') - .resolves(this.deletedUsers) - for (const deletedUser of this.deletedUsers) { - this.DeletedUserMock.expects('findOne') + .resolves(ctx.deletedUsers) + for (const deletedUser of ctx.deletedUsers) { + ctx.DeletedUserMock.expects('findOne') .withArgs({ 'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId, }) @@ -577,18 +635,18 @@ describe('UserDeleter', function () { } }) - it('clears data from all deleted users', async function () { - await this.UserDeleter.promises.expireDeletedUsersAfterDuration() - for (const deletedUser of this.deletedUsers) { + it('clears data from all deleted users', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUsersAfterDuration() + for (const deletedUser of ctx.deletedUsers) { expect(deletedUser.user).to.be.undefined expect(deletedUser.save.called).to.be.true } }) - it('deletes audit logs for all deleted users', async function () { - await this.UserDeleter.promises.expireDeletedUsersAfterDuration() - for (const deletedUser of this.deletedUsers) { - expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({ + it('deletes audit logs for all deleted users', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUsersAfterDuration() + for (const deletedUser of ctx.deletedUsers) { + expect(ctx.UserAuditLogEntry.deleteMany).to.have.been.calledWith({ userId: deletedUser.deleterData.deletedUserId, }) } @@ -596,79 +654,81 @@ describe('UserDeleter', function () { }) describe('expireDeletedUser', function () { - beforeEach(function () { - this.mockedDeletedUser = sinon.mock( + beforeEach(function (ctx) { + ctx.mockedDeletedUser = sinon.mock( new DeletedUser({ - user: this.user, + user: ctx.user, deleterData: { deleterIpAddress: '1.1.1.1', - deletedUserId: this.userId, + deletedUserId: ctx.userId, }, }) ) - this.deletedUser = this.mockedDeletedUser.object + ctx.deletedUser = ctx.mockedDeletedUser.object - this.mockedDeletedUser.expects('save').resolves() + ctx.mockedDeletedUser.expects('save').resolves() - this.DeletedUserMock.expects('findOne') - .withArgs({ 'deleterData.deletedUserId': this.userId }) + ctx.DeletedUserMock.expects('findOne') + .withArgs({ 'deleterData.deletedUserId': ctx.userId }) .chain('exec') - .resolves(this.deletedUser) + .resolves(ctx.deletedUser) }) - afterEach(function () { - this.mockedDeletedUser.restore() + afterEach(function (ctx) { + ctx.mockedDeletedUser.restore() }) - it('should find the user by user ID', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - this.DeletedUserMock.verify() + it('should find the user by user ID', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + ctx.DeletedUserMock.verify() }) - it('should remove the user data from mongo', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - expect(this.deletedUser.user).not.to.exist + it('should remove the user data from mongo', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + expect(ctx.deletedUser.user).not.to.exist }) - it('should remove the IP address from mongo', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - expect(this.deletedUser.deleterData.ipAddress).not.to.exist + it('should remove the IP address from mongo', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + expect(ctx.deletedUser.deleterData.ipAddress).not.to.exist }) - it('should not delete other deleterData fields', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId) + it('should not delete other deleterData fields', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + expect(ctx.deletedUser.deleterData.deletedUserId).to.equal(ctx.userId) }) - it('should save the record to mongo', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - this.mockedDeletedUser.verify() + it('should save the record to mongo', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + ctx.mockedDeletedUser.verify() }) - it('should fire the expireDeletedUser hook for modules', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + it('should fire the expireDeletedUser hook for modules', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'expireDeletedUser', - this.userId + ctx.userId ) }) - it('should delete Feeback', async function () { - await this.UserDeleter.promises.expireDeletedUser(this.userId) - expect(this.Feedback.deleteMany).to.have.been.calledWith({ - userId: this.userId, + it('should delete Feeback', async function (ctx) { + await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId) + expect(ctx.Feedback.deleteMany).to.have.been.calledWith({ + userId: ctx.userId, }) }) describe('when called as a callback', function () { - it('should expire the user', function (done) { - this.UserDeleter.expireDeletedUser(this.userId, err => { - expect(err).not.to.exist - this.DeletedUserMock.verify() - this.mockedDeletedUser.verify() - expect(this.deletedUser.user).not.to.exist - expect(this.deletedUser.deleterData.ipAddress).not.to.exist - done() + it('should expire the user', async function (ctx) { + await new Promise(resolve => { + ctx.UserDeleter.expireDeletedUser(ctx.userId, err => { + expect(err).not.to.exist + ctx.DeletedUserMock.verify() + ctx.mockedDeletedUser.verify() + expect(ctx.deletedUser.user).not.to.exist + expect(ctx.deletedUser.deleterData.ipAddress).not.to.exist + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/User/UserHandler.test.mjs b/services/web/test/unit/src/User/UserHandler.test.mjs index 66dc524395..494e04566b 100644 --- a/services/web/test/unit/src/User/UserHandler.test.mjs +++ b/services/web/test/unit/src/User/UserHandler.test.mjs @@ -1,51 +1,57 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/User/UserHandler.js' -const SandboxedModule = require('sandboxed-module') +import { vi, expect } from 'vitest' +import sinon from 'sinon' +const modulePath = '../../../../app/src/Features/User/UserHandler.mjs' describe('UserHandler', function () { - beforeEach(function () { - this.user = { + beforeEach(async function (ctx) { + ctx.user = { _id: '12390i', email: 'bob@bob.com', remove: sinon.stub().callsArgWith(0), } - this.TeamInvitesHandler = { + ctx.TeamInvitesHandler = { promises: { createTeamInvitesForLegacyInvitedEmail: sinon.stub().resolves(), }, } - this.db = { + ctx.db = { users: { countDocuments: sinon.stub().resolves(2), }, } - this.UserHandler = SandboxedModule.require(modulePath, { - requires: { - '../Subscription/TeamInvitesHandler': this.TeamInvitesHandler, - '../../infrastructure/mongodb': { db: this.db }, - }, - }) + vi.doMock( + '../../../../app/src/Features/Subscription/TeamInvitesHandler', + () => ({ + default: ctx.TeamInvitesHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({ + db: ctx.db, + READ_PREFERENCE_SECONDARY: 'read-preference-secondary', + })) + + ctx.UserHandler = (await import(modulePath)).default }) describe('populateTeamInvites', function () { - beforeEach(async function () { - await this.UserHandler.promises.populateTeamInvites(this.user) + beforeEach(async function (ctx) { + await ctx.UserHandler.promises.populateTeamInvites(ctx.user) }) - it('notifies the user about legacy team invites', function () { - this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail - .calledWith(this.user.email) + it('notifies the user about legacy team invites', function (ctx) { + ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail + .calledWith(ctx.user.email) .should.eq(true) }) }) describe('countActiveUsers', function () { - it('return user count from DB lookup', async function () { - expect(await this.UserHandler.promises.countActiveUsers()).to.equal(2) + it('return user count from DB lookup', async function (ctx) { + expect(await ctx.UserHandler.promises.countActiveUsers()).to.equal(2) }) }) })