diff --git a/services/web/app/src/Features/Exports/ExportsHandler.mjs b/services/web/app/src/Features/Exports/ExportsHandler.mjs index 4ef5fead4c..56cc10936b 100644 --- a/services/web/app/src/Features/Exports/ExportsHandler.mjs +++ b/services/web/app/src/Features/Exports/ExportsHandler.mjs @@ -13,7 +13,7 @@ */ import OError from '@overleaf/o-error' import ProjectGetter from '../Project/ProjectGetter.js' -import ProjectHistoryHandler from '../Project/ProjectHistoryHandler.js' +import ProjectHistoryHandler from '../Project/ProjectHistoryHandler.mjs' import ProjectLocator from '../Project/ProjectLocator.js' import ProjectRootDocManager from '../Project/ProjectRootDocManager.js' import UserGetter from '../User/UserGetter.js' diff --git a/services/web/app/src/Features/Project/ProjectEntityRestoreHandler.js b/services/web/app/src/Features/Project/ProjectEntityRestoreHandler.mjs similarity index 84% rename from services/web/app/src/Features/Project/ProjectEntityRestoreHandler.js rename to services/web/app/src/Features/Project/ProjectEntityRestoreHandler.mjs index 2ab8a2342c..9bb2202bc9 100644 --- a/services/web/app/src/Features/Project/ProjectEntityRestoreHandler.js +++ b/services/web/app/src/Features/Project/ProjectEntityRestoreHandler.mjs @@ -1,7 +1,7 @@ -const { callbackify } = require('util') -const Path = require('path') -const ProjectEntityHandler = require('./ProjectEntityHandler') -const EditorController = require('../Editor/EditorController') +import { callbackify } from 'node:util' +import Path from 'node:path' +import ProjectEntityHandler from './ProjectEntityHandler.js' +import EditorController from '../Editor/EditorController.js' // generate a new name based on the original, with an optional label. // e.g. origname-20210101-122345.tex (default) @@ -36,7 +36,7 @@ async function restoreDeletedDoc(projectId, docId, docName, userId) { ) } -module.exports = { +export default { restoreDeletedDoc: callbackify(restoreDeletedDoc), generateRestoredName, promises: { diff --git a/services/web/app/src/Features/Project/ProjectHistoryHandler.js b/services/web/app/src/Features/Project/ProjectHistoryHandler.mjs similarity index 85% rename from services/web/app/src/Features/Project/ProjectHistoryHandler.js rename to services/web/app/src/Features/Project/ProjectHistoryHandler.mjs index 5ba3ee911a..f9ef9ceb41 100644 --- a/services/web/app/src/Features/Project/ProjectHistoryHandler.js +++ b/services/web/app/src/Features/Project/ProjectHistoryHandler.mjs @@ -1,8 +1,8 @@ -const { Project } = require('../../models/Project') -const ProjectDetailsHandler = require('./ProjectDetailsHandler') -const HistoryManager = require('../History/HistoryManager') -const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') -const { callbackify } = require('util') +import { Project } from '../../models/Project.js' +import ProjectDetailsHandler from './ProjectDetailsHandler.js' +import HistoryManager from '../History/HistoryManager.js' +import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.js' +import { callbackify } from 'node:util' const ProjectHistoryHandler = { async setHistoryId(projectId, historyId) { @@ -55,7 +55,7 @@ const ProjectHistoryHandler = { }, } -module.exports = { +export default { setHistoryId: callbackify(ProjectHistoryHandler.setHistoryId), getHistoryId: callbackify(ProjectHistoryHandler.getHistoryId), ensureHistoryExistsForProject: callbackify( diff --git a/services/web/app/src/Features/Referal/ReferalAllocator.js b/services/web/app/src/Features/Referal/ReferalAllocator.mjs similarity index 78% rename from services/web/app/src/Features/Referal/ReferalAllocator.js rename to services/web/app/src/Features/Referal/ReferalAllocator.mjs index bf42fb840f..4d703788a9 100644 --- a/services/web/app/src/Features/Referal/ReferalAllocator.js +++ b/services/web/app/src/Features/Referal/ReferalAllocator.mjs @@ -1,7 +1,7 @@ -const OError = require('@overleaf/o-error') -const { User } = require('../../models/User') -const FeaturesUpdater = require('../Subscription/FeaturesUpdater') -const { callbackify } = require('@overleaf/promise-utils') +import OError from '@overleaf/o-error' +import { User } from '../../models/User.js' +import FeaturesUpdater from '../Subscription/FeaturesUpdater.js' +import { callbackify } from '@overleaf/promise-utils' async function allocate(referalId, newUserId, referalSource, referalMedium) { if (referalId == null) { @@ -40,7 +40,7 @@ async function allocate(referalId, newUserId, referalSource, referalMedium) { } } -module.exports = { +export default { allocate: callbackify(allocate), promises: { allocate, diff --git a/services/web/app/src/Features/ServerAdmin/AdminController.js b/services/web/app/src/Features/ServerAdmin/AdminController.mjs similarity index 82% rename from services/web/app/src/Features/ServerAdmin/AdminController.js rename to services/web/app/src/Features/ServerAdmin/AdminController.mjs index 261a5c23af..d5b81dfa9e 100644 --- a/services/web/app/src/Features/ServerAdmin/AdminController.js +++ b/services/web/app/src/Features/ServerAdmin/AdminController.mjs @@ -1,11 +1,11 @@ -const logger = require('@overleaf/logger') -const http = require('http') -const https = require('https') -const Settings = require('@overleaf/settings') -const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') -const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') -const EditorRealTimeController = require('../Editor/EditorRealTimeController') -const SystemMessageManager = require('../SystemMessages/SystemMessageManager') +import logger from '@overleaf/logger' +import http from 'node:http' +import https from 'node:https' +import Settings from '@overleaf/settings' +import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.js' +import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js' +import EditorRealTimeController from '../Editor/EditorRealTimeController.js' +import SystemMessageManager from '../SystemMessages/SystemMessageManager.js' const AdminController = { _sendDisconnectAllUsersMessage: delay => { @@ -94,4 +94,4 @@ const AdminController = { }, } -module.exports = AdminController +export default AdminController diff --git a/services/web/app/src/Features/SplitTests/SlackNotificationManager.js b/services/web/app/src/Features/SplitTests/SlackNotificationManager.mjs similarity index 87% rename from services/web/app/src/Features/SplitTests/SlackNotificationManager.js rename to services/web/app/src/Features/SplitTests/SlackNotificationManager.mjs index e90d4954b1..469810bf26 100644 --- a/services/web/app/src/Features/SplitTests/SlackNotificationManager.js +++ b/services/web/app/src/Features/SplitTests/SlackNotificationManager.mjs @@ -1,8 +1,8 @@ -const logger = require('@overleaf/logger') -const Settings = require('@overleaf/settings') -const { IncomingWebhook } = require('@slack/webhook') -const moment = require('moment') -const SplitTestUtils = require('./SplitTestUtils') +import logger from '@overleaf/logger' +import Settings from '@overleaf/settings' +import { IncomingWebhook } from '@slack/webhook' +import moment from 'moment' +import SplitTestUtils from './SplitTestUtils.js' async function sendNotification(splitTest, action, user) { const lastVersion = SplitTestUtils.getCurrentVersion(splitTest) @@ -60,6 +60,6 @@ async function sendNotification(splitTest, action, user) { } } -module.exports = { +export default { sendNotification, } diff --git a/services/web/app/src/Features/SplitTests/SplitTestMiddleware.js b/services/web/app/src/Features/SplitTests/SplitTestMiddleware.mjs similarity index 82% rename from services/web/app/src/Features/SplitTests/SplitTestMiddleware.js rename to services/web/app/src/Features/SplitTests/SplitTestMiddleware.mjs index 2e8e1ff97f..3e87b75468 100644 --- a/services/web/app/src/Features/SplitTests/SplitTestMiddleware.js +++ b/services/web/app/src/Features/SplitTests/SplitTestMiddleware.mjs @@ -1,7 +1,7 @@ -const SplitTestHandler = require('./SplitTestHandler') -const logger = require('@overleaf/logger') -const { expressify } = require('@overleaf/promise-utils') -const Errors = require('../Errors/Errors') +import SplitTestHandler from './SplitTestHandler.js' +import logger from '@overleaf/logger' +import { expressify } from '@overleaf/promise-utils' +import Errors from '../Errors/Errors.js' function loadAssignmentsInLocals(splitTestNames) { return async function (req, res, next) { @@ -43,7 +43,7 @@ function ensureSplitTestEnabledForUser( }) } -module.exports = { +export default { loadAssignmentsInLocals, ensureSplitTestEnabledForUser, } diff --git a/services/web/app/src/Features/Subscription/GroupUtils.js b/services/web/app/src/Features/Subscription/GroupUtils.mjs similarity index 94% rename from services/web/app/src/Features/Subscription/GroupUtils.js rename to services/web/app/src/Features/Subscription/GroupUtils.mjs index 55aea1a180..38f343941d 100644 --- a/services/web/app/src/Features/Subscription/GroupUtils.js +++ b/services/web/app/src/Features/Subscription/GroupUtils.mjs @@ -9,6 +9,6 @@ function getProviderId(subscriptionId) { return `ol-group-subscription-id:${subscriptionId.toString()}` } -module.exports = { +export default { getProviderId, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index 8c5c2bb976..b61a9d9860 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -1,4 +1,4 @@ -import SubscriptionGroupHandler from './SubscriptionGroupHandler.js' +import SubscriptionGroupHandler from './SubscriptionGroupHandler.mjs' import OError from '@overleaf/o-error' import logger from '@overleaf/logger' diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.mjs similarity index 93% rename from services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js rename to services/web/app/src/Features/Subscription/SubscriptionGroupHandler.mjs index d5d94a89a7..c6eadc6cca 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.mjs @@ -1,26 +1,29 @@ -const { callbackify } = require('util') -const _ = require('lodash') -const OError = require('@overleaf/o-error') -const SubscriptionUpdater = require('./SubscriptionUpdater') -const SubscriptionLocator = require('./SubscriptionLocator') -const SubscriptionController = require('./SubscriptionController') -const SubscriptionHelper = require('./SubscriptionHelper') -const { Subscription } = require('../../models/Subscription') -const { User } = require('../../models/User') -const PlansLocator = require('./PlansLocator') -const TeamInvitesHandler = require('./TeamInvitesHandler') -const GroupPlansData = require('./GroupPlansData') -const Modules = require('../../infrastructure/Modules') -const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./PaymentProviderEntities') -const { +import { callbackify } from 'node:util' +import _ from 'lodash' +import OError from '@overleaf/o-error' +import SubscriptionUpdater from './SubscriptionUpdater.js' +import SubscriptionLocator from './SubscriptionLocator.js' +import SubscriptionController from './SubscriptionController.js' +import SubscriptionHelper from './SubscriptionHelper.js' +import { Subscription } from '../../models/Subscription.js' +import { User } from '../../models/User.js' +import PlansLocator from './PlansLocator.js' +import TeamInvitesHandler from './TeamInvitesHandler.js' +import GroupPlansData from './GroupPlansData.js' +import Modules from '../../infrastructure/Modules.js' +import PaymentProviderEntities from './PaymentProviderEntities.js' +import { ManuallyCollectedError, PendingChangeError, InactiveError, HasPastDueInvoiceError, HasNoAdditionalLicenseWhenManuallyCollectedError, -} = require('./Errors') -const EmailHelper = require('../Helpers/EmailHelper') -const { InvalidEmailError } = require('../Errors/Errors') +} from './Errors.js' +import EmailHelper from '../Helpers/EmailHelper.js' +import { InvalidEmailError } from '../Errors/Errors.js' + +const MEMBERS_LIMIT_ADD_ON_CODE = + PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE async function removeUserFromGroup(subscriptionId, userIdToRemove, auditLog) { await SubscriptionUpdater.promises.removeUserFromGroup( @@ -470,7 +473,7 @@ async function updateGroupMembersBulk( return result } -module.exports = { +export default { removeUserFromGroup: callbackify(removeUserFromGroup), replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups), ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled), diff --git a/services/web/app/src/Features/Survey/SurveyCache.mjs b/services/web/app/src/Features/Survey/SurveyCache.mjs index 28b43fed1f..d3552c25d7 100644 --- a/services/web/app/src/Features/Survey/SurveyCache.mjs +++ b/services/web/app/src/Features/Survey/SurveyCache.mjs @@ -1,4 +1,4 @@ -import SurveyManager from './SurveyManager.js' +import SurveyManager from './SurveyManager.mjs' import { Survey } from '../../models/Survey.js' import { CacheLoader } from 'cache-flow' diff --git a/services/web/app/src/Features/Survey/SurveyManager.js b/services/web/app/src/Features/Survey/SurveyManager.mjs similarity index 93% rename from services/web/app/src/Features/Survey/SurveyManager.js rename to services/web/app/src/Features/Survey/SurveyManager.mjs index 7eac05f5d3..cbb23ca0fa 100644 --- a/services/web/app/src/Features/Survey/SurveyManager.js +++ b/services/web/app/src/Features/Survey/SurveyManager.mjs @@ -1,5 +1,5 @@ -const { Survey } = require('../../models/Survey') -const OError = require('@overleaf/o-error') +import { Survey } from '../../models/Survey.js' +import OError from '@overleaf/o-error' async function getSurvey() { try { @@ -67,7 +67,7 @@ async function deleteSurvey() { } } -module.exports = { +export default { getSurvey, updateSurvey, deleteSurvey, diff --git a/services/web/app/src/Features/SystemMessages/SystemMessageController.js b/services/web/app/src/Features/SystemMessages/SystemMessageController.mjs similarity index 74% rename from services/web/app/src/Features/SystemMessages/SystemMessageController.js rename to services/web/app/src/Features/SystemMessages/SystemMessageController.mjs index 41339aaf6f..5bcd27c6f3 100644 --- a/services/web/app/src/Features/SystemMessages/SystemMessageController.js +++ b/services/web/app/src/Features/SystemMessages/SystemMessageController.mjs @@ -1,6 +1,6 @@ -const Settings = require('@overleaf/settings') -const SessionManager = require('../Authentication/SessionManager') -const SystemMessageManager = require('./SystemMessageManager') +import Settings from '@overleaf/settings' +import SessionManager from '../Authentication/SessionManager.js' +import SystemMessageManager from './SystemMessageManager.js' const ProjectController = { getMessages(req, res, next) { @@ -24,4 +24,4 @@ const ProjectController = { }, } -module.exports = ProjectController +export default ProjectController diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index 1bd0b25237..ddfabaea22 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -1,4 +1,4 @@ -import AdminController from './Features/ServerAdmin/AdminController.js' +import AdminController from './Features/ServerAdmin/AdminController.mjs' import ErrorController from './Features/Errors/ErrorController.mjs' import Features from './infrastructure/Features.js' import ProjectController from './Features/Project/ProjectController.mjs' @@ -54,7 +54,7 @@ import TokenAccessRouter from './Features/TokenAccess/TokenAccessRouter.mjs' import LinkedFilesRouter from './Features/LinkedFiles/LinkedFilesRouter.mjs' import TemplatesRouter from './Features/Templates/TemplatesRouter.js' import UserMembershipRouter from './Features/UserMembership/UserMembershipRouter.mjs' -import SystemMessageController from './Features/SystemMessages/SystemMessageController.js' +import SystemMessageController from './Features/SystemMessages/SystemMessageController.mjs' import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.js' import AnalyticsUTMTrackingMiddleware from './Features/Analytics/AnalyticsUTMTrackingMiddleware.mjs' import CaptchaMiddleware from './Features/Captcha/CaptchaMiddleware.mjs' diff --git a/services/web/scripts/add_subscription_members_csv.mjs b/services/web/scripts/add_subscription_members_csv.mjs index 4120e16fdb..1dbd5d8084 100644 --- a/services/web/scripts/add_subscription_members_csv.mjs +++ b/services/web/scripts/add_subscription_members_csv.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs' import minimist from 'minimist' import { parse } from 'csv' import Stream from 'node:stream/promises' -import SubscriptionGroupHandler from '../app/src/Features/Subscription/SubscriptionGroupHandler.js' +import SubscriptionGroupHandler from '../app/src/Features/Subscription/SubscriptionGroupHandler.mjs' import { Subscription } from '../app/src/models/Subscription.js' import { InvalidEmailError } from '../app/src/Features/Errors/Errors.js' diff --git a/services/web/scripts/disconnect_all_users.mjs b/services/web/scripts/disconnect_all_users.mjs index f8e43ad849..d2b1147af8 100644 --- a/services/web/scripts/disconnect_all_users.mjs +++ b/services/web/scripts/disconnect_all_users.mjs @@ -1,6 +1,6 @@ import { promisify } from 'node:util' import Settings from '@overleaf/settings' -import AdminController from '../app/src/Features/ServerAdmin/AdminController.js' +import AdminController from '../app/src/Features/ServerAdmin/AdminController.mjs' import minimist from 'minimist' import { fileURLToPath } from 'node:url' import { scriptRunner } from './lib/ScriptRunner.mjs' diff --git a/services/web/scripts/recover_docs_from_redis.mjs b/services/web/scripts/recover_docs_from_redis.mjs index 85709b471b..ac2cc1c029 100644 --- a/services/web/scripts/recover_docs_from_redis.mjs +++ b/services/web/scripts/recover_docs_from_redis.mjs @@ -4,7 +4,7 @@ import minimist from 'minimist' import { db, ObjectId } from '../app/src/infrastructure/mongodb.js' import ProjectEntityUpdateHandler from '../app/src/Features/Project/ProjectEntityUpdateHandler.js' -import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.js' +import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.mjs' import RedisWrapper from '@overleaf/redis-wrapper' import Settings from '@overleaf/settings' import logger from '@overleaf/logger' diff --git a/services/web/scripts/restore_orphaned_docs.mjs b/services/web/scripts/restore_orphaned_docs.mjs index 7b4df055a9..3edbc5d15a 100644 --- a/services/web/scripts/restore_orphaned_docs.mjs +++ b/services/web/scripts/restore_orphaned_docs.mjs @@ -1,4 +1,4 @@ -import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.js' +import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.mjs' import ProjectEntityHandler from '../app/src/Features/Project/ProjectEntityHandler.js' import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js' import { scriptRunner } from './lib/ScriptRunner.mjs' diff --git a/services/web/scripts/restore_soft_deleted_docs.mjs b/services/web/scripts/restore_soft_deleted_docs.mjs index 5faf85559e..a3694ff40b 100644 --- a/services/web/scripts/restore_soft_deleted_docs.mjs +++ b/services/web/scripts/restore_soft_deleted_docs.mjs @@ -1,4 +1,4 @@ -import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.js' +import ProjectEntityRestoreHandler from '../app/src/Features/Project/ProjectEntityRestoreHandler.mjs' import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js' import { scriptRunner } from './lib/ScriptRunner.mjs' diff --git a/services/web/test/acceptance/src/helpers/groupSSO.mjs b/services/web/test/acceptance/src/helpers/groupSSO.mjs index 6a5714825b..fd92fcebda 100644 --- a/services/web/test/acceptance/src/helpers/groupSSO.mjs +++ b/services/web/test/acceptance/src/helpers/groupSSO.mjs @@ -6,7 +6,7 @@ import { SSOConfig } from '../../../../app/src/models/SSOConfig.js' import UserHelper from './UserHelper.mjs' import SAMLHelper from './SAMLHelper.mjs' import Settings from '@overleaf/settings' -import { getProviderId } from '../../../../app/src/Features/Subscription/GroupUtils.js' +import GroupUtils from '../../../../app/src/Features/Subscription/GroupUtils.mjs' import UserGetter from '../../../../app/src/Features/User/UserGetter.js' import { fileURLToPath } from 'node:url' import { Subscription as SubscriptionModel } from '../../../../app/src/models/Subscription.js' @@ -75,7 +75,7 @@ export async function createGroupSSO( await subscription.ensureExists() const subscriptionId = subscription._id.toString() const enrollmentUrl = getEnrollmentUrl(subscriptionId) - const internalProviderId = getProviderId(subscriptionId) + const internalProviderId = GroupUtils.getProviderId(subscriptionId) if (SSOConfigValidated) { await linkGroupMember( @@ -122,7 +122,7 @@ export async function linkGroupMember( .exec() const userIdAttribute = subscription?.ssoConfig?.userIdAttribute - const internalProviderId = getProviderId(groupId) + const internalProviderId = GroupUtils.getProviderId(groupId) const enrollmentUrl = getEnrollmentUrl(groupId) const userHelper = await UserHelper.loginUser( { @@ -189,7 +189,7 @@ export async function linkGroupMember( } export async function checkUserHasSSOLinked(userId, groupId) { - const internalProviderId = getProviderId(groupId) + const internalProviderId = GroupUtils.getProviderId(groupId) const user = await UserGetter.promises.getUser( { _id: userId }, { samlIdentifiers: 1, enrollment: 1 } diff --git a/services/web/test/unit/src/Project/ProjectEntityRestoreHandler.test.mjs b/services/web/test/unit/src/Project/ProjectEntityRestoreHandler.test.mjs new file mode 100644 index 0000000000..80d30ceacc --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectEntityRestoreHandler.test.mjs @@ -0,0 +1,105 @@ +import { vi, expect } from 'vitest' +import sinon from 'sinon' + +const MODULE_PATH = + '../../../../app/src/Features/Project/ProjectEntityRestoreHandler.mjs' + +describe('ProjectEntityRestoreHandler', function () { + beforeEach(async function (ctx) { + ctx.project = { + _id: '123213jlkj9kdlsaj', + } + + ctx.user = { + _id: '588f3ddae8ebc1bac07c9fa4', + first_name: 'bjkdsjfk', + features: {}, + } + + ctx.docId = '4eecb1c1bffa66588e0000a2' + + ctx.DocModel = class Doc { + constructor(options) { + this.name = options.name + this.lines = options.lines + this._id = this.docId + this.rev = 0 + } + } + + ctx.ProjectEntityHandler = { + promises: { + getDoc: sinon.stub(), + }, + } + + ctx.EditorController = { + promises: { + addDocWithRanges: sinon.stub(), + }, + } + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler.js', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorController.js', + () => ({ + default: ctx.EditorController, + }) + ) + + ctx.ProjectEntityRestoreHandler = (await import(MODULE_PATH)).default + }) + + it('should add a new doc with timestamp name and old content', async function (ctx) { + const docName = 'deletedDoc' + + ctx.docLines = ['line one', 'line two'] + ctx.rev = 3 + ctx.ranges = { comments: [{ id: 123 }] } + + ctx.newDoc = new ctx.DocModel({ + name: ctx.docName, + lines: undefined, + _id: ctx.docId, + rev: 0, + }) + + ctx.ProjectEntityHandler.promises.getDoc.resolves({ + lines: ctx.docLines, + rev: ctx.rev, + version: 'version', + ranges: ctx.ranges, + }) + + ctx.EditorController.promises.addDocWithRanges = sinon + .stub() + .resolves(ctx.newDoc) + + await ctx.ProjectEntityRestoreHandler.promises.restoreDeletedDoc( + ctx.project._id, + ctx.docId, + docName, + ctx.user._id + ) + + const docNameMatcher = new RegExp(docName + '-\\d{4}-\\d{2}-\\d{2}-\\d+') + + expect( + ctx.EditorController.promises.addDocWithRanges + ).to.have.been.calledWith( + ctx.project._id, + null, + sinon.match(docNameMatcher), + ctx.docLines, + ctx.ranges, + null, + ctx.user._id + ) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectEntityRestoreHandlerTests.js b/services/web/test/unit/src/Project/ProjectEntityRestoreHandlerTests.js deleted file mode 100644 index c41d61c60a..0000000000 --- a/services/web/test/unit/src/Project/ProjectEntityRestoreHandlerTests.js +++ /dev/null @@ -1,97 +0,0 @@ -const { expect } = require('chai') -const sinon = require('sinon') -const SandboxedModule = require('sandboxed-module') - -const MODULE_PATH = - '../../../../app/src/Features/Project/ProjectEntityRestoreHandler.js' - -describe('ProjectEntityRestoreHandler', function () { - beforeEach(function () { - this.project = { - _id: '123213jlkj9kdlsaj', - } - - this.user = { - _id: '588f3ddae8ebc1bac07c9fa4', - first_name: 'bjkdsjfk', - features: {}, - } - - this.docId = '4eecb1c1bffa66588e0000a2' - - this.DocModel = class Doc { - constructor(options) { - this.name = options.name - this.lines = options.lines - this._id = this.docId - this.rev = 0 - } - } - - this.ProjectEntityHandler = { - promises: { - getDoc: sinon.stub(), - }, - } - - this.EditorController = { - promises: { - addDocWithRanges: sinon.stub(), - }, - } - - this.ProjectEntityRestoreHandler = SandboxedModule.require(MODULE_PATH, { - requires: { - './ProjectEntityHandler': this.ProjectEntityHandler, - '../Editor/EditorController': this.EditorController, - }, - }) - }) - - it('should add a new doc with timestamp name and old content', async function () { - const docName = 'deletedDoc' - - this.docLines = ['line one', 'line two'] - this.rev = 3 - this.ranges = { comments: [{ id: 123 }] } - - this.newDoc = new this.DocModel({ - name: this.docName, - lines: undefined, - _id: this.docId, - rev: 0, - }) - - this.ProjectEntityHandler.promises.getDoc.resolves({ - lines: this.docLines, - rev: this.rev, - version: 'version', - ranges: this.ranges, - }) - - this.EditorController.promises.addDocWithRanges = sinon - .stub() - .resolves(this.newDoc) - - await this.ProjectEntityRestoreHandler.promises.restoreDeletedDoc( - this.project._id, - this.docId, - docName, - this.user._id - ) - - const docNameMatcher = new RegExp(docName + '-\\d{4}-\\d{2}-\\d{2}-\\d+') - - expect( - this.EditorController.promises.addDocWithRanges - ).to.have.been.calledWith( - this.project._id, - null, - sinon.match(docNameMatcher), - this.docLines, - this.ranges, - null, - this.user._id - ) - }) -}) diff --git a/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs b/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs new file mode 100644 index 0000000000..8604fcd053 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs @@ -0,0 +1,166 @@ +import { vi } from 'vitest' +import sinon from 'sinon' + +const modulePath = + '../../../../app/src/Features/Project/ProjectHistoryHandler.mjs' + +describe('ProjectHistoryHandler', function () { + const projectId = '4eecb1c1bffa66588e0000a1' + + beforeEach(async function (ctx) { + let Project + ctx.ProjectModel = Project = (function () { + Project = class Project { + static initClass() { + this.prototype.rootFolder = [this.rootFolder] + } + + constructor(options) { + this._id = projectId + this.name = 'project_name_here' + this.rev = 0 + } + } + Project.initClass() + return Project + })() + ctx.project = new ctx.ProjectModel() + ctx.historyId = ctx.project._id.toString() + + ctx.callback = sinon.stub() + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.Settings = {}), + })) + + vi.doMock('../../../../app/src/models/Project.js', () => ({ + Project: ctx.ProjectModel, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler.js', + () => ({ + default: (ctx.ProjectDetailsHandler = { + promises: {}, + }), + }) + ) + + vi.doMock('../../../../app/src/Features/History/HistoryManager.js', () => ({ + default: (ctx.HistoryManager = { + promises: {}, + }), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', + () => ({ + default: (ctx.ProjectEntityUpdateHandler = { + promises: {}, + }), + }) + ) + + return (ctx.ProjectHistoryHandler = (await import(modulePath)).default) + }) + + describe('starting history for an existing project', function () { + beforeEach(async function (ctx) { + ctx.HistoryManager.promises.initializeProject = sinon + .stub() + .resolves(ctx.historyId) + ctx.HistoryManager.promises.flushProject = sinon.stub() + + return (ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory = + sinon.stub()) + }) + + describe('when the history does not already exist', function () { + beforeEach(async function (ctx) { + ctx.ProjectDetailsHandler.promises.getDetails = sinon + .stub() + .withArgs(projectId) + .resolves(ctx.project) + ctx.ProjectModel.updateOne = sinon.stub().resolves({ matchedCount: 1 }) + return ctx.ProjectHistoryHandler.promises.ensureHistoryExistsForProject( + projectId + ) + }) + + it('should get any existing history id for the project', async function (ctx) { + return ctx.ProjectDetailsHandler.promises.getDetails + .calledWith(projectId) + .should.equal(true) + }) + + it('should initialize a new history in the v1 history service', async function (ctx) { + return ctx.HistoryManager.promises.initializeProject.called.should.equal( + true + ) + }) + + it('should set the new history id on the project', async function (ctx) { + return ctx.ProjectModel.updateOne + .calledWith( + { _id: projectId, 'overleaf.history.id': { $exists: false } }, + { 'overleaf.history.id': ctx.historyId } + ) + .should.equal(true) + }) + + it('should resync the project history', async function (ctx) { + return ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory + .calledWith(projectId) + .should.equal(true) + }) + + it('should flush the project history', async function (ctx) { + return ctx.HistoryManager.promises.flushProject + .calledWith(projectId) + .should.equal(true) + }) + }) + + describe('when the history already exists', function () { + beforeEach(function (ctx) { + ctx.project.overleaf = { history: { id: 1234 } } + ctx.ProjectDetailsHandler.promises.getDetails = sinon + .stub() + .withArgs(projectId) + .resolves(ctx.project) + ctx.ProjectModel.updateOne = sinon.stub().resolves({ matchedCount: 1 }) + return ctx.ProjectHistoryHandler.promises.ensureHistoryExistsForProject( + projectId + ) + }) + + it('should get any existing history id for the project', async function (ctx) { + return ctx.ProjectDetailsHandler.promises.getDetails + .calledWith(projectId) + .should.equal(true) + }) + + it('should not initialize a new history in the v1 history service', async function (ctx) { + return ctx.HistoryManager.promises.initializeProject.called.should.equal( + false + ) + }) + + it('should not set the new history id on the project', async function (ctx) { + return ctx.ProjectModel.updateOne.called.should.equal(false) + }) + + it('should not resync the project history', async function (ctx) { + return ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory.called.should.equal( + false + ) + }) + + it('should not flush the project history', async function (ctx) { + return ctx.HistoryManager.promises.flushProject.called.should.equal( + false + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js b/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js deleted file mode 100644 index 3d279777a3..0000000000 --- a/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable - max-len, - no-return-assign, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const { assert, expect } = require('chai') -const sinon = require('sinon') -const modulePath = '../../../../app/src/Features/Project/ProjectHistoryHandler' -const SandboxedModule = require('sandboxed-module') - -describe('ProjectHistoryHandler', function () { - const projectId = '4eecb1c1bffa66588e0000a1' - const userId = 1234 - - beforeEach(function () { - let Project - this.ProjectModel = Project = (function () { - Project = class Project { - static initClass() { - this.prototype.rootFolder = [this.rootFolder] - } - - constructor(options) { - this._id = projectId - this.name = 'project_name_here' - this.rev = 0 - } - } - Project.initClass() - return Project - })() - this.project = new this.ProjectModel() - this.historyId = this.project._id.toString() - - this.callback = sinon.stub() - - return (this.ProjectHistoryHandler = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/settings': (this.Settings = {}), - '../../models/Project': { - Project: this.ProjectModel, - }, - './ProjectDetailsHandler': (this.ProjectDetailsHandler = { - promises: {}, - }), - '../History/HistoryManager': (this.HistoryManager = { - promises: {}, - }), - './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = { - promises: {}, - }), - }, - })) - }) - - describe('starting history for an existing project', function () { - beforeEach(async function () { - this.HistoryManager.promises.initializeProject = sinon - .stub() - .resolves(this.historyId) - this.HistoryManager.promises.flushProject = sinon.stub() - - return (this.ProjectEntityUpdateHandler.promises.resyncProjectHistory = - sinon.stub()) - }) - - describe('when the history does not already exist', function () { - beforeEach(async function () { - this.ProjectDetailsHandler.promises.getDetails = sinon - .stub() - .withArgs(projectId) - .resolves(this.project) - this.ProjectModel.updateOne = sinon.stub().resolves({ matchedCount: 1 }) - return this.ProjectHistoryHandler.promises.ensureHistoryExistsForProject( - projectId - ) - }) - - it('should get any existing history id for the project', async function () { - return this.ProjectDetailsHandler.promises.getDetails - .calledWith(projectId) - .should.equal(true) - }) - - it('should initialize a new history in the v1 history service', async function () { - return this.HistoryManager.promises.initializeProject.called.should.equal( - true - ) - }) - - it('should set the new history id on the project', async function () { - return this.ProjectModel.updateOne - .calledWith( - { _id: projectId, 'overleaf.history.id': { $exists: false } }, - { 'overleaf.history.id': this.historyId } - ) - .should.equal(true) - }) - - it('should resync the project history', async function () { - return this.ProjectEntityUpdateHandler.promises.resyncProjectHistory - .calledWith(projectId) - .should.equal(true) - }) - - it('should flush the project history', async function () { - return this.HistoryManager.promises.flushProject - .calledWith(projectId) - .should.equal(true) - }) - }) - - describe('when the history already exists', function () { - beforeEach(function () { - this.project.overleaf = { history: { id: 1234 } } - this.ProjectDetailsHandler.promises.getDetails = sinon - .stub() - .withArgs(projectId) - .resolves(this.project) - this.ProjectModel.updateOne = sinon.stub().resolves({ matchedCount: 1 }) - return this.ProjectHistoryHandler.promises.ensureHistoryExistsForProject( - projectId - ) - }) - - it('should get any existing history id for the project', async function () { - return this.ProjectDetailsHandler.promises.getDetails - .calledWith(projectId) - .should.equal(true) - }) - - it('should not initialize a new history in the v1 history service', async function () { - return this.HistoryManager.promises.initializeProject.called.should.equal( - false - ) - }) - - it('should not set the new history id on the project', async function () { - return this.ProjectModel.updateOne.called.should.equal(false) - }) - - it('should not resync the project history', async function () { - return this.ProjectEntityUpdateHandler.promises.resyncProjectHistory.called.should.equal( - false - ) - }) - - it('should not flush the project history', async function () { - return this.HistoryManager.promises.flushProject.called.should.equal( - false - ) - }) - }) - }) -}) diff --git a/services/web/test/unit/src/Referal/ReferalAllocator.test.mjs b/services/web/test/unit/src/Referal/ReferalAllocator.test.mjs new file mode 100644 index 0000000000..091f86ff1f --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalAllocator.test.mjs @@ -0,0 +1,138 @@ +import { vi } from 'vitest' +import sinon from 'sinon' + +const modulePath = '../../../../app/src/Features/Referal/ReferalAllocator.mjs' + +describe('ReferalAllocator', function () { + beforeEach(async function (ctx) { + vi.doMock('../../../../app/src/models/User.js', () => ({ + User: (ctx.User = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/FeaturesUpdater.js', + () => ({ + default: (ctx.FeaturesUpdater = {}), + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.Settings = {}), + })) + + ctx.ReferalAllocator = (await import(modulePath)).default + ctx.referal_id = 'referal-id-123' + ctx.referal_medium = 'twitter' + ctx.user_id = 'user-id-123' + ctx.new_user_id = 'new-user-id-123' + ctx.FeaturesUpdater.promises = { + refreshFeatures: sinon.stub().resolves(), + } + ctx.User.updateOne = sinon.stub().returns({ + exec: sinon.stub().resolves(), + }) + ctx.User.findOne = sinon.stub().returns({ + exec: sinon.stub().resolves({ _id: ctx.user_id }), + }) + }) + + describe('allocate', function () { + describe('when the referal was a bonus referal', function () { + beforeEach(async function (ctx) { + ctx.referal_source = 'bonus' + await ctx.ReferalAllocator.promises.allocate( + ctx.referal_id, + ctx.new_user_id, + ctx.referal_source, + ctx.referal_medium + ) + }) + + it('should update the referring user with the refered users id', function (ctx) { + ctx.User.updateOne + .calledWith( + { + referal_id: ctx.referal_id, + }, + { + $push: { + refered_users: ctx.new_user_id, + }, + $inc: { + refered_user_count: 1, + }, + } + ) + .should.equal(true) + }) + + it('find the referring users id', function (ctx) { + ctx.User.findOne + .calledWith({ referal_id: ctx.referal_id }) + .should.equal(true) + }) + + it("should refresh the user's subscription", function (ctx) { + ctx.FeaturesUpdater.promises.refreshFeatures + .calledWith(ctx.user_id) + .should.equal(true) + }) + }) + + describe('when there is no user for the referal id', function () { + beforeEach(async function (ctx) { + ctx.referal_source = 'bonus' + ctx.referal_id = 'wombat' + ctx.User.findOne = sinon.stub().returns({ + exec: sinon.stub().resolves(null), + }) + await ctx.ReferalAllocator.promises.allocate( + ctx.referal_id, + ctx.new_user_id, + ctx.referal_source, + ctx.referal_medium + ) + }) + + it('should find the referring users id', function (ctx) { + ctx.User.findOne + .calledWith({ referal_id: ctx.referal_id }) + .should.equal(true) + }) + + it('should not update the referring user with the refered users id', function (ctx) { + ctx.User.updateOne.called.should.equal(false) + }) + + it('should not assign the user a bonus', function (ctx) { + ctx.FeaturesUpdater.promises.refreshFeatures.called.should.equal(false) + }) + }) + + describe('when the referal is not a bonus referal', function () { + beforeEach(async function (ctx) { + ctx.referal_source = 'public_share' + await ctx.ReferalAllocator.promises.allocate( + ctx.referal_id, + ctx.new_user_id, + ctx.referal_source, + ctx.referal_medium + ) + }) + + it('should not update the referring user with the refered users id', function (ctx) { + ctx.User.updateOne.called.should.equal(false) + }) + + it('find the referring users id', function (ctx) { + ctx.User.findOne + .calledWith({ referal_id: ctx.referal_id }) + .should.equal(true) + }) + + it('should not assign the user a bonus', function (ctx) { + ctx.FeaturesUpdater.promises.refreshFeatures.called.should.equal(false) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Referal/ReferalAllocatorTests.js b/services/web/test/unit/src/Referal/ReferalAllocatorTests.js deleted file mode 100644 index ecc0c6ea89..0000000000 --- a/services/web/test/unit/src/Referal/ReferalAllocatorTests.js +++ /dev/null @@ -1,133 +0,0 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const modulePath = require('path').join( - __dirname, - '../../../../app/src/Features/Referal/ReferalAllocator.js' -) - -describe('ReferalAllocator', function () { - beforeEach(function () { - this.ReferalAllocator = SandboxedModule.require(modulePath, { - requires: { - '../../models/User': { - User: (this.User = {}), - }, - '../Subscription/FeaturesUpdater': (this.FeaturesUpdater = {}), - '@overleaf/settings': (this.Settings = {}), - }, - }) - this.referal_id = 'referal-id-123' - this.referal_medium = 'twitter' - this.user_id = 'user-id-123' - this.new_user_id = 'new-user-id-123' - this.FeaturesUpdater.promises = { - refreshFeatures: sinon.stub().resolves(), - } - this.User.updateOne = sinon.stub().returns({ - exec: sinon.stub().resolves(), - }) - this.User.findOne = sinon.stub().returns({ - exec: sinon.stub().resolves({ _id: this.user_id }), - }) - }) - - describe('allocate', function () { - describe('when the referal was a bonus referal', function () { - beforeEach(async function () { - this.referal_source = 'bonus' - await this.ReferalAllocator.promises.allocate( - this.referal_id, - this.new_user_id, - this.referal_source, - this.referal_medium - ) - }) - - it('should update the referring user with the refered users id', function () { - this.User.updateOne - .calledWith( - { - referal_id: this.referal_id, - }, - { - $push: { - refered_users: this.new_user_id, - }, - $inc: { - refered_user_count: 1, - }, - } - ) - .should.equal(true) - }) - - it('find the referring users id', function () { - this.User.findOne - .calledWith({ referal_id: this.referal_id }) - .should.equal(true) - }) - - it("should refresh the user's subscription", function () { - this.FeaturesUpdater.promises.refreshFeatures - .calledWith(this.user_id) - .should.equal(true) - }) - }) - - describe('when there is no user for the referal id', function () { - beforeEach(async function () { - this.referal_source = 'bonus' - this.referal_id = 'wombat' - this.User.findOne = sinon.stub().returns({ - exec: sinon.stub().resolves(null), - }) - await this.ReferalAllocator.promises.allocate( - this.referal_id, - this.new_user_id, - this.referal_source, - this.referal_medium - ) - }) - - it('should find the referring users id', function () { - this.User.findOne - .calledWith({ referal_id: this.referal_id }) - .should.equal(true) - }) - - it('should not update the referring user with the refered users id', function () { - this.User.updateOne.called.should.equal(false) - }) - - it('should not assign the user a bonus', function () { - this.FeaturesUpdater.promises.refreshFeatures.called.should.equal(false) - }) - }) - - describe('when the referal is not a bonus referal', function () { - beforeEach(async function () { - this.referal_source = 'public_share' - await this.ReferalAllocator.promises.allocate( - this.referal_id, - this.new_user_id, - this.referal_source, - this.referal_medium - ) - }) - - it('should not update the referring user with the refered users id', function () { - this.User.updateOne.called.should.equal(false) - }) - - it('find the referring users id', function () { - this.User.findOne - .calledWith({ referal_id: this.referal_id }) - .should.equal(true) - }) - - it('should not assign the user a bonus', function () { - this.FeaturesUpdater.promises.refreshFeatures.called.should.equal(false) - }) - }) - }) -}) diff --git a/services/web/test/unit/src/SplitTests/SplitTestMiddleware.test.mjs b/services/web/test/unit/src/SplitTests/SplitTestMiddleware.test.mjs new file mode 100644 index 0000000000..a69991137c --- /dev/null +++ b/services/web/test/unit/src/SplitTests/SplitTestMiddleware.test.mjs @@ -0,0 +1,89 @@ +import { vi } from 'vitest' +import sinon from 'sinon' +import MockResponse from '../helpers/MockResponse.js' +import MockRequest from '../helpers/MockRequest.js' + +const modulePath = '../../../../app/src/Features/SplitTests/SplitTestMiddleware' + +describe('SplitTestMiddleware', function () { + beforeEach(async function (ctx) { + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.js', + () => ({ + default: (ctx.SplitTestHandler = { + promises: { + getAssignment: sinon.stub().resolves(), + }, + }), + }) + ) + + ctx.SplitTestMiddleware = (await import(modulePath)).default + + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() + }) + + it('assign multiple split test variants in locals', async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment + .withArgs(ctx.req, 'ui-overhaul') + .resolves({ + variant: 'default', + }) + ctx.SplitTestHandler.promises.getAssignment + .withArgs(ctx.req, 'other-test') + .resolves({ + variant: 'foobar', + }) + + const middleware = ctx.SplitTestMiddleware.loadAssignmentsInLocals([ + 'ui-overhaul', + 'other-test', + ]) + await middleware(ctx.req, ctx.res, ctx.next) + + sinon.assert.calledWith( + ctx.SplitTestHandler.promises.getAssignment, + ctx.req, + ctx.res, + 'ui-overhaul' + ) + sinon.assert.calledWith( + ctx.SplitTestHandler.promises.getAssignment, + ctx.req, + ctx.res, + 'other-test' + ) + sinon.assert.calledOnce(ctx.next) + }) + + it('assign no split test variant in locals', async function (ctx) { + const middleware = ctx.SplitTestMiddleware.loadAssignmentsInLocals([]) + + await middleware(ctx.req, ctx.res, ctx.next) + + sinon.assert.notCalled(ctx.SplitTestHandler.promises.getAssignment) + sinon.assert.calledOnce(ctx.next) + }) + + it('exception thrown by assignment does not fail the request', async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment + .withArgs(ctx.req, ctx.res, 'some-test') + .throws(new Error('failure')) + + const middleware = ctx.SplitTestMiddleware.loadAssignmentsInLocals([ + 'some-test', + ]) + + await middleware(ctx.req, ctx.res, ctx.next) + + sinon.assert.calledWith( + ctx.SplitTestHandler.promises.getAssignment, + ctx.req, + ctx.res, + 'some-test' + ) + sinon.assert.calledOnce(ctx.next) + }) +}) diff --git a/services/web/test/unit/src/SplitTests/SplitTestMiddlewareTests.js b/services/web/test/unit/src/SplitTests/SplitTestMiddlewareTests.js deleted file mode 100644 index 88acbbeced..0000000000 --- a/services/web/test/unit/src/SplitTests/SplitTestMiddlewareTests.js +++ /dev/null @@ -1,89 +0,0 @@ -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const modulePath = path.join( - __dirname, - '../../../../app/src/Features/SplitTests/SplitTestMiddleware' -) -const sinon = require('sinon') -const MockResponse = require('../helpers/MockResponse') -const MockRequest = require('../helpers/MockRequest') - -describe('SplitTestMiddleware', function () { - beforeEach(function () { - this.SplitTestMiddleware = SandboxedModule.require(modulePath, { - requires: { - './SplitTestHandler': (this.SplitTestHandler = { - promises: { - getAssignment: sinon.stub().resolves(), - }, - }), - }, - }) - - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() - }) - - it('assign multiple split test variants in locals', async function () { - this.SplitTestHandler.promises.getAssignment - .withArgs(this.req, 'ui-overhaul') - .resolves({ - variant: 'default', - }) - this.SplitTestHandler.promises.getAssignment - .withArgs(this.req, 'other-test') - .resolves({ - variant: 'foobar', - }) - - const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([ - 'ui-overhaul', - 'other-test', - ]) - await middleware(this.req, this.res, this.next) - - sinon.assert.calledWith( - this.SplitTestHandler.promises.getAssignment, - this.req, - this.res, - 'ui-overhaul' - ) - sinon.assert.calledWith( - this.SplitTestHandler.promises.getAssignment, - this.req, - this.res, - 'other-test' - ) - sinon.assert.calledOnce(this.next) - }) - - it('assign no split test variant in locals', async function () { - const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([]) - - await middleware(this.req, this.res, this.next) - - sinon.assert.notCalled(this.SplitTestHandler.promises.getAssignment) - sinon.assert.calledOnce(this.next) - }) - - it('exception thrown by assignment does not fail the request', async function () { - this.SplitTestHandler.promises.getAssignment - .withArgs(this.req, this.res, 'some-test') - .throws(new Error('failure')) - - const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([ - 'some-test', - ]) - - await middleware(this.req, this.res, this.next) - - sinon.assert.calledWith( - this.SplitTestHandler.promises.getAssignment, - this.req, - this.res, - 'some-test' - ) - sinon.assert.calledOnce(this.next) - }) -}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandler.test.mjs similarity index 53% rename from services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js rename to services/web/test/unit/src/Subscription/SubscriptionGroupHandler.test.mjs index fa82a03f08..a599404093 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandler.test.mjs @@ -1,75 +1,75 @@ -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb-legacy') -const sinon = require('sinon') -const { expect } = require('chai') -const MockRequest = require('../helpers/MockRequest') -const { - InvalidEmailError, -} = require('../../../../app/src/Features/Errors/Errors') +import { vi, expect } from 'vitest' +import mongodb from 'mongodb-legacy' +import sinon from 'sinon' +import MockRequest from '../helpers/MockRequest.js' +import { InvalidEmailError } from '../../../../app/src/Features/Errors/Errors.js' + +const { ObjectId } = mongodb + const modulePath = - '../../../../app/src/Features/Subscription/SubscriptionGroupHandler' + '../../../../app/src/Features/Subscription/SubscriptionGroupHandler.mjs' describe('SubscriptionGroupHandler', function () { - beforeEach(function () { - this.adminUser_id = '12321' - this.newEmail = 'bob@smith.com' - this.user_id = '3121321' - this.email = 'jim@example.com' - this.user = { _id: this.user_id, email: this.newEmail } - this.subscription_id = '31DSd1123D' - this.adding = 1 - this.paymentMethod = { cardType: 'Visa', lastFour: '1111' } - this.PaymentProviderEntities = { + beforeEach(async function (ctx) { + ctx.adminUser_id = '12321' + ctx.newEmail = 'bob@smith.com' + ctx.user_id = '3121321' + ctx.email = 'jim@example.com' + ctx.user = { _id: ctx.user_id, email: ctx.newEmail } + ctx.subscription_id = '31DSd1123D' + ctx.adding = 1 + ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' } + ctx.PaymentProviderEntities = { MEMBERS_LIMIT_ADD_ON_CODE: 'additional-license', } - this.localPlanInSettings = { + ctx.localPlanInSettings = { membersLimit: 5, - membersLimitAddOn: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + membersLimitAddOn: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, } - this.subscription = { - admin_id: this.adminUser_id, - manager_ids: [this.adminUser_id], - _id: this.subscription_id, + ctx.subscription = { + admin_id: ctx.adminUser_id, + manager_ids: [ctx.adminUser_id], + _id: ctx.subscription_id, membersLimit: 100, } - this.changeRequest = { + ctx.changeRequest = { timeframe: 'now', subscription: { id: 'test_id', }, } - this.termsAndConditionsUpdate = { + ctx.termsAndConditionsUpdate = { termsAndConditions: 'T&C copy', } - this.poNumberAndTermsAndConditionsUpdate = { + ctx.poNumberAndTermsAndConditionsUpdate = { poNumber: '4444', - ...this.termsAndConditionsUpdate, + ...ctx.termsAndConditionsUpdate, } - this.recurlySubscription = { + ctx.recurlySubscription = { id: 123, addOns: [ { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, quantity: 1, }, ], - getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest), - getRequestForGroupPlanUpgrade: sinon.stub().returns(this.changeRequest), - getRequestForAddOnPurchase: sinon.stub().returns(this.changeRequest), + getRequestForAddOnUpdate: sinon.stub().returns(ctx.changeRequest), + getRequestForGroupPlanUpgrade: sinon.stub().returns(ctx.changeRequest), + getRequestForAddOnPurchase: sinon.stub().returns(ctx.changeRequest), getRequestForFlexibleLicensingGroupPlanUpgrade: sinon .stub() - .returns(this.changeRequest), + .returns(ctx.changeRequest), getRequestForPoNumberAndTermsAndConditionsUpdate: sinon .stub() - .returns(this.poNumberAndTermsAndConditionsUpdate), + .returns(ctx.poNumberAndTermsAndConditionsUpdate), getRequestForTermsAndConditionsUpdate: sinon .stub() - .returns(this.termsAndConditionsUpdate), + .returns(ctx.termsAndConditionsUpdate), currency: 'USD', hasAddOn(code) { return this.addOns.some(addOn => addOn.code === code) @@ -79,7 +79,7 @@ describe('SubscriptionGroupHandler', function () { }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves({ groupPlan: true, @@ -88,45 +88,45 @@ describe('SubscriptionGroupHandler', function () { }, }), getSubscriptionByMemberIdAndId: sinon.stub(), - getSubscription: sinon.stub().resolves(this.subscription), + getSubscription: sinon.stub().resolves(ctx.subscription), }, } - this.changePreview = { + ctx.changePreview = { currency: 'USD', } - this.SubscriptionController = { - makeChangePreview: sinon.stub().resolves(this.changePreview), + ctx.SubscriptionController = { + makeChangePreview: sinon.stub().resolves(ctx.changePreview), getPlanNameForDisplay: sinon.stub().resolves(), } - this.SubscriptionUpdater = { + ctx.SubscriptionUpdater = { promises: { removeUserFromGroup: sinon.stub().resolves(), getSubscription: sinon.stub().resolves(), }, } - this.Subscription = { + ctx.Subscription = { updateOne: sinon.stub().returns({ exec: sinon.stub().resolves }), updateMany: sinon.stub().returns({ exec: sinon.stub().resolves }), findOne: sinon.stub().returns({ exec: sinon.stub().resolves }), } - this.User = { + ctx.User = { find: sinon.stub().returns({ exec: sinon.stub().resolves }), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.previewSubscriptionChange = { + ctx.previewSubscriptionChange = { nextAddOns: [ { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - quantity: this.recurlySubscription.addOns[0].quantity + this.adding, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + quantity: ctx.recurlySubscription.addOns[0].quantity + ctx.adding, }, ], subscription: { @@ -134,40 +134,40 @@ describe('SubscriptionGroupHandler', function () { }, } - this.applySubscriptionChange = {} + ctx.applySubscriptionChange = {} - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { - getSubscription: sinon.stub().resolves(this.recurlySubscription), - getPaymentMethod: sinon.stub().resolves(this.paymentMethod), + getSubscription: sinon.stub().resolves(ctx.recurlySubscription), + getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod), previewSubscriptionChange: sinon .stub() - .resolves(this.previewSubscriptionChange), + .resolves(ctx.previewSubscriptionChange), applySubscriptionChangeRequest: sinon .stub() - .resolves(this.applySubscriptionChange), + .resolves(ctx.applySubscriptionChange), updateSubscriptionDetails: sinon.stub().resolves(), }, } - this.PlansLocator = { - findLocalPlanInSettings: sinon.stub().returns(this.localPlanInSettings), + ctx.PlansLocator = { + findLocalPlanInSettings: sinon.stub().returns(ctx.localPlanInSettings), } - this.SubscriptionHandler = { + ctx.SubscriptionHandler = { promises: { syncSubscription: sinon.stub().resolves(), }, } - this.TeamInvitesHandler = { + ctx.TeamInvitesHandler = { promises: { revokeInvite: sinon.stub().resolves(), createInvite: sinon.stub().resolves(), }, } - this.GroupPlansData = { + ctx.GroupPlansData = { enterprise: { collaborator: { USD: { @@ -190,7 +190,7 @@ describe('SubscriptionGroupHandler', function () { }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub(), @@ -198,84 +198,146 @@ describe('SubscriptionGroupHandler', function () { }, } - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .withArgs('generateTermsAndConditions') .resolves(['T&Cs']) .withArgs('getPaymentFromRecord') .resolves([ { - subscription: this.recurlySubscription, + subscription: ctx.recurlySubscription, account: { hasPastDueInvoice: false }, }, ]) .withArgs('previewSubscriptionChangeRequest') - .resolves([this.previewSubscriptionChange]) + .resolves([ctx.previewSubscriptionChange]) .withArgs('previewGroupPlanUpgrade') - .resolves([{ subscriptionChange: this.previewSubscriptionChange }]) + .resolves([{ subscriptionChange: ctx.previewSubscriptionChange }]) - this.Handler = SandboxedModule.require(modulePath, { - requires: { - './SubscriptionUpdater': this.SubscriptionUpdater, - './SubscriptionLocator': this.SubscriptionLocator, - './SubscriptionController': this.SubscriptionController, - './SubscriptionHandler': this.SubscriptionHandler, - './TeamInvitesHandler': this.TeamInvitesHandler, - '../../models/Subscription': { - Subscription: this.Subscription, - }, - '../../models/User': { - User: this.User, - }, - './RecurlyClient': this.RecurlyClient, - './PlansLocator': this.PlansLocator, - './PaymentProviderEntities': this.PaymentProviderEntities, - '../Authentication/SessionManager': this.SessionManager, - './GroupPlansData': this.GroupPlansData, - '../../infrastructure/Modules': this.Modules, - }, - }) + vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') + ) + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionUpdater.js', + () => ({ + default: ctx.SubscriptionUpdater, + }) + ) + + 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/SubscriptionHandler', + () => ({ + default: ctx.SubscriptionHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/TeamInvitesHandler', + () => ({ + default: ctx.TeamInvitesHandler, + }) + ) + + vi.doMock('../../../../app/src/models/Subscription', () => ({ + Subscription: ctx.Subscription, + })) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({ + default: ctx.PlansLocator, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/PaymentProviderEntities', + () => ({ + default: ctx.PaymentProviderEntities, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/GroupPlansData', + () => ({ + default: ctx.GroupPlansData, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + ctx.Handler = (await import(modulePath)).default }) describe('removeUserFromGroup', function () { - it('should call the subscription updater to remove the user', async function () { - const auditLog = { ipAddress: '0:0:0:0', initiatorId: this.user._id } - await this.Handler.promises.removeUserFromGroup( - this.adminUser_id, - this.user._id, + it('should call the subscription updater to remove the user', async function (ctx) { + const auditLog = { ipAddress: '0:0:0:0', initiatorId: ctx.user._id } + await ctx.Handler.promises.removeUserFromGroup( + ctx.adminUser_id, + ctx.user._id, auditLog ) - this.SubscriptionUpdater.promises.removeUserFromGroup - .calledWith(this.adminUser_id, this.user._id, auditLog) + ctx.SubscriptionUpdater.promises.removeUserFromGroup + .calledWith(ctx.adminUser_id, ctx.user._id, auditLog) .should.equal(true) }) }) describe('replaceUserReferencesInGroups', function () { - beforeEach(async function () { - this.oldId = 'ba5eba11' - this.newId = '5ca1ab1e' - await this.Handler.promises.replaceUserReferencesInGroups( - this.oldId, - this.newId + beforeEach(async function (ctx) { + ctx.oldId = 'ba5eba11' + ctx.newId = '5ca1ab1e' + await ctx.Handler.promises.replaceUserReferencesInGroups( + ctx.oldId, + ctx.newId ) }) - it('replaces the admin_id', function () { - this.Subscription.updateOne - .calledWith({ admin_id: this.oldId }, { admin_id: this.newId }) + it('replaces the admin_id', function (ctx) { + ctx.Subscription.updateOne + .calledWith({ admin_id: ctx.oldId }, { admin_id: ctx.newId }) .should.equal(true) }) - it('replaces the manager_ids', function () { - this.Subscription.updateMany + it('replaces the manager_ids', function (ctx) { + ctx.Subscription.updateMany .calledWith( { manager_ids: 'ba5eba11' }, { $addToSet: { manager_ids: '5ca1ab1e' } } ) .should.equal(true) - this.Subscription.updateMany + ctx.Subscription.updateMany .calledWith( { manager_ids: 'ba5eba11' }, { $pull: { manager_ids: 'ba5eba11' } } @@ -283,48 +345,46 @@ describe('SubscriptionGroupHandler', function () { .should.equal(true) }) - it('replaces the member ids', function () { - this.Subscription.updateMany + it('replaces the member ids', function (ctx) { + ctx.Subscription.updateMany .calledWith( - { member_ids: this.oldId }, - { $addToSet: { member_ids: this.newId } } + { member_ids: ctx.oldId }, + { $addToSet: { member_ids: ctx.newId } } ) .should.equal(true) - this.Subscription.updateMany + ctx.Subscription.updateMany .calledWith( - { member_ids: this.oldId }, - { $pull: { member_ids: this.oldId } } + { member_ids: ctx.oldId }, + { $pull: { member_ids: ctx.oldId } } ) .should.equal(true) }) }) describe('isUserPartOfGroup', function () { - beforeEach(function () { - this.subscription_id = '123ed13123' + beforeEach(function (ctx) { + ctx.subscription_id = '123ed13123' }) - it('should return true when user is part of subscription', async function () { - this.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves( - { - _id: this.subscription_id, - } - ) - const partOfGroup = await this.Handler.promises.isUserPartOfGroup( - this.user_id, - this.subscription_id + it('should return true when user is part of subscription', async function (ctx) { + ctx.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves({ + _id: ctx.subscription_id, + }) + const partOfGroup = await ctx.Handler.promises.isUserPartOfGroup( + ctx.user_id, + ctx.subscription_id ) partOfGroup.should.equal(true) }) - it('should return false when no subscription is found', async function () { - this.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves( + it('should return false when no subscription is found', async function (ctx) { + ctx.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves( null ) - const partOfGroup = await this.Handler.promises.isUserPartOfGroup( - this.user_id, - this.subscription_id + const partOfGroup = await ctx.Handler.promises.isUserPartOfGroup( + ctx.user_id, + ctx.subscription_id ) partOfGroup.should.equal(false) }) @@ -332,56 +392,54 @@ describe('SubscriptionGroupHandler', function () { describe('getTotalConfirmedUsersInGroup', function () { describe('for existing subscriptions', function () { - beforeEach(function () { - this.subscription.member_ids = ['12321', '3121321'] + beforeEach(function (ctx) { + ctx.subscription.member_ids = ['12321', '3121321'] }) - it('should call the subscription locator and return 2 users', async function () { - const count = await this.Handler.promises.getTotalConfirmedUsersInGroup( - this.subscription_id + it('should call the subscription locator and return 2 users', async function (ctx) { + const count = await ctx.Handler.promises.getTotalConfirmedUsersInGroup( + ctx.subscription_id ) - this.SubscriptionLocator.promises.getSubscription - .calledWith(this.subscription_id) + ctx.SubscriptionLocator.promises.getSubscription + .calledWith(ctx.subscription_id) .should.equal(true) count.should.equal(2) }) }) describe('for nonexistent subscriptions', function () { - it('should return undefined', async function () { + it('should return undefined', async function (ctx) { const count = - await this.Handler.promises.getTotalConfirmedUsersInGroup('fake-id') + await ctx.Handler.promises.getTotalConfirmedUsersInGroup('fake-id') expect(count).not.to.exist }) }) }) describe('getUsersGroupSubscriptionDetails', function () { - beforeEach(function () { - this.req = new MockRequest() - this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ - ...this.localPlanInSettings, + beforeEach(function (ctx) { + ctx.req = new MockRequest() + ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ + ...ctx.localPlanInSettings, canUseFlexibleLicensing: true, }) }) - it('should throw if the subscription is not a group plan', async function () { - this.SubscriptionLocator.promises.getUsersSubscription = sinon + it('should throw if the subscription is not a group plan', async function (ctx) { + ctx.SubscriptionLocator.promises.getUsersSubscription = sinon .stub() .resolves({ groupPlan: false }) await expect( - this.Handler.promises.getUsersGroupSubscriptionDetails( - this.adminUser_id - ) + ctx.Handler.promises.getUsersGroupSubscriptionDetails(ctx.adminUser_id) ).to.be.rejectedWith('User subscription is not a group plan') }) - it('should return users group subscription details', async function () { - const data = await this.Handler.promises.getUsersGroupSubscriptionDetails( - this.adminUser_id + it('should return users group subscription details', async function (ctx) { + const data = await ctx.Handler.promises.getUsersGroupSubscriptionDetails( + ctx.adminUser_id ) expect(data).to.deep.equal({ - userId: this.adminUser_id, + userId: ctx.adminUser_id, subscription: { groupPlan: true, recurlyStatus: { @@ -391,62 +449,62 @@ describe('SubscriptionGroupHandler', function () { plan: { membersLimit: 5, membersLimitAddOn: - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, canUseFlexibleLicensing: true, }, - paymentProviderSubscription: this.recurlySubscription, + paymentProviderSubscription: ctx.recurlySubscription, }) }) }) describe('add seats subscription change', function () { - beforeEach(function () { - this.req = new MockRequest() - Object.assign(this.req.body, { adding: this.adding }) - this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ - ...this.localPlanInSettings, + beforeEach(function (ctx) { + ctx.req = new MockRequest() + Object.assign(ctx.req.body, { adding: ctx.adding }) + ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ + ...ctx.localPlanInSettings, canUseFlexibleLicensing: true, }) }) describe('has "additional-license" add-on', function () { - beforeEach(function () { - this.recurlySubscription.addOns = [ + beforeEach(function (ctx) { + ctx.recurlySubscription.addOns = [ { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, quantity: 6, }, ] - this.prevQuantity = this.recurlySubscription.addOns[0].quantity - this.previewSubscriptionChange.nextAddOns = [ + ctx.prevQuantity = ctx.recurlySubscription.addOns[0].quantity + ctx.previewSubscriptionChange.nextAddOns = [ { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - quantity: this.prevQuantity + this.adding, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + quantity: ctx.prevQuantity + ctx.adding, }, ] }) - afterEach(function () { + afterEach(function (ctx) { sinon.assert.notCalled( - this.recurlySubscription.getRequestForAddOnPurchase + ctx.recurlySubscription.getRequestForAddOnPurchase ) - this.recurlySubscription.getRequestForAddOnUpdate + ctx.recurlySubscription.getRequestForAddOnUpdate .calledWith( - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - this.recurlySubscription.addOns[0].quantity + this.adding + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.recurlySubscription.addOns[0].quantity + ctx.adding ) .should.equal(true) }) describe('previewAddSeatsSubscriptionChange', function () { - it('should return the subscription change preview', async function () { + it('should return the subscription change preview', async function (ctx) { const preview = - await this.Handler.promises.previewAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding + await ctx.Handler.promises.previewAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding ) - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .calledWith('getPaymentFromRecord', { groupPlan: true, recurlyStatus: { @@ -454,51 +512,51 @@ describe('SubscriptionGroupHandler', function () { }, }) .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith('previewSubscriptionChangeRequest', this.changeRequest) + ctx.Modules.promises.hooks.fire + .calledWith('previewSubscriptionChangeRequest', ctx.changeRequest) .should.equal(true) - this.SubscriptionController.makeChangePreview + ctx.SubscriptionController.makeChangePreview .calledWith( { type: 'add-on-update', addOn: { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, quantity: - this.previewSubscriptionChange.nextAddOns[0].quantity, - prevQuantity: this.prevQuantity, + ctx.previewSubscriptionChange.nextAddOns[0].quantity, + prevQuantity: ctx.prevQuantity, }, }, - this.previewSubscriptionChange + ctx.previewSubscriptionChange ) .should.equal(true) - preview.should.equal(this.changePreview) + preview.should.equal(ctx.changePreview) }) }) describe('createAddSeatsSubscriptionChange', function () { - it('should change the subscription', async function () { - this.recurlySubscription = { - ...this.recurlySubscription, + it('should change the subscription', async function (ctx) { + ctx.recurlySubscription = { + ...ctx.recurlySubscription, get isCollectionMethodManual() { return true }, } - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .withArgs('getPaymentFromRecord') .resolves([ { - subscription: this.recurlySubscription, + subscription: ctx.recurlySubscription, account: { hasPastDueInvoice: false }, }, ]) const result = - await this.Handler.promises.createAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding, + await ctx.Handler.promises.createAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding, '123' ) - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .calledWith( 'updateSubscriptionDetails', sinon.match @@ -506,14 +564,14 @@ describe('SubscriptionGroupHandler', function () { .and(sinon.match.has('termsAndConditions')) ) .should.equal(true) - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .calledWith( 'applySubscriptionChangeRequestAndSync', - this.changeRequest + ctx.changeRequest ) .should.equal(true) expect(result).to.deep.equal({ - adding: this.req.body.adding, + adding: ctx.req.body.adding, }) }) }) @@ -521,38 +579,38 @@ describe('SubscriptionGroupHandler', function () { describe('updateSubscriptionPaymentTerms', function () { describe('accounts with PO number', function () { - it('should update the subscription PO number and T&C', async function () { - await this.Handler.promises.updateSubscriptionPaymentTerms( - this.recurlySubscription, - this.poNumberAndTermsAndConditionsUpdate.poNumber + it('should update the subscription PO number and T&C', async function (ctx) { + await ctx.Handler.promises.updateSubscriptionPaymentTerms( + ctx.recurlySubscription, + ctx.poNumberAndTermsAndConditionsUpdate.poNumber ) - this.recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate + ctx.recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate .calledWithMatch( - this.poNumberAndTermsAndConditionsUpdate.poNumber, + ctx.poNumberAndTermsAndConditionsUpdate.poNumber, 'T&Cs' ) .should.equal(true) - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .calledWith( 'updateSubscriptionDetails', - this.poNumberAndTermsAndConditionsUpdate + ctx.poNumberAndTermsAndConditionsUpdate ) .should.equal(true) }) }) describe('accounts with no PO number', function () { - it('should update the subscription T&C only', async function () { - await this.Handler.promises.updateSubscriptionPaymentTerms( - this.recurlySubscription + it('should update the subscription T&C only', async function (ctx) { + await ctx.Handler.promises.updateSubscriptionPaymentTerms( + ctx.recurlySubscription ) - this.recurlySubscription.getRequestForTermsAndConditionsUpdate + ctx.recurlySubscription.getRequestForTermsAndConditionsUpdate .calledWithMatch('T&Cs') .should.equal(true) - this.Modules.promises.hooks.fire + ctx.Modules.promises.hooks.fire .calledWith( 'updateSubscriptionDetails', - this.termsAndConditionsUpdate + ctx.termsAndConditionsUpdate ) .should.equal(true) }) @@ -560,33 +618,31 @@ describe('SubscriptionGroupHandler', function () { }) describe('has no "additional-license" add-on', function () { - beforeEach(function () { - this.recurlySubscription.addOns = [] - this.prevQuantity = this.recurlySubscription.addOns[0]?.quantity ?? 0 - this.previewSubscriptionChange.nextAddOns = [ + beforeEach(function (ctx) { + ctx.recurlySubscription.addOns = [] + ctx.prevQuantity = ctx.recurlySubscription.addOns[0]?.quantity ?? 0 + ctx.previewSubscriptionChange.nextAddOns = [ { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - quantity: this.prevQuantity + this.adding, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + quantity: ctx.prevQuantity + ctx.adding, }, ] - this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ - ...this.localPlanInSettings, + ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ + ...ctx.localPlanInSettings, planCode: 'group_collaborator_5_enterprise', canUseFlexibleLicensing: true, }) }) - afterEach(function () { - sinon.assert.notCalled( - this.recurlySubscription.getRequestForAddOnUpdate - ) + afterEach(function (ctx) { + sinon.assert.notCalled(ctx.recurlySubscription.getRequestForAddOnUpdate) }) describe('previewAddSeatsSubscriptionChange', function () { let preview - afterEach(function () { - this.Modules.promises.hooks.fire + afterEach(function (ctx) { + ctx.Modules.promises.hooks.fire .calledWith('getPaymentFromRecord', { groupPlan: true, recurlyStatus: { @@ -594,111 +650,111 @@ describe('SubscriptionGroupHandler', function () { }, }) .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith('previewSubscriptionChangeRequest', this.changeRequest) + ctx.Modules.promises.hooks.fire + .calledWith('previewSubscriptionChangeRequest', ctx.changeRequest) .should.equal(true) - this.SubscriptionController.makeChangePreview + ctx.SubscriptionController.makeChangePreview .calledWith( { type: 'add-on-update', addOn: { - code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + code: ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, quantity: - this.previewSubscriptionChange.nextAddOns[0].quantity, - prevQuantity: this.prevQuantity, + ctx.previewSubscriptionChange.nextAddOns[0].quantity, + prevQuantity: ctx.prevQuantity, }, }, - this.previewSubscriptionChange + ctx.previewSubscriptionChange ) .should.equal(true) - preview.should.equal(this.changePreview) + preview.should.equal(ctx.changePreview) }) - it('should return the subscription change preview with legacy add-on price', async function () { - this.recurlySubscription.planPrice = - this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / + it('should return the subscription change preview with legacy add-on price', async function (ctx) { + ctx.recurlySubscription.planPrice = + ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / 100 - 1 preview = - await this.Handler.promises.previewAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding + await ctx.Handler.promises.previewAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding ) - this.recurlySubscription.getRequestForAddOnPurchase + ctx.recurlySubscription.getRequestForAddOnPurchase .calledWithExactly( - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - this.adding, - this.GroupPlansData.enterprise.collaborator.USD[5] + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.adding, + ctx.GroupPlansData.enterprise.collaborator.USD[5] .additional_license_legacy_price_in_cents / 100 ) .should.equal(true) }) - it('should return the subscription change preview with non-legacy add-on price', async function () { - this.recurlySubscription.planPrice = - this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / + it('should return the subscription change preview with non-legacy add-on price', async function (ctx) { + ctx.recurlySubscription.planPrice = + ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / 100 preview = - await this.Handler.promises.previewAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding + await ctx.Handler.promises.previewAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding ) - this.recurlySubscription.getRequestForAddOnPurchase + ctx.recurlySubscription.getRequestForAddOnPurchase .calledWithExactly( - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - this.adding, + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.adding, undefined ) .should.equal(true) }) - it('should return the subscription change preview with legacy add-on price for small educational group', async function () { - this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ - ...this.localPlanInSettings, + it('should return the subscription change preview with legacy add-on price for small educational group', async function (ctx) { + ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ + ...ctx.localPlanInSettings, planCode: 'group_collaborator_5_educational', canUseFlexibleLicensing: true, }) - this.recurlySubscription.planPrice = - this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / + ctx.recurlySubscription.planPrice = + ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / 100 + 1 preview = - await this.Handler.promises.previewAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding + await ctx.Handler.promises.previewAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding ) - this.recurlySubscription.getRequestForAddOnPurchase + ctx.recurlySubscription.getRequestForAddOnPurchase .calledWithExactly( - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - this.adding, - this.GroupPlansData.enterprise.collaborator.USD[5] + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.adding, + ctx.GroupPlansData.enterprise.collaborator.USD[5] .additional_license_legacy_price_in_cents / 100 ) .should.equal(true) }) - it('should return the subscription change preview with non-legacy add-on price for small educational group', async function () { - this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ - ...this.localPlanInSettings, + it('should return the subscription change preview with non-legacy add-on price for small educational group', async function (ctx) { + ctx.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({ + ...ctx.localPlanInSettings, planCode: 'group_collaborator_5_educational', canUseFlexibleLicensing: true, }) - this.recurlySubscription.planPrice = - this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / + ctx.recurlySubscription.planPrice = + ctx.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents / 100 preview = - await this.Handler.promises.previewAddSeatsSubscriptionChange( - this.adminUser_id, - this.adding + await ctx.Handler.promises.previewAddSeatsSubscriptionChange( + ctx.adminUser_id, + ctx.adding ) - this.recurlySubscription.getRequestForAddOnPurchase + ctx.recurlySubscription.getRequestForAddOnPurchase .calledWithExactly( - this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, - this.adding, + ctx.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE, + ctx.adding, undefined ) .should.equal(true) @@ -708,17 +764,17 @@ describe('SubscriptionGroupHandler', function () { }) describe('ensureFlexibleLicensingEnabled', function () { - it('should throw if the subscription can not use flexible licensing', async function () { + it('should throw if the subscription can not use flexible licensing', async function (ctx) { await expect( - this.Handler.promises.ensureFlexibleLicensingEnabled({ + ctx.Handler.promises.ensureFlexibleLicensingEnabled({ canUseFlexibleLicensing: false, }) ).to.be.rejectedWith('The group plan does not support flexible licensing') }) - it('should not throw if the subscription can use flexible licensing', async function () { + it('should not throw if the subscription can use flexible licensing', async function (ctx) { await expect( - this.Handler.promises.ensureFlexibleLicensingEnabled({ + ctx.Handler.promises.ensureFlexibleLicensingEnabled({ canUseFlexibleLicensing: true, }) ).to.not.be.rejected @@ -726,15 +782,15 @@ describe('SubscriptionGroupHandler', function () { }) describe('ensureSubscriptionIsActive', function () { - it('should throw if the subscription is not active', async function () { + it('should throw if the subscription is not active', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionIsActive(this.subscription) + ctx.Handler.promises.ensureSubscriptionIsActive(ctx.subscription) ).to.be.rejectedWith('The subscription is not active') }) - it('should not throw if the subscription is active', async function () { + it('should not throw if the subscription is active', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionIsActive({ + ctx.Handler.promises.ensureSubscriptionIsActive({ recurlyStatus: { state: 'active' }, }) ).to.not.be.rejected @@ -742,9 +798,9 @@ describe('SubscriptionGroupHandler', function () { }) describe('ensureSubscriptionCollectionMethodIsNotManual', function () { - it('should throw if the subscription is manually collected', async function () { + it('should throw if the subscription is manually collected', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({ + ctx.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({ get isCollectionMethodManual() { return true }, @@ -752,9 +808,9 @@ describe('SubscriptionGroupHandler', function () { ).to.be.rejectedWith('This subscription is being collected manually') }) - it('should not throw if the subscription is automatically collected', async function () { + it('should not throw if the subscription is automatically collected', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({ + ctx.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({ get isCollectionMethodManual() { return false }, @@ -764,46 +820,46 @@ describe('SubscriptionGroupHandler', function () { }) describe('ensureSubscriptionHasNoPendingChanges', function () { - it('should throw if the subscription has pending change', async function () { + it('should throw if the subscription has pending change', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasNoPendingChanges({ + ctx.Handler.promises.ensureSubscriptionHasNoPendingChanges({ pendingChange: {}, }) ).to.be.rejectedWith('This subscription has a pending change') }) - it('should not throw if the subscription has no pending change', async function () { + it('should not throw if the subscription has no pending change', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasNoPendingChanges({}) + ctx.Handler.promises.ensureSubscriptionHasNoPendingChanges({}) ).to.not.be.rejected }) }) describe('ensureSubscriptionHasNoPastDueInvoice', function () { - it('should throw if the subscription has past due invoice', async function () { - this.Modules.promises.hooks.fire + it('should throw if the subscription has past due invoice', async function (ctx) { + ctx.Modules.promises.hooks.fire .withArgs('getPaymentFromRecord') .resolves([{ account: { hasPastDueInvoice: true } }]) await expect( - this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice( - this.subscription + ctx.Handler.promises.ensureSubscriptionHasNoPastDueInvoice( + ctx.subscription ) ).to.be.rejectedWith('This subscription has a past due invoice') }) - it('should not throw if the subscription has no past due invoice', async function () { + it('should not throw if the subscription has no past due invoice', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice( - this.subscription + ctx.Handler.promises.ensureSubscriptionHasNoPastDueInvoice( + ctx.subscription ) ).to.not.be.rejected }) }) describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () { - it('should throw if the subscription is manually collected and has no additional license add-on', async function () { + it('should throw if the subscription is manually collected and has no additional license add-on', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( + ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( { isCollectionMethodManual: true, hasAddOn: sinon @@ -817,9 +873,9 @@ describe('SubscriptionGroupHandler', function () { ) }) - it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () { + it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( + ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( { isCollectionMethodManual: false, hasAddOn: sinon @@ -831,9 +887,9 @@ describe('SubscriptionGroupHandler', function () { ).to.not.be.rejected }) - it('should not throw if the subscription is not manually collected and has additional license add-on', async function () { + it('should not throw if the subscription is not manually collected and has additional license add-on', async function (ctx) { await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( + ctx.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( { isCollectionMethodManual: true, hasAddOn: sinon.stub().withArgs('additional-license').returns(true), @@ -844,37 +900,37 @@ describe('SubscriptionGroupHandler', function () { }) describe('getGroupPlanUpgradePreview', function () { - it('should generate preview for subscription upgrade', async function () { - const result = await this.Handler.promises.getGroupPlanUpgradePreview( - this.user_id + it('should generate preview for subscription upgrade', async function (ctx) { + const result = await ctx.Handler.promises.getGroupPlanUpgradePreview( + ctx.user_id ) - result.should.equal(this.changePreview) + result.should.equal(ctx.changePreview) }) }) describe('checkBillingInfoExistence', function () { - it('should invoke the payment method function when collection method is "automatic"', async function () { - await this.Handler.promises.checkBillingInfoExistence( - this.recurlySubscription, - this.adminUser_id + it('should invoke the payment method function when collection method is "automatic"', async function (ctx) { + await ctx.Handler.promises.checkBillingInfoExistence( + ctx.recurlySubscription, + ctx.adminUser_id ) - this.Modules.promises.hooks.fire - .calledWith('getPaymentMethod', this.adminUser_id) + ctx.Modules.promises.hooks.fire + .calledWith('getPaymentMethod', ctx.adminUser_id) .should.equal(true) }) - it('shouldn’t invoke the payment method function when collection method is "manual"', async function () { + it('shouldn’t invoke the payment method function when collection method is "manual"', async function (ctx) { const recurlySubscription = { - ...this.recurlySubscription, + ...ctx.recurlySubscription, get isCollectionMethodManual() { return true }, } - await this.Handler.promises.checkBillingInfoExistence( + await ctx.Handler.promises.checkBillingInfoExistence( recurlySubscription, - this.adminUser_id + ctx.adminUser_id ) - this.RecurlyClient.promises.getPaymentMethod.should.not.have.been.called + ctx.RecurlyClient.promises.getPaymentMethod.should.not.have.been.called }) }) @@ -885,7 +941,7 @@ describe('SubscriptionGroupHandler', function () { let emailList let callUpdateGroupMembersBulk - beforeEach(function () { + beforeEach(function (ctx) { members = [ { _id: new ObjectId(), @@ -918,17 +974,17 @@ describe('SubscriptionGroupHandler', function () { 'new-user-2@example.com', // secondary email of existing user ] callUpdateGroupMembersBulk = async (options = {}) => { - this.Subscription.findOne = sinon + ctx.Subscription.findOne = sinon .stub() - .returns({ exec: sinon.stub().resolves(this.subscription) }) + .returns({ exec: sinon.stub().resolves(ctx.subscription) }) - this.User.find = sinon + ctx.User.find = sinon .stub() .returns({ exec: sinon.stub().resolves(members) }) - return await this.Handler.promises.updateGroupMembersBulk( + return await ctx.Handler.promises.updateGroupMembersBulk( inviterId, - this.subscription._id, + ctx.subscription._id, emailList, options ) @@ -945,7 +1001,7 @@ describe('SubscriptionGroupHandler', function () { describe('with commit = false', function () { describe('with removeMembersNotIncluded = false', function () { - it('should preview zero users to delete, and should not send invites', async function () { + it('should preview zero users to delete, and should not send invites', async function (ctx) { const result = await callUpdateGroupMembersBulk() expect(result).to.deep.equal({ @@ -957,19 +1013,19 @@ describe('SubscriptionGroupHandler', function () { membersToRemove: [], currentMemberCount: 3, newTotalCount: 5, - membersLimit: this.subscription.membersLimit, + membersLimit: ctx.subscription.membersLimit, }) - expect(this.TeamInvitesHandler.promises.createInvite).not.to.have.been + expect(ctx.TeamInvitesHandler.promises.createInvite).not.to.have.been .called - expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to + expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to .have.been.called }) }) describe('with removeMembersNotIncluded = true', function () { - it('should preview the users to be deleted, and should not send invites', async function () { + it('should preview the users to be deleted, and should not send invites', async function (ctx) { const result = await callUpdateGroupMembersBulk({ removeMembersNotIncluded: true, }) @@ -983,18 +1039,18 @@ describe('SubscriptionGroupHandler', function () { membersToRemove: [members[2]._id], currentMemberCount: 3, newTotalCount: 4, - membersLimit: this.subscription.membersLimit, + membersLimit: ctx.subscription.membersLimit, }) - expect(this.TeamInvitesHandler.promises.createInvite).not.to.have.been + expect(ctx.TeamInvitesHandler.promises.createInvite).not.to.have.been .called - expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to + expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to .have.been.called }) - it('should preview but not revoke invites to emails that are no longer invited', async function () { - this.subscription.teamInvites = [ + it('should preview but not revoke invites to emails that are no longer invited', async function (ctx) { + ctx.subscription.teamInvites = [ { email: 'new-user@example.com' }, { email: 'no-longer-invited@example.com' }, ] @@ -1007,13 +1063,13 @@ describe('SubscriptionGroupHandler', function () { 'no-longer-invited@example.com', ]) - expect(this.TeamInvitesHandler.promises.revokeInvite).not.to.have.been + expect(ctx.TeamInvitesHandler.promises.revokeInvite).not.to.have.been .called }) }) - it('does not throw an error when the member limit is reached', async function () { - this.subscription.membersLimit = 3 + it('does not throw an error when the member limit is reached', async function (ctx) { + ctx.subscription.membersLimit = 3 const result = await callUpdateGroupMembersBulk() expect(result.membersLimit).to.equal(3) @@ -1023,7 +1079,7 @@ describe('SubscriptionGroupHandler', function () { describe('with commit = true', function () { describe('with removeMembersNotIncluded = false', function () { - it('should preview zero users to delete, and should send invites', async function () { + it('should preview zero users to delete, and should send invites', async function (ctx) { const result = await callUpdateGroupMembersBulk({ commit: true }) expect(result).to.deep.equal({ @@ -1035,35 +1091,35 @@ describe('SubscriptionGroupHandler', function () { membersToRemove: [], currentMemberCount: 3, newTotalCount: 5, - membersLimit: this.subscription.membersLimit, + membersLimit: ctx.subscription.membersLimit, }) - expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to + expect(ctx.SubscriptionUpdater.promises.removeUserFromGroup).not.to .have.been.called expect( - this.TeamInvitesHandler.promises.createInvite.callCount + ctx.TeamInvitesHandler.promises.createInvite.callCount ).to.equal(2) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user@example.com' ) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user-2@example.com' ) }) - it('should not send invites to emails already invited', async function () { - this.subscription.teamInvites = [{ email: 'new-user@example.com' }] + it('should not send invites to emails already invited', async function (ctx) { + ctx.subscription.teamInvites = [{ email: 'new-user@example.com' }] const result = await callUpdateGroupMembersBulk({ commit: true }) @@ -1072,20 +1128,20 @@ describe('SubscriptionGroupHandler', function () { ]) expect( - this.TeamInvitesHandler.promises.createInvite.callCount + ctx.TeamInvitesHandler.promises.createInvite.callCount ).to.equal(1) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user-2@example.com' ) }) - it('should preview and not revoke invites to emails that are no longer invited', async function () { - this.subscription.teamInvites = [ + it('should preview and not revoke invites to emails that are no longer invited', async function (ctx) { + ctx.subscription.teamInvites = [ { email: 'new-user@example.com' }, { email: 'no-longer-invited@example.com' }, ] @@ -1096,13 +1152,13 @@ describe('SubscriptionGroupHandler', function () { expect(result.emailsToRevokeInvite).to.deep.equal([]) - expect(this.TeamInvitesHandler.promises.revokeInvite).not.to.have.been + expect(ctx.TeamInvitesHandler.promises.revokeInvite).not.to.have.been .called }) }) describe('with removeMembersNotIncluded = true', function () { - it('should remove users from group, and should send invites', async function () { + it('should remove users from group, and should send invites', async function (ctx) { const result = await callUpdateGroupMembersBulk({ commit: true, removeMembersNotIncluded: true, @@ -1117,42 +1173,42 @@ describe('SubscriptionGroupHandler', function () { membersToRemove: [members[2]._id], currentMemberCount: 3, newTotalCount: 4, - membersLimit: this.subscription.membersLimit, + membersLimit: ctx.subscription.membersLimit, }) expect( - this.SubscriptionUpdater.promises.removeUserFromGroup.callCount + ctx.SubscriptionUpdater.promises.removeUserFromGroup.callCount ).to.equal(1) expect( - this.SubscriptionUpdater.promises.removeUserFromGroup - ).to.have.been.calledWith(this.subscription._id, members[2]._id, { + ctx.SubscriptionUpdater.promises.removeUserFromGroup + ).to.have.been.calledWith(ctx.subscription._id, members[2]._id, { initiatorId: inviterId, }) expect( - this.TeamInvitesHandler.promises.createInvite.callCount + ctx.TeamInvitesHandler.promises.createInvite.callCount ).to.equal(2) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user@example.com' ) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user-2@example.com' ) }) - it('should send invites and revoke invites to emails no longer invited', async function () { - this.subscription.teamInvites = [ + it('should send invites and revoke invites to emails no longer invited', async function (ctx) { + ctx.subscription.teamInvites = [ { email: 'new-user@example.com' }, { email: 'no-longer-invited@example.com' }, ] @@ -1171,33 +1227,33 @@ describe('SubscriptionGroupHandler', function () { ]) expect( - this.TeamInvitesHandler.promises.createInvite.callCount + ctx.TeamInvitesHandler.promises.createInvite.callCount ).to.equal(1) expect( - this.TeamInvitesHandler.promises.createInvite + ctx.TeamInvitesHandler.promises.createInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'new-user-2@example.com' ) expect( - this.TeamInvitesHandler.promises.revokeInvite.callCount + ctx.TeamInvitesHandler.promises.revokeInvite.callCount ).to.equal(1) expect( - this.TeamInvitesHandler.promises.revokeInvite + ctx.TeamInvitesHandler.promises.revokeInvite ).to.have.been.calledWith( inviterId, - this.subscription, + ctx.subscription, 'no-longer-invited@example.com' ) }) }) - it('throws an error when the member limit is reached', async function () { - this.subscription.membersLimit = 3 + it('throws an error when the member limit is reached', async function (ctx) { + ctx.subscription.membersLimit = 3 await expect( callUpdateGroupMembersBulk({ commit: true }) ).to.be.rejectedWith('limit reached')