diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 445b6fb6b0..7405b3a2e7 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -1,55 +1,56 @@ -const _ = require('lodash') -const OError = require('@overleaf/o-error') -const crypto = require('crypto') -const { setTimeout } = require('timers/promises') -const pProps = require('p-props') -const logger = require('@overleaf/logger') -const { expressify } = require('@overleaf/promise-utils') -const { ObjectId } = require('mongodb-legacy') -const ProjectDeleter = require('./ProjectDeleter') -const ProjectDuplicator = require('./ProjectDuplicator') -const ProjectCreationHandler = require('./ProjectCreationHandler') -const EditorController = require('../Editor/EditorController') -const ProjectHelper = require('./ProjectHelper') -const metrics = require('@overleaf/metrics') -const { User } = require('../../models/User') -const SubscriptionLocator = require('../Subscription/SubscriptionLocator') -const { isPaidSubscription } = require('../Subscription/SubscriptionHelper') -const LimitationsManager = require('../Subscription/LimitationsManager') -const Settings = require('@overleaf/settings') -const AuthorizationManager = require('../Authorization/AuthorizationManager') -const InactiveProjectManager = require('../InactiveData/InactiveProjectManager') -const ProjectUpdateHandler = require('./ProjectUpdateHandler') -const ProjectGetter = require('./ProjectGetter') -const PrivilegeLevels = require('../Authorization/PrivilegeLevels') -const SessionManager = require('../Authentication/SessionManager') -const Sources = require('../Authorization/Sources') -const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') -const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') -const ProjectEntityHandler = require('./ProjectEntityHandler') -const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') -const Features = require('../../infrastructure/Features') -const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler') -const UserController = require('../User/UserController') -const AnalyticsManager = require('../Analytics/AnalyticsManager') -const SplitTestHandler = require('../SplitTests/SplitTestHandler') -const SplitTestSessionHandler = require('../SplitTests/SplitTestSessionHandler') -const FeaturesUpdater = require('../Subscription/FeaturesUpdater') -const SpellingHandler = require('../Spelling/SpellingHandler') -const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper') -const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures') -const InstitutionsGetter = require('../Institutions/InstitutionsGetter') -const ProjectAuditLogHandler = require('./ProjectAuditLogHandler') -const PublicAccessLevels = require('../Authorization/PublicAccessLevels') -const TagsHandler = require('../Tags/TagsHandler') -const TutorialHandler = require('../Tutorial/TutorialHandler') -const UserUpdater = require('../User/UserUpdater') -const Modules = require('../../infrastructure/Modules') -const UserGetter = require('../User/UserGetter') -const { isStandaloneAiAddOnPlanCode } = require('../Subscription/AiHelper') -const SubscriptionController = require('../Subscription/SubscriptionController.js') -const { formatCurrency } = require('../../util/currency') +import _ from 'lodash' +import OError from '@overleaf/o-error' +import crypto from 'node:crypto' +import { setTimeout } from 'node:timers/promises' +import pProps from 'p-props' +import logger from '@overleaf/logger' +import { expressify } from '@overleaf/promise-utils' +import mongodb from 'mongodb-legacy' +import ProjectDeleter from './ProjectDeleter.js' +import ProjectDuplicator from './ProjectDuplicator.js' +import ProjectCreationHandler from './ProjectCreationHandler.js' +import EditorController from '../Editor/EditorController.js' +import ProjectHelper from './ProjectHelper.js' +import metrics from '@overleaf/metrics' +import { User } from '../../models/User.js' +import SubscriptionLocator from '../Subscription/SubscriptionLocator.js' +import { isPaidSubscription } from '../Subscription/SubscriptionHelper.js' +import LimitationsManager from '../Subscription/LimitationsManager.js' +import Settings from '@overleaf/settings' +import AuthorizationManager from '../Authorization/AuthorizationManager.js' +import InactiveProjectManager from '../InactiveData/InactiveProjectManager.js' +import ProjectUpdateHandler from './ProjectUpdateHandler.js' +import ProjectGetter from './ProjectGetter.js' +import PrivilegeLevels from '../Authorization/PrivilegeLevels.js' +import SessionManager from '../Authentication/SessionManager.js' +import Sources from '../Authorization/Sources.js' +import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.js' +import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.js' +import ProjectEntityHandler from './ProjectEntityHandler.js' +import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js' +import Features from '../../infrastructure/Features.js' +import BrandVariationsHandler from '../BrandVariations/BrandVariationsHandler.js' +import UserController from '../User/UserController.mjs' +import AnalyticsManager from '../Analytics/AnalyticsManager.js' +import SplitTestHandler from '../SplitTests/SplitTestHandler.js' +import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js' +import FeaturesUpdater from '../Subscription/FeaturesUpdater.js' +import SpellingHandler from '../Spelling/SpellingHandler.js' +import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js' +import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.js' +import InstitutionsGetter from '../Institutions/InstitutionsGetter.js' +import ProjectAuditLogHandler from './ProjectAuditLogHandler.js' +import PublicAccessLevels from '../Authorization/PublicAccessLevels.js' +import TagsHandler from '../Tags/TagsHandler.js' +import TutorialHandler from '../Tutorial/TutorialHandler.js' +import UserUpdater from '../User/UserUpdater.js' +import Modules from '../../infrastructure/Modules.js' +import UserGetter from '../User/UserGetter.js' +import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js' +import SubscriptionController from '../Subscription/SubscriptionController.js' +import { formatCurrency } from '../../util/currency.js' +const { ObjectId } = mongodb /** * @import { GetProjectsRequest, GetProjectsResponse, Project } from "./types" */ @@ -1252,4 +1253,4 @@ const ProjectController = { _getAddonPrices: _ProjectController._getAddonPrices, } -module.exports = ProjectController +export default ProjectController diff --git a/services/web/app/src/Features/User/UserController.mjs b/services/web/app/src/Features/User/UserController.mjs index b767dcd4a1..257735cad5 100644 --- a/services/web/app/src/Features/User/UserController.mjs +++ b/services/web/app/src/Features/User/UserController.mjs @@ -1,28 +1,26 @@ -const UserHandler = require('./UserHandler') -const UserDeleter = require('./UserDeleter') -const UserGetter = require('./UserGetter') -const { User } = require('../../models/User') -const NewsletterManager = require('../Newsletter/NewsletterManager') -const logger = require('@overleaf/logger') -const metrics = require('@overleaf/metrics') -const AuthenticationManager = require('../Authentication/AuthenticationManager') -const SessionManager = require('../Authentication/SessionManager') -const Features = require('../../infrastructure/Features') -const UserAuditLogHandler = require('./UserAuditLogHandler') -const UserSessionsManager = require('./UserSessionsManager') -const UserUpdater = require('./UserUpdater') -const Errors = require('../Errors/Errors') -const HttpErrorHandler = require('../Errors/HttpErrorHandler') -const OError = require('@overleaf/o-error') -const EmailHandler = require('../Email/EmailHandler') -const UrlHelper = require('../Helpers/UrlHelper') -const { promisify } = require('util') -const { expressify } = require('@overleaf/promise-utils') -const { - acceptsJson, -} = require('../../infrastructure/RequestContentTypeDetection') -const Modules = require('../../infrastructure/Modules') -const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +import UserHandler from './UserHandler.js' +import UserDeleter from './UserDeleter.js' +import UserGetter from './UserGetter.js' +import { User } from '../../models/User.js' +import NewsletterManager from '../Newsletter/NewsletterManager.js' +import logger from '@overleaf/logger' +import metrics from '@overleaf/metrics' +import AuthenticationManager from '../Authentication/AuthenticationManager.js' +import SessionManager from '../Authentication/SessionManager.js' +import Features from '../../infrastructure/Features.js' +import UserAuditLogHandler from './UserAuditLogHandler.js' +import UserSessionsManager from './UserSessionsManager.js' +import UserUpdater from './UserUpdater.js' +import Errors from '../Errors/Errors.js' +import HttpErrorHandler from '../Errors/HttpErrorHandler.js' +import OError from '@overleaf/o-error' +import EmailHandler from '../Email/EmailHandler.js' +import UrlHelper from '../Helpers/UrlHelper.js' +import { promisify } from 'node:util' +import { expressify } from '@overleaf/promise-utils' +import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.js' +import Modules from '../../infrastructure/Modules.js' +import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.js' async function _sendSecurityAlertClearedSessions(user) { const emailOptions = { @@ -506,7 +504,7 @@ async function expireDeletedUsersAfterDuration(req, res, next) { res.sendStatus(204) } -module.exports = { +export default { clearSessions: expressify(clearSessions), changePassword: expressify(changePassword), tryDeleteUser: expressify(tryDeleteUser), diff --git a/services/web/test/unit/src/Project/ProjectController.test.mjs b/services/web/test/unit/src/Project/ProjectController.test.mjs index 02a75b213a..e0c9a7b4aa 100644 --- a/services/web/test/unit/src/Project/ProjectController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectController.test.mjs @@ -1,26 +1,27 @@ -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const sinon = require('sinon') -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') +import { beforeEach, describe, it, vi, expect } from 'vitest' + +import path from 'path' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +const { ObjectId } = mongodb const MODULE_PATH = path.join( - __dirname, + import.meta.dirname, '../../../../app/src/Features/Project/ProjectController' ) describe('ProjectController', function () { - beforeEach(function () { - this.project_id = new ObjectId('abcdefabcdefabcdefabcdef') + beforeEach(async function (ctx) { + ctx.project_id = new ObjectId('abcdefabcdefabcdefabcdef') - this.user = { + ctx.user = { _id: new ObjectId('123456123456123456123456'), email: 'test@overleaf.com', first_name: 'bjkdsjfk', features: {}, emails: [{ email: 'test@overleaf.com' }], } - this.settings = { + ctx.settings = { apis: { chat: { url: 'chat.com', @@ -31,106 +32,106 @@ describe('ProjectController', function () { plans: [], features: {}, } - this.brandVariationDetails = { + ctx.brandVariationDetails = { id: '12', active: true, brand_name: 'The journal', home_url: 'http://www.thejournal.com/', publish_menu_link_html: 'Submit your paper to the The Journal', } - this.token = 'some-token' - this.ProjectDeleter = { + ctx.token = 'some-token' + ctx.ProjectDeleter = { promises: { deleteProject: sinon.stub().resolves(), restoreProject: sinon.stub().resolves(), }, findArchivedProjects: sinon.stub(), } - this.ProjectDuplicator = { + ctx.ProjectDuplicator = { promises: { - duplicate: sinon.stub().resolves({ _id: this.project_id }), + duplicate: sinon.stub().resolves({ _id: ctx.project_id }), }, } - this.ProjectCreationHandler = { + ctx.ProjectCreationHandler = { promises: { - createExampleProject: sinon.stub().resolves({ _id: this.project_id }), - createBasicProject: sinon.stub().resolves({ _id: this.project_id }), + createExampleProject: sinon.stub().resolves({ _id: ctx.project_id }), + createBasicProject: sinon.stub().resolves({ _id: ctx.project_id }), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves(), }, } - this.SubscriptionController = { + ctx.SubscriptionController = { promises: { getRecommendedCurrency: sinon.stub().resolves({ currency: 'USD' }), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { hasPaidSubscription: sinon.stub(), promises: { userIsMemberOfGroupSubscription: sinon.stub().resolves(false), }, } - this.TagsHandler = { + ctx.TagsHandler = { promises: { getTagsForProject: sinon.stub().resolves([ { name: 'test', - project_ids: [this.project_id], + project_ids: [ctx.project_id], }, ]), }, addProjectToTags: sinon.stub(), } - this.UserModel = { + ctx.UserModel = { findById: sinon.stub().returns({ exec: sinon.stub().resolves() }), updateOne: sinon.stub().returns({ exec: sinon.stub().resolves() }), } - this.AuthorizationManager = { + ctx.AuthorizationManager = { promises: { getPrivilegeLevelForProject: sinon.stub(), }, isRestrictedUser: sinon.stub().returns(false), } - this.EditorController = { + ctx.EditorController = { promises: { renameProject: sinon.stub().resolves(), }, } - this.InactiveProjectManager = { + ctx.InactiveProjectManager = { promises: { reactivateProjectIfRequired: sinon.stub() }, } - this.ProjectUpdateHandler = { + ctx.ProjectUpdateHandler = { promises: { markAsOpened: sinon.stub().resolves(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { findAllUsersProjects: sinon.stub().resolves(), getProject: sinon.stub().resolves(), }, } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchived: sinon.stub(), isTrashed: sinon.stub(), isArchivedOrTrashed: sinon.stub(), getAllowedImagesForUser: sinon.stub().returns([]), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.user), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.user), isUserLoggedIn: sinon.stub().returns(true), } - this.UserController = { + ctx.UserController = { logout: sinon.stub(), } - this.TokenAccessHandler = { - getRequestToken: sinon.stub().returns(this.token), + ctx.TokenAccessHandler = { + getRequestToken: sinon.stub().returns(ctx.token), } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { userIsTokenMember: sinon.stub().resolves(false), isUserInvitedMemberOfProject: sinon.stub().resolves(true), @@ -138,47 +139,45 @@ describe('ProjectController', function () { isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(true), }, } - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { setCollaboratorPrivilegeLevel: sinon.stub().resolves(), }, } - this.ProjectEntityHandler = {} - this.UserGetter = { + ctx.ProjectEntityHandler = {} + ctx.UserGetter = { getUserFullEmails: sinon.stub().yields(null, []), getUser: sinon.stub().resolves({ lastLoginIp: '192.170.18.2' }), promises: { getUserFeatures: sinon.stub().resolves(null, { collaborators: 1 }), }, } - this.Features = { + ctx.Features = { hasFeature: sinon.stub(), } - this.FeaturesUpdater = { + ctx.FeaturesUpdater = { featuresEpochIsCurrent: sinon.stub().returns(true), promises: { - refreshFeatures: sinon.stub().resolves(this.user), + refreshFeatures: sinon.stub().resolves(ctx.user), }, } - this.BrandVariationsHandler = { + ctx.BrandVariationsHandler = { promises: { - getBrandVariationById: sinon - .stub() - .resolves(this.brandVariationDetails), + getBrandVariationById: sinon.stub().resolves(ctx.brandVariationDetails), }, } - this.TpdsProjectFlusher = { + ctx.TpdsProjectFlusher = { promises: { flushProjectToTpdsIfNeeded: sinon.stub().resolves(), }, } - this.Metrics = { + ctx.Metrics = { Timer: class { done() {} }, inc: sinon.stub(), } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), @@ -186,116 +185,310 @@ describe('ProjectController', function () { }, getAssignment: sinon.stub().yields(null, { variant: 'default' }), } - this.SplitTestSessionHandler = { + ctx.SplitTestSessionHandler = { promises: { sessionMaintenance: sinon.stub().resolves(), }, } - this.InstitutionsFeatures = { + ctx.InstitutionsFeatures = { promises: { hasLicence: sinon.stub().resolves(false), }, } - this.InstitutionsGetter = { + ctx.InstitutionsGetter = { promises: { getCurrentAffiliations: sinon.stub().resolves([]), }, } - this.SurveyHandler = { + ctx.SurveyHandler = { getSurvey: sinon.stub().yields(null, {}), } - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.TutorialHandler = { + ctx.TutorialHandler = { getInactiveTutorials: sinon.stub().returns([]), } - this.OnboardingDataCollectionManager = { + ctx.OnboardingDataCollectionManager = { getOnboardingDataValue: sinon.stub().resolves(null), } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves() } }, } - this.ProjectController = SandboxedModule.require(MODULE_PATH, { - requires: { - 'mongodb-legacy': { ObjectId }, - '@overleaf/settings': this.settings, - '@overleaf/metrics': this.Metrics, - '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, - '../SplitTests/SplitTestHandler': this.SplitTestHandler, - '../SplitTests/SplitTestSessionHandler': this.SplitTestSessionHandler, - './ProjectDeleter': this.ProjectDeleter, - './ProjectDuplicator': this.ProjectDuplicator, - './ProjectCreationHandler': this.ProjectCreationHandler, - '../Editor/EditorController': this.EditorController, - '../User/UserController': this.UserController, - './ProjectHelper': this.ProjectHelper, - '../Subscription/SubscriptionLocator': this.SubscriptionLocator, - '../Subscription/SubscriptionController': this.SubscriptionController, - '../Subscription/LimitationsManager': this.LimitationsManager, - '../Tags/TagsHandler': this.TagsHandler, - '../../models/User': { User: this.UserModel }, - '../../models/Subscription': {}, - '../Authorization/AuthorizationManager': this.AuthorizationManager, - '../InactiveData/InactiveProjectManager': this.InactiveProjectManager, - './ProjectUpdateHandler': this.ProjectUpdateHandler, - './ProjectGetter': this.ProjectGetter, - './ProjectDetailsHandler': this.ProjectDetailsHandler, - '../Authentication/SessionManager': this.SessionManager, - '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, - '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, - './ProjectEntityHandler': this.ProjectEntityHandler, - '../../infrastructure/Features': this.Features, - '../Subscription/FeaturesUpdater': this.FeaturesUpdater, - '../User/UserGetter': this.UserGetter, - '../BrandVariations/BrandVariationsHandler': - this.BrandVariationsHandler, - '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher, - '../../models/Project': {}, - '../Analytics/AnalyticsManager': { + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.Metrics, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestSessionHandler', + () => ({ + default: ctx.SplitTestSessionHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: ctx.ProjectDeleter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectDuplicator', () => ({ + default: ctx.ProjectDuplicator, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + vi.doMock('../../../../app/src/Features/User/UserController', () => ({ + default: ctx.UserController, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionController', + () => ({ + default: ctx.SubscriptionController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.UserModel, + })) + + vi.doMock('../../../../app/src/models/Subscription', () => ({})) + + vi.doMock( + '../../../../app/src/Features/Authorization/AuthorizationManager', + () => ({ + default: ctx.AuthorizationManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/InactiveData/InactiveProjectManager', + () => ({ + default: ctx.InactiveProjectManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectUpdateHandler', + () => ({ + default: ctx.ProjectUpdateHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/FeaturesUpdater', + () => ({ + default: ctx.FeaturesUpdater, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/BrandVariations/BrandVariationsHandler', + () => ({ + default: ctx.BrandVariationsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher', + () => ({ + default: ctx.TpdsProjectFlusher, + }) + ) + + vi.doMock('../../../../app/src/models/Project', () => ({})) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: { recordEventForUserInBackground: () => {}, }, - '../Subscription/SubscriptionViewModelBuilder': - this.SubscriptionViewModelBuilder, - '../Spelling/SpellingHandler': { - promises: { - getUserDictionary: sinon.stub().resolves([]), - }, - }, - '../Institutions/InstitutionsFeatures': this.InstitutionsFeatures, - '../Institutions/InstitutionsGetter': this.InstitutionsGetter, - '../Survey/SurveyHandler': this.SurveyHandler, - './ProjectAuditLogHandler': this.ProjectAuditLogHandler, - '../Tutorial/TutorialHandler': this.TutorialHandler, - '../OnboardingDataCollection/OnboardingDataCollectionManager': - this.OnboardingDataCollectionManager, - '../User/UserUpdater': { - promises: { - updateUser: sinon.stub().resolves(), - }, - }, - '../../infrastructure/Modules': this.Modules, - }, - }) + }) + ) - this.projectName = '£12321jkj9ujkljds' - this.req = { + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder', + () => ({ + default: ctx.SubscriptionViewModelBuilder, + }) + ) + + vi.doMock('../../../../app/src/Features/Spelling/SpellingHandler', () => ({ + default: { + promises: { + getUserDictionary: sinon.stub().resolves([]), + }, + }, + })) + + vi.doMock( + '../../../../app/src/Features/Institutions/InstitutionsFeatures', + () => ({ + default: ctx.InstitutionsFeatures, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Institutions/InstitutionsGetter', + () => ({ + default: ctx.InstitutionsGetter, + }) + ) + + vi.doMock('../../../../app/src/Features/Survey/SurveyHandler', () => ({ + default: ctx.SurveyHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Tutorial/TutorialHandler', () => ({ + default: ctx.TutorialHandler, + })) + + vi.doMock( + '../../../../app/src/Features/OnboardingDataCollection/OnboardingDataCollectionManager', + () => ({ + default: ctx.OnboardingDataCollectionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: { + promises: { + updateUser: sinon.stub().resolves(), + }, + }, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + ctx.ProjectController = (await import(MODULE_PATH)).default + + ctx.projectName = '£12321jkj9ujkljds' + ctx.req = { query: {}, params: { - Project_id: this.project_id, + Project_id: ctx.project_id, }, headers: {}, connection: { remoteAddress: '192.170.18.1', }, session: { - user: this.user, + user: ctx.user, }, body: { - projectName: this.projectName, + projectName: ctx.projectName, }, i18n: { translate() {}, @@ -303,7 +496,7 @@ describe('ProjectController', function () { ip: '192.170.18.1', capabilitySet: new Set(['chat']), } - this.res = { + ctx.res = { locals: { jsPath: 'js path here', }, @@ -312,257 +505,288 @@ describe('ProjectController', function () { }) describe('updateProjectSettings', function () { - it('should update the name', function (done) { - this.EditorController.promises.renameProject = sinon.stub().resolves() - this.req.body = { name: (this.name = 'New name') } - this.res.sendStatus = code => { - this.EditorController.promises.renameProject - .calledWith(this.project_id, this.name) - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectSettings(this.req, this.res) + it('should update the name', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.renameProject = sinon.stub().resolves() + ctx.req.body = { name: (ctx.projectName = 'New name') } + ctx.res.sendStatus = code => { + ctx.EditorController.promises.renameProject + .calledWith(ctx.project_id, ctx.projectName) + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectSettings(ctx.req, ctx.res) + }) }) - it('should update the compiler', function (done) { - this.EditorController.promises.setCompiler = sinon.stub().resolves() - this.req.body = { compiler: (this.compiler = 'pdflatex') } - this.res.sendStatus = code => { - this.EditorController.promises.setCompiler - .calledWith(this.project_id, this.compiler) - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectSettings(this.req, this.res) + it('should update the compiler', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.setCompiler = sinon.stub().resolves() + ctx.req.body = { compiler: (ctx.compiler = 'pdflatex') } + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setCompiler + .calledWith(ctx.project_id, ctx.compiler) + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectSettings(ctx.req, ctx.res) + }) }) - it('should update the imageName', function (done) { - this.EditorController.promises.setImageName = sinon.stub().resolves() - this.req.body = { imageName: (this.imageName = 'texlive-1234.5') } - this.res.sendStatus = code => { - this.EditorController.promises.setImageName - .calledWith(this.project_id, this.imageName) - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectSettings(this.req, this.res) + it('should update the imageName', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.setImageName = sinon.stub().resolves() + ctx.req.body = { imageName: (ctx.imageName = 'texlive-1234.5') } + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setImageName + .calledWith(ctx.project_id, ctx.imageName) + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectSettings(ctx.req, ctx.res) + }) }) - it('should update the spell check language', function (done) { - this.EditorController.promises.setSpellCheckLanguage = sinon - .stub() - .resolves() - this.req.body = { spellCheckLanguage: (this.languageCode = 'fr') } - this.res.sendStatus = code => { - this.EditorController.promises.setSpellCheckLanguage - .calledWith(this.project_id, this.languageCode) - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectSettings(this.req, this.res) + it('should update the spell check language', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.setSpellCheckLanguage = sinon + .stub() + .resolves() + ctx.req.body = { spellCheckLanguage: (ctx.languageCode = 'fr') } + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setSpellCheckLanguage + .calledWith(ctx.project_id, ctx.languageCode) + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectSettings(ctx.req, ctx.res) + }) }) - it('should update the root doc', function (done) { - this.EditorController.promises.setRootDoc = sinon.stub().resolves() - this.req.body = { rootDocId: (this.rootDocId = 'root-doc-id') } - this.res.sendStatus = code => { - this.EditorController.promises.setRootDoc - .calledWith(this.project_id, this.rootDocId) - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectSettings(this.req, this.res) + it('should update the root doc', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.setRootDoc = sinon.stub().resolves() + ctx.req.body = { rootDocId: (ctx.rootDocId = 'root-doc-id') } + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setRootDoc + .calledWith(ctx.project_id, ctx.rootDocId) + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectSettings(ctx.req, ctx.res) + }) }) }) describe('updateProjectAdminSettings', function () { - it('should update the public access level', function (done) { - this.Features.hasFeature.withArgs('link-sharing').returns(true) - this.EditorController.promises.setPublicAccessLevel = sinon + it('should update the public access level', async function (ctx) { + ctx.Features.hasFeature.withArgs('link-sharing').returns(true) + + ctx.EditorController.promises.setPublicAccessLevel = sinon .stub() .resolves() - this.req.body = { + ctx.req.body = { publicAccessLevel: 'readOnly', } - this.res.sendStatus = code => { - this.EditorController.promises.setPublicAccessLevel - .calledWith(this.project_id, 'readOnly') - .should.equal(true) - code.should.equal(204) - done() - } - this.ProjectController.updateProjectAdminSettings(this.req, this.res) + await new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setPublicAccessLevel + .calledWith(ctx.project_id, 'readOnly') + .should.equal(true) + code.should.equal(204) + resolve() + } + ctx.ProjectController.updateProjectAdminSettings(ctx.req, ctx.res) + }) }) - it('should record the change in the project audit log', function (done) { - this.Features.hasFeature.withArgs('link-sharing').returns(true) - this.EditorController.promises.setPublicAccessLevel = sinon + it('should record the change in the project audit log', async function (ctx) { + ctx.Features.hasFeature.withArgs('link-sharing').returns(true) + ctx.EditorController.promises.setPublicAccessLevel = sinon .stub() .resolves() - this.req.body = { + ctx.req.body = { publicAccessLevel: 'readOnly', } - this.res.sendStatus = code => { - this.ProjectAuditLogHandler.promises.addEntry - .calledWith( - this.project_id, - 'toggle-access-level', - this.user._id, - this.req.ip, - { - publicAccessLevel: 'readOnly', - status: 'OK', - } + await new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.ProjectAuditLogHandler.promises.addEntry + .calledWith( + ctx.project_id, + 'toggle-access-level', + ctx.user._id, + ctx.req.ip, + { + publicAccessLevel: 'readOnly', + status: 'OK', + } + ) + .should.equal(true) + resolve() + } + ctx.ProjectController.updateProjectAdminSettings(ctx.req, ctx.res) + }) + }) + + it('should refuse to update the public access level when link sharing is disabled', async function (ctx) { + ctx.Features.hasFeature.withArgs('link-sharing').returns(false) + ctx.EditorController.promises.setPublicAccessLevel = sinon + .stub() + .resolves() + ctx.req.body = { + publicAccessLevel: 'readOnly', + } + await new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.EditorController.promises.setPublicAccessLevel.called.should.equal( + false ) - .should.equal(true) - done() - } - this.ProjectController.updateProjectAdminSettings(this.req, this.res) - }) - - it('should refuse to update the public access level when link sharing is disabled', function (done) { - this.Features.hasFeature.withArgs('link-sharing').returns(false) - this.EditorController.promises.setPublicAccessLevel = sinon - .stub() - .resolves() - this.req.body = { - publicAccessLevel: 'readOnly', - } - this.res.sendStatus = code => { - this.EditorController.promises.setPublicAccessLevel.called.should.equal( - false - ) - code.should.equal(403) // Forbidden - done() - } - this.ProjectController.updateProjectAdminSettings(this.req, this.res) + code.should.equal(403) // Forbidden + resolve() + } + ctx.ProjectController.updateProjectAdminSettings(ctx.req, ctx.res) + }) }) }) describe('deleteProject', function () { - it('should call the project deleter', function (done) { - this.res.sendStatus = code => { - this.ProjectDeleter.promises.deleteProject - .calledWith(this.project_id, { - deleterUser: this.user, - ipAddress: this.req.ip, - }) - .should.equal(true) - code.should.equal(200) - done() - } - this.ProjectController.deleteProject(this.req, this.res) + it('should call the project deleter', async function (ctx) { + await new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.ProjectDeleter.promises.deleteProject + .calledWith(ctx.project_id, { + deleterUser: ctx.user, + ipAddress: ctx.req.ip, + }) + .should.equal(true) + code.should.equal(200) + resolve() + } + ctx.ProjectController.deleteProject(ctx.req, ctx.res) + }) }) }) describe('restoreProject', function () { - it('should tell the project deleter', function (done) { - this.res.sendStatus = code => { - this.ProjectDeleter.promises.restoreProject - .calledWith(this.project_id) - .should.equal(true) - code.should.equal(200) - done() - } - this.ProjectController.restoreProject(this.req, this.res) + it('should tell the project deleter', async function (ctx) { + await new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.ProjectDeleter.promises.restoreProject + .calledWith(ctx.project_id) + .should.equal(true) + code.should.equal(200) + resolve() + } + ctx.ProjectController.restoreProject(ctx.req, ctx.res) + }) }) }) describe('cloneProject', function () { - it('should call the project duplicator', function (done) { - this.res.json = json => { - this.ProjectDuplicator.promises.duplicate - .calledWith(this.user, this.project_id, this.projectName) - .should.equal(true) - json.project_id.should.equal(this.project_id) - done() - } - this.ProjectController.cloneProject(this.req, this.res) + it('should call the project duplicator', async function (ctx) { + await new Promise(resolve => { + ctx.res.json = json => { + ctx.ProjectDuplicator.promises.duplicate + .calledWith(ctx.user, ctx.project_id, ctx.projectName) + .should.equal(true) + json.project_id.should.equal(ctx.project_id) + resolve() + } + ctx.ProjectController.cloneProject(ctx.req, ctx.res) + }) }) }) describe('newProject', function () { - it('should call the projectCreationHandler with createExampleProject', function (done) { - this.req.body.template = 'example' - this.res.json = json => { - this.ProjectCreationHandler.promises.createExampleProject - .calledWith(this.user._id, this.projectName) - .should.equal(true) - this.ProjectCreationHandler.promises.createBasicProject.called.should.equal( - false - ) - done() - } - this.ProjectController.newProject(this.req, this.res) + it('should call the projectCreationHandler with createExampleProject', async function (ctx) { + await new Promise(resolve => { + ctx.req.body.template = 'example' + ctx.res.json = json => { + ctx.ProjectCreationHandler.promises.createExampleProject + .calledWith(ctx.user._id, ctx.projectName) + .should.equal(true) + ctx.ProjectCreationHandler.promises.createBasicProject.called.should.equal( + false + ) + resolve() + } + ctx.ProjectController.newProject(ctx.req, ctx.res) + }) }) - it('should call the projectCreationHandler with createBasicProject', function (done) { - this.req.body.template = 'basic' - this.res.json = json => { - this.ProjectCreationHandler.promises.createExampleProject.called.should.equal( - false - ) - this.ProjectCreationHandler.promises.createBasicProject - .calledWith(this.user._id, this.projectName) - .should.equal(true) - done() - } - this.ProjectController.newProject(this.req, this.res) + it('should call the projectCreationHandler with createBasicProject', async function (ctx) { + await new Promise(resolve => { + ctx.req.body.template = 'basic' + ctx.res.json = json => { + ctx.ProjectCreationHandler.promises.createExampleProject.called.should.equal( + false + ) + ctx.ProjectCreationHandler.promises.createBasicProject + .calledWith(ctx.user._id, ctx.projectName) + .should.equal(true) + resolve() + } + ctx.ProjectController.newProject(ctx.req, ctx.res) + }) }) }) describe('renameProject', function () { - beforeEach(function () { - this.newProjectName = 'my supper great new project' - this.req.body.newProjectName = this.newProjectName + beforeEach(function (ctx) { + ctx.newProjectName = 'my supper great new project' + ctx.req.body.newProjectName = ctx.newProjectName }) - it('should call the editor controller', function (done) { - this.EditorController.promises.renameProject.resolves() - this.res.sendStatus = code => { - code.should.equal(200) - this.EditorController.promises.renameProject - .calledWith(this.project_id, this.newProjectName) - .should.equal(true) - done() - } - this.ProjectController.renameProject(this.req, this.res) + it('should call the editor controller', async function (ctx) { + await new Promise(resolve => { + ctx.EditorController.promises.renameProject.resolves() + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.EditorController.promises.renameProject + .calledWith(ctx.project_id, ctx.newProjectName) + .should.equal(true) + resolve() + } + ctx.ProjectController.renameProject(ctx.req, ctx.res) + }) }) - it('should send an error to next() if there is a problem', function (done) { - let error - this.EditorController.promises.renameProject.rejects( - (error = new Error('problem')) - ) - const next = e => { - e.should.equal(error) - done() - } - this.ProjectController.renameProject(this.req, this.res, next) + it('should send an error to next() if there is a problem', async function (ctx) { + await new Promise(resolve => { + let error + ctx.EditorController.promises.renameProject.rejects( + (error = new Error('problem')) + ) + const next = e => { + e.should.equal(error) + resolve() + } + ctx.ProjectController.renameProject(ctx.req, ctx.res, next) + }) }) }) describe('loadEditor', function () { - beforeEach(function () { - this.settings.editorIsOpen = true - this.project = { + beforeEach(function (ctx) { + ctx.settings.editorIsOpen = true + ctx.project = { name: 'my proj', _id: '213123kjlkj', owner_ref: '59fc84d5fbea77482d436e1b', } - this.brandedProject = { + ctx.brandedProject = { name: 'my branded proj', _id: '3252332', owner_ref: '59fc84d5fbea77482d436e1b', brandVariationId: '12', } - this.user = { - _id: this.user._id, + ctx.user = { + _id: ctx.user._id, ace: { fontSize: 'massive', theme: 'sexy', @@ -573,243 +797,285 @@ describe('ProjectController', function () { zotero: { encrypted: 'bbbb' }, }, } - this.ProjectGetter.promises.getProject.resolves(this.project) - this.UserModel.findById.returns({ - exec: sinon.stub().resolves(this.user), + ctx.ProjectGetter.promises.getProject.resolves(ctx.project) + ctx.UserModel.findById.returns({ + exec: sinon.stub().resolves(ctx.user), }) - this.SubscriptionLocator.promises.getUsersSubscription.resolves({}) - this.AuthorizationManager.promises.getPrivilegeLevelForProject.resolves( + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({}) + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.resolves( 'owner' ) - this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() - this.InactiveProjectManager.promises.reactivateProjectIfRequired.resolves() - this.ProjectUpdateHandler.promises.markAsOpened.resolves() + ctx.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() + ctx.InactiveProjectManager.promises.reactivateProjectIfRequired.resolves() + ctx.ProjectUpdateHandler.promises.markAsOpened.resolves() }) - it('should render the project/ide-react page', function (done) { - this.res.render = (pageName, opts) => { - pageName.should.equal('project/ide-react') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should render the project/ide-react page', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + pageName.should.equal('project/ide-react') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should redirect to domain capture page', function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.SplitTestHandler.promises.getAssignment - .withArgs(this.req, this.res, 'domain-capture-redirect') + it('should redirect to domain capture page', async function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SplitTestHandler.promises.getAssignment + .withArgs(ctx.req, ctx.res, 'domain-capture-redirect') .resolves({ variant: 'enabled' }) - this.Modules.promises.hooks.fire - .withArgs('findDomainCaptureGroupUserCouldBePartOf', this.user._id) + ctx.Modules.promises.hooks.fire + .withArgs('findDomainCaptureGroupUserCouldBePartOf', ctx.user._id) .resolves([{ _id: new ObjectId(), managedUsersEnabled: true }]) - this.res.redirect = url => { - url.should.equal('/domain-capture') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + await new Promise(resolve => { + ctx.res.redirect = url => { + url.should.equal('/domain-capture') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should add user', function (done) { - this.res.render = (pageName, opts) => { - opts.user.email.should.equal(this.user.email) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should add user', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.user.email.should.equal(ctx.user.email) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should sanitize refProviders', function (done) { - this.res.render = (_pageName, opts) => { - expect(opts.user.refProviders).to.deep.equal({ - mendeley: true, - zotero: true, - }) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should sanitize refProviders', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (_pageName, opts) => { + expect(opts.user.refProviders).to.deep.equal({ + mendeley: true, + zotero: true, + }) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should add on userSettings', function (done) { - this.res.render = (pageName, opts) => { - opts.userSettings.fontSize.should.equal(this.user.ace.fontSize) - opts.userSettings.editorTheme.should.equal(this.user.ace.theme) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should add on userSettings', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.userSettings.fontSize.should.equal(ctx.user.ace.fontSize) + opts.userSettings.editorTheme.should.equal(ctx.user.ace.theme) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should add isRestrictedTokenMember', function (done) { - this.AuthorizationManager.isRestrictedUser.returns(false) - this.res.render = (pageName, opts) => { - opts.isRestrictedTokenMember.should.exist - opts.isRestrictedTokenMember.should.equal(false) - return done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should add isRestrictedTokenMember', async function (ctx) { + await new Promise(resolve => { + ctx.AuthorizationManager.isRestrictedUser.returns(false) + ctx.res.render = (pageName, opts) => { + opts.isRestrictedTokenMember.should.exist + opts.isRestrictedTokenMember.should.equal(false) + return resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should set isRestrictedTokenMember when appropriate', function (done) { - this.AuthorizationManager.isRestrictedUser.returns(true) - this.res.render = (pageName, opts) => { - opts.isRestrictedTokenMember.should.exist - opts.isRestrictedTokenMember.should.equal(true) - return done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set isRestrictedTokenMember when appropriate', async function (ctx) { + await new Promise(resolve => { + ctx.AuthorizationManager.isRestrictedUser.returns(true) + ctx.res.render = (pageName, opts) => { + opts.isRestrictedTokenMember.should.exist + opts.isRestrictedTokenMember.should.equal(true) + return resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should invoke the session maintenance for logged in user', function (done) { - this.res.render = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req, - this.user - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should invoke the session maintenance for logged in user', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req, + ctx.user + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should invoke the session maintenance for anonymous user', function (done) { - this.SessionManager.getLoggedInUserId.returns(null) - this.res.render = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should invoke the session maintenance for anonymous user', async function (ctx) { + await new Promise(resolve => { + ctx.SessionManager.getLoggedInUserId.returns(null) + ctx.res.render = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should render the closed page if the editor is closed', function (done) { - this.settings.editorIsOpen = false - this.res.render = (pageName, opts) => { - pageName.should.equal('general/closed') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should render the closed page if the editor is closed', async function (ctx) { + await new Promise(resolve => { + ctx.settings.editorIsOpen = false + ctx.res.render = (pageName, opts) => { + pageName.should.equal('general/closed') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should not render the page if the project can not be accessed', function (done) { - this.AuthorizationManager.promises.getPrivilegeLevelForProject = sinon - .stub() - .resolves(null) - this.res.sendStatus = (resCode, opts) => { - resCode.should.equal(401) - this.AuthorizationManager.promises.getPrivilegeLevelForProject.should.have.been.calledWith( - this.user._id, - this.project_id, - 'some-token' - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should not render the page if the project can not be accessed', async function (ctx) { + await new Promise(resolve => { + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject = sinon + .stub() + .resolves(null) + ctx.res.sendStatus = (resCode, opts) => { + resCode.should.equal(401) + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.should.have.been.calledWith( + ctx.user._id, + ctx.project_id, + 'some-token' + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should reactivateProjectIfRequired', function (done) { - this.res.render = (pageName, opts) => { - this.InactiveProjectManager.promises.reactivateProjectIfRequired - .calledWith(this.project_id) - .should.equal(true) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should reactivateProjectIfRequired', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + ctx.InactiveProjectManager.promises.reactivateProjectIfRequired + .calledWith(ctx.project_id) + .should.equal(true) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should mark user as active', function (done) { - this.res.render = (pageName, opts) => { - expect(this.UserModel.updateOne).to.have.been.calledOnce - expect(this.UserModel.updateOne.args[0][0]).to.deep.equal({ - _id: new ObjectId(this.user._id), - }) - expect(this.UserModel.updateOne.args[0][1].$set.lastActive).to.exist - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should mark user as active', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(ctx.UserModel.updateOne).to.have.been.calledOnce + expect(ctx.UserModel.updateOne.args[0][0]).to.deep.equal({ + _id: new ObjectId(ctx.user._id), + }) + expect(ctx.UserModel.updateOne.args[0][1].$set.lastActive).to.exist + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should mark project as opened', function (done) { - this.res.render = (pageName, opts) => { - this.ProjectUpdateHandler.promises.markAsOpened - .calledWith(this.project_id) - .should.equal(true) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should mark project as opened', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + ctx.ProjectUpdateHandler.promises.markAsOpened + .calledWith(ctx.project_id) + .should.equal(true) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should call the brand variations handler for branded projects', function (done) { - this.ProjectGetter.promises.getProject.resolves(this.brandedProject) - this.res.render = (pageName, opts) => { - this.BrandVariationsHandler.promises.getBrandVariationById - .calledWith() - .should.equal(true) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should call the brand variations handler for branded projects', async function (ctx) { + await new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(ctx.brandedProject) + ctx.res.render = (pageName, opts) => { + ctx.BrandVariationsHandler.promises.getBrandVariationById + .calledWith() + .should.equal(true) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should not call the brand variations handler for unbranded projects', function (done) { - this.res.render = (pageName, opts) => { - this.BrandVariationsHandler.promises.getBrandVariationById.called.should.equal( - false - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should not call the brand variations handler for unbranded projects', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + ctx.BrandVariationsHandler.promises.getBrandVariationById.called.should.equal( + false + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should expose the brand variation details as locals for branded projects', function (done) { - this.ProjectGetter.promises.getProject.resolves(this.brandedProject) - this.res.render = (pageName, opts) => { - opts.brandVariation.should.deep.equal(this.brandVariationDetails) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should expose the brand variation details as locals for branded projects', async function (ctx) { + await new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(ctx.brandedProject) + ctx.res.render = (pageName, opts) => { + opts.brandVariation.should.deep.equal(ctx.brandVariationDetails) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('flushes the project to TPDS if a flush is pending', function (done) { - this.res.render = () => { - this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded.should.have.been.calledWith( - this.project_id - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('flushes the project to TPDS if a flush is pending', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = () => { + ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded.should.have.been.calledWith( + ctx.project_id + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should refresh the user features if the epoch is outdated', function (done) { - this.FeaturesUpdater.featuresEpochIsCurrent = sinon.stub().returns(false) - this.res.render = () => { - this.FeaturesUpdater.promises.refreshFeatures.should.have.been.calledWith( - this.user._id, - 'load-editor' - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should refresh the user features if the epoch is outdated', async function (ctx) { + await new Promise(resolve => { + ctx.FeaturesUpdater.featuresEpochIsCurrent = sinon.stub().returns(false) + ctx.res.render = () => { + ctx.FeaturesUpdater.promises.refreshFeatures.should.have.been.calledWith( + ctx.user._id, + 'load-editor' + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) describe('wsUrl', function () { function checkLoadEditorWsMetric(metric) { - it(`should inc metric ${metric}`, function (done) { - this.res.render = () => { - this.Metrics.inc.calledWith(metric).should.equal(true) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it(`should inc metric ${metric}`, async function (ctx) { + await new Promise(resolve => { + ctx.res.render = () => { + ctx.Metrics.inc.calledWith(metric).should.equal(true) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) } function checkWsFallback(isBeta, isV2) { describe('with ws=fallback', function () { - beforeEach(function () { - this.req.query = {} - this.req.query.ws = 'fallback' + beforeEach(function (ctx) { + ctx.req.query = {} + ctx.req.query.ws = 'fallback' }) - it('should unset the wsUrl', function (done) { - this.res.render = (pageName, opts) => { - ;(opts.wsUrl || '/socket.io').should.equal('/socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should unset the wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + ;(opts.wsUrl || '/socket.io').should.equal('/socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric( `load-editor-ws${isBeta ? '-beta' : ''}${ @@ -819,45 +1085,51 @@ describe('ProjectController', function () { }) } - beforeEach(function () { - this.settings.wsUrl = '/other.socket.io' + beforeEach(function (ctx) { + ctx.settings.wsUrl = '/other.socket.io' }) - it('should set the custom wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/other.socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the custom wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/other.socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws') checkWsFallback(false) describe('beta program', function () { - beforeEach(function () { - this.settings.wsUrlBeta = '/beta.socket.io' + beforeEach(function (ctx) { + ctx.settings.wsUrlBeta = '/beta.socket.io' }) describe('for a normal user', function () { - it('should set the normal custom wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/other.socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the normal custom wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/other.socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws') checkWsFallback(false) }) describe('for a beta user', function () { - beforeEach(function () { - this.user.betaProgram = true + beforeEach(function (ctx) { + ctx.user.betaProgram = true }) - it('should set the beta wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/beta.socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the beta wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/beta.socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws-beta') checkWsFallback(true) @@ -865,44 +1137,50 @@ describe('ProjectController', function () { }) describe('v2-rollout', function () { - beforeEach(function () { - this.settings.wsUrlBeta = '/beta.socket.io' - this.settings.wsUrlV2 = '/socket.io.v2' + beforeEach(function (ctx) { + ctx.settings.wsUrlBeta = '/beta.socket.io' + ctx.settings.wsUrlV2 = '/socket.io.v2' }) function checkNonMatch() { - it('should set the normal custom wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/other.socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the normal custom wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/other.socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws') checkWsFallback(false) } function checkMatch() { - it('should set the v2 wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/socket.io.v2') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the v2 wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/socket.io.v2') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws-v2') checkWsFallback(false, true) } function checkForBetaUser() { describe('for a beta user', function () { - beforeEach(function () { - this.user.betaProgram = true + beforeEach(function (ctx) { + ctx.user.betaProgram = true }) - it('should set the beta wsUrl', function (done) { - this.res.render = (pageName, opts) => { - opts.wsUrl.should.equal('/beta.socket.io') - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should set the beta wsUrl', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.wsUrl.should.equal('/beta.socket.io') + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) checkLoadEditorWsMetric('load-editor-ws-beta') checkWsFallback(true) @@ -910,105 +1188,105 @@ describe('ProjectController', function () { } describe('when the roll out percentage is 0', function () { - beforeEach(function () { - this.settings.wsUrlV2Percentage = 0 + beforeEach(function (ctx) { + ctx.settings.wsUrlV2Percentage = 0 }) describe('when the projectId does not match (0)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(0) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(0) }) checkNonMatch() }) describe('when the projectId does not match (42)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(42) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(42) }) checkNonMatch() }) checkForBetaUser() }) describe('when the roll out percentage is 1', function () { - beforeEach(function () { - this.settings.wsUrlV2Percentage = 1 + beforeEach(function (ctx) { + ctx.settings.wsUrlV2Percentage = 1 }) describe('when the projectId matches (0)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(0) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(0) }) checkMatch() checkForBetaUser() }) describe('when the projectId does not match (1)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(1) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(1) }) checkNonMatch() checkForBetaUser() }) describe('when the projectId does not match (42)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(42) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(42) }) checkNonMatch() }) }) describe('when the roll out percentage is 10', function () { - beforeEach(function () { - this.settings.wsUrlV2Percentage = 10 + beforeEach(function (ctx) { + ctx.settings.wsUrlV2Percentage = 10 }) describe('when the projectId matches (0)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(0) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(0) }) checkMatch() }) describe('when the projectId matches (9)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(9) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(9) }) checkMatch() checkForBetaUser() }) describe('when the projectId does not match (10)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(10) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(10) }) checkNonMatch() }) describe('when the projectId does not match (42)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(42) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(42) }) checkNonMatch() checkForBetaUser() }) }) describe('when the roll out percentage is 100', function () { - beforeEach(function () { - this.settings.wsUrlV2Percentage = 100 + beforeEach(function (ctx) { + ctx.settings.wsUrlV2Percentage = 100 }) describe('when the projectId matches (0)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(0) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(0) }) checkMatch() checkForBetaUser() }) describe('when the projectId matches (10)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(10) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(10) }) checkMatch() }) describe('when the projectId matches (42)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(42) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(42) }) checkMatch() }) describe('when the projectId matches (99)', function () { - beforeEach(function () { - this.req.params.Project_id = ObjectId.createFromTime(99) + beforeEach(function (ctx) { + ctx.req.params.Project_id = ObjectId.createFromTime(99) }) checkMatch() }) @@ -1017,126 +1295,142 @@ describe('ProjectController', function () { }) describe('upgrade prompt (on header and share project modal)', function () { - beforeEach(function () { + beforeEach(function (ctx) { // default to saas enabled - this.Features.hasFeature.withArgs('saas').returns(true) + ctx.Features.hasFeature.withArgs('saas').returns(true) // default to without a subscription - this.SubscriptionLocator.promises.getUsersSubscription = sinon + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves(null) }) - it('should not show without the saas feature', function (done) { - this.Features.hasFeature.withArgs('saas').returns(false) - this.res.render = (pageName, opts) => { - expect(opts.showUpgradePrompt).to.equal(false) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should not show without the saas feature', async function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(false) + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(opts.showUpgradePrompt).to.equal(false) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should show for a user without a subscription or only non-paid affiliations', function (done) { - this.res.render = (pageName, opts) => { - expect(opts.showUpgradePrompt).to.equal(true) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should show for a user without a subscription or only non-paid affiliations', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(opts.showUpgradePrompt).to.equal(true) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should not show for a user with a personal subscription', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + it('should not show for a user with a personal subscription', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves({}) - this.res.render = (pageName, opts) => { - expect(opts.showUpgradePrompt).to.equal(false) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(opts.showUpgradePrompt).to.equal(false) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should not show for a user who is a member of a group subscription', function (done) { - this.LimitationsManager.promises.userIsMemberOfGroupSubscription = sinon - .stub() - .resolves({ isMember: true }) - this.res.render = (pageName, opts) => { - expect(opts.showUpgradePrompt).to.equal(false) - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - it('should not show for a user with an affiliated paid university', function (done) { - this.InstitutionsFeatures.promises.hasLicence = sinon + it('should not show for a user who is a member of a group subscription', async function (ctx) { + ctx.InstitutionsFeatures.promises.hasLicence = sinon .stub() .resolves(true) - this.res.render = (pageName, opts) => { - expect(opts.showUpgradePrompt).to.equal(false) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(opts.showUpgradePrompt).to.equal(false) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - }) - - describe('when user is a read write token member (and not already a named editor)', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - false - ) + it('should not show for a user with an affiliated paid university', async function (ctx) { + await new Promise(resolve => { + ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription = + sinon.stub().resolves({ isMember: true }) + ctx.res.render = (pageName, opts) => { + expect(opts.showUpgradePrompt).to.equal(false) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) - it('should redirect to the sharing-updates page', function (done) { - this.res.redirect = url => { - expect(url).to.equal(`/project/${this.project_id}/sharing-updates`) - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - }) + describe('when user is a read write token member (and not already a named editor)', function () { + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + false + ) + }) - describe('when user is a read write token member but also a named editor', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - true - ) + it('should redirect to the sharing-updates page', async function (ctx) { + await new Promise(resolve => { + ctx.res.redirect = url => { + expect(url).to.equal(`/project/${ctx.project_id}/sharing-updates`) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) + }) }) - it('should not redirect to the sharing-updates page, and should load the editor', function (done) { - this.res.render = (pageName, opts) => { - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - }) + describe('when user is a read write token member but also a named editor', function () { + beforeEach(function (ctx) { + ctx.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + true + ) + }) - it('should call the collaborator limit enforcement check', function (done) { - this.res.render = (pageName, opts) => { - this.Modules.promises.hooks.fire.should.have.been.calledWith( - 'enforceCollaboratorLimit', - this.project_id - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should not redirect to the sharing-updates page, and should load the editor', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) + }) + }) + + it('should call the collaborator limit enforcement check', async function (ctx) { + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + ctx.Modules.promises.hooks.fire.should.have.been.calledWith( + 'enforceCollaboratorLimit', + ctx.project_id + ) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) + }) }) describe('capabilitySet', function () { - it('should be passed as an array when loading the editor', function (done) { - this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false) - - this.res.render = (pageName, opts) => { - expect(opts.capabilities).to.deep.equal(['chat']) - done() - } - this.ProjectController.loadEditor(this.req, this.res) + it('should be passed as an array when loading the editor', async function (ctx) { + ctx.Features.hasFeature = sinon.stub().withArgs('chat').returns(false) + await new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + expect(opts.capabilities).to.deep.equal(['chat']) + resolve() + } + ctx.ProjectController.loadEditor(ctx.req, ctx.res) + }) }) }) }) describe('userProjectsJson', function () { - beforeEach(function (done) { + beforeEach(function (ctx) { const projects = [ { archived: true, @@ -1171,98 +1465,101 @@ describe('ProjectController', function () { }, ] - this.ProjectHelper.isArchivedOrTrashed - .withArgs(projects[0], this.user._id) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(projects[0], ctx.user._id) .returns(true) - this.ProjectHelper.isArchivedOrTrashed - .withArgs(projects[1], this.user._id) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(projects[1], ctx.user._id) .returns(false) - this.ProjectHelper.isArchivedOrTrashed - .withArgs(projects[2], this.user._id) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(projects[2], ctx.user._id) .returns(true) - this.ProjectHelper.isArchivedOrTrashed - .withArgs(projects[3], this.user._id) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(projects[3], ctx.user._id) .returns(false) - this.ProjectGetter.promises.findAllUsersProjects = sinon + ctx.ProjectGetter.promises.findAllUsersProjects = sinon .stub() .resolves([]) - this.ProjectController._buildProjectList = sinon.stub().returns(projects) - this.SessionManager.getLoggedInUserId = sinon - .stub() - .returns(this.user._id) - done() + ctx.ProjectController._buildProjectList = sinon.stub().returns(projects) + ctx.SessionManager.getLoggedInUserId = sinon.stub().returns(ctx.user._id) }) - it('should produce a list of projects', function (done) { - this.res.json = data => { - expect(data).to.deep.equal({ - projects: [ - { _id: 'b', name: 'B', accessLevel: 'b' }, - { _id: 'd', name: 'D', accessLevel: 'd' }, - ], - }) - done() - } - this.ProjectController.userProjectsJson(this.req, this.res, this.next) + it('should produce a list of projects', async function (ctx) { + await new Promise((resolve, reject) => { + ctx.res.json = data => { + expect(data).to.deep.equal({ + projects: [ + { _id: 'b', name: 'B', accessLevel: 'b' }, + { _id: 'd', name: 'D', accessLevel: 'd' }, + ], + }) + resolve() + } + ctx.ProjectController.userProjectsJson( + ctx.req, + ctx.res, + ctx.rejectOnError(reject) + ) + }) }) }) describe('projectEntitiesJson', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId = sinon.stub().returns('abc') - this.req.params = { Project_id: 'abcd' } - this.project = { _id: 'abcd' } - this.docs = [ + beforeEach(function (ctx) { + ctx.SessionManager.getLoggedInUserId = sinon.stub().returns('abc') + ctx.req.params = { Project_id: 'abcd' } + ctx.project = { _id: 'abcd' } + ctx.docs = [ { path: '/things/b.txt', doc: true }, { path: '/main.tex', doc: true }, ] - this.files = [{ path: '/things/a.txt' }] - this.ProjectGetter.promises.getProject = sinon + ctx.files = [{ path: '/things/a.txt' }] + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project) + ctx.ProjectEntityHandler.getAllEntitiesFromProject = sinon .stub() - .resolves(this.project) - this.ProjectEntityHandler.getAllEntitiesFromProject = sinon - .stub() - .returns({ docs: this.docs, files: this.files }) + .returns({ docs: ctx.docs, files: ctx.files }) }) - it('should produce a list of entities', function (done) { - this.res.json = data => { - expect(data).to.deep.equal({ - project_id: 'abcd', - entities: [ - { path: '/main.tex', type: 'doc' }, - { path: '/things/a.txt', type: 'file' }, - { path: '/things/b.txt', type: 'doc' }, - ], - }) - expect(this.ProjectGetter.promises.getProject.callCount).to.equal(1) - expect( - this.ProjectEntityHandler.getAllEntitiesFromProject.callCount - ).to.equal(1) - done() - } - this.ProjectController.projectEntitiesJson(this.req, this.res, this.next) + it('should produce a list of entities', async function (ctx) { + await new Promise(resolve => { + ctx.res.json = data => { + expect(data).to.deep.equal({ + project_id: 'abcd', + entities: [ + { path: '/main.tex', type: 'doc' }, + { path: '/things/a.txt', type: 'file' }, + { path: '/things/b.txt', type: 'doc' }, + ], + }) + expect(ctx.ProjectGetter.promises.getProject.callCount).to.equal(1) + expect( + ctx.ProjectEntityHandler.getAllEntitiesFromProject.callCount + ).to.equal(1) + resolve() + } + ctx.ProjectController.projectEntitiesJson(ctx.req, ctx.res, ctx.next) + }) }) - it('should call next with an error if the project file tree is invalid', function (done) { - this.ProjectEntityHandler.getAllEntitiesFromProject = sinon - .stub() - .throws() - this.next = err => { - expect(err).to.be.an.instanceof(Error) - done() - } - this.ProjectController.projectEntitiesJson(this.req, this.res, this.next) + it('should call next with an error if the project file tree is invalid', async function (ctx) { + ctx.ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().throws() + await new Promise(resolve => { + ctx.next = err => { + expect(err).to.be.an.instanceof(Error) + resolve() + } + ctx.ProjectController.projectEntitiesJson(ctx.req, ctx.res, ctx.next) + }) }) }) describe('_buildProjectViewModel', function () { - beforeEach(function () { - this.ProjectHelper.isArchived.returns(false) - this.ProjectHelper.isTrashed.returns(false) + beforeEach(function (ctx) { + ctx.ProjectHelper.isArchived.returns(false) + ctx.ProjectHelper.isTrashed.returns(false) - this.project = { + ctx.project = { _id: 'abcd', name: 'netsenits', lastUpdated: 1, @@ -1279,12 +1576,12 @@ describe('ProjectController', function () { }) describe('project not being archived or trashed', function () { - it('should produce a model of the project', function () { - const result = this.ProjectController._buildProjectViewModel( - this.project, + it('should produce a model of the project', function (ctx) { + const result = ctx.ProjectController._buildProjectViewModel( + ctx.project, 'readAndWrite', 'owner', - this.user._id + ctx.user._id ) expect(result).to.exist expect(result).to.be.an('object') @@ -1305,17 +1602,17 @@ describe('ProjectController', function () { }) describe('project being simultaneously archived and trashed', function () { - beforeEach(function () { - this.ProjectHelper.isArchived.returns(true) - this.ProjectHelper.isTrashed.returns(true) + beforeEach(function (ctx) { + ctx.ProjectHelper.isArchived.returns(true) + ctx.ProjectHelper.isTrashed.returns(true) }) - it('should produce a model of the project', function () { - const result = this.ProjectController._buildProjectViewModel( - this.project, + it('should produce a model of the project', function (ctx) { + const result = ctx.ProjectController._buildProjectViewModel( + ctx.project, 'readAndWrite', 'owner', - this.user._id + ctx.user._id ) expect(result).to.exist expect(result).to.be.an('object') @@ -1336,12 +1633,12 @@ describe('ProjectController', function () { }) describe('when token-read-only access', function () { - it('should redact the owner and last-updated data', function () { - const result = this.ProjectController._buildProjectViewModel( - this.project, + it('should redact the owner and last-updated data', function (ctx) { + const result = ctx.ProjectController._buildProjectViewModel( + ctx.project, 'readOnly', 'token', - this.user._id + ctx.user._id ) expect(result).to.exist expect(result).to.be.an('object') @@ -1362,35 +1659,33 @@ describe('ProjectController', function () { }) }) describe('_isInPercentageRollout', function () { - before(function () { - this.ids = [ - '5a05cd7621f9fe22be131740', - '5a05cd7821f9fe22be131741', - '5a05cd7921f9fe22be131742', - '5a05cd7a21f9fe22be131743', - '5a05cd7b21f9fe22be131744', - '5a05cd7c21f9fe22be131745', - '5a05cd7d21f9fe22be131746', - '5a05cd7e21f9fe22be131747', - '5a05cd7f21f9fe22be131748', - '5a05cd8021f9fe22be131749', - '5a05cd8021f9fe22be13174a', - '5a05cd8121f9fe22be13174b', - '5a05cd8221f9fe22be13174c', - '5a05cd8221f9fe22be13174d', - '5a05cd8321f9fe22be13174e', - '5a05cd8321f9fe22be13174f', - '5a05cd8421f9fe22be131750', - '5a05cd8421f9fe22be131751', - '5a05cd8421f9fe22be131752', - '5a05cd8521f9fe22be131753', - ] - }) + const ids = [ + '5a05cd7621f9fe22be131740', + '5a05cd7821f9fe22be131741', + '5a05cd7921f9fe22be131742', + '5a05cd7a21f9fe22be131743', + '5a05cd7b21f9fe22be131744', + '5a05cd7c21f9fe22be131745', + '5a05cd7d21f9fe22be131746', + '5a05cd7e21f9fe22be131747', + '5a05cd7f21f9fe22be131748', + '5a05cd8021f9fe22be131749', + '5a05cd8021f9fe22be13174a', + '5a05cd8121f9fe22be13174b', + '5a05cd8221f9fe22be13174c', + '5a05cd8221f9fe22be13174d', + '5a05cd8321f9fe22be13174e', + '5a05cd8321f9fe22be13174f', + '5a05cd8421f9fe22be131750', + '5a05cd8421f9fe22be131751', + '5a05cd8421f9fe22be131752', + '5a05cd8521f9fe22be131753', + ] - it('should produce the expected results', function () { + it('should produce the expected results', function (ctx) { expect( - this.ids.map(i => - this.ProjectController._isInPercentageRollout('abcd', i, 50) + ids.map(i => + ctx.ProjectController._isInPercentageRollout('abcd', i, 50) ) ).to.deep.equal([ false, @@ -1415,8 +1710,8 @@ describe('ProjectController', function () { true, ]) expect( - this.ids.map(i => - this.ProjectController._isInPercentageRollout('efgh', i, 50) + ids.map(i => + ctx.ProjectController._isInPercentageRollout('efgh', i, 50) ) ).to.deep.equal([ false, diff --git a/services/web/test/unit/src/User/UserController.test.mjs b/services/web/test/unit/src/User/UserController.test.mjs index 8dc8f034b7..22a371723c 100644 --- a/services/web/test/unit/src/User/UserController.test.mjs +++ b/services/web/test/unit/src/User/UserController.test.mjs @@ -1,30 +1,33 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/User/UserController.js' -const SandboxedModule = require('sandboxed-module') -const OError = require('@overleaf/o-error') -const Errors = require('../../../../app/src/Features/Errors/Errors') +import { beforeEach, describe, expect, it, vi } from 'vitest' +import sinon from 'sinon' +import OError from '@overleaf/o-error' +import Errors from '../../../../app/src/Features/Errors/Errors.js' +const modulePath = '../../../../app/src/Features/User/UserController.mjs' + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => { + return vi.importActual('../../../../app/src/Features/Errors/Errors.js') +}) describe('UserController', function () { - beforeEach(function () { - this.user_id = '323123' + beforeEach(async function (ctx) { + ctx.user_id = '323123' - this.user = { - _id: this.user_id, + ctx.user = { + _id: ctx.user_id, email: 'email@overleaf.com', save: sinon.stub().resolves(), ace: {}, } - this.req = { + ctx.req = { user: {}, session: { destroy() {}, user: { - _id: this.user_id, + _id: ctx.user_id, email: 'old@something.com', }, - analyticsId: this.user_id, + analyticsId: ctx.user_id, }, sessionID: '123', body: {}, @@ -39,32 +42,30 @@ describe('UserController', function () { }, } - this.UserDeleter = { promises: { deleteUser: sinon.stub().resolves() } } + ctx.UserDeleter = { promises: { deleteUser: sinon.stub().resolves() } } - this.UserGetter = { - promises: { getUser: sinon.stub().resolves(this.user) }, + ctx.UserGetter = { + promises: { getUser: sinon.stub().resolves(ctx.user) }, } - this.User = { - findById: sinon - .stub() - .returns({ exec: sinon.stub().resolves(this.user) }), + ctx.User = { + findById: sinon.stub().returns({ exec: sinon.stub().resolves(ctx.user) }), } - this.NewsLetterManager = { + ctx.NewsLetterManager = { promises: { subscribe: sinon.stub().resolves(), unsubscribe: sinon.stub().resolves(), }, } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.req.session.user), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.req.session.user), setInSessionUser: sinon.stub(), } - this.AuthenticationManager = { + ctx.AuthenticationManager = { promises: { authenticate: sinon.stub(), setUserPassword: sinon.stub(), @@ -74,7 +75,7 @@ describe('UserController', function () { .returns({ type: 'error', key: 'some-key' }), } - this.UserUpdater = { + ctx.UserUpdater = { promises: { changeEmailAddress: sinon.stub().resolves(), confirmEmail: sinon.stub().resolves(), @@ -82,13 +83,13 @@ describe('UserController', function () { }, } - this.settings = { siteUrl: 'overleaf.example.com' } + ctx.settings = { siteUrl: 'overleaf.example.com' } - this.UserHandler = { + ctx.UserHandler = { promises: { populateTeamInvites: sinon.stub().resolves() }, } - this.UserSessionsManager = { + ctx.UserSessionsManager = { promises: { getAllUserSessions: sinon.stub().resolves(), removeSessionsFromRedis: sinon.stub().resolves(), @@ -96,44 +97,44 @@ describe('UserController', function () { }, } - this.HttpErrorHandler = { + ctx.HttpErrorHandler = { badRequest: sinon.stub(), conflict: sinon.stub(), unprocessableEntity: sinon.stub(), legacyInternal: sinon.stub(), } - this.UrlHelper = { + ctx.UrlHelper = { getSafeRedirectPath: sinon.stub(), } - this.UrlHelper.getSafeRedirectPath + ctx.UrlHelper.getSafeRedirectPath .withArgs('https://evil.com') .returns(undefined) - this.UrlHelper.getSafeRedirectPath.returnsArg(0) + ctx.UrlHelper.getSafeRedirectPath.returnsArg(0) - this.Features = { + ctx.Features = { hasFeature: sinon.stub(), } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.RequestContentTypeDetection = { + ctx.RequestContentTypeDetection = { acceptsJson: sinon.stub().returns(false), } - this.EmailHandler = { + ctx.EmailHandler = { promises: { sendEmail: sinon.stub().resolves() }, } - this.OneTimeTokenHandler = { + ctx.OneTimeTokenHandler = { promises: { expireAllTokensForUser: sinon.stub().resolves() }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves(), @@ -141,631 +142,792 @@ describe('UserController', function () { }, } - this.UserController = SandboxedModule.require(modulePath, { - requires: { - '../Helpers/UrlHelper': this.UrlHelper, - './UserGetter': this.UserGetter, - './UserDeleter': this.UserDeleter, - './UserUpdater': this.UserUpdater, - '../../models/User': { User: this.User }, - '../Newsletter/NewsletterManager': this.NewsLetterManager, - '../Authentication/AuthenticationController': - this.AuthenticationController, - '../Authentication/SessionManager': this.SessionManager, - '../Authentication/AuthenticationManager': this.AuthenticationManager, - '../../infrastructure/Features': this.Features, - './UserAuditLogHandler': this.UserAuditLogHandler, - './UserHandler': this.UserHandler, - './UserSessionsManager': this.UserSessionsManager, - '../Errors/HttpErrorHandler': this.HttpErrorHandler, - '@overleaf/settings': this.settings, - '@overleaf/o-error': OError, - '../Email/EmailHandler': this.EmailHandler, - '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler, - '../../infrastructure/RequestContentTypeDetection': - this.RequestContentTypeDetection, - '../../infrastructure/Modules': this.Modules, - }, - }) + vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({ + default: ctx.UrlHelper, + })) - this.res = { + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/User/UserDeleter', () => ({ + default: ctx.UserDeleter, + })) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: ctx.UserUpdater, + })) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../app/src/Features/Newsletter/NewsletterManager', + () => ({ + default: ctx.NewsLetterManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: ctx.AuthenticationManager, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/Features/User/UserHandler', () => ({ + default: ctx.UserHandler, + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({ + default: ctx.HttpErrorHandler, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('@overleaf/o-error', () => ({ + default: OError, + })) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Security/OneTimeTokenHandler', + () => ({ + default: ctx.OneTimeTokenHandler, + }) + ) + + vi.doMock( + '../../../../app/src/infrastructure/RequestContentTypeDetection', + () => ctx.RequestContentTypeDetection + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + ctx.UserController = (await import(modulePath)).default + + ctx.res = { send: sinon.stub(), status: sinon.stub(), sendStatus: sinon.stub(), json: sinon.stub(), } - this.res.status.returns(this.res) - this.next = sinon.stub() - this.callback = sinon.stub() + ctx.res.status.returns(ctx.res) + ctx.next = sinon.stub() + ctx.callback = sinon.stub() }) describe('tryDeleteUser', function () { - beforeEach(function () { - this.req.body.password = 'wat' - this.req.logout = sinon.stub().yields() - this.req.session.destroy = sinon.stub().yields() - this.SessionManager.getLoggedInUserId = sinon - .stub() - .returns(this.user._id) - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, + beforeEach(function (ctx) { + ctx.req.body.password = 'wat' + ctx.req.logout = sinon.stub().yields() + ctx.req.session.destroy = sinon.stub().yields() + ctx.SessionManager.getLoggedInUserId = sinon.stub().returns(ctx.user._id) + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, }) }) - it('should send 200', function (done) { - this.res.sendStatus = code => { - code.should.equal(200) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should send 200', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + code.should.equal(200) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) - it('should try to authenticate user', function (done) { - this.res.sendStatus = code => { - this.AuthenticationManager.promises.authenticate.should.have.been - .calledOnce - this.AuthenticationManager.promises.authenticate.should.have.been.calledWith( - { _id: this.user._id }, - this.req.body.password - ) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should try to authenticate user', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.AuthenticationManager.promises.authenticate.should.have.been + .calledOnce + ctx.AuthenticationManager.promises.authenticate.should.have.been.calledWith( + { _id: ctx.user._id }, + ctx.req.body.password + ) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) - it('should delete the user', function (done) { - this.res.sendStatus = code => { - this.UserDeleter.promises.deleteUser.should.have.been.calledOnce - this.UserDeleter.promises.deleteUser.should.have.been.calledWith( - this.user._id - ) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should delete the user', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.UserDeleter.promises.deleteUser.should.have.been.calledOnce + ctx.UserDeleter.promises.deleteUser.should.have.been.calledWith( + ctx.user._id + ) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) - it('should call hook to try to delete v1 account', function (done) { - this.res.sendStatus = code => { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( - 'tryDeleteV1Account', - this.user - ) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should call hook to try to delete v1 account', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( + 'tryDeleteV1Account', + ctx.user + ) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) describe('when no password is supplied', function () { - beforeEach(function () { - this.req.body.password = '' + beforeEach(function (ctx) { + ctx.req.body.password = '' }) - it('should return 403', function (done) { - this.res.sendStatus = code => { - code.should.equal(403) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should return 403', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + code.should.equal(403) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) }) describe('when authenticate produces an error', function () { - beforeEach(function () { - this.AuthenticationManager.promises.authenticate.rejects( + beforeEach(function (ctx) { + ctx.AuthenticationManager.promises.authenticate.rejects( new Error('woops') ) }) - it('should call next with an error', function (done) { - this.next = err => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) }) describe('when authenticate does not produce a user', function () { - beforeEach(function () { - this.AuthenticationManager.promises.authenticate.resolves({ + beforeEach(function (ctx) { + ctx.AuthenticationManager.promises.authenticate.resolves({ user: null, }) }) - it('should return 403', function (done) { - this.res.sendStatus = code => { - code.should.equal(403) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should return 403', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + code.should.equal(403) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) }) describe('when deleteUser produces an error', function () { - beforeEach(function () { - this.UserDeleter.promises.deleteUser.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.UserDeleter.promises.deleteUser.rejects(new Error('woops')) }) - it('should call next with an error', function (done) { - this.next = err => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) }) describe('when deleteUser produces a known error', function () { - beforeEach(function () { - this.UserDeleter.promises.deleteUser.rejects( + beforeEach(function (ctx) { + ctx.UserDeleter.promises.deleteUser.rejects( new Errors.SubscriptionAdminDeletionError() ) }) - it('should return a HTTP Unprocessable Entity error', function (done) { - this.HttpErrorHandler.unprocessableEntity = sinon.spy( - (req, res, message, info) => { - expect(req).to.exist - expect(res).to.exist - expect(message).to.equal('error while deleting user account') - expect(info).to.deep.equal({ - error: 'SubscriptionAdminDeletionError', - }) - done() - } - ) - this.UserController.tryDeleteUser(this.req, this.res) + it('should return a HTTP Unprocessable Entity error', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.unprocessableEntity = sinon.spy( + (req, res, message, info) => { + expect(req).to.exist + expect(res).to.exist + expect(message).to.equal('error while deleting user account') + expect(info).to.deep.equal({ + error: 'SubscriptionAdminDeletionError', + }) + resolve() + } + ) + ctx.UserController.tryDeleteUser(ctx.req, ctx.res) + }) }) }) describe('when session.destroy produces an error', function () { - beforeEach(function () { - this.req.session.destroy = sinon + beforeEach(function (ctx) { + ctx.req.session.destroy = sinon .stub() .callsArgWith(0, new Error('woops')) }) - it('should call next with an error', function (done) { - this.next = err => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - done() - } - this.UserController.tryDeleteUser(this.req, this.res, this.next) + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + resolve() + } + ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next) + }) }) }) }) describe('subscribe', function () { - it('should send the user to subscribe', function (done) { - this.res.json = data => { - expect(data.message).to.equal('thanks_settings_updated') - this.NewsLetterManager.promises.subscribe.should.have.been.calledWith( - this.user - ) - done() - } - this.UserController.subscribe(this.req, this.res) + it('should send the user to subscribe', function (ctx) { + return new Promise(resolve => { + ctx.res.json = data => { + expect(data.message).to.equal('thanks_settings_updated') + ctx.NewsLetterManager.promises.subscribe.should.have.been.calledWith( + ctx.user + ) + resolve() + } + ctx.UserController.subscribe(ctx.req, ctx.res) + }) }) }) describe('unsubscribe', function () { - it('should send the user to unsubscribe', function (done) { - this.res.json = data => { - expect(data.message).to.equal('thanks_settings_updated') - this.NewsLetterManager.promises.unsubscribe.should.have.been.calledWith( - this.user - ) - done() - } - this.UserController.unsubscribe(this.req, this.res, this.next) + it('should send the user to unsubscribe', function (ctx) { + return new Promise(resolve => { + ctx.res.json = data => { + expect(data.message).to.equal('thanks_settings_updated') + ctx.NewsLetterManager.promises.unsubscribe.should.have.been.calledWith( + ctx.user + ) + resolve() + } + ctx.UserController.unsubscribe(ctx.req, ctx.res, ctx.next) + }) }) }) describe('updateUserSettings', function () { - beforeEach(function () { - this.auditLog = { initiatorId: this.user_id, ipAddress: this.req.ip } - this.newEmail = 'hello@world.com' - this.req.externalAuthenticationSystemUsed = sinon.stub().returns(false) + beforeEach(function (ctx) { + ctx.auditLog = { initiatorId: ctx.user_id, ipAddress: ctx.req.ip } + ctx.newEmail = 'hello@world.com' + ctx.req.externalAuthenticationSystemUsed = sinon.stub().returns(false) }) - it('should call save', function (done) { - this.req.body = {} - this.res.sendStatus = code => { - this.user.save.called.should.equal(true) - done() - } - this.UserController.updateUserSettings(this.req, this.res, this.next) - }) - - it('should set the first name', function (done) { - this.req.body = { first_name: 'bobby ' } - this.res.sendStatus = code => { - this.user.first_name.should.equal('bobby') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set the role', function (done) { - this.req.body = { role: 'student' } - this.res.sendStatus = code => { - this.user.role.should.equal('student') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set the institution', function (done) { - this.req.body = { institution: 'MIT' } - this.res.sendStatus = code => { - this.user.institution.should.equal('MIT') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set some props on ace', function (done) { - this.req.body = { editorTheme: 'something' } - this.res.sendStatus = code => { - this.user.ace.theme.should.equal('something') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set the overall theme', function (done) { - this.req.body = { overallTheme: 'green-ish' } - this.res.sendStatus = code => { - this.user.ace.overallTheme.should.equal('green-ish') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set referencesSearchMode to advanced', function (done) { - this.req.body = { referencesSearchMode: 'advanced' } - this.res.sendStatus = code => { - this.user.ace.referencesSearchMode.should.equal('advanced') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set referencesSearchMode to simple', function (done) { - this.req.body = { referencesSearchMode: 'simple' } - this.res.sendStatus = code => { - this.user.ace.referencesSearchMode.should.equal('simple') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should not allow arbitrary referencesSearchMode', function (done) { - this.req.body = { referencesSearchMode: 'foobar' } - this.res.sendStatus = code => { - this.user.ace.referencesSearchMode.should.equal('advanced') - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set enableNewEditor to true', function (done) { - this.req.body = { enableNewEditor: true } - this.res.sendStatus = code => { - this.user.ace.enableNewEditor.should.equal(true) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should set enableNewEditor to false', function (done) { - this.req.body = { enableNewEditor: false } - this.res.sendStatus = code => { - this.user.ace.enableNewEditor.should.equal(false) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should keep enableNewEditor a boolean', function (done) { - this.req.body = { enableNewEditor: 'foobar' } - this.res.sendStatus = code => { - this.user.ace.enableNewEditor.should.equal(true) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should send an error if the email is 0 len', function (done) { - this.req.body.email = '' - this.res.sendStatus = function (code) { - code.should.equal(400) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should send an error if the email does not contain an @', function (done) { - this.req.body.email = 'bob at something dot com' - this.res.sendStatus = function (code) { - code.should.equal(400) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should call the user updater with the new email and user _id', function (done) { - this.req.body.email = this.newEmail.toUpperCase() - this.res.sendStatus = code => { - code.should.equal(200) - this.UserUpdater.promises.changeEmailAddress.should.have.been.calledWith( - this.user_id, - this.newEmail, - this.auditLog - ) - done() - } - this.UserController.updateUserSettings(this.req, this.res) - }) - - it('should update the email on the session', function (done) { - this.req.body.email = this.newEmail.toUpperCase() - let callcount = 0 - this.User.findById = id => ({ - exec: async () => { - if (++callcount === 2) { - this.user.email = this.newEmail - } - return this.user - }, + it('should call save', function (ctx) { + return new Promise(resolve => { + ctx.req.body = {} + ctx.res.sendStatus = code => { + ctx.user.save.called.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res, ctx.next) }) - this.res.sendStatus = code => { - code.should.equal(200) - this.SessionManager.setInSessionUser - .calledWith(this.req.session, { - email: this.newEmail, - first_name: undefined, - last_name: undefined, - }) - .should.equal(true) - done() - } - this.UserController.updateUserSettings(this.req, this.res) }) - it('should call populateTeamInvites', function (done) { - this.req.body.email = this.newEmail.toUpperCase() - this.res.sendStatus = code => { - code.should.equal(200) - this.UserHandler.promises.populateTeamInvites.should.have.been.calledWith( - this.user - ) - done() - } - this.UserController.updateUserSettings(this.req, this.res) + it('should set the first name', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { first_name: 'bobby ' } + ctx.res.sendStatus = code => { + ctx.user.first_name.should.equal('bobby') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set the role', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { role: 'student' } + ctx.res.sendStatus = code => { + ctx.user.role.should.equal('student') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set the institution', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { institution: 'MIT' } + ctx.res.sendStatus = code => { + ctx.user.institution.should.equal('MIT') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set some props on ace', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { editorTheme: 'something' } + ctx.res.sendStatus = code => { + ctx.user.ace.theme.should.equal('something') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set the overall theme', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { overallTheme: 'green-ish' } + ctx.res.sendStatus = code => { + ctx.user.ace.overallTheme.should.equal('green-ish') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set referencesSearchMode to advanced', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { referencesSearchMode: 'advanced' } + ctx.res.sendStatus = code => { + ctx.user.ace.referencesSearchMode.should.equal('advanced') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set referencesSearchMode to simple', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { referencesSearchMode: 'simple' } + ctx.res.sendStatus = code => { + ctx.user.ace.referencesSearchMode.should.equal('simple') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should not allow arbitrary referencesSearchMode', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { referencesSearchMode: 'foobar' } + ctx.res.sendStatus = code => { + ctx.user.ace.referencesSearchMode.should.equal('advanced') + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set enableNewEditor to true', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: true } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set enableNewEditor to false', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: false } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(false) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should keep enableNewEditor a boolean', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: 'foobar' } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should send an error if the email is 0 len', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = '' + ctx.res.sendStatus = function (code) { + code.should.equal(400) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should send an error if the email does not contain an @', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = 'bob at something dot com' + ctx.res.sendStatus = function (code) { + code.should.equal(400) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should call the user updater with the new email and user _id', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = ctx.newEmail.toUpperCase() + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.UserUpdater.promises.changeEmailAddress.should.have.been.calledWith( + ctx.user_id, + ctx.newEmail, + ctx.auditLog + ) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should update the email on the session', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = ctx.newEmail.toUpperCase() + let callcount = 0 + ctx.User.findById = id => ({ + exec: async () => { + if (++callcount === 2) { + ctx.user.email = ctx.newEmail + } + return ctx.user + }, + }) + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.SessionManager.setInSessionUser + .calledWith(ctx.req.session, { + email: ctx.newEmail, + first_name: undefined, + last_name: undefined, + }) + .should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should call populateTeamInvites', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = ctx.newEmail.toUpperCase() + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.UserHandler.promises.populateTeamInvites.should.have.been.calledWith( + ctx.user + ) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) }) describe('when changeEmailAddress yields an error', function () { - it('should pass on an error and not send a success status', function (done) { - this.req.body.email = this.newEmail.toUpperCase() - this.UserUpdater.promises.changeEmailAddress.rejects(new OError()) - this.HttpErrorHandler.legacyInternal = sinon.spy( - (req, res, message, error) => { - expect(req).to.exist - expect(req).to.exist - message.should.equal('problem_changing_email_address') - expect(error).to.be.instanceof(OError) - done() - } - ) - this.UserController.updateUserSettings(this.req, this.res, this.next) + it('should pass on an error and not send a success status', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = ctx.newEmail.toUpperCase() + ctx.UserUpdater.promises.changeEmailAddress.rejects(new OError()) + ctx.HttpErrorHandler.legacyInternal = sinon.spy( + (req, res, message, error) => { + expect(req).to.exist + expect(req).to.exist + message.should.equal('problem_changing_email_address') + expect(error).to.be.instanceof(OError) + resolve() + } + ) + ctx.UserController.updateUserSettings(ctx.req, ctx.res, ctx.next) + }) }) - it('should call the HTTP conflict error handler when the email already exists', function (done) { - this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => { - expect(req).to.exist - expect(req).to.exist - message.should.equal('email_already_registered') - done() + it('should call the HTTP conflict error handler when the email already exists', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.conflict = sinon.spy((req, res, message) => { + expect(req).to.exist + expect(req).to.exist + message.should.equal('email_already_registered') + resolve() + }) + ctx.req.body.email = ctx.newEmail.toUpperCase() + ctx.UserUpdater.promises.changeEmailAddress.rejects( + new Errors.EmailExistsError() + ) + ctx.UserController.updateUserSettings(ctx.req, ctx.res) }) - this.req.body.email = this.newEmail.toUpperCase() - this.UserUpdater.promises.changeEmailAddress.rejects( - new Errors.EmailExistsError() - ) - this.UserController.updateUserSettings(this.req, this.res) }) }) describe('when using an external auth source', function () { - beforeEach(function () { - this.newEmail = 'someone23@example.com' - this.req.externalAuthenticationSystemUsed = sinon.stub().returns(true) + beforeEach(function (ctx) { + ctx.newEmail = 'someone23@example.com' + ctx.req.externalAuthenticationSystemUsed = sinon.stub().returns(true) }) - it('should not set a new email', function (done) { - this.req.body.email = this.newEmail - this.res.sendStatus = code => { - code.should.equal(200) - this.UserUpdater.promises.changeEmailAddress - .calledWith(this.user_id, this.newEmail) - .should.equal(false) - done() - } - this.UserController.updateUserSettings(this.req, this.res) + it('should not set a new email', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = ctx.newEmail + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.UserUpdater.promises.changeEmailAddress + .calledWith(ctx.user_id, ctx.newEmail) + .should.equal(false) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) }) }) }) describe('logout', function () { - beforeEach(function () { - this.RequestContentTypeDetection.acceptsJson.returns(false) + beforeEach(function (ctx) { + ctx.RequestContentTypeDetection.acceptsJson.returns(false) }) - it('should destroy the session', function (done) { - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.redirect = url => { - url.should.equal('/login') - this.req.session.destroy.called.should.equal(true) - done() - } + it('should destroy the session', function (ctx) { + return new Promise(resolve => { + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.redirect = url => { + url.should.equal('/login') + ctx.req.session.destroy.called.should.equal(true) + resolve() + } - this.UserController.logout(this.req, this.res) + ctx.UserController.logout(ctx.req, ctx.res) + }) }) - it('should untrack session', function (done) { - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.redirect = url => { - url.should.equal('/login') - this.UserSessionsManager.promises.untrackSession.should.have.been - .calledOnce - this.UserSessionsManager.promises.untrackSession.should.have.been.calledWith( - sinon.match(this.req.user), - this.req.sessionID - ) - done() - } + it('should untrack session', function (ctx) { + return new Promise(resolve => { + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.redirect = url => { + url.should.equal('/login') + ctx.UserSessionsManager.promises.untrackSession.should.have.been + .calledOnce + ctx.UserSessionsManager.promises.untrackSession.should.have.been.calledWith( + sinon.match(ctx.req.user), + ctx.req.sessionID + ) + resolve() + } - this.UserController.logout(this.req, this.res) + ctx.UserController.logout(ctx.req, ctx.res) + }) }) - it('should redirect after logout', function (done) { - this.req.body.redirect = '/sso-login' - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.redirect = url => { - url.should.equal(this.req.body.redirect) - done() - } - this.UserController.logout(this.req, this.res) + it('should redirect after logout', function (ctx) { + return new Promise(resolve => { + ctx.req.body.redirect = '/sso-login' + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.redirect = url => { + url.should.equal(ctx.req.body.redirect) + resolve() + } + ctx.UserController.logout(ctx.req, ctx.res) + }) }) - it('should redirect after logout, but not to evil.com', function (done) { - this.req.body.redirect = 'https://evil.com' - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.redirect = url => { - url.should.equal('/login') - done() - } - this.UserController.logout(this.req, this.res) + it('should redirect after logout, but not to evil.com', function (ctx) { + return new Promise(resolve => { + ctx.req.body.redirect = 'https://evil.com' + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.redirect = url => { + url.should.equal('/login') + resolve() + } + ctx.UserController.logout(ctx.req, ctx.res) + }) }) - it('should redirect to login after logout when no redirect set', function (done) { - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.redirect = url => { - url.should.equal('/login') - done() - } - this.UserController.logout(this.req, this.res) + it('should redirect to login after logout when no redirect set', function (ctx) { + return new Promise(resolve => { + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.redirect = url => { + url.should.equal('/login') + resolve() + } + ctx.UserController.logout(ctx.req, ctx.res) + }) }) - it('should send json with redir property for json request', function (done) { - this.RequestContentTypeDetection.acceptsJson.returns(true) - this.req.session.destroy = sinon.stub().callsArgWith(0) - this.res.status = code => { - code.should.equal(200) - return this.res - } - this.res.json = data => { - data.redir.should.equal('/login') - done() - } - this.UserController.logout(this.req, this.res) + it('should send json with redir property for json request', function (ctx) { + return new Promise(resolve => { + ctx.RequestContentTypeDetection.acceptsJson.returns(true) + ctx.req.session.destroy = sinon.stub().callsArgWith(0) + ctx.res.status = code => { + code.should.equal(200) + return ctx.res + } + ctx.res.json = data => { + data.redir.should.equal('/login') + resolve() + } + ctx.UserController.logout(ctx.req, ctx.res) + }) }) }) describe('clearSessions', function () { describe('success', function () { - it('should call removeSessionsFromRedis', function (done) { - this.res.sendStatus.callsFake(() => { - this.UserSessionsManager.promises.removeSessionsFromRedis.should.have - .been.calledOnce - done() + it('should call removeSessionsFromRedis', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus.callsFake(() => { + ctx.UserSessionsManager.promises.removeSessionsFromRedis.should.have + .been.calledOnce + resolve() + }) + ctx.UserController.clearSessions(ctx.req, ctx.res) }) - this.UserController.clearSessions(this.req, this.res) }) - it('send a 201 response', function (done) { - this.res.sendStatus.callsFake(status => { - status.should.equal(201) - done() - }) + it('send a 201 response', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus.callsFake(status => { + status.should.equal(201) + resolve() + }) - this.UserController.clearSessions(this.req, this.res) + ctx.UserController.clearSessions(ctx.req, ctx.res) + }) }) - it('sends a security alert email', function (done) { - this.res.sendStatus.callsFake(status => { - this.EmailHandler.promises.sendEmail.callCount.should.equal(1) - const expectedArg = { - to: this.user.email, - actionDescribed: `active sessions were cleared on your account ${this.user.email}`, - action: 'active sessions cleared', - } - const emailCall = this.EmailHandler.promises.sendEmail.lastCall - expect(emailCall.args[0]).to.equal('securityAlert') - expect(emailCall.args[1]).to.deep.equal(expectedArg) - done() - }) + it('sends a security alert email', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus.callsFake(status => { + ctx.EmailHandler.promises.sendEmail.callCount.should.equal(1) + const expectedArg = { + to: ctx.user.email, + actionDescribed: `active sessions were cleared on your account ${ctx.user.email}`, + action: 'active sessions cleared', + } + const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall + expect(emailCall.args[0]).to.equal('securityAlert') + expect(emailCall.args[1]).to.deep.equal(expectedArg) + resolve() + }) - this.UserController.clearSessions(this.req, this.res) + ctx.UserController.clearSessions(ctx.req, ctx.res) + }) }) }) describe('errors', function () { describe('when getAllUserSessions produces an error', function () { - it('should return an error', function (done) { - this.UserSessionsManager.promises.getAllUserSessions.rejects( - new Error('woops') - ) - this.UserController.clearSessions(this.req, this.res, error => { - expect(error).to.be.instanceof(Error) - done() + it('should return an error', function (ctx) { + return new Promise(resolve => { + ctx.UserSessionsManager.promises.getAllUserSessions.rejects( + new Error('woops') + ) + ctx.UserController.clearSessions(ctx.req, ctx.res, error => { + expect(error).to.be.instanceof(Error) + resolve() + }) }) }) }) describe('when audit log addEntry produces an error', function () { - it('should call next with an error', function (done) { - this.UserAuditLogHandler.promises.addEntry.rejects(new Error('woops')) - this.UserController.clearSessions(this.req, this.res, error => { - expect(error).to.be.instanceof(Error) - done() + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.UserAuditLogHandler.promises.addEntry.rejects( + new Error('woops') + ) + ctx.UserController.clearSessions(ctx.req, ctx.res, error => { + expect(error).to.be.instanceof(Error) + resolve() + }) }) }) }) describe('when removeSessionsFromRedis produces an error', function () { - it('should call next with an error', function (done) { - this.UserSessionsManager.promises.removeSessionsFromRedis.rejects( - new Error('woops') - ) - this.UserController.clearSessions(this.req, this.res, error => { - expect(error).to.be.instanceof(Error) - done() + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.UserSessionsManager.promises.removeSessionsFromRedis.rejects( + new Error('woops') + ) + ctx.UserController.clearSessions(ctx.req, ctx.res, error => { + expect(error).to.be.instanceof(Error) + resolve() + }) }) }) }) describe('when EmailHandler produces an error', function () { const anError = new Error('oops') - it('send a 201 response but log error', function (done) { - this.EmailHandler.promises.sendEmail.rejects(anError) - this.res.sendStatus.callsFake(status => { - status.should.equal(201) - this.logger.error.callCount.should.equal(1) - const loggerCall = this.logger.error.getCall(0) - expect(loggerCall.args[0]).to.deep.equal({ - error: anError, - userId: this.user_id, + it('send a 201 response but log error', function (ctx) { + return new Promise(resolve => { + ctx.EmailHandler.promises.sendEmail.rejects(anError) + ctx.res.sendStatus.callsFake(status => { + status.should.equal(201) + expect(ctx.logger.error).toHaveBeenCalledTimes(1) + const loggerCall = ctx.logger.error.mock.calls[0] + expect(loggerCall[0]).to.deep.equal({ + error: anError, + userId: ctx.user_id, + }) + expect(loggerCall[1]).to.contain( + 'could not send security alert email when sessions cleared' + ) + resolve() }) - expect(loggerCall.args[1]).to.contain( - 'could not send security alert email when sessions cleared' - ) - done() + ctx.UserController.clearSessions(ctx.req, ctx.res) }) - this.UserController.clearSessions(this.req, this.res) }) }) }) @@ -773,195 +935,213 @@ describe('UserController', function () { describe('changePassword', function () { describe('success', function () { - beforeEach(function () { - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, + beforeEach(function (ctx) { + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, }) - this.AuthenticationManager.promises.setUserPassword.resolves() - this.req.body = { + ctx.AuthenticationManager.promises.setUserPassword.resolves() + ctx.req.body = { newPassword1: 'newpass', newPassword2: 'newpass', } }) - it('should set the new password if they do match', function (done) { - this.res.json.callsFake(() => { - this.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith( - this.user, - 'newpass' - ) - done() + it('should set the new password if they do match', function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + ctx.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith( + ctx.user, + 'newpass' + ) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) }) - this.UserController.changePassword(this.req, this.res) }) - it('should log the update', function (done) { - this.res.json.callsFake(() => { - this.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.user._id, - 'update-password', - this.user._id, - this.req.ip - ) - this.AuthenticationManager.promises.setUserPassword.callCount.should.equal( - 1 - ) - done() + it('should log the update', function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + ctx.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith( + ctx.user._id, + 'update-password', + ctx.user._id, + ctx.req.ip + ) + ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal( + 1 + ) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) }) - this.UserController.changePassword(this.req, this.res) }) - it('should send security alert email', function (done) { - this.res.json.callsFake(() => { - const expectedArg = { - to: this.user.email, - actionDescribed: `your password has been changed on your account ${this.user.email}`, - action: 'password changed', - } - const emailCall = this.EmailHandler.promises.sendEmail.lastCall - expect(emailCall.args[0]).to.equal('securityAlert') - expect(emailCall.args[1]).to.deep.equal(expectedArg) - done() + it('should send security alert email', function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + const expectedArg = { + to: ctx.user.email, + actionDescribed: `your password has been changed on your account ${ctx.user.email}`, + action: 'password changed', + } + const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall + expect(emailCall.args[0]).to.equal('securityAlert') + expect(emailCall.args[1]).to.deep.equal(expectedArg) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) }) - this.UserController.changePassword(this.req, this.res) }) - it('should expire password reset tokens', function (done) { - this.res.json.callsFake(() => { - this.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith( - this.user._id, - 'password' - ) - done() + it('should expire password reset tokens', function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + ctx.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith( + ctx.user._id, + 'password' + ) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) }) - this.UserController.changePassword(this.req, this.res) }) }) describe('errors', function () { - it('should check the old password is the current one at the moment', function (done) { - this.AuthenticationManager.promises.authenticate.resolves({}) - this.req.body = { currentPassword: 'oldpasshere' } - this.HttpErrorHandler.badRequest.callsFake(() => { - expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith( - this.req, - this.res, - 'password_change_old_password_wrong' - ) - this.AuthenticationManager.promises.authenticate.should.have.been.calledWith( - { _id: this.user._id }, - 'oldpasshere' - ) - this.AuthenticationManager.promises.setUserPassword.callCount.should.equal( - 0 - ) - done() - }) - this.UserController.changePassword(this.req, this.res) - }) - - it('it should not set the new password if they do not match', function (done) { - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, - }) - this.req.body = { - newPassword1: '1', - newPassword2: '2', - } - this.HttpErrorHandler.badRequest.callsFake(() => { - expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith( - this.req, - this.res, - 'password_change_passwords_do_not_match' - ) - this.AuthenticationManager.promises.setUserPassword.callCount.should.equal( - 0 - ) - done() - }) - this.UserController.changePassword(this.req, this.res) - }) - - it('it should not set the new password if it is invalid', function (done) { - // this.AuthenticationManager.validatePassword = sinon - // .stub() - // .returns({ message: 'validation-error' }) - const err = new Error('bad') - err.name = 'InvalidPasswordError' - const message = { - type: 'error', - key: 'some-message-key', - } - this.AuthenticationManager.getMessageForInvalidPasswordError.returns( - message - ) - this.AuthenticationManager.promises.setUserPassword.rejects(err) - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, - }) - this.req.body = { - newPassword1: 'newpass', - newPassword2: 'newpass', - } - this.res.json.callsFake(result => { - expect(result.message).to.deep.equal(message) - this.AuthenticationManager.promises.setUserPassword.callCount.should.equal( - 1 - ) - done() - }) - this.UserController.changePassword(this.req, this.res) - }) - - describe('UserAuditLogHandler error', function () { - it('should return error and not update password', function (done) { - this.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops')) - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, + it('should check the old password is the current one at the moment', function (ctx) { + return new Promise(resolve => { + ctx.AuthenticationManager.promises.authenticate.resolves({}) + ctx.req.body = { currentPassword: 'oldpasshere' } + ctx.HttpErrorHandler.badRequest.callsFake(() => { + expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith( + ctx.req, + ctx.res, + 'password_change_old_password_wrong' + ) + ctx.AuthenticationManager.promises.authenticate.should.have.been.calledWith( + { _id: ctx.user._id }, + 'oldpasshere' + ) + ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal( + 0 + ) + resolve() }) - this.AuthenticationManager.promises.setUserPassword.resolves() - this.req.body = { + ctx.UserController.changePassword(ctx.req, ctx.res) + }) + }) + + it('it should not set the new password if they do not match', function (ctx) { + return new Promise(resolve => { + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, + }) + ctx.req.body = { + newPassword1: '1', + newPassword2: '2', + } + ctx.HttpErrorHandler.badRequest.callsFake(() => { + expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith( + ctx.req, + ctx.res, + 'password_change_passwords_do_not_match' + ) + ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal( + 0 + ) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) + }) + }) + + it('it should not set the new password if it is invalid', function (ctx) { + return new Promise(resolve => { + // this.AuthenticationManager.validatePassword = sinon + // .stub() + // .returns({ message: 'validation-error' }) + const err = new Error('bad') + err.name = 'InvalidPasswordError' + const message = { + type: 'error', + key: 'some-message-key', + } + ctx.AuthenticationManager.getMessageForInvalidPasswordError.returns( + message + ) + ctx.AuthenticationManager.promises.setUserPassword.rejects(err) + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, + }) + ctx.req.body = { newPassword1: 'newpass', newPassword2: 'newpass', } - - this.UserController.changePassword(this.req, this.res, error => { - expect(error).to.be.instanceof(Error) - this.AuthenticationManager.promises.setUserPassword.callCount.should.equal( + ctx.res.json.callsFake(result => { + expect(result.message).to.deep.equal(message) + ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal( 1 ) - done() + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) + }) + }) + + describe('UserAuditLogHandler error', function () { + it('should return error and not update password', function (ctx) { + return new Promise(resolve => { + ctx.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops')) + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, + }) + ctx.AuthenticationManager.promises.setUserPassword.resolves() + ctx.req.body = { + newPassword1: 'newpass', + newPassword2: 'newpass', + } + + ctx.UserController.changePassword(ctx.req, ctx.res, error => { + expect(error).to.be.instanceof(Error) + ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal( + 1 + ) + resolve() + }) }) }) }) describe('EmailHandler error', function () { const anError = new Error('oops') - beforeEach(function () { - this.AuthenticationManager.promises.authenticate.resolves({ - user: this.user, + beforeEach(function (ctx) { + ctx.AuthenticationManager.promises.authenticate.resolves({ + user: ctx.user, }) - this.AuthenticationManager.promises.setUserPassword.resolves() - this.req.body = { + ctx.AuthenticationManager.promises.setUserPassword.resolves() + ctx.req.body = { newPassword1: 'newpass', newPassword2: 'newpass', } - this.EmailHandler.promises.sendEmail.rejects(anError) + ctx.EmailHandler.promises.sendEmail.rejects(anError) }) - it('should not return error but should log it', function (done) { - this.res.json.callsFake(result => { - expect(result.message.type).to.equal('success') - this.logger.error.callCount.should.equal(1) - expect(this.logger.error).to.have.been.calledWithExactly( - { - error: anError, - userId: this.user_id, - }, - 'could not send security alert email when password changed' - ) - done() + it('should not return error but should log it', function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(result => { + expect(result.message.type).to.equal('success') + expect(ctx.logger.error).toHaveBeenCalledTimes(1) + expect(ctx.logger.error).toHaveBeenCalledWith( + { + error: anError, + userId: ctx.user_id, + }, + 'could not send security alert email when password changed' + ) + resolve() + }) + ctx.UserController.changePassword(ctx.req, ctx.res) }) - this.UserController.changePassword(this.req, this.res) }) }) }) @@ -969,212 +1149,212 @@ describe('UserController', function () { describe('ensureAffiliationMiddleware', function () { describe('without affiliations feature', function () { - beforeEach(async function () { - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should not run affiliation check', function () { - expect(this.UserGetter.promises.getUser).to.not.have.been.called - expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called - expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have + it('should not run affiliation check', function (ctx) { + expect(ctx.UserGetter.promises.getUser).to.not.have.been.called + expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called + expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have .been.called }) - it('should not return an error', function () { - expect(this.next).to.be.calledWith() + it('should not return an error', function (ctx) { + expect(ctx.next).to.be.calledWith() }) }) describe('without ensureAffiliation query parameter', function () { - beforeEach(async function () { - this.Features.hasFeature.withArgs('affiliations').returns(true) - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should not run middleware', function () { - expect(this.UserGetter.promises.getUser).to.not.have.been.called - expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called - expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have + it('should not run middleware', function (ctx) { + expect(ctx.UserGetter.promises.getUser).to.not.have.been.called + expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called + expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have .been.called }) - it('should not return an error', function () { - expect(this.next).to.be.calledWith() + it('should not return an error', function (ctx) { + expect(ctx.next).to.be.calledWith() }) }) describe('no flagged email', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { const email = 'unit-test@overleaf.com' - this.user.email = email - this.user.emails = [ + ctx.user.email = email + ctx.user.emails = [ { email, }, ] - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.req.query.ensureAffiliation = true - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.req.query.ensureAffiliation = true + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should get the user', function () { - expect(this.UserGetter.promises.getUser).to.have.been.calledWith( - this.user._id + it('should get the user', function (ctx) { + expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith( + ctx.user._id ) }) - it('should not try to add affiliation or update user', function () { - expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have + it('should not try to add affiliation or update user', function (ctx) { + expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have .been.called }) - it('should not return an error', function () { - expect(this.next).to.be.calledWith() + it('should not return an error', function (ctx) { + expect(ctx.next).to.be.calledWith() }) }) describe('flagged non-SSO email', function () { let emailFlagged - beforeEach(async function () { + beforeEach(async function (ctx) { emailFlagged = 'flagged@overleaf.com' - this.user.email = emailFlagged - this.user.emails = [ + ctx.user.email = emailFlagged + ctx.user.emails = [ { email: emailFlagged, affiliationUnchecked: true, }, ] - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.req.query.ensureAffiliation = true - this.req.assertPermission = sinon.stub() - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.req.query.ensureAffiliation = true + ctx.req.assertPermission = sinon.stub() + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should check the user has permission', function () { - expect(this.req.assertPermission).to.have.been.calledWith( + it('should check the user has permission', function (ctx) { + expect(ctx.req.assertPermission).to.have.been.calledWith( 'add-affiliation' ) }) - it('should unflag the emails but not confirm', function () { + it('should unflag the emails but not confirm', function (ctx) { expect( - this.UserUpdater.promises.addAffiliationForNewUser - ).to.have.been.calledWith(this.user._id, emailFlagged) + ctx.UserUpdater.promises.addAffiliationForNewUser + ).to.have.been.calledWith(ctx.user._id, emailFlagged) expect( - this.UserUpdater.promises.confirmEmail - ).to.not.have.been.calledWith(this.user._id, emailFlagged) + ctx.UserUpdater.promises.confirmEmail + ).to.not.have.been.calledWith(ctx.user._id, emailFlagged) }) - it('should not return an error', function () { - expect(this.next).to.be.calledWith() + it('should not return an error', function (ctx) { + expect(ctx.next).to.be.calledWith() }) }) describe('flagged SSO email', function () { let emailFlagged - beforeEach(async function () { + beforeEach(async function (ctx) { emailFlagged = 'flagged@overleaf.com' - this.user.email = emailFlagged - this.user.emails = [ + ctx.user.email = emailFlagged + ctx.user.emails = [ { email: emailFlagged, affiliationUnchecked: true, samlProviderId: '123', }, ] - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.req.query.ensureAffiliation = true - this.req.assertPermission = sinon.stub() - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.req.query.ensureAffiliation = true + ctx.req.assertPermission = sinon.stub() + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should check the user has permission', function () { - expect(this.req.assertPermission).to.have.been.calledWith( + it('should check the user has permission', function (ctx) { + expect(ctx.req.assertPermission).to.have.been.calledWith( 'add-affiliation' ) }) - it('should add affiliation to v1, unflag and confirm on v2', function () { - expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not + it('should add affiliation to v1, unflag and confirm on v2', function (ctx) { + expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.have.not .been.called - expect(this.UserUpdater.promises.confirmEmail).to.have.been.calledWith( - this.user._id, + expect(ctx.UserUpdater.promises.confirmEmail).to.have.been.calledWith( + ctx.user._id, emailFlagged ) }) - it('should not return an error', function () { - expect(this.next).to.be.calledWith() + it('should not return an error', function (ctx) { + expect(ctx.next).to.be.calledWith() }) }) describe('when v1 returns an error', function () { let emailFlagged - beforeEach(async function () { - this.UserUpdater.promises.addAffiliationForNewUser.rejects() + beforeEach(async function (ctx) { + ctx.UserUpdater.promises.addAffiliationForNewUser.rejects() emailFlagged = 'flagged@overleaf.com' - this.user.email = emailFlagged - this.user.emails = [ + ctx.user.email = emailFlagged + ctx.user.emails = [ { email: emailFlagged, affiliationUnchecked: true, }, ] - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.req.query.ensureAffiliation = true - this.req.assertPermission = sinon.stub() - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.req.query.ensureAffiliation = true + ctx.req.assertPermission = sinon.stub() + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should check the user has permission', function () { - expect(this.req.assertPermission).to.have.been.calledWith( + it('should check the user has permission', function (ctx) { + expect(ctx.req.assertPermission).to.have.been.calledWith( 'add-affiliation' ) }) - it('should return the error', function () { - expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error)) + it('should return the error', function (ctx) { + expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error)) }) }) describe('when user is not found', function () { - beforeEach(async function () { - this.UserGetter.promises.getUser.rejects(new Error('not found')) - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.req.query.ensureAffiliation = true - await this.UserController.ensureAffiliationMiddleware( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUser.rejects(new Error('not found')) + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.req.query.ensureAffiliation = true + await ctx.UserController.ensureAffiliationMiddleware( + ctx.req, + ctx.res, + ctx.next ) }) - it('should return the error', function () { - expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error)) + it('should return the error', function (ctx) { + expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error)) }) }) })