Merge pull request #28584 from overleaf/ac-some-web-esm-migration-5

[web] Convert some Features files to ES modules (part 5)

GitOrigin-RevId: 0cad67f9afe0095e2b066bf2f4d3717c00540dab
This commit is contained in:
Antoine Clausse
2025-10-08 09:56:43 +02:00
committed by Copybot
parent f3581c9743
commit a2d9c8f813
42 changed files with 2365 additions and 2192 deletions

View File

@@ -1,7 +1,7 @@
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
const AnalyticsRegistrationSourceHelper = require('./AnalyticsRegistrationSourceHelper')
const SessionManager = require('../../Features/Authentication/SessionManager')
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import AnalyticsRegistrationSourceHelper from './AnalyticsRegistrationSourceHelper.js'
import SessionManager from '../../Features/Authentication/SessionManager.js'
function setSource(medium, source) {
return function (req, res, next) {
@@ -51,7 +51,7 @@ function setInbound() {
}
}
module.exports = {
export default {
setSource,
clearSource,
setInbound,

View File

@@ -2,7 +2,7 @@ import AuthenticationController from './../Authentication/AuthenticationControll
import AnalyticsController from './AnalyticsController.mjs'
import AnalyticsProxy from './AnalyticsProxy.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
const rateLimiters = {
recordEvent: new RateLimiter('analytics-record-event', {

View File

@@ -1,6 +1,6 @@
// @ts-check
import { ForbiddenError, UserNotFoundError } from '../Errors/Errors.js'
import PermissionsManager from './PermissionsManager.js'
import PermissionsManager from './PermissionsManager.mjs'
import Modules from '../../infrastructure/Modules.js'
import { expressify } from '@overleaf/promise-utils'
import Features from '../../infrastructure/Features.js'
@@ -9,7 +9,7 @@ import Features from '../../infrastructure/Features.js'
* @typedef {(import('express').Request)} Request
* @typedef {(import('express').Response)} Response
* @typedef {(import('express').NextFunction)} NextFunction
* @typedef {import('./PermissionsManager').Capability} Capability
* @typedef {import('./PermissionsManager.mjs').Capability} Capability
*/
const {

View File

@@ -41,9 +41,12 @@
* }
*/
const { callbackify } = require('util')
const { ForbiddenError } = require('../Errors/Errors')
const Modules = require('../../infrastructure/Modules')
import { callbackify } from 'node:util'
import Errors from '../Errors/Errors.js'
import Modules from '../../infrastructure/Modules.js'
const { ForbiddenError } = Errors
/**
* @typedef {(import('../../../../types/capabilities').Capability)} Capability
@@ -466,7 +469,7 @@ async function checkUserListPermissions(userList, capabilities) {
return true
}
module.exports = {
export default {
validatePolicies,
registerCapability,
registerPolicy,

View File

@@ -3,9 +3,9 @@ import AuthenticationController from '../Authentication/AuthenticationController
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import CollaboratorsInviteController from './CollaboratorsInviteController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import CaptchaMiddleware from '../Captcha/CaptchaMiddleware.mjs'
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.js'
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.mjs'
const rateLimiters = {
inviteToProjectByProjectId: new RateLimiter(

View File

@@ -1,33 +1,35 @@
const { callbackify } = require('util')
const { callbackifyMultiResult } = require('@overleaf/promise-utils')
const {
import { callbackify } from 'node:util'
import { callbackifyMultiResult } from '@overleaf/promise-utils'
import {
fetchString,
fetchStringWithResponse,
fetchStream,
RequestFailedError,
} = require('@overleaf/fetch-utils')
const Settings = require('@overleaf/settings')
const ProjectGetter = require('../Project/ProjectGetter')
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
const { Cookie } = require('tough-cookie')
const ClsiCookieManager = require('./ClsiCookieManager')(
} from '@overleaf/fetch-utils'
import Settings from '@overleaf/settings'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import { Cookie } from 'tough-cookie'
import ClsiCookieManagerFactory from './ClsiCookieManager.js'
import ClsiStateManager from './ClsiStateManager.js'
import _ from 'lodash'
import ClsiFormatChecker from './ClsiFormatChecker.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import Metrics from '@overleaf/metrics'
import Errors from '../Errors/Errors.js'
import ClsiCacheHandler from './ClsiCacheHandler.js'
import HistoryManager from '../History/HistoryManager.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
const ClsiCookieManager = ClsiCookieManagerFactory(
Settings.apis.clsi?.backendGroupName
)
const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')(
const NewBackendCloudClsiCookieManager = ClsiCookieManagerFactory(
Settings.apis.clsi_new?.backendGroupName
)
const ClsiStateManager = require('./ClsiStateManager')
const _ = require('lodash')
const ClsiFormatChecker = require('./ClsiFormatChecker')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const Metrics = require('@overleaf/metrics')
const Errors = require('../Errors/Errors')
const ClsiCacheHandler = require('./ClsiCacheHandler')
const { getFilestoreBlobURL } = require('../History/HistoryManager')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
const OUTPUT_FILE_TIMEOUT_MS = 60000
@@ -843,7 +845,7 @@ function _finaliseRequest(projectId, options, project, docs, files) {
path = path.replace(/^\//, '') // Remove leading /
resources.push({
path,
url: getFilestoreBlobURL(historyId, file.hash),
url: HistoryManager.getFilestoreBlobURL(historyId, file.hash),
modified: file.created?.getTime(),
})
}
@@ -975,7 +977,7 @@ function _getClsiServerIdFromResponse(response) {
return null
}
module.exports = {
export default {
sendRequest: callbackifyMultiResult(sendRequest, [
'status',
'outputFiles',

View File

@@ -5,7 +5,7 @@ import OError from '@overleaf/o-error'
import Metrics from '@overleaf/metrics'
import ProjectGetter from '../Project/ProjectGetter.js'
import CompileManager from './CompileManager.mjs'
import ClsiManager from './ClsiManager.js'
import ClsiManager from './ClsiManager.mjs'
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import Errors from '../Errors/Errors.js'

View File

@@ -4,7 +4,7 @@ import RedisWrapper from '../../infrastructure/RedisWrapper.js'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectRootDocManager from '../Project/ProjectRootDocManager.js'
import UserGetter from '../User/UserGetter.js'
import ClsiManager from './ClsiManager.js'
import ClsiManager from './ClsiManager.mjs'
import Metrics from '@overleaf/metrics'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import UserAnalyticsIdCache from '../Analytics/UserAnalyticsIdCache.js'

View File

@@ -2,7 +2,7 @@ import EditorHttpController from './EditorHttpController.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
const rateLimiters = {
addDocToProject: new RateLimiter('add-doc-to-project', {

View File

@@ -4,7 +4,7 @@ import Settings from '@overleaf/settings'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import HistoryController from './HistoryController.mjs'
const rateLimiters = {

View File

@@ -1,7 +1,7 @@
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import LinkedFilesController from './LinkedFilesController.mjs'
const rateLimiters = {

View File

@@ -1,6 +1,6 @@
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
import CompileManager from '../Compile/CompileManager.mjs'
import ClsiManager from '../Compile/ClsiManager.js'
import ClsiManager from '../Compile/ClsiManager.mjs'
import ProjectFileAgent from './ProjectFileAgent.mjs'
import _ from 'lodash'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'

View File

@@ -5,7 +5,7 @@ import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.js'
import EmailHandler from '../Email/EmailHandler.js'
import AuthenticationManager from '../Authentication/AuthenticationManager.js'
import { callbackify, promisify } from 'node:util'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
const assertUserPermissions = PermissionsManager.promises.assertUserPermissions

View File

@@ -2,7 +2,7 @@ import PasswordResetController from './PasswordResetController.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import CaptchaMiddleware from '../../Features/Captcha/CaptchaMiddleware.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
const rateLimiter = new RateLimiter('password_reset_rate_limit', {
points: 6,

View File

@@ -14,7 +14,7 @@ import TagsHandler from '../Tags/TagsHandler.js'
import { expressify } from '@overleaf/promise-utils'
import logger from '@overleaf/logger'
import Features from '../../infrastructure/Features.js'
import SubscriptionViewModelBuilder from '../Subscription/SubscriptionViewModelBuilder.js'
import SubscriptionViewModelBuilder from '../Subscription/SubscriptionViewModelBuilder.mjs'
import NotificationsHandler from '../Notifications/NotificationsHandler.js'
import Modules from '../../infrastructure/Modules.js'
import { OError, V1ConnectionError } from '../Errors/Errors.js'
@@ -27,7 +27,7 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
/**

View File

@@ -1,7 +1,7 @@
const logger = require('@overleaf/logger')
const SessionManager = require('../Authentication/SessionManager')
const LoginRateLimiter = require('./LoginRateLimiter')
const settings = require('@overleaf/settings')
import logger from '@overleaf/logger'
import SessionManager from '../Authentication/SessionManager.js'
import LoginRateLimiter from './LoginRateLimiter.js'
import settings from '@overleaf/settings'
/**
* Return a rate limiting middleware
@@ -88,4 +88,4 @@ const RateLimiterMiddleware = {
loginRateLimitEmail,
}
module.exports = RateLimiterMiddleware
export default RateLimiterMiddleware

View File

@@ -1,9 +1,9 @@
// @ts-check
import mongodb from '../../infrastructure/mongodb.js'
import { callbackify } from 'node:util'
import Settings from '@overleaf/settings'
import Errors from '../Errors/Errors.js'
const { db } = require('../../infrastructure/mongodb')
const { callbackify } = require('util')
const Settings = require('@overleaf/settings')
const { InvalidError } = require('../Errors/Errors')
const { db } = mongodb
const LearnedWordsManager = {
/**
@@ -15,7 +15,7 @@ const LearnedWordsManager = {
const wordSize = Buffer.from(word).length
if (wordsSize + wordSize > Settings.maxDictionarySize) {
throw new InvalidError('Max dictionary size reached')
throw new Errors.InvalidError('Max dictionary size reached')
}
return await db.spellingPreferences.updateOne(
@@ -86,7 +86,7 @@ const LearnedWordsManager = {
},
}
module.exports = {
export default {
learnWord: callbackify(LearnedWordsManager.learnWord),
unlearnWord: callbackify(LearnedWordsManager.unlearnWord),
getLearnedWords: callbackify(LearnedWordsManager.getLearnedWords),

View File

@@ -1,7 +1,7 @@
// @ts-check
import SessionManager from '../Authentication/SessionManager.js'
import LearnedWordsManager from './LearnedWordsManager.js'
import LearnedWordsManager from './LearnedWordsManager.mjs'
import { z, validateReq } from '../../infrastructure/Validation.js'
const learnSchema = z.object({

View File

@@ -1,7 +1,7 @@
import OError from '@overleaf/o-error'
import Metrics from '@overleaf/metrics'
import { promisifyAll } from '@overleaf/promise-utils'
import LearnedWordsManager from './LearnedWordsManager.js'
import LearnedWordsManager from './LearnedWordsManager.mjs'
const SpellingHandler = {
getUserDictionary(userId, callback) {

View File

@@ -1,8 +1,10 @@
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SubscriptionEmailHandler = require('./SubscriptionEmailHandler')
const { AI_ADD_ON_CODE } = require('./AiHelper')
const { ObjectId } = require('mongodb-legacy')
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import SubscriptionEmailHandler from './SubscriptionEmailHandler.js'
import { AI_ADD_ON_CODE } from './AiHelper.js'
import mongodb from 'mongodb-legacy'
const { ObjectId } = mongodb
const INVOICE_SUBSCRIPTION_LIMIT = 10
@@ -389,6 +391,6 @@ function _getSubscriptionData(eventData) {
}
}
module.exports = {
export default {
sendRecurlyAnalyticsEvent,
}

View File

@@ -3,7 +3,7 @@
import SessionManager from '../Authentication/SessionManager.js'
import SubscriptionHandler from './SubscriptionHandler.js'
import SubscriptionHelper from './SubscriptionHelper.js'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.js'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.mjs'
import LimitationsManager from './LimitationsManager.js'
import RecurlyWrapper from './RecurlyWrapper.js'
import Settings from '@overleaf/settings'
@@ -13,7 +13,7 @@ import FeaturesUpdater from './FeaturesUpdater.js'
import GroupPlansData from './GroupPlansData.js'
import V1SubscriptionManager from './V1SubscriptionManager.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import RecurlyEventHandler from './RecurlyEventHandler.js'
import RecurlyEventHandler from './RecurlyEventHandler.mjs'
import { expressify } from '@overleaf/promise-utils'
import OError from '@overleaf/o-error'
import Errors from './Errors.js'
@@ -30,7 +30,7 @@ import {
import PlansLocator from './PlansLocator.js'
import { User } from '../../models/User.js'
import UserGetter from '../User/UserGetter.js'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
import { sanitizeSessionUserForFrontEnd } from '../../infrastructure/FrontEndUser.js'
import { z, validateReq } from '../../infrastructure/Validation.js'
import { IndeterminateInvoiceError } from '../Errors/Errors.js'

View File

@@ -4,7 +4,7 @@ import SubscriptionController from './SubscriptionController.mjs'
import SubscriptionGroupController from './SubscriptionGroupController.mjs'
import TeamInvitesController from './TeamInvitesController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import Settings from '@overleaf/settings'
const teamInviteRateLimiter = new RateLimiter('team-invite', {

View File

@@ -1,23 +1,24 @@
// ts-check
const Settings = require('@overleaf/settings')
const PlansLocator = require('./PlansLocator')
const { isStandaloneAiAddOnPlanCode } = require('./AiHelper')
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./PaymentProviderEntities')
const SubscriptionFormatters = require('./SubscriptionFormatters')
const SubscriptionLocator = require('./SubscriptionLocator')
const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
const InstitutionsManager = require('../Institutions/InstitutionsManager')
const PublishersGetter = require('../Publishers/PublishersGetter')
const sanitizeHtml = require('sanitize-html')
const _ = require('lodash')
const async = require('async')
const SubscriptionHelper = require('./SubscriptionHelper')
const { callbackify } = require('@overleaf/promise-utils')
const { V1ConnectionError } = require('../Errors/Errors')
const FeaturesHelper = require('./FeaturesHelper')
const { formatCurrency } = require('../../util/currency')
const Modules = require('../../infrastructure/Modules')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
import Settings from '@overleaf/settings'
import PlansLocator from './PlansLocator.js'
import { isStandaloneAiAddOnPlanCode } from './AiHelper.js'
import { MEMBERS_LIMIT_ADD_ON_CODE } from './PaymentProviderEntities.js'
import SubscriptionFormatters from './SubscriptionFormatters.js'
import SubscriptionLocator from './SubscriptionLocator.js'
import InstitutionsGetter from '../Institutions/InstitutionsGetter.js'
import InstitutionsManager from '../Institutions/InstitutionsManager.js'
import PublishersGetter from '../Publishers/PublishersGetter.js'
import sanitizeHtml from 'sanitize-html'
import _ from 'lodash'
import async from 'async'
import SubscriptionHelper from './SubscriptionHelper.js'
import { callbackify } from '@overleaf/promise-utils'
import { V1ConnectionError } from '../Errors/Errors.js'
import FeaturesHelper from './FeaturesHelper.js'
import { formatCurrency } from '../../util/currency.js'
import Modules from '../../infrastructure/Modules.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
/**
* @import { Subscription } from "../../../../types/project/dashboard/subscription"
@@ -629,7 +630,7 @@ function buildPlansListForSubscriptionDash(currentPlan, isInTrial) {
}
}
module.exports = {
export default {
buildUsersSubscriptionViewModel: callbackify(buildUsersSubscriptionViewModel),
buildPlansList,
buildPlansListForSubscriptionDash,

View File

@@ -10,7 +10,7 @@ import EmailHelper from '../Helpers/EmailHelper.js'
import UserGetter from '../User/UserGetter.js'
import { expressify } from '@overleaf/promise-utils'
import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
import EmailHandler from '../Email/EmailHandler.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import Modules from '../../infrastructure/Modules.js'

View File

@@ -1,9 +1,9 @@
const path = require('path')
const SessionManager = require('../Authentication/SessionManager')
const TemplatesManager = require('./TemplatesManager')
const ProjectHelper = require('../Project/ProjectHelper')
const logger = require('@overleaf/logger')
const { expressify } = require('@overleaf/promise-utils')
import path from 'node:path'
import SessionManager from '../Authentication/SessionManager.js'
import TemplatesManager from './TemplatesManager.js'
import ProjectHelper from '../Project/ProjectHelper.js'
import logger from '@overleaf/logger'
import { expressify } from '@overleaf/promise-utils'
const TemplatesController = {
async getV1Template(req, res) {
@@ -27,7 +27,7 @@ const TemplatesController = {
}
res.render(
path.resolve(
__dirname,
import.meta.dirname,
'../../../views/project/editor/new_from_template'
),
data
@@ -54,7 +54,7 @@ const TemplatesController = {
},
}
module.exports = {
export default {
getV1Template: expressify(TemplatesController.getV1Template),
createProjectFromV1Template: expressify(
TemplatesController.createProjectFromV1Template

View File

@@ -8,10 +8,11 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
import settings from '@overleaf/settings'
module.exports = {
import logger from '@overleaf/logger'
export default {
saveTemplateDataInSession(req, res, next) {
if (req.query.templateName) {
req.session.templateData = req.query

View File

@@ -1,9 +1,9 @@
import AuthenticationController from '../Authentication/AuthenticationController.js'
import TemplatesController from './TemplatesController.js'
import TemplatesMiddleware from './TemplatesMiddleware.js'
import TemplatesController from './TemplatesController.mjs'
import TemplatesMiddleware from './TemplatesMiddleware.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.mjs'
const rateLimiter = new RateLimiter('create-project-from-template', {
points: 20,

View File

@@ -2,7 +2,7 @@ import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mj
import AuthenticationController from '../Authentication/AuthenticationController.js'
import ProjectUploadController from './ProjectUploadController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import Settings from '@overleaf/settings'
const rateLimiters = {

View File

@@ -3,7 +3,7 @@ import UserMembershipController from './UserMembershipController.mjs'
import SubscriptionGroupController from '../Subscription/SubscriptionGroupController.mjs'
import TeamInvitesController from '../Subscription/TeamInvitesController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
const rateLimiters = {
createTeamInvite: new RateLimiter('create-team-invite', {

View File

@@ -41,7 +41,7 @@ import {
openProjectRateLimiter,
overleafLoginRateLimiter,
} from './infrastructure/RateLimiter.js'
import RateLimiterMiddleware from './Features/Security/RateLimiterMiddleware.js'
import RateLimiterMiddleware from './Features/Security/RateLimiterMiddleware.mjs'
import InactiveProjectController from './Features/InactiveData/InactiveProjectController.mjs'
import ContactRouter from './Features/Contacts/ContactRouter.mjs'
import ReferencesController from './Features/References/ReferencesController.mjs'
@@ -55,7 +55,7 @@ import LinkedFilesRouter from './Features/LinkedFiles/LinkedFilesRouter.mjs'
import TemplatesRouter from './Features/Templates/TemplatesRouter.mjs'
import UserMembershipRouter from './Features/UserMembership/UserMembershipRouter.mjs'
import SystemMessageController from './Features/SystemMessages/SystemMessageController.mjs'
import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.js'
import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.mjs'
import AnalyticsUTMTrackingMiddleware from './Features/Analytics/AnalyticsUTMTrackingMiddleware.mjs'
import CaptchaMiddleware from './Features/Captcha/CaptchaMiddleware.mjs'
import UnsupportedBrowserMiddleware from './infrastructure/UnsupportedBrowserMiddleware.js'

View File

@@ -2,7 +2,7 @@ import { db, ObjectId } from '../../../../app/src/infrastructure/mongodb.js'
import { expect } from 'chai'
import { callbackifyClass } from '@overleaf/promise-utils'
import SubscriptionUpdater from '../../../../app/src/Features/Subscription/SubscriptionUpdater.js'
import PermissionsManager from '../../../../app/src/Features/Authorization/PermissionsManager.js'
import PermissionsManager from '../../../../app/src/Features/Authorization/PermissionsManager.mjs'
import SSOConfigManager from '../../../../modules/group-settings/app/src/sso/SSOConfigManager.mjs'
import { Subscription as SubscriptionModel } from '../../../../app/src/models/Subscription.js'
import { DeletedSubscription as DeletedSubscriptionModel } from '../../../../app/src/models/DeletedSubscription.js'

View File

@@ -1,44 +1,43 @@
const sinon = require('sinon')
const { expect } = require('chai')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/Authorization/PermissionsManager.js'
const SandboxedModule = require('sandboxed-module')
const { ForbiddenError } = require('../../../../app/src/Features/Errors/Errors')
'../../../../app/src/Features/Authorization/PermissionsManager.mjs'
describe('PermissionsManager', function () {
beforeEach(function () {
this.PermissionsManager = SandboxedModule.require(modulePath, {
requires: {
'../../infrastructure/Modules': (this.Modules = {
promises: {
hooks: {
fire: (this.hooksFire = sinon.stub().resolves([[]])),
},
beforeEach(async function (ctx) {
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: (ctx.Modules = {
promises: {
hooks: {
fire: (ctx.hooksFire = sinon.stub().resolves([[]])),
},
}),
},
})
this.PermissionsManager.registerCapability('capability1', {
},
}),
}))
ctx.PermissionsManager = (await import(modulePath)).default
ctx.PermissionsManager.registerCapability('capability1', {
default: true,
})
this.PermissionsManager.registerCapability('capability2', {
ctx.PermissionsManager.registerCapability('capability2', {
default: true,
})
this.PermissionsManager.registerCapability('capability3', {
ctx.PermissionsManager.registerCapability('capability3', {
default: true,
})
this.PermissionsManager.registerCapability('capability4', {
ctx.PermissionsManager.registerCapability('capability4', {
default: false,
})
this.PermissionsManager.registerPolicy('openPolicy', {
ctx.PermissionsManager.registerPolicy('openPolicy', {
capability1: true,
capability2: true,
})
this.PermissionsManager.registerPolicy('restrictivePolicy', {
ctx.PermissionsManager.registerPolicy('restrictivePolicy', {
capability1: true,
capability2: false,
})
this.openPolicyResponseSet = [
ctx.openPolicyResponseSet = [
[
{
managedUsersEnabled: true,
@@ -50,7 +49,7 @@ describe('PermissionsManager', function () {
},
],
]
this.restrictivePolicyResponseSet = [
ctx.restrictivePolicyResponseSet = [
[
{
managedUsersEnabled: true,
@@ -65,40 +64,40 @@ describe('PermissionsManager', function () {
})
describe('validatePolicies', function () {
it('accepts empty object', function () {
expect(() => this.PermissionsManager.validatePolicies({})).not.to.throw
it('accepts empty object', function (ctx) {
expect(() => ctx.PermissionsManager.validatePolicies({})).not.to.throw
})
it('accepts object with registered policies', function () {
it('accepts object with registered policies', function (ctx) {
expect(() =>
this.PermissionsManager.validatePolicies({
ctx.PermissionsManager.validatePolicies({
openPolicy: true,
restrictivePolicy: false,
})
).not.to.throw
})
it('accepts object with policies containing non-boolean values', function () {
it('accepts object with policies containing non-boolean values', function (ctx) {
expect(() =>
this.PermissionsManager.validatePolicies({
ctx.PermissionsManager.validatePolicies({
openPolicy: 1,
})
).to.throw('policy value must be a boolean: openPolicy = 1')
expect(() =>
this.PermissionsManager.validatePolicies({
ctx.PermissionsManager.validatePolicies({
openPolicy: undefined,
})
).to.throw('policy value must be a boolean: openPolicy = undefined')
expect(() =>
this.PermissionsManager.validatePolicies({
ctx.PermissionsManager.validatePolicies({
openPolicy: null,
})
).to.throw('policy value must be a boolean: openPolicy = null')
})
it('throws error on object with policies that are not registered', function () {
it('throws error on object with policies that are not registered', function (ctx) {
expect(() =>
this.PermissionsManager.validatePolicies({
ctx.PermissionsManager.validatePolicies({
openPolicy: true,
unregisteredPolicy: false,
})
@@ -108,20 +107,20 @@ describe('PermissionsManager', function () {
describe('hasPermission', function () {
describe('when no policies apply to the user', function () {
it('should return true if default permission is true', function () {
it('should return true if default permission is true', function (ctx) {
const groupPolicy = {}
const capability = 'capability1'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.true
})
it('should return false if the default permission is false', function () {
it('should return false if the default permission is false', function (ctx) {
const groupPolicy = {}
const capability = 'capability4'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
@@ -130,8 +129,8 @@ describe('PermissionsManager', function () {
})
describe('when a policy applies to the user', function () {
it('should return true if the user has the capability after the policy is applied', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return true if the user has the capability after the policy is applied', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -139,15 +138,15 @@ describe('PermissionsManager', function () {
policy: true,
}
const capability = 'capability1'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.true
})
it('should return false if the user does not have the capability after the policy is applied', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return false if the user does not have the capability after the policy is applied', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -155,15 +154,15 @@ describe('PermissionsManager', function () {
policy: true,
}
const capability = 'capability2'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.false
})
it('should return the default permission if the policy does not apply to the capability', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return the default permission if the policy does not apply to the capability', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -172,7 +171,7 @@ describe('PermissionsManager', function () {
}
{
const capability = 'capability3'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
@@ -180,7 +179,7 @@ describe('PermissionsManager', function () {
}
{
const capability = 'capability4'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
@@ -188,8 +187,8 @@ describe('PermissionsManager', function () {
}
})
it('should return the default permission if the policy is not enforced', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return the default permission if the policy is not enforced', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -197,12 +196,12 @@ describe('PermissionsManager', function () {
policy: false,
}
const capability1 = 'capability1'
const result1 = this.PermissionsManager.hasPermission(
const result1 = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability1
)
const capability2 = 'capability2'
const result2 = this.PermissionsManager.hasPermission(
const result2 = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability2
)
@@ -212,13 +211,13 @@ describe('PermissionsManager', function () {
})
describe('when multiple policies apply to the user', function () {
it('should return true if all policies allow the capability', function () {
this.PermissionsManager.registerPolicy('policy1', {
it('should return true if all policies allow the capability', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy1', {
capability1: true,
capability2: true,
})
this.PermissionsManager.registerPolicy('policy2', {
ctx.PermissionsManager.registerPolicy('policy2', {
capability1: true,
capability2: true,
})
@@ -227,20 +226,20 @@ describe('PermissionsManager', function () {
policy2: true,
}
const capability = 'capability1'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.true
})
it('should return false if any policy denies the capability', function () {
this.PermissionsManager.registerPolicy('policy1', {
it('should return false if any policy denies the capability', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy1', {
capability1: true,
capability2: true,
})
this.PermissionsManager.registerPolicy('policy2', {
ctx.PermissionsManager.registerPolicy('policy2', {
capability1: false,
capability2: true,
})
@@ -249,20 +248,20 @@ describe('PermissionsManager', function () {
policy2: true,
}
const capability = 'capability1'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.false
})
it('should return the default permssion when the applicable policy is not enforced', function () {
this.PermissionsManager.registerPolicy('policy1', {
it('should return the default permssion when the applicable policy is not enforced', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy1', {
capability1: true,
capability2: true,
})
this.PermissionsManager.registerPolicy('policy2', {
ctx.PermissionsManager.registerPolicy('policy2', {
capability1: false,
capability2: true,
})
@@ -271,15 +270,15 @@ describe('PermissionsManager', function () {
policy2: false,
}
const capability = 'capability1'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
expect(result).to.be.true
})
it('should return the default permission if the policies do not restrict to the capability', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return the default permission if the policies do not restrict to the capability', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -288,7 +287,7 @@ describe('PermissionsManager', function () {
}
{
const capability = 'capability3'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
@@ -296,7 +295,7 @@ describe('PermissionsManager', function () {
}
{
const capability = 'capability4'
const result = this.PermissionsManager.hasPermission(
const result = ctx.PermissionsManager.hasPermission(
groupPolicy,
capability
)
@@ -307,17 +306,17 @@ describe('PermissionsManager', function () {
})
describe('getUserCapabilities', function () {
it('should return the default capabilities when no group policy is provided', function () {
it('should return the default capabilities when no group policy is provided', function (ctx) {
const groupPolicy = {}
const capabilities =
this.PermissionsManager.getUserCapabilities(groupPolicy)
ctx.PermissionsManager.getUserCapabilities(groupPolicy)
expect(capabilities).to.deep.equal(
new Set(['capability1', 'capability2', 'capability3'])
)
})
it('should return a reduced capability set when a group policy is provided', function () {
this.PermissionsManager.registerPolicy('policy', {
it('should return a reduced capability set when a group policy is provided', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy', {
capability1: true,
capability2: false,
})
@@ -325,18 +324,18 @@ describe('PermissionsManager', function () {
policy: true,
}
const capabilities =
this.PermissionsManager.getUserCapabilities(groupPolicy)
ctx.PermissionsManager.getUserCapabilities(groupPolicy)
expect(capabilities).to.deep.equal(
new Set(['capability1', 'capability3'])
)
})
it('should return a reduced capability set when multiple group policies are provided', function () {
this.PermissionsManager.registerPolicy('policy1', {
it('should return a reduced capability set when multiple group policies are provided', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy1', {
capability1: true,
capability2: false,
})
this.PermissionsManager.registerPolicy('policy2', {
ctx.PermissionsManager.registerPolicy('policy2', {
capability1: false,
capability2: true,
})
@@ -346,20 +345,20 @@ describe('PermissionsManager', function () {
policy2: true,
}
const capabilities =
this.PermissionsManager.getUserCapabilities(groupPolicy)
ctx.PermissionsManager.getUserCapabilities(groupPolicy)
expect(capabilities).to.deep.equal(new Set(['capability3']))
})
it('should return an empty capability set when group policies remove all permissions', function () {
this.PermissionsManager.registerPolicy('policy1', {
it('should return an empty capability set when group policies remove all permissions', function (ctx) {
ctx.PermissionsManager.registerPolicy('policy1', {
capability1: true,
capability2: false,
})
this.PermissionsManager.registerPolicy('policy2', {
ctx.PermissionsManager.registerPolicy('policy2', {
capability1: false,
capability2: true,
})
this.PermissionsManager.registerPolicy('policy3', {
ctx.PermissionsManager.registerPolicy('policy3', {
capability1: true,
capability2: true,
capability3: false,
@@ -370,14 +369,14 @@ describe('PermissionsManager', function () {
policy3: true,
}
const capabilities =
this.PermissionsManager.getUserCapabilities(groupPolicy)
ctx.PermissionsManager.getUserCapabilities(groupPolicy)
expect(capabilities).to.deep.equal(new Set())
})
})
describe('getUserValidationStatus', function () {
it('should return the status for the policy when the user conforms', async function () {
this.PermissionsManager.registerPolicy(
it('should return the status for the policy when the user conforms', async function (ctx) {
ctx.PermissionsManager.registerPolicy(
'policy',
{},
{
@@ -392,7 +391,7 @@ describe('PermissionsManager', function () {
const user = { prop: 'allowed' }
const subscription = { prop: 'managed' }
const result =
await this.PermissionsManager.promises.getUserValidationStatus({
await ctx.PermissionsManager.promises.getUserValidationStatus({
user,
groupPolicy,
subscription,
@@ -400,8 +399,8 @@ describe('PermissionsManager', function () {
expect(result).to.deep.equal(new Map([['policy', true]]))
})
it('should return the status for the policy when the user does not conform', async function () {
this.PermissionsManager.registerPolicy(
it('should return the status for the policy when the user does not conform', async function (ctx) {
ctx.PermissionsManager.registerPolicy(
'policy',
{},
{
@@ -416,15 +415,15 @@ describe('PermissionsManager', function () {
const user = { prop: 'not allowed' }
const subscription = { prop: 'managed' }
const result =
await this.PermissionsManager.promises.getUserValidationStatus({
await ctx.PermissionsManager.promises.getUserValidationStatus({
user,
groupPolicy,
subscription,
})
expect(result).to.deep.equal(new Map([['policy', false]]))
})
it('should return the status for multiple policies according to whether the user conforms', async function () {
this.PermissionsManager.registerPolicy(
it('should return the status for multiple policies according to whether the user conforms', async function (ctx) {
ctx.PermissionsManager.registerPolicy(
'policy1',
{},
{
@@ -433,7 +432,7 @@ describe('PermissionsManager', function () {
},
}
)
this.PermissionsManager.registerPolicy(
ctx.PermissionsManager.registerPolicy(
'policy2',
{},
{
@@ -442,7 +441,7 @@ describe('PermissionsManager', function () {
},
}
)
this.PermissionsManager.registerPolicy(
ctx.PermissionsManager.registerPolicy(
'policy3',
{},
{
@@ -460,7 +459,7 @@ describe('PermissionsManager', function () {
const user = { prop: 'allowed' }
const subscription = { prop: 'managed' }
const result =
await this.PermissionsManager.promises.getUserValidationStatus({
await ctx.PermissionsManager.promises.getUserValidationStatus({
user,
groupPolicy,
subscription,
@@ -475,20 +474,20 @@ describe('PermissionsManager', function () {
})
describe('assertUserPermissions', function () {
describe('allowed', function () {
it('should not error when managedUsersEnabled is not enabled for user', async function () {
it('should not error when managedUsersEnabled is not enabled for user', async function (ctx) {
const result =
await this.PermissionsManager.promises.assertUserPermissions(
await ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['add-secondary-email']
)
expect(result).to.be.undefined
})
it('should not error when default capability is true', async function () {
this.PermissionsManager.registerCapability('some-policy-to-check', {
it('should not error when default capability is true', async function (ctx) {
ctx.PermissionsManager.registerCapability('some-policy-to-check', {
default: true,
})
this.hooksFire.resolves([
ctx.hooksFire.resolves([
[
{
managedUsersEnabled: true,
@@ -497,21 +496,21 @@ describe('PermissionsManager', function () {
],
])
const result =
await this.PermissionsManager.promises.assertUserPermissions(
await ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['some-policy-to-check']
)
expect(result).to.be.undefined
})
it('should not error when default permission is false but user has permission', async function () {
this.PermissionsManager.registerCapability('some-policy-to-check', {
it('should not error when default permission is false but user has permission', async function (ctx) {
ctx.PermissionsManager.registerCapability('some-policy-to-check', {
default: false,
})
this.PermissionsManager.registerPolicy('userCanDoSomePolicy', {
ctx.PermissionsManager.registerPolicy('userCanDoSomePolicy', {
'some-policy-to-check': true,
})
this.hooksFire.resolves([
ctx.hooksFire.resolves([
[
{
managedUsersEnabled: true,
@@ -522,7 +521,7 @@ describe('PermissionsManager', function () {
],
])
const result =
await this.PermissionsManager.promises.assertUserPermissions(
await ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['some-policy-to-check']
)
@@ -531,21 +530,21 @@ describe('PermissionsManager', function () {
})
describe('not allowed', function () {
it('should return error when managedUsersEnabled is enabled for user but there is no group policy', async function () {
this.hooksFire.resolves([[{ managedUsersEnabled: true }]])
it('should return error when managedUsersEnabled is enabled for user but there is no group policy', async function (ctx) {
ctx.hooksFire.resolves([[{ managedUsersEnabled: true }]])
await expect(
this.PermissionsManager.promises.assertUserPermissions(
ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['add-secondary-email']
)
).to.be.rejectedWith(Error, 'unknown capability: add-secondary-email')
})
it('should return error when default permission is false', async function () {
this.PermissionsManager.registerCapability('some-policy-to-check', {
it('should return error when default permission is false', async function (ctx) {
ctx.PermissionsManager.registerCapability('some-policy-to-check', {
default: false,
})
this.hooksFire.resolves([
ctx.hooksFire.resolves([
[
{
managedUsersEnabled: true,
@@ -554,21 +553,23 @@ describe('PermissionsManager', function () {
],
])
await expect(
this.PermissionsManager.promises.assertUserPermissions(
ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['some-policy-to-check']
)
).to.be.rejectedWith(ForbiddenError)
).to.be.rejectedWith(
'user does not have one or more permissions within some-policy-to-check'
)
})
it('should return error when default permission is true but user does not have permission', async function () {
this.PermissionsManager.registerCapability('some-policy-to-check', {
it('should return error when default permission is true but user does not have permission', async function (ctx) {
ctx.PermissionsManager.registerCapability('some-policy-to-check', {
default: true,
})
this.PermissionsManager.registerPolicy('userCannotDoSomePolicy', {
ctx.PermissionsManager.registerPolicy('userCannotDoSomePolicy', {
'some-policy-to-check': false,
})
this.hooksFire.resolves([
ctx.hooksFire.resolves([
[
{
managedUsersEnabled: true,
@@ -577,33 +578,35 @@ describe('PermissionsManager', function () {
],
])
await expect(
this.PermissionsManager.promises.assertUserPermissions(
ctx.PermissionsManager.promises.assertUserPermissions(
{ _id: 'user123' },
['some-policy-to-check']
)
).to.be.rejectedWith(ForbiddenError)
).to.be.rejectedWith(
'user does not have one or more permissions within some-policy-to-check'
)
})
})
})
describe('registerAllowedProperty', function () {
it('allows us to register a property', async function () {
this.PermissionsManager.registerAllowedProperty('metadata1')
const result = await this.PermissionsManager.getAllowedProperties()
it('allows us to register a property', async function (ctx) {
ctx.PermissionsManager.registerAllowedProperty('metadata1')
const result = await ctx.PermissionsManager.getAllowedProperties()
expect(result).to.deep.equal(new Set(['metadata1']))
})
// used if multiple modules would require the same prop, since we dont know which will load first, both must register
it('should handle multiple registrations of the same property', async function () {
this.PermissionsManager.registerAllowedProperty('metadata1')
this.PermissionsManager.registerAllowedProperty('metadata1')
const result = await this.PermissionsManager.getAllowedProperties()
it('should handle multiple registrations of the same property', async function (ctx) {
ctx.PermissionsManager.registerAllowedProperty('metadata1')
ctx.PermissionsManager.registerAllowedProperty('metadata1')
const result = await ctx.PermissionsManager.getAllowedProperties()
expect(result).to.deep.equal(new Set(['metadata1']))
})
})
describe('combineAllowedProperties', function () {
it('should handle multiple occurences of the same property, preserving the first occurence', async function () {
it('should handle multiple occurences of the same property, preserving the first occurence', async function (ctx) {
const policy1 = {
groupPolicy: {
policy: false,
@@ -618,17 +621,17 @@ describe('PermissionsManager', function () {
}
const results = [policy1, policy2]
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const combinedProps =
this.PermissionsManager.combineAllowedProperties(results)
ctx.PermissionsManager.combineAllowedProperties(results)
expect(combinedProps).to.deep.equal({
prop1: 'some other value here',
})
})
it('should add registered properties to the set', async function () {
it('should add registered properties to the set', async function (ctx) {
const policy = {
groupPolicy: {
policy: false,
@@ -645,11 +648,11 @@ describe('PermissionsManager', function () {
}
const results = [policy, policy2]
this.PermissionsManager.registerAllowedProperty('prop1')
this.PermissionsManager.registerAllowedProperty('prop2')
ctx.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop2')
const combinedProps =
this.PermissionsManager.combineAllowedProperties(results)
ctx.PermissionsManager.combineAllowedProperties(results)
expect(combinedProps).to.deep.equal({
prop1: 'some value here',
@@ -657,7 +660,7 @@ describe('PermissionsManager', function () {
})
})
it('should not add unregistered properties to the req object', async function () {
it('should not add unregistered properties to the req object', async function (ctx) {
const policy = {
groupPolicy: {
policy: false,
@@ -671,36 +674,36 @@ describe('PermissionsManager', function () {
},
prop2: 'some value here',
}
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const results = [policy, policy2]
const combinedProps =
this.PermissionsManager.combineAllowedProperties(results)
ctx.PermissionsManager.combineAllowedProperties(results)
expect(combinedProps).to.deep.equal({ prop1: 'some value here' })
})
it('should handle an empty array', async function () {
it('should handle an empty array', async function (ctx) {
const results = []
const combinedProps =
this.PermissionsManager.combineAllowedProperties(results)
ctx.PermissionsManager.combineAllowedProperties(results)
expect(combinedProps).to.deep.equal({})
})
})
describe('combineGroupPolicies', function () {
it('should return an empty object when an empty array is passed', async function () {
it('should return an empty object when an empty array is passed', async function (ctx) {
const results = []
const combinedPolicy =
this.PermissionsManager.combineGroupPolicies(results)
ctx.PermissionsManager.combineGroupPolicies(results)
expect(combinedPolicy).to.deep.equal({})
})
it('should combine multiple group policies into a single policy object', async function () {
it('should combine multiple group policies into a single policy object', async function (ctx) {
const groupPolicy = {
policy1: true,
}
@@ -709,12 +712,12 @@ describe('PermissionsManager', function () {
policy2: false,
policy3: true,
}
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const results = [groupPolicy, groupPolicy2]
const combinedPolicy =
this.PermissionsManager.combineGroupPolicies(results)
ctx.PermissionsManager.combineGroupPolicies(results)
expect(combinedPolicy).to.deep.equal({
policy1: true,
@@ -722,7 +725,7 @@ describe('PermissionsManager', function () {
})
})
it('should handle duplicate enforced policies across different group policies', async function () {
it('should handle duplicate enforced policies across different group policies', async function (ctx) {
const groupPolicy = {
policy1: false,
policy2: true,
@@ -732,12 +735,12 @@ describe('PermissionsManager', function () {
policy2: true,
policy3: true,
}
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const results = [groupPolicy, groupPolicy2]
const combinedPolicy =
this.PermissionsManager.combineGroupPolicies(results)
ctx.PermissionsManager.combineGroupPolicies(results)
expect(combinedPolicy).to.deep.equal({
policy2: true,
@@ -745,7 +748,7 @@ describe('PermissionsManager', function () {
})
})
it('should handle group policies with no enforced policies', async function () {
it('should handle group policies with no enforced policies', async function (ctx) {
const groupPolicy = {
policy1: false,
policy2: false,
@@ -755,17 +758,17 @@ describe('PermissionsManager', function () {
policy2: false,
policy3: true,
}
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const results = [groupPolicy, groupPolicy2]
const combinedPolicy =
this.PermissionsManager.combineGroupPolicies(results)
ctx.PermissionsManager.combineGroupPolicies(results)
expect(combinedPolicy).to.deep.equal({ policy3: true })
})
it('should choose the stricter option between two policy values', async function () {
it('should choose the stricter option between two policy values', async function (ctx) {
const groupPolicy = {
policy1: false,
policy2: true,
@@ -777,12 +780,12 @@ describe('PermissionsManager', function () {
policy3: true,
policy4: false,
}
this.PermissionsManager.registerAllowedProperty('prop1')
ctx.PermissionsManager.registerAllowedProperty('prop1')
const results = [groupPolicy, groupPolicy2]
const combinedPolicy =
this.PermissionsManager.combineGroupPolicies(results)
ctx.PermissionsManager.combineGroupPolicies(results)
expect(combinedPolicy).to.deep.equal({
policy2: true,
@@ -793,30 +796,30 @@ describe('PermissionsManager', function () {
})
describe('checkUserListPermissions', function () {
it('should return true when all users have permissions required', async function () {
it('should return true when all users have permissions required', async function (ctx) {
const userList = ['user1', 'user2', 'user3']
const capabilities = ['capability1', 'capability2']
this.hooksFire.onCall(0).resolves(this.openPolicyResponseSet)
this.hooksFire.onCall(1).resolves(this.openPolicyResponseSet)
this.hooksFire.onCall(2).resolves(this.openPolicyResponseSet)
ctx.hooksFire.onCall(0).resolves(ctx.openPolicyResponseSet)
ctx.hooksFire.onCall(1).resolves(ctx.openPolicyResponseSet)
ctx.hooksFire.onCall(2).resolves(ctx.openPolicyResponseSet)
const usersHavePermission =
await this.PermissionsManager.promises.checkUserListPermissions(
await ctx.PermissionsManager.promises.checkUserListPermissions(
userList,
capabilities
)
expect(usersHavePermission).to.equal(true)
})
it('should return false if any user does not have permission', async function () {
it('should return false if any user does not have permission', async function (ctx) {
const userList = ['user1', 'user2', 'user3']
const capabilities = ['capability1', 'capability2']
this.hooksFire.onCall(0).resolves(this.openPolicyResponseSet)
this.hooksFire.onCall(1).resolves(this.restrictivePolicyResponseSet)
this.hooksFire.onCall(2).resolves(this.openPolicyResponseSet)
ctx.hooksFire.onCall(0).resolves(ctx.openPolicyResponseSet)
ctx.hooksFire.onCall(1).resolves(ctx.restrictivePolicyResponseSet)
ctx.hooksFire.onCall(2).resolves(ctx.openPolicyResponseSet)
const usersHavePermission =
await this.PermissionsManager.promises.checkUserListPermissions(
await ctx.PermissionsManager.promises.checkUserListPermissions(
userList,
capabilities
)

View File

@@ -0,0 +1,148 @@
import { vi } from 'vitest'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/Security/RateLimiterMiddleware.mjs'
describe('RateLimiterMiddleware', function () {
beforeEach(async function (ctx) {
ctx.SessionManager = {
getLoggedInUserId: () => ctx.req.session?.user?._id,
}
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {}),
}))
vi.doMock('../../../../app/src/Features/Security/LoginRateLimiter', () => ({
default: {},
}))
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({
default: ctx.SessionManager,
})
)
ctx.RateLimiterMiddleware = (await import(modulePath)).default
ctx.req = { params: {} }
ctx.res = {
status: sinon.stub(),
write: sinon.stub(),
end: sinon.stub(),
}
ctx.next = sinon.stub()
})
describe('rateLimit', function () {
beforeEach(function (ctx) {
ctx.projectId = 'project-id'
ctx.docId = 'doc-id'
ctx.rateLimiter = {
consume: sinon.stub().resolves({ remainingPoints: 2 }),
}
ctx.middleware = ctx.RateLimiterMiddleware.rateLimit(ctx.rateLimiter, {
params: ['projectId', 'docId'],
})
ctx.req.params = { projectId: ctx.projectId, docId: ctx.docId }
})
describe('when there is no session', function () {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.req.ip = ctx.ip = '1.2.3.4'
ctx.middleware(ctx.req, ctx.res, () => {
resolve()
})
})
})
it('should call the rate limiter with the ip address', function (ctx) {
ctx.rateLimiter.consume.should.have.been.calledWith(
`${ctx.projectId}:${ctx.docId}:${ctx.ip}`
)
})
})
describe('when smoke test user', function () {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.userId = 'smoke-test-user-id'
ctx.req.session = {
user: { _id: ctx.userId },
}
ctx.settings.smokeTest = { userId: ctx.userId }
ctx.middleware(ctx.req, ctx.res, () => {
resolve()
})
})
})
it('should not call the rate limiter', function (ctx) {
ctx.rateLimiter.consume.should.not.have.been.called
})
})
describe('when under the rate limit with logged in user', function () {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.userId = 'user-id'
ctx.req.session = {
user: { _id: ctx.userId },
}
ctx.middleware(ctx.req, ctx.res, () => {
resolve()
})
})
})
it('should call the rate limiter backend with the userId', function (ctx) {
ctx.rateLimiter.consume.should.have.been.calledWith(
`${ctx.projectId}:${ctx.docId}:${ctx.userId}`
)
})
})
describe('when under the rate limit with anonymous user', function () {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.req.ip = '1.2.3.4'
ctx.middleware(ctx.req, ctx.res, () => {
resolve()
})
})
})
it('should call the rate limiter backend with the ip address', function (ctx) {
ctx.rateLimiter.consume.should.have.been.calledWith(
`${ctx.projectId}:${ctx.docId}:${ctx.req.ip}`
)
})
})
describe('when over the rate limit', function () {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.userId = 'user-id'
ctx.req.session = {
user: { _id: ctx.userId },
}
ctx.res.end.callsFake(() => {
resolve()
})
ctx.rateLimiter.consume.rejects({ remainingPoints: 0 })
ctx.middleware(ctx.req, ctx.res, ctx.next)
})
})
it('should return a 429', function (ctx) {
ctx.res.status.should.have.been.calledWith(429)
})
it('should not continue', function (ctx) {
ctx.next.should.not.have.been.called
})
})
})
})

View File

@@ -1,129 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/Security/RateLimiterMiddleware'
)
describe('RateLimiterMiddleware', function () {
beforeEach(function () {
this.SessionManager = {
getLoggedInUserId: () => this.req.session?.user?._id,
}
this.RateLimiterMiddleware = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': (this.settings = {}),
'./LoginRateLimiter': {},
'../Authentication/SessionManager': this.SessionManager,
},
})
this.req = { params: {} }
this.res = {
status: sinon.stub(),
write: sinon.stub(),
end: sinon.stub(),
}
this.next = sinon.stub()
})
describe('rateLimit', function () {
beforeEach(function () {
this.projectId = 'project-id'
this.docId = 'doc-id'
this.rateLimiter = {
consume: sinon.stub().resolves({ remainingPoints: 2 }),
}
this.middleware = this.RateLimiterMiddleware.rateLimit(this.rateLimiter, {
params: ['projectId', 'docId'],
})
this.req.params = { projectId: this.projectId, docId: this.docId }
})
describe('when there is no session', function () {
beforeEach(function (done) {
this.req.ip = this.ip = '1.2.3.4'
this.middleware(this.req, this.res, () => {
done()
})
})
it('should call the rate limiter with the ip address', function () {
this.rateLimiter.consume.should.have.been.calledWith(
`${this.projectId}:${this.docId}:${this.ip}`
)
})
})
describe('when smoke test user', function () {
beforeEach(function (done) {
this.userId = 'smoke-test-user-id'
this.req.session = {
user: { _id: this.userId },
}
this.settings.smokeTest = { userId: this.userId }
this.middleware(this.req, this.res, () => {
done()
})
})
it('should not call the rate limiter', function () {
this.rateLimiter.consume.should.not.have.been.called
})
})
describe('when under the rate limit with logged in user', function () {
beforeEach(function (done) {
this.userId = 'user-id'
this.req.session = {
user: { _id: this.userId },
}
this.middleware(this.req, this.res, () => {
done()
})
})
it('should call the rate limiter backend with the userId', function () {
this.rateLimiter.consume.should.have.been.calledWith(
`${this.projectId}:${this.docId}:${this.userId}`
)
})
})
describe('when under the rate limit with anonymous user', function () {
beforeEach(function (done) {
this.req.ip = '1.2.3.4'
this.middleware(this.req, this.res, () => {
done()
})
})
it('should call the rate limiter backend with the ip address', function () {
this.rateLimiter.consume.should.have.been.calledWith(
`${this.projectId}:${this.docId}:${this.req.ip}`
)
})
})
describe('when over the rate limit', function () {
beforeEach(function (done) {
this.userId = 'user-id'
this.req.session = {
user: { _id: this.userId },
}
this.res.end.callsFake(() => {
done()
})
this.rateLimiter.consume.rejects({ remainingPoints: 0 })
this.middleware(this.req, this.res, this.next)
})
it('should return a 429', function () {
this.res.status.should.have.been.calledWith(429)
})
it('should not continue', function () {
this.next.should.not.have.been.called
})
})
})
})

View File

@@ -0,0 +1,142 @@
import { vi, expect } from 'vitest'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath =
'../../../../app/src/Features/Spelling/LearnedWordsManager.mjs'
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('LearnedWordsManager', function () {
beforeEach(async function (ctx) {
ctx.token = 'a6b3cd919ge'
ctx.db = {
spellingPreferences: {
updateOne: vi.fn(),
findOne: vi.fn().mockResolvedValue(['pear']),
},
}
vi.doMock('../../../../app/src/infrastructure/mongodb.js', () => ({
default: { db: ctx.db },
}))
vi.doMock('@overleaf/metrics', () => ({
default: {
inc: vi.fn(),
},
}))
vi.doMock('@overleaf/settings', () => ({
default: {
maxDictionarySize: 20,
},
}))
ctx.LearnedWordsManager = (await import(modulePath)).default
})
describe('learnWord', function () {
describe('under size limit', function () {
beforeEach(async function (ctx) {
ctx.word = 'instanton'
await ctx.LearnedWordsManager.promises.learnWord(ctx.token, ctx.word)
})
it('should insert the word in the word list in the database', function (ctx) {
expect(ctx.db.spellingPreferences.updateOne).toHaveBeenCalledWith(
{
token: ctx.token,
},
{
$addToSet: { learnedWords: ctx.word },
},
{
upsert: true,
}
)
})
})
describe('over size limit', function () {
beforeEach(function (ctx) {
ctx.word = 'superlongwordthatwillgobeyondthelimit'
})
it('should throw an error and not insert the word in the word list in the database', async function (ctx) {
await expect(
ctx.LearnedWordsManager.promises.learnWord(ctx.token, ctx.word)
).to.be.rejectedWith(Errors.InvalidError)
expect(ctx.db.spellingPreferences.updateOne).not.toHaveBeenCalled()
})
})
})
describe('unlearnWord', function () {
beforeEach(async function (ctx) {
ctx.word = 'instanton'
await ctx.LearnedWordsManager.promises.unlearnWord(ctx.token, ctx.word)
})
it('should remove the word from the word list in the database', function (ctx) {
expect(ctx.db.spellingPreferences.updateOne).toHaveBeenCalledWith(
{
token: ctx.token,
},
{
$pull: { learnedWords: ctx.word },
}
)
})
})
describe('getLearnedWords', function () {
beforeEach(async function (ctx) {
ctx.wordList = ['apples', 'bananas', 'pears']
ctx.wordListWithDuplicates = ctx.wordList.slice()
ctx.wordListWithDuplicates.push('bananas')
ctx.db.spellingPreferences.findOne = vi
.fn()
.mockResolvedValue({ learnedWords: ctx.wordListWithDuplicates })
ctx.learnedWords = await ctx.LearnedWordsManager.promises.getLearnedWords(
ctx.token
)
})
it('should get the word list for the given user', function (ctx) {
expect(ctx.db.spellingPreferences.findOne).toHaveBeenCalledWith({
token: ctx.token,
})
})
it('should return the word list without duplicates', function (ctx) {
expect(ctx.learnedWords).to.deep.equal(ctx.wordList)
})
})
describe('getLearnedWordsSize', function () {
it('should return the word list size in the callback', async function (ctx) {
ctx.db.spellingPreferences.findOne = conditions => {
return Promise.resolve({
learnedWords: ['apples', 'bananas', 'pears', 'bananas'],
})
}
const learnedWordsSize =
await ctx.LearnedWordsManager.promises.getLearnedWordsSize(ctx.token)
expect(learnedWordsSize).to.equal(38)
})
})
describe('deleteUsersLearnedWords', function () {
beforeEach(function (ctx) {
ctx.db.spellingPreferences.deleteOne = vi.fn()
})
it('should get the word list for the given user', async function (ctx) {
await ctx.LearnedWordsManager.promises.deleteUsersLearnedWords(ctx.token)
expect(ctx.db.spellingPreferences.deleteOne).toHaveBeenCalledWith({
token: ctx.token,
})
})
})
})

View File

@@ -1,141 +0,0 @@
const sinon = require('sinon')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const modulePath = require('path').join(
__dirname,
'/../../../../app/src/Features/Spelling/LearnedWordsManager'
)
const { InvalidError } = require('../../../../app/src/Features/Errors/Errors')
describe('LearnedWordsManager', function () {
beforeEach(function () {
this.token = 'a6b3cd919ge'
this.db = {
spellingPreferences: {
updateOne: sinon.stub().resolves(),
findOne: sinon.stub().resolves(['pear']),
},
}
this.LearnedWordsManager = SandboxedModule.require(modulePath, {
requires: {
'../../infrastructure/mongodb': { db: this.db },
'@overleaf/metrics': {
inc: sinon.stub(),
},
'@overleaf/settings': {
maxDictionarySize: 20,
},
},
})
})
describe('learnWord', function () {
describe('under size limit', function () {
beforeEach(async function () {
this.word = 'instanton'
await this.LearnedWordsManager.promises.learnWord(this.token, this.word)
})
it('should insert the word in the word list in the database', function () {
expect(
this.db.spellingPreferences.updateOne.calledWith(
{
token: this.token,
},
{
$addToSet: { learnedWords: this.word },
},
{
upsert: true,
}
)
).to.equal(true)
})
})
describe('over size limit', function () {
beforeEach(function () {
this.word = 'superlongwordthatwillgobeyondthelimit'
})
it('should throw an error and not insert the word in the word list in the database', async function () {
await expect(
this.LearnedWordsManager.promises.learnWord(this.token, this.word)
).to.be.rejectedWith(InvalidError)
expect(this.db.spellingPreferences.updateOne.notCalled).to.equal(true)
})
})
})
describe('unlearnWord', function () {
beforeEach(async function () {
this.word = 'instanton'
await this.LearnedWordsManager.promises.unlearnWord(this.token, this.word)
})
it('should remove the word from the word list in the database', function () {
expect(
this.db.spellingPreferences.updateOne.calledWith(
{
token: this.token,
},
{
$pull: { learnedWords: this.word },
}
)
).to.equal(true)
})
})
describe('getLearnedWords', function () {
beforeEach(async function () {
this.wordList = ['apples', 'bananas', 'pears']
this.wordListWithDuplicates = this.wordList.slice()
this.wordListWithDuplicates.push('bananas')
this.db.spellingPreferences.findOne = conditions => {
return Promise.resolve({ learnedWords: this.wordListWithDuplicates })
}
sinon.spy(this.db.spellingPreferences, 'findOne')
this.learnedWords =
await this.LearnedWordsManager.promises.getLearnedWords(this.token)
})
it('should get the word list for the given user', function () {
expect(
this.db.spellingPreferences.findOne.calledWith({ token: this.token })
).to.equal(true)
})
it('should return the word list without duplicates', function () {
expect(this.learnedWords).to.deep.equal(this.wordList)
})
})
describe('getLearnedWordsSize', function () {
it('should return the word list size in the callback', async function () {
this.db.spellingPreferences.findOne = conditions => {
return Promise.resolve({
learnedWords: ['apples', 'bananas', 'pears', 'bananas'],
})
}
const learnedWordsSize =
await this.LearnedWordsManager.promises.getLearnedWordsSize(this.token)
expect(learnedWordsSize).to.equal(38)
})
})
describe('deleteUsersLearnedWords', function () {
beforeEach(function () {
this.db.spellingPreferences.deleteOne = sinon.stub().resolves()
})
it('should get the word list for the given user', async function () {
await this.LearnedWordsManager.promises.deleteUsersLearnedWords(
this.token
)
expect(
this.db.spellingPreferences.deleteOne.calledWith({ token: this.token })
).to.equal(true)
})
})
})

View File

@@ -1,16 +1,19 @@
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const sinon = require('sinon')
import { vi } from 'vitest'
import mongodb from 'mongodb-legacy'
import sinon from 'sinon'
const { ObjectId } = mongodb
const modulePath =
'../../../../app/src/Features/Subscription/RecurlyEventHandler'
'../../../../app/src/Features/Subscription/RecurlyEventHandler.mjs'
describe('RecurlyEventHandler', function () {
beforeEach(function () {
this.userId = '123abc234bcd456cde567def'
this.planCode = 'collaborator-annual'
this.eventData = {
beforeEach(async function (ctx) {
ctx.userId = '123abc234bcd456cde567def'
ctx.planCode = 'collaborator-annual'
ctx.eventData = {
account: {
account_code: this.userId,
account_code: ctx.userId,
},
subscription: {
uuid: '8435ad98c1ce45da99b07f6a6a2e780f',
@@ -26,17 +29,33 @@ describe('RecurlyEventHandler', function () {
},
}
this.RecurlyEventHandler = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
'./SubscriptionEmailHandler': (this.SubscriptionEmailHandler = {
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionEmailHandler',
() => ({
default: (ctx.SubscriptionEmailHandler = {
sendTrialOnboardingEmail: sinon.stub(),
}),
'../Analytics/AnalyticsManager': (this.AnalyticsManager = {
})
)
vi.doMock(
'../../../../app/src/Features/Analytics/AnalyticsManager',
() => ({
default: (ctx.AnalyticsManager = {
recordEventForUserInBackground: sinon.stub(),
setUserPropertyForUserInBackground: sinon.stub(),
}),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
})
)
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: (ctx.SplitTestHandler = {
promises: {
getAssignmentForUser: sinon.stub().resolves({
variant: 'default',
@@ -44,340 +63,342 @@ describe('RecurlyEventHandler', function () {
hasUserBeenAssignedToVariant: sinon.stub().resolves(false),
},
}),
},
})
})
)
ctx.RecurlyEventHandler = (await import(modulePath)).default
})
it('with new_subscription_notification - free trial', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with new_subscription_notification - free trial', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-started',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
this.planCode
ctx.planCode
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with new_subscription_notification - free trial with customerio integration enabled', async function () {
this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
it('with new_subscription_notification - free trial with customerio integration enabled', async function (ctx) {
ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-started',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': true,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
this.planCode
ctx.planCode
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('sends free trial onboarding email if user starting a trial', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('sends free trial onboarding email if user starting a trial', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.called(this.SubscriptionEmailHandler.sendTrialOnboardingEmail)
sinon.assert.called(ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail)
})
it('with new_subscription_notification - no free trial', async function () {
this.eventData.subscription.current_period_started_at = new Date(
it('with new_subscription_notification - no free trial', async function (ctx) {
ctx.eventData.subscription.current_period_started_at = new Date(
'2021-02-10 12:34:56'
)
this.eventData.subscription.current_period_ends_at = new Date(
ctx.eventData.subscription.current_period_ends_at = new Date(
'2021-02-17 12:34:56'
)
this.eventData.subscription.quantity = 3
ctx.eventData.subscription.quantity = 3
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-started',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 3,
is_trial: false,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
false
)
})
it('with updated_subscription_notification', async function () {
this.planCode = 'new-plan-code'
this.eventData.subscription.plan.plan_code = this.planCode
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with updated_subscription_notification', async function (ctx) {
ctx.planCode = 'new-plan-code'
ctx.eventData.subscription.plan.plan_code = ctx.planCode
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'updated_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-updated',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
this.planCode
ctx.planCode
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with updated_subscription_notification with customerio integration enabled', async function () {
this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
this.planCode = 'new-plan-code'
this.eventData.subscription.plan.plan_code = this.planCode
it('with updated_subscription_notification with customerio integration enabled', async function (ctx) {
ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
ctx.planCode = 'new-plan-code'
ctx.eventData.subscription.plan.plan_code = ctx.planCode
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'updated_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-updated',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': true,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
this.planCode
ctx.planCode
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with canceled_subscription_notification', async function () {
this.eventData.subscription.state = 'cancelled'
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with canceled_subscription_notification', async function (ctx) {
ctx.eventData.subscription.state = 'cancelled'
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'canceled_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-cancelled',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'cancelled'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with expired_subscription_notification', async function () {
this.eventData.subscription.state = 'expired'
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with expired_subscription_notification', async function (ctx) {
ctx.eventData.subscription.state = 'expired'
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'expired_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-expired',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
this.planCode
ctx.planCode
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'expired'
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUserInBackground,
this.userId,
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with renewed_subscription_notification', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with renewed_subscription_notification', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'renewed_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-renewed',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
})
it('with reactivated_account_notification', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with reactivated_account_notification', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'reactivated_account_notification',
this.eventData
ctx.eventData
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-reactivated',
{
plan_code: this.planCode,
plan_code: ctx.planCode,
quantity: 1,
has_ai_add_on: false,
subscriptionId: this.eventData.subscription.uuid,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
})
it('with paid_charge_invoice_notification', async function () {
it('with paid_charge_invoice_notification', async function (ctx) {
const invoice = {
invoice_number: 1234,
currency: 'USD',
@@ -390,18 +411,18 @@ describe('RecurlyEventHandler', function () {
collection_method: 'automatic',
subscription_ids: ['abcd1234', 'defa3214'],
}
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'paid_charge_invoice_notification',
{
account: {
account_code: this.userId,
account_code: ctx.userId,
},
invoice,
}
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-invoice-collected',
{
invoiceNumber: invoice.invoice_number,
@@ -417,12 +438,12 @@ describe('RecurlyEventHandler', function () {
)
})
it('with paid_charge_invoice_notification and total_in_cents 0', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with paid_charge_invoice_notification and total_in_cents 0', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'paid_charge_invoice_notification',
{
account: {
account_code: this.userId,
account_code: ctx.userId,
},
invoice: {
state: 'paid',
@@ -430,15 +451,15 @@ describe('RecurlyEventHandler', function () {
},
}
)
sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
})
it('with closed_invoice_notification', async function () {
await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with closed_invoice_notification', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'closed_invoice_notification',
{
account: {
account_code: this.userId,
account_code: ctx.userId,
},
invoice: {
state: 'collected',
@@ -447,18 +468,18 @@ describe('RecurlyEventHandler', function () {
}
)
sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUserInBackground,
this.userId,
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-invoice-collected'
)
})
it('with closed_invoice_notification and total_in_cents 0', function () {
this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
it('with closed_invoice_notification and total_in_cents 0', function (ctx) {
ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'closed_invoice_notification',
{
account: {
account_code: this.userId,
account_code: ctx.userId,
},
invoice: {
state: 'collected',
@@ -466,25 +487,25 @@ describe('RecurlyEventHandler', function () {
},
}
)
sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
})
it('nothing is called with invalid account code', function () {
this.eventData.account.account_code = 'foo_bar'
it('nothing is called with invalid account code', function (ctx) {
ctx.eventData.account.account_code = 'foo_bar'
this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
this.eventData
ctx.eventData
)
sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
sinon.assert.notCalled(
this.AnalyticsManager.setUserPropertyForUserInBackground
ctx.AnalyticsManager.setUserPropertyForUserInBackground
)
sinon.assert.notCalled(
this.AnalyticsManager.setUserPropertyForUserInBackground
ctx.AnalyticsManager.setUserPropertyForUserInBackground
)
sinon.assert.notCalled(
this.AnalyticsManager.setUserPropertyForUserInBackground
ctx.AnalyticsManager.setUserPropertyForUserInBackground
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js'
const modulePath =
'../../../../app/src/Features/Templates/TemplatesController.mjs'
describe('TemplatesController', function () {
beforeEach(async function (ctx) {
ctx.user_id = 'user-id'
vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
default: ProjectHelper,
}))
vi.doMock(
'../../../../app/src/Features/Authentication/AuthenticationController',
() => ({
default: (ctx.AuthenticationController = {
getLoggedInUserId: sinon.stub().returns(ctx.user_id),
}),
})
)
vi.doMock(
'../../../../app/src/Features/Templates/TemplatesManager',
() => ({
default: (ctx.TemplatesManager = {
promises: { createProjectFromV1Template: sinon.stub() },
}),
})
)
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: (ctx.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'default' }),
},
}),
})
)
ctx.TemplatesController = (await import(modulePath)).default
ctx.next = sinon.stub()
ctx.req = {
body: {
brandVariationId: 'brand-variation-id',
compiler: 'compiler',
mainFile: 'main-file',
templateId: 'template-id',
templateName: 'template-name',
templateVersionId: 'template-version-id',
},
session: {
templateData: 'template-data',
user: {
_id: ctx.user_id,
},
},
}
return (ctx.res = { redirect: sinon.stub() })
})
describe('createProjectFromV1Template', function () {
describe('on success', function () {
beforeEach(function (ctx) {
ctx.project = { _id: 'project-id' }
ctx.TemplatesManager.promises.createProjectFromV1Template.resolves(
ctx.project
)
return ctx.TemplatesController.createProjectFromV1Template(
ctx.req,
ctx.res,
ctx.next
)
})
it('should call TemplatesManager', function (ctx) {
return ctx.TemplatesManager.promises.createProjectFromV1Template.should.have.been.calledWithMatch(
'brand-variation-id',
'compiler',
'main-file',
'template-id',
'template-name',
'template-version-id',
'user-id'
)
})
it('should redirect to project', function (ctx) {
return ctx.res.redirect.should.have.been.calledWith(
'/project/project-id'
)
})
it('should delete session', function (ctx) {
return expect(ctx.req.session.templateData).to.be.undefined
})
})
describe('on error', function () {
beforeEach(function (ctx) {
ctx.TemplatesManager.promises.createProjectFromV1Template.rejects(
'error'
)
return ctx.TemplatesController.createProjectFromV1Template(
ctx.req,
ctx.res,
ctx.next
)
})
it('should call next with error', function (ctx) {
return ctx.next.should.have.been.calledWithMatch(
sinon.match.instanceOf(Error)
)
})
it('should not redirect', function (ctx) {
return ctx.res.redirect.called.should.equal(false)
})
})
})
})

View File

@@ -1,108 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const ProjectHelper = require('../../../../app/src/Features/Project/ProjectHelper')
const modulePath = '../../../../app/src/Features/Templates/TemplatesController'
describe('TemplatesController', function () {
beforeEach(function () {
this.user_id = 'user-id'
this.TemplatesController = SandboxedModule.require(modulePath, {
requires: {
'../Project/ProjectHelper': ProjectHelper,
'../Authentication/AuthenticationController':
(this.AuthenticationController = {
getLoggedInUserId: sinon.stub().returns(this.user_id),
}),
'./TemplatesManager': (this.TemplatesManager = {
promises: { createProjectFromV1Template: sinon.stub() },
}),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'default' }),
},
}),
},
})
this.next = sinon.stub()
this.req = {
body: {
brandVariationId: 'brand-variation-id',
compiler: 'compiler',
mainFile: 'main-file',
templateId: 'template-id',
templateName: 'template-name',
templateVersionId: 'template-version-id',
},
session: {
templateData: 'template-data',
user: {
_id: this.user_id,
},
},
}
return (this.res = { redirect: sinon.stub() })
})
describe('createProjectFromV1Template', function () {
describe('on success', function () {
beforeEach(function () {
this.project = { _id: 'project-id' }
this.TemplatesManager.promises.createProjectFromV1Template.resolves(
this.project
)
return this.TemplatesController.createProjectFromV1Template(
this.req,
this.res,
this.next
)
})
it('should call TemplatesManager', function () {
return this.TemplatesManager.promises.createProjectFromV1Template.should.have.been.calledWithMatch(
'brand-variation-id',
'compiler',
'main-file',
'template-id',
'template-name',
'template-version-id',
'user-id'
)
})
it('should redirect to project', function () {
return this.res.redirect.should.have.been.calledWith(
'/project/project-id'
)
})
it('should delete session', function () {
return expect(this.req.session.templateData).to.be.undefined
})
})
describe('on error', function () {
beforeEach(function () {
this.TemplatesManager.promises.createProjectFromV1Template.rejects(
'error'
)
return this.TemplatesController.createProjectFromV1Template(
this.req,
this.res,
this.next
)
})
it('should call next with error', function () {
return this.next.should.have.been.calledWithMatch(
sinon.match.instanceOf(Error)
)
})
it('should not redirect', function () {
return this.res.redirect.called.should.equal(false)
})
})
})
})