diff --git a/services/web/app/src/Features/Compile/ClsiCookieManager.mjs b/services/web/app/src/Features/Compile/ClsiCookieManager.mjs index 4eac9b1adc..cc21fcd09a 100644 --- a/services/web/app/src/Features/Compile/ClsiCookieManager.mjs +++ b/services/web/app/src/Features/Compile/ClsiCookieManager.mjs @@ -1,15 +1,15 @@ -const { URL, URLSearchParams } = require('url') -const OError = require('@overleaf/o-error') -const Settings = require('@overleaf/settings') -const { +import { URL, URLSearchParams } from 'node:url' +import OError from '@overleaf/o-error' +import Settings from '@overleaf/settings' +import { fetchNothing, fetchStringWithResponse, RequestFailedError, -} = require('@overleaf/fetch-utils') -const RedisWrapper = require('../../infrastructure/RedisWrapper') -const Cookie = require('cookie') -const logger = require('@overleaf/logger') -const Metrics = require('@overleaf/metrics') +} from '@overleaf/fetch-utils' +import RedisWrapper from '../../infrastructure/RedisWrapper.js' +import Cookie from 'cookie' +import logger from '@overleaf/logger' +import Metrics from '@overleaf/metrics' const clsiCookiesEnabled = (Settings.clsiCookie?.key ?? '') !== '' @@ -256,4 +256,4 @@ const ClsiCookieManagerFactory = function (backendGroup) { return cookieManager } -module.exports = ClsiCookieManagerFactory +export default ClsiCookieManagerFactory diff --git a/services/web/app/src/Features/Compile/ClsiFormatChecker.mjs b/services/web/app/src/Features/Compile/ClsiFormatChecker.mjs index 74b2d55725..31898d5896 100644 --- a/services/web/app/src/Features/Compile/ClsiFormatChecker.mjs +++ b/services/web/app/src/Features/Compile/ClsiFormatChecker.mjs @@ -1,5 +1,5 @@ -const _ = require('lodash') -const settings = require('@overleaf/settings') +import _ from 'lodash' +import settings from '@overleaf/settings' const ClsiFormatChecker = { checkRecoursesForProblems(resources) { @@ -56,4 +56,4 @@ const ClsiFormatChecker = { }, } -module.exports = ClsiFormatChecker +export default ClsiFormatChecker diff --git a/services/web/app/src/Features/Compile/ClsiStateManager.mjs b/services/web/app/src/Features/Compile/ClsiStateManager.mjs index e1914b9248..f90b484d6b 100644 --- a/services/web/app/src/Features/Compile/ClsiStateManager.mjs +++ b/services/web/app/src/Features/Compile/ClsiStateManager.mjs @@ -1,8 +1,3 @@ -/* eslint-disable - n/handle-callback-err, - max-len, - no-unused-vars, -*/ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -13,11 +8,8 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ClsiStateManager -const Settings = require('@overleaf/settings') -const logger = require('@overleaf/logger') -const crypto = require('crypto') -const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +import crypto from 'node:crypto' +import ProjectEntityHandler from '../Project/ProjectEntityHandler.js' // The "state" of a project is a hash of the relevant attributes in the // project object in this case we only need the rootFolder. @@ -36,7 +28,7 @@ const ProjectEntityHandler = require('../Project/ProjectEntityHandler') const buildState = s => crypto.createHash('sha1').update(s, 'utf8').digest('hex') -module.exports = ClsiStateManager = { +export default { computeHash(project, options) { const { docs, files } = ProjectEntityHandler.getAllEntitiesFromProject(project) diff --git a/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs b/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs index 7b6ef48ad0..1f81635fce 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs @@ -1,7 +1,7 @@ -const { promisify, callbackify } = require('util') -const UserGetter = require('../User/UserGetter') -const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') -const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs') +import { promisify, callbackify } from 'node:util' +import UserGetter from '../User/UserGetter.js' +import UserMembershipsHandler from '../UserMembership/UserMembershipsHandler.js' +import UserMembershipEntityConfigs from '../UserMembership/UserMembershipEntityConfigs.js' async function getCurrentAffiliations(userId) { const fullEmails = await UserGetter.promises.getUserFullEmails(userId) @@ -98,4 +98,4 @@ InstitutionsGetter.promises = { getManagedInstitutions: promisify(InstitutionsGetter.getManagedInstitutions), } -module.exports = InstitutionsGetter +export default InstitutionsGetter diff --git a/services/web/app/src/Features/Institutions/InstitutionsManager.mjs b/services/web/app/src/Features/Institutions/InstitutionsManager.mjs index 6acf0787e3..605df1acbb 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsManager.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsManager.mjs @@ -1,21 +1,20 @@ -const { - callbackifyAll, - promiseMapWithLimit, -} = require('@overleaf/promise-utils') -const { ObjectId } = require('mongodb-legacy') -const Settings = require('@overleaf/settings') -const logger = require('@overleaf/logger') -const { fetchJson } = require('@overleaf/fetch-utils') -const InstitutionsAPI = require('./InstitutionsAPI') -const FeaturesUpdater = require('../Subscription/FeaturesUpdater') -const FeaturesHelper = require('../Subscription/FeaturesHelper') -const UserGetter = require('../User/UserGetter') -const NotificationsBuilder = require('../Notifications/NotificationsBuilder') -const NotificationsHandler = require('../Notifications/NotificationsHandler') -const SubscriptionLocator = require('../Subscription/SubscriptionLocator') -const { Institution } = require('../../models/Institution') -const { Subscription } = require('../../models/Subscription') -const OError = require('@overleaf/o-error') +import { callbackifyAll, promiseMapWithLimit } from '@overleaf/promise-utils' +import mongodb from 'mongodb-legacy' +import Settings from '@overleaf/settings' +import logger from '@overleaf/logger' +import { fetchJson } from '@overleaf/fetch-utils' +import InstitutionsAPI from './InstitutionsAPI.js' +import FeaturesUpdater from '../Subscription/FeaturesUpdater.js' +import FeaturesHelper from '../Subscription/FeaturesHelper.js' +import UserGetter from '../User/UserGetter.js' +import NotificationsBuilder from '../Notifications/NotificationsBuilder.js' +import NotificationsHandler from '../Notifications/NotificationsHandler.js' +import SubscriptionLocator from '../Subscription/SubscriptionLocator.js' +import { Institution } from '../../models/Institution.js' +import { Subscription } from '../../models/Subscription.js' +import OError from '@overleaf/o-error' + +const { ObjectId } = mongodb const ASYNC_LIMIT = parseInt(process.env.ASYNC_LIMIT, 10) || 5 @@ -355,7 +354,7 @@ async function affiliateUserByReversedHostname(user, reversedHostname) { ) } -module.exports = { +export default { ...callbackifyAll(InstitutionsManager), promises: InstitutionsManager, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionEmailHandler.mjs b/services/web/app/src/Features/Subscription/SubscriptionEmailHandler.mjs index 8002845c8e..8ccad2dd29 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionEmailHandler.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionEmailHandler.mjs @@ -1,6 +1,6 @@ import EmailHandler from '../Email/EmailHandler.js' import UserGetter from '../User/UserGetter.js' -import './SubscriptionEmailBuilder.js' +import './SubscriptionEmailBuilder.mjs' import PlansLocator from './PlansLocator.js' import Settings from '@overleaf/settings' diff --git a/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs b/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs index b4c7b902c1..3073f46fb3 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs @@ -1,4 +1,4 @@ -import dateformat from 'dateformat'; +import dateformat from 'dateformat' function formatDateTime(date) { if (!date) { @@ -17,4 +17,4 @@ function formatDate(date) { export default { formatDateTime, formatDate, -}; +} diff --git a/services/web/test/unit/src/Compile/ClsiCookieManager.test.mjs b/services/web/test/unit/src/Compile/ClsiCookieManager.test.mjs index 6198300111..92acf57e30 100644 --- a/services/web/test/unit/src/Compile/ClsiCookieManager.test.mjs +++ b/services/web/test/unit/src/Compile/ClsiCookieManager.test.mjs @@ -1,24 +1,23 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/Compile/ClsiCookieManager.js' -const SandboxedModule = require('sandboxed-module') +import sinon from 'sinon' +import { beforeEach, describe, expect, it, vi } from 'vitest' +const modulePath = '../../../../app/src/Features/Compile/ClsiCookieManager.mjs' describe('ClsiCookieManager', function () { - beforeEach(function () { - this.redis = { + beforeEach(async function (ctx) { + ctx.redis = { auth() {}, del: sinon.stub(), get: sinon.stub(), setex: sinon.stub().resolves(), } - this.project_id = '123423431321-proj-id' - this.user_id = 'abc-user-id' - this.fetchUtils = { + ctx.project_id = '123423431321-proj-id' + ctx.user_id = 'abc-user-id' + ctx.fetchUtils = { fetchNothing: sinon.stub().returns(Promise.resolve()), fetchStringWithResponse: sinon.stub().returns(Promise.resolve()), } - this.metrics = { inc: sinon.stub() } - this.settings = { + ctx.metrics = { inc: sinon.stub() } + ctx.settings = { redis: { web: 'redis.something', }, @@ -33,314 +32,319 @@ describe('ClsiCookieManager', function () { key: 'coooookie', }, } - this.requires = { - '../../infrastructure/RedisWrapper': (this.RedisWrapper = { - client: () => this.redis, + vi.doMock('../../../../app/src/infrastructure/RedisWrapper', () => ({ + default: (ctx.RedisWrapper = { + client: () => ctx.redis, }), - '@overleaf/settings': this.settings, - '@overleaf/fetch-utils': this.fetchUtils, - '@overleaf/metrics': this.metrics, - } - this.ClsiCookieManager = SandboxedModule.require(modulePath, { - requires: this.requires, - })() + })) + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + vi.doMock('@overleaf/fetch-utils', () => ctx.fetchUtils) + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.metrics, + })) + + ctx.ClsiCookieManager = (await import(modulePath)).default() }) describe('getServerId', function () { - it('should call get for the key', async function () { - this.redis.get.resolves('clsi-7') - const serverId = await this.ClsiCookieManager.promises.getServerId( - this.project_id, - this.user_id, + it('should call get for the key', async function (ctx) { + ctx.redis.get.resolves('clsi-7') + const serverId = await ctx.ClsiCookieManager.promises.getServerId( + ctx.project_id, + ctx.user_id, '', 'n2d' ) - this.redis.get - .calledWith(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + ctx.redis.get + .calledWith(`clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`) .should.equal(true) serverId.should.equal('clsi-7') }) - it('should fallback to old key', async function () { - this.redis.get - .withArgs(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + it('should fallback to old key', async function (ctx) { + ctx.redis.get + .withArgs(`clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`) .resolves(null) - this.redis.get - .withArgs(`clsiserver:${this.project_id}:${this.user_id}`) + ctx.redis.get + .withArgs(`clsiserver:${ctx.project_id}:${ctx.user_id}`) .resolves('clsi-7') - const serverId = await this.ClsiCookieManager.promises.getServerId( - this.project_id, - this.user_id, + const serverId = await ctx.ClsiCookieManager.promises.getServerId( + ctx.project_id, + ctx.user_id, '', 'n2d' ) - this.redis.get - .calledWith(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + ctx.redis.get + .calledWith(`clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`) .should.equal(true) - this.redis.get - .calledWith(`clsiserver:${this.project_id}:${this.user_id}`) + ctx.redis.get + .calledWith(`clsiserver:${ctx.project_id}:${ctx.user_id}`) .should.equal(true) serverId.should.equal('clsi-7') }) - it('should _populateServerIdViaRequest if no key is found', async function () { - this.ClsiCookieManager.promises._populateServerIdViaRequest = sinon + it('should _populateServerIdViaRequest if no key is found', async function (ctx) { + ctx.ClsiCookieManager.promises._populateServerIdViaRequest = sinon .stub() .resolves() - this.redis.get.resolves(null) - await this.ClsiCookieManager.promises.getServerId( - this.project_id, - this.user_id, + ctx.redis.get.resolves(null) + await ctx.ClsiCookieManager.promises.getServerId( + ctx.project_id, + ctx.user_id, '' ) - this.ClsiCookieManager.promises._populateServerIdViaRequest - .calledWith(this.project_id, this.user_id) + ctx.ClsiCookieManager.promises._populateServerIdViaRequest + .calledWith(ctx.project_id, ctx.user_id) .should.equal(true) }) - it('should _populateServerIdViaRequest if no key is blank', async function () { - this.ClsiCookieManager.promises._populateServerIdViaRequest = sinon + it('should _populateServerIdViaRequest if no key is blank', async function (ctx) { + ctx.ClsiCookieManager.promises._populateServerIdViaRequest = sinon .stub() .resolves(null) - this.redis.get.resolves('') - await this.ClsiCookieManager.promises.getServerId( - this.project_id, - this.user_id, + ctx.redis.get.resolves('') + await ctx.ClsiCookieManager.promises.getServerId( + ctx.project_id, + ctx.user_id, '', 'n2d' ) - this.ClsiCookieManager.promises._populateServerIdViaRequest - .calledWith(this.project_id, this.user_id) + ctx.ClsiCookieManager.promises._populateServerIdViaRequest + .calledWith(ctx.project_id, ctx.user_id) .should.equal(true) }) }) describe('_populateServerIdViaRequest', function () { - beforeEach(function () { - this.clsiServerId = 'server-id' - this.ClsiCookieManager.promises.setServerId = sinon.stub().resolves() + beforeEach(function (ctx) { + ctx.clsiServerId = 'server-id' + ctx.ClsiCookieManager.promises.setServerId = sinon.stub().resolves() }) describe('with a server id in the response', function () { - beforeEach(function () { - this.response = { + beforeEach(function (ctx) { + ctx.response = { headers: { 'set-cookie': [ - `${this.settings.clsiCookie.key}=${this.clsiServerId}`, + `${ctx.settings.clsiCookie.key}=${ctx.clsiServerId}`, ], }, } - this.fetchUtils.fetchNothing.returns(this.response) + ctx.fetchUtils.fetchNothing.returns(ctx.response) }) - it('should make a request to the clsi', async function () { - await this.ClsiCookieManager.promises._populateServerIdViaRequest( - this.project_id, - this.user_id, + it('should make a request to the clsi', async function (ctx) { + await ctx.ClsiCookieManager.promises._populateServerIdViaRequest( + ctx.project_id, + ctx.user_id, 'standard', 'n2d' ) - const args = this.ClsiCookieManager.promises.setServerId.args[0] - args[0].should.equal(this.project_id) - args[1].should.equal(this.user_id) + const args = ctx.ClsiCookieManager.promises.setServerId.args[0] + args[0].should.equal(ctx.project_id) + args[1].should.equal(ctx.user_id) args[2].should.equal('standard') args[3].should.equal('n2d') - args[4].should.deep.equal(this.clsiServerId) + args[4].should.deep.equal(ctx.clsiServerId) }) - it('should return the server id', async function () { + it('should return the server id', async function (ctx) { const serverId = - await this.ClsiCookieManager.promises._populateServerIdViaRequest( - this.project_id, - this.user_id, + await ctx.ClsiCookieManager.promises._populateServerIdViaRequest( + ctx.project_id, + ctx.user_id, '', 'n2d' ) - serverId.should.equal(this.clsiServerId) + serverId.should.equal(ctx.clsiServerId) }) }) describe('without a server id in the response', function () { - beforeEach(function () { - this.response = { headers: {} } - this.fetchUtils.fetchNothing.returns(this.response) + beforeEach(function (ctx) { + ctx.response = { headers: {} } + ctx.fetchUtils.fetchNothing.returns(ctx.response) }) - it('should not set the server id there is no server id in the response', async function () { - this.ClsiCookieManager._parseServerIdFromResponse = sinon + it('should not set the server id there is no server id in the response', async function (ctx) { + ctx.ClsiCookieManager._parseServerIdFromResponse = sinon .stub() .returns(null) - await this.ClsiCookieManager.promises.setServerId( - this.project_id, - this.user_id, + await ctx.ClsiCookieManager.promises.setServerId( + ctx.project_id, + ctx.user_id, 'standard', 'n2d', - this.clsiServerId, + ctx.clsiServerId, null ) - this.redis.setex.called.should.equal(false) + ctx.redis.setex.called.should.equal(false) }) }) }) describe('clearServerId', function () { - it('should clear both keys', async function () { - await this.ClsiCookieManager.promises.clearServerId( - this.project_id, - this.user_id, + it('should clear both keys', async function (ctx) { + await ctx.ClsiCookieManager.promises.clearServerId( + ctx.project_id, + ctx.user_id, 'n2d' ) - this.redis.del.should.have.been.calledWith( - `clsiserver:n2d:${this.project_id}:${this.user_id}`, - `clsiserver:${this.project_id}:${this.user_id}` + ctx.redis.del.should.have.been.calledWith( + `clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`, + `clsiserver:${ctx.project_id}:${ctx.user_id}` ) }) }) describe('setServerId', function () { - beforeEach(function () { - this.clsiServerId = 'server-id' - this.ClsiCookieManager._parseServerIdFromResponse = sinon + beforeEach(function (ctx) { + ctx.clsiServerId = 'server-id' + ctx.ClsiCookieManager._parseServerIdFromResponse = sinon .stub() .returns('clsi-8') }) - it('should set the server id with a ttl', async function () { - await this.ClsiCookieManager.promises.setServerId( - this.project_id, - this.user_id, + it('should set the server id with a ttl', async function (ctx) { + await ctx.ClsiCookieManager.promises.setServerId( + ctx.project_id, + ctx.user_id, 'standard', 'n2d', - this.clsiServerId, + ctx.clsiServerId, null ) - this.redis.setex.should.have.been.calledWith( - `clsiserver:n2d:${this.project_id}:${this.user_id}`, - this.settings.clsiCookie.ttlInSeconds, - this.clsiServerId + ctx.redis.setex.should.have.been.calledWith( + `clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`, + ctx.settings.clsiCookie.ttlInSeconds, + ctx.clsiServerId ) }) - it('should set the server id with the regular ttl for reg instance', async function () { - this.clsiServerId = 'clsi-reg-8' - await this.ClsiCookieManager.promises.setServerId( - this.project_id, - this.user_id, + it('should set the server id with the regular ttl for reg instance', async function (ctx) { + ctx.clsiServerId = 'clsi-reg-8' + await ctx.ClsiCookieManager.promises.setServerId( + ctx.project_id, + ctx.user_id, 'standard', 'n2d', - this.clsiServerId, + ctx.clsiServerId, null ) - expect(this.redis.setex).to.have.been.calledWith( - `clsiserver:n2d:${this.project_id}:${this.user_id}`, - this.settings.clsiCookie.ttlInSecondsRegular, - this.clsiServerId + expect(ctx.redis.setex).to.have.been.calledWith( + `clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`, + ctx.settings.clsiCookie.ttlInSecondsRegular, + ctx.clsiServerId ) }) - it('should not set the server id if clsiCookies are not enabled', async function () { - delete this.settings.clsiCookie.key - this.ClsiCookieManager2 = SandboxedModule.require(modulePath, { - globals: { - console, - }, - requires: this.requires, - })() - await this.ClsiCookieManager2.promises.setServerId( - this.project_id, - this.user_id, - 'standard', - 'n2d', - this.clsiServerId, - null - ) - this.redis.setex.called.should.equal(false) + describe('when clsiCookies are not enabled', function (ctx) { + let oldKey + beforeEach(async function (ctx) { + oldKey = ctx.settings.clsiCookie.key + delete ctx.settings.clsiCookie.key + vi.resetModules() + ctx.ClsiCookieManager2 = (await import(modulePath)).default() + }) + afterEach(function (ctx) { + ctx.settings.clsiCookie.key = oldKey + }) + + it('should not set the server id if clsiCookies are not enabled', async function (ctx) { + await ctx.ClsiCookieManager2.promises.setServerId( + ctx.project_id, + ctx.user_id, + 'standard', + 'n2d', + ctx.clsiServerId, + null + ) + ctx.redis.setex.called.should.equal(false) + }) }) - it('should also set in the secondary if secondary redis is enabled', async function () { - this.redis_secondary = { setex: sinon.stub().resolves() } - this.settings.redis.clsi_cookie_secondary = {} - this.RedisWrapper.client = sinon.stub() - this.RedisWrapper.client.withArgs('clsi_cookie').returns(this.redis) - this.RedisWrapper.client + it('should also set in the secondary if secondary redis is enabled', async function (ctx) { + ctx.redis_secondary = { setex: sinon.stub().resolves() } + ctx.settings.redis.clsi_cookie_secondary = {} + ctx.RedisWrapper.client = sinon.stub() + ctx.RedisWrapper.client.withArgs('clsi_cookie').returns(ctx.redis) + ctx.RedisWrapper.client .withArgs('clsi_cookie_secondary') - .returns(this.redis_secondary) - this.ClsiCookieManager2 = SandboxedModule.require(modulePath, { - globals: { - console, - }, - requires: this.requires, - })() - this.ClsiCookieManager2._parseServerIdFromResponse = sinon + .returns(ctx.redis_secondary) + vi.resetModules() + ctx.ClsiCookieManager2 = (await import(modulePath)).default() + ctx.ClsiCookieManager2._parseServerIdFromResponse = sinon .stub() .returns('clsi-8') - await this.ClsiCookieManager2.promises.setServerId( - this.project_id, - this.user_id, + await ctx.ClsiCookieManager2.promises.setServerId( + ctx.project_id, + ctx.user_id, 'standard', 'n2d', - this.clsiServerId, + ctx.clsiServerId, null ) - this.redis_secondary.setex.should.have.been.calledWith( - `clsiserver:n2d:${this.project_id}:${this.user_id}`, - this.settings.clsiCookie.ttlInSeconds, - this.clsiServerId + ctx.redis_secondary.setex.should.have.been.calledWith( + `clsiserver:n2d:${ctx.project_id}:${ctx.user_id}`, + ctx.settings.clsiCookie.ttlInSeconds, + ctx.clsiServerId ) }) describe('checkIsLoadSheddingEvent', function () { - beforeEach(function () { - this.fetchUtils.fetchStringWithResponse.reset() - this.call = async () => { - await this.ClsiCookieManager.promises.setServerId( - this.project_id, - this.user_id, + beforeEach(function (ctx) { + ctx.fetchUtils.fetchStringWithResponse.reset() + ctx.call = async () => { + await ctx.ClsiCookieManager.promises.setServerId( + ctx.project_id, + ctx.user_id, 'standard', 'n2d', - this.clsiServerId, + ctx.clsiServerId, 'previous-clsi-server-id' ) expect( - this.fetchUtils.fetchStringWithResponse + ctx.fetchUtils.fetchStringWithResponse ).to.have.been.calledWith( - `${this.settings.apis.clsi.url}/instance-state?clsiserverid=previous-clsi-server-id&compileGroup=standard&compileBackendClass=n2d`, + `${ctx.settings.apis.clsi.url}/instance-state?clsiserverid=previous-clsi-server-id&compileGroup=standard&compileBackendClass=n2d`, { method: 'GET', signal: sinon.match.instanceOf(AbortSignal) } ) } }) - it('should report "load-shedding" when previous is UP', async function () { - this.fetchUtils.fetchStringWithResponse.resolves({ + it('should report "load-shedding" when previous is UP', async function (ctx) { + ctx.fetchUtils.fetchStringWithResponse.resolves({ response: { status: 200 }, body: 'previous-clsi-server-id,UP\n', }) - await this.call() - expect(this.metrics.inc).to.have.been.calledWith( + await ctx.call() + expect(ctx.metrics.inc).to.have.been.calledWith( 'clsi-lb-switch-backend', 1, { status: 'load-shedding' } ) }) - it('should report "cycle" when other is UP', async function () { - this.fetchUtils.fetchStringWithResponse.resolves({ + it('should report "cycle" when other is UP', async function (ctx) { + ctx.fetchUtils.fetchStringWithResponse.resolves({ response: { status: 200 }, body: 'other-clsi-server-id,UP\n', }) - await this.call() - expect(this.metrics.inc).to.have.been.calledWith( + await ctx.call() + expect(ctx.metrics.inc).to.have.been.calledWith( 'clsi-lb-switch-backend', 1, { status: 'cycle' } ) }) - it('should report "cycle" when previous is 404', async function () { - this.fetchUtils.fetchStringWithResponse.resolves({ + it('should report "cycle" when previous is 404', async function (ctx) { + ctx.fetchUtils.fetchStringWithResponse.resolves({ response: { status: 404 }, }) - await this.call() - expect(this.metrics.inc).to.have.been.calledWith( + await ctx.call() + expect(ctx.metrics.inc).to.have.been.calledWith( 'clsi-lb-switch-backend', 1, { status: 'cycle' } diff --git a/services/web/test/unit/src/Compile/ClsiFormatChecker.test.mjs b/services/web/test/unit/src/Compile/ClsiFormatChecker.test.mjs index 52eb029223..732c2a6270 100644 --- a/services/web/test/unit/src/Compile/ClsiFormatChecker.test.mjs +++ b/services/web/test/unit/src/Compile/ClsiFormatChecker.test.mjs @@ -1,23 +1,23 @@ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/Compile/ClsiFormatChecker.js' -const SandboxedModule = require('sandboxed-module') +import { vi, expect } from 'vitest' +import sinon from 'sinon' + +const modulePath = '../../../../app/src/Features/Compile/ClsiFormatChecker.mjs' describe('ClsiFormatChecker', function () { - beforeEach(function () { - this.ClsiFormatChecker = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/settings': (this.settings = { - compileBodySizeLimitMb: 5, - }), - }, - }) - return (this.project_id = 'project-id') + beforeEach(async function (ctx) { + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { + compileBodySizeLimitMb: 5, + }), + })) + + ctx.ClsiFormatChecker = (await import(modulePath)).default + ctx.project_id = 'project-id' }) describe('checkRecoursesForProblems', function () { - beforeEach(function () { - return (this.resources = [ + beforeEach(function (ctx) { + ctx.resources = [ { path: 'main.tex', content: 'stuff', @@ -28,65 +28,59 @@ describe('ClsiFormatChecker', function () { }, { path: 'stuff/image/image.png', - url: `http:somewhere.com/project/${this.project_id}/file/1234124321312`, + url: `http:somewhere.com/project/${ctx.project_id}/file/1234124321312`, modified: 'more stuff', }, - ]) + ] }) - it('should call _checkDocsAreUnderSizeLimit and _checkForConflictingPaths', async function () { - this.ClsiFormatChecker._checkForConflictingPaths = sinon + it('should call _checkDocsAreUnderSizeLimit and _checkForConflictingPaths', async function (ctx) { + ctx.ClsiFormatChecker._checkForConflictingPaths = sinon .stub() .returns(null) - this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon .stub() .returns(null) - this.ClsiFormatChecker.checkRecoursesForProblems(this.resources) - this.ClsiFormatChecker._checkForConflictingPaths.called.should.equal(true) - this.ClsiFormatChecker._checkDocsAreUnderSizeLimit.called.should.equal( + await ctx.ClsiFormatChecker.checkRecoursesForProblems(ctx.resources) + ctx.ClsiFormatChecker._checkForConflictingPaths.called.should.equal(true) + ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit.called.should.equal( true ) }) - it('should remove undefined errors', async function () { - this.ClsiFormatChecker._checkForConflictingPaths = sinon - .stub() - .returns([]) - this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + it('should remove undefined errors', async function (ctx) { + ctx.ClsiFormatChecker._checkForConflictingPaths = sinon.stub().returns([]) + ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon .stub() .returns({}) - const problems = this.ClsiFormatChecker.checkRecoursesForProblems( - this.resources + const problems = await ctx.ClsiFormatChecker.checkRecoursesForProblems( + ctx.resources ) expect(problems).to.not.exist }) - it('should keep populated arrays', async function () { - this.ClsiFormatChecker._checkForConflictingPaths = sinon + it('should keep populated arrays', async function (ctx) { + ctx.ClsiFormatChecker._checkForConflictingPaths = sinon .stub() .returns([{ path: 'somewhere/main.tex' }]) - this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon .stub() .returns({}) - const problems = this.ClsiFormatChecker.checkRecoursesForProblems( - this.resources + const problems = await ctx.ClsiFormatChecker.checkRecoursesForProblems( + ctx.resources ) problems.conflictedPaths[0].path.should.equal('somewhere/main.tex') expect(problems.sizeCheck).to.not.exist }) - it('should keep populated object', async function () { - this.ClsiFormatChecker._checkForConflictingPaths = sinon - .stub() - .returns([]) - this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon - .stub() - .returns({ - resources: [{ 'a.tex': 'a.tex' }, { 'b.tex': 'b.tex' }], - totalSize: 1000000, - }) - const problems = this.ClsiFormatChecker.checkRecoursesForProblems( - this.resources + it('should keep populated object', async function (ctx) { + ctx.ClsiFormatChecker._checkForConflictingPaths = sinon.stub().returns([]) + ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon.stub().returns({ + resources: [{ 'a.tex': 'a.tex' }, { 'b.tex': 'b.tex' }], + totalSize: 1000000, + }) + const problems = await ctx.ClsiFormatChecker.checkRecoursesForProblems( + ctx.resources ) problems.sizeCheck.resources.length.should.equal(2) problems.sizeCheck.totalSize.should.equal(1000000) @@ -94,93 +88,91 @@ describe('ClsiFormatChecker', function () { }) describe('_checkForConflictingPaths', function () { - beforeEach(function () { - this.resources.push({ + beforeEach(function (ctx) { + ctx.resources.push({ path: 'chapters/chapter1.tex', content: 'other stuff', }) - return this.resources.push({ + ctx.resources.push({ path: 'chapters.tex', content: 'other stuff', }) }) - it('should flag up when a nested file has folder with same subpath as file elsewhere', async function () { - this.resources.push({ + it('should flag up when a nested file has folder with same subpath as file elsewhere', async function (ctx) { + ctx.resources.push({ path: 'stuff/image', url: 'http://somwhere.com', }) const conflictPathErrors = - this.ClsiFormatChecker._checkForConflictingPaths(this.resources) + await ctx.ClsiFormatChecker._checkForConflictingPaths(ctx.resources) conflictPathErrors.length.should.equal(1) conflictPathErrors[0].path.should.equal('stuff/image') }) - it('should flag up when a root level file has folder with same subpath as file elsewhere', async function () { - this.resources.push({ + it('should flag up when a root level file has folder with same subpath as file elsewhere', async function (ctx) { + ctx.resources.push({ path: 'stuff', content: 'other stuff', }) const conflictPathErrors = - this.ClsiFormatChecker._checkForConflictingPaths(this.resources) + await ctx.ClsiFormatChecker._checkForConflictingPaths(ctx.resources) conflictPathErrors.length.should.equal(1) conflictPathErrors[0].path.should.equal('stuff') }) - it('should not flag up when the file is a substring of a path', async function () { - this.resources.push({ + it('should not flag up when the file is a substring of a path', async function (ctx) { + ctx.resources.push({ path: 'stuf', content: 'other stuff', }) const conflictPathErrors = - this.ClsiFormatChecker._checkForConflictingPaths(this.resources) + await ctx.ClsiFormatChecker._checkForConflictingPaths(ctx.resources) conflictPathErrors.length.should.equal(0) }) }) describe('_checkDocsAreUnderSizeLimit', function () { - it('should error when there is more than 5mb of data', async function () { - this.resources.push({ + it('should error when there is more than 5mb of data', async function (ctx) { + ctx.resources.push({ path: 'massive.tex', content: 'hello world'.repeat(833333), // over 5mb limit }) - while (this.resources.length < 20) { - this.resources.push({ + while (ctx.resources.length < 20) { + ctx.resources.push({ path: 'chapters/chapter1.tex', url: 'http://somwhere.com', }) } - const sizeError = this.ClsiFormatChecker._checkDocsAreUnderSizeLimit( - this.resources - ) + const sizeError = + await ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit(ctx.resources) sizeError.totalSize.should.equal(16 + 833333 * 11) // 16 is for earlier resources sizeError.resources.length.should.equal(10) sizeError.resources[0].path.should.equal('massive.tex') sizeError.resources[0].size.should.equal(833333 * 11) }) - it('should return nothing when project is correct size', async function () { - this.resources.push({ + it('should return nothing when project is correct size', async function (ctx) { + ctx.resources.push({ path: 'massive.tex', content: 'x'.repeat(2 * 1000 * 1000), }) - while (this.resources.length < 20) { - this.resources.push({ + while (ctx.resources.length < 20) { + ctx.resources.push({ path: 'chapters/chapter1.tex', url: 'http://somwhere.com', }) } - const sizeError = this.ClsiFormatChecker._checkDocsAreUnderSizeLimit( - this.resources - ) + const sizeError = + await ctx.ClsiFormatChecker._checkDocsAreUnderSizeLimit(ctx.resources) expect(sizeError).to.not.exist }) }) diff --git a/services/web/test/unit/src/Compile/ClsiStateManager.test.mjs b/services/web/test/unit/src/Compile/ClsiStateManager.test.mjs index c11240efa0..73fee5ffc7 100644 --- a/services/web/test/unit/src/Compile/ClsiStateManager.test.mjs +++ b/services/web/test/unit/src/Compile/ClsiStateManager.test.mjs @@ -1,42 +1,34 @@ -/* eslint-disable - n/handle-callback-err, - max-len, - no-return-assign, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const sinon = require('sinon') -const { expect } = require('chai') -const modulePath = '../../../../app/src/Features/Compile/ClsiStateManager.js' -const SandboxedModule = require('sandboxed-module') +import { vi, expect } from 'vitest' +import sinon from 'sinon' + +const modulePath = '../../../../app/src/Features/Compile/ClsiStateManager.mjs' describe('ClsiStateManager', function () { - beforeEach(function () { - this.ClsiStateManager = SandboxedModule.require(modulePath, { - requires: { - '@overleaf/settings': (this.settings = {}), - '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), - }, - }) - this.project = 'project' - this.options = { draft: true, isAutoCompile: false } - return (this.callback = sinon.stub()) + beforeEach(async function (ctx) { + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: (ctx.ProjectEntityHandler = {}), + }) + ) + + ctx.ClsiStateManager = (await import(modulePath)).default + ctx.project = 'project' + ctx.options = { draft: true, isAutoCompile: false } + ctx.callback = sinon.stub() }) describe('computeHash', function () { - beforeEach(function () { - this.docs = [ + beforeEach(function (ctx) { + ctx.docs = [ { path: '/main.tex', doc: { _id: 'doc-id-1' } }, { path: '/folder/sub.tex', doc: { _id: 'doc-id-2' } }, ] - this.files = [ + ctx.files = [ { path: '/figure.pdf', file: { _id: 'file-id-1', rev: 123, created: 'aaaaaa' }, @@ -46,158 +38,155 @@ describe('ClsiStateManager', function () { file: { _id: 'file-id-2', rev: 456, created: 'bbbbbb' }, }, ] - this.ProjectEntityHandler.getAllEntitiesFromProject = sinon + ctx.ProjectEntityHandler.getAllEntitiesFromProject = sinon .stub() - .returns({ docs: this.docs, files: this.files }) - this.hash0 = this.ClsiStateManager.computeHash(this.project, this.options) + .returns({ docs: ctx.docs, files: ctx.files }) + ctx.hash0 = ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) }) describe('with a sample project', function () { beforeEach(function () {}) - it('should return a hash value', function () { + it('should return a hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) ).to.equal('21b1ab73aa3892bec452baf8ffa0956179e1880f') }) }) describe('when the files and docs are in a different order', function () { - beforeEach(function () { - ;[this.docs[0], this.docs[1]] = Array.from([this.docs[1], this.docs[0]]) - ;[this.files[0], this.files[1]] = Array.from([ - this.files[1], - this.files[0], - ]) + beforeEach(function (ctx) { + ;[ctx.docs[0], ctx.docs[1]] = [ctx.docs[1], ctx.docs[0]] + ;[ctx.files[0], ctx.files[1]] = [ctx.files[1], ctx.files[0]] }) - it('should return the same hash value', function () { + it('should return the same hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).to.equal(ctx.hash0) }) }) describe('when a doc is renamed', function () { - beforeEach(function () { - this.docs[0].path = '/new.tex' + beforeEach(function (ctx) { + ctx.docs[0].path = '/new.tex' }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when a file is renamed', function () { - beforeEach(function () { - this.files[0].path = '/newfigure.pdf' + beforeEach(function (ctx) { + ctx.files[0].path = '/newfigure.pdf' }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when a doc is added', function () { - beforeEach(function () { - this.docs.push({ path: '/newdoc.tex', doc: { _id: 'newdoc-id' } }) + beforeEach(function (ctx) { + ctx.docs.push({ path: '/newdoc.tex', doc: { _id: 'newdoc-id' } }) }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when a file is added', function () { - beforeEach(function () { - this.files.push({ + beforeEach(function (ctx) { + ctx.files.push({ path: '/newfile.tex', file: { _id: 'newfile-id', rev: 123 }, }) }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when a doc is removed', function () { - beforeEach(function () { - this.docs.pop() + beforeEach(function (ctx) { + ctx.docs.pop() }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when a file is removed', function () { - beforeEach(function () { - this.files.pop() + beforeEach(function (ctx) { + ctx.files.pop() }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe("when a file's revision is updated", function () { - beforeEach(function () { - this.files[0].file.rev++ + beforeEach(function (ctx) { + ctx.files[0].file.rev++ }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe("when a file's date is updated", function () { - beforeEach(function () { - this.files[0].file.created = 'zzzzzz' + beforeEach(function (ctx) { + ctx.files[0].file.created = 'zzzzzz' }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when the compile options are changed', function () { - beforeEach(function () { - this.options.draft = !this.options.draft + beforeEach(function (ctx) { + ctx.options.draft = !ctx.options.draft }) - it('should return a different hash value', function () { + it('should return a different hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).not.to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).not.to.equal(ctx.hash0) }) }) describe('when the isAutoCompile option is changed', function () { - beforeEach(function () { - this.options.isAutoCompile = !this.options.isAutoCompile + beforeEach(function (ctx) { + ctx.options.isAutoCompile = !ctx.options.isAutoCompile }) - it('should return the same hash value', function () { + it('should return the same hash value', function (ctx) { expect( - this.ClsiStateManager.computeHash(this.project, this.options) - ).to.equal(this.hash0) + ctx.ClsiStateManager.computeHash(ctx.project, ctx.options) + ).to.equal(ctx.hash0) }) }) }) diff --git a/services/web/test/unit/src/Institutions/InstitutionsGetter.test.mjs b/services/web/test/unit/src/Institutions/InstitutionsGetter.test.mjs index 76a445c3a7..43215ee0f0 100644 --- a/services/web/test/unit/src/Institutions/InstitutionsGetter.test.mjs +++ b/services/web/test/unit/src/Institutions/InstitutionsGetter.test.mjs @@ -1,31 +1,39 @@ -const SandboxedModule = require('sandboxed-module') -const { expect } = require('chai') -const sinon = require('sinon') -const modulePath = require('path').join( - __dirname, - '../../../../app/src/Features/Institutions/InstitutionsGetter.js' -) +import { vi, expect } from 'vitest' +import sinon from 'sinon' +const modulePath = + '../../../../app/src/Features/Institutions/InstitutionsGetter.mjs' describe('InstitutionsGetter', function () { - beforeEach(function () { - this.UserGetter = { + beforeEach(async function (ctx) { + ctx.UserGetter = { getUserFullEmails: sinon.stub(), promises: { getUserFullEmails: sinon.stub(), }, } - this.InstitutionsGetter = SandboxedModule.require(modulePath, { - requires: { - '../User/UserGetter': this.UserGetter, - '../UserMembership/UserMembershipsHandler': - (this.UserMembershipsHandler = {}), - '../UserMembership/UserMembershipEntityConfigs': - (this.UserMembershipEntityConfigs = {}), - }, - }) - this.userId = '12345abcde' - this.confirmedAffiliation = { + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipsHandler', + () => ({ + default: (ctx.UserMembershipsHandler = {}), + }) + ) + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs', + () => ({ + default: (ctx.UserMembershipEntityConfigs = {}), + }) + ) + + ctx.InstitutionsGetter = (await import(modulePath)).default + + ctx.userId = '12345abcde' + ctx.confirmedAffiliation = { confirmedAt: new Date(), affiliation: { institution: { id: 456, confirmed: true }, @@ -33,7 +41,7 @@ describe('InstitutionsGetter', function () { pastReconfirmDate: false, }, } - this.confirmedAffiliationPastReconfirmation = { + ctx.confirmedAffiliationPastReconfirmation = { confirmedAt: new Date('2000-01-01'), affiliation: { institution: { id: 135, confirmed: true }, @@ -41,7 +49,7 @@ describe('InstitutionsGetter', function () { pastReconfirmDate: true, }, } - this.licencedAffiliation = { + ctx.licencedAffiliation = { confirmedAt: new Date(), affiliation: { licence: 'pro_plus', @@ -50,7 +58,7 @@ describe('InstitutionsGetter', function () { pastReconfirmDate: false, }, } - this.licencedAffiliationPastReconfirmation = { + ctx.licencedAffiliationPastReconfirmation = { confirmedAt: new Date('2000-01-01'), affiliation: { licence: 'pro_plus', @@ -59,7 +67,7 @@ describe('InstitutionsGetter', function () { pastReconfirmDate: true, }, } - this.unconfirmedEmailLicensedAffiliation = { + ctx.unconfirmedEmailLicensedAffiliation = { confirmedAt: null, affiliation: { licence: 'pro_plus', @@ -71,7 +79,7 @@ describe('InstitutionsGetter', function () { }, }, } - this.unconfirmedDomainLicensedAffiliation = { + ctx.unconfirmedDomainLicensedAffiliation = { confirmedAt: new Date(), affiliation: { licence: 'pro_plus', @@ -83,7 +91,7 @@ describe('InstitutionsGetter', function () { }, }, } - this.userEmails = [ + ctx.userEmails = [ { confirmedAt: null, affiliation: { @@ -95,9 +103,9 @@ describe('InstitutionsGetter', function () { }, }, }, - this.confirmedAffiliation, - this.confirmedAffiliation, - this.confirmedAffiliationPastReconfirmation, + ctx.confirmedAffiliation, + ctx.confirmedAffiliation, + ctx.confirmedAffiliationPastReconfirmation, { confirmedAt: new Date(), affiliation: null, @@ -124,41 +132,41 @@ describe('InstitutionsGetter', function () { }, }, ] - this.fullEmailCollection = [ - this.licencedAffiliation, - this.licencedAffiliation, - this.licencedAffiliationPastReconfirmation, - this.confirmedAffiliation, - this.confirmedAffiliationPastReconfirmation, - this.unconfirmedDomainLicensedAffiliation, - this.unconfirmedEmailLicensedAffiliation, + ctx.fullEmailCollection = [ + ctx.licencedAffiliation, + ctx.licencedAffiliation, + ctx.licencedAffiliationPastReconfirmation, + ctx.confirmedAffiliation, + ctx.confirmedAffiliationPastReconfirmation, + ctx.unconfirmedDomainLicensedAffiliation, + ctx.unconfirmedEmailLicensedAffiliation, ] }) describe('getCurrentInstitutionIds', function () { - it('filters unconfirmed affiliations, those past reconfirmation, and returns only 1 result per institution', async function () { - this.UserGetter.promises.getUserFullEmails.resolves(this.userEmails) + it('filters unconfirmed affiliations, those past reconfirmation, and returns only 1 result per institution', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves(ctx.userEmails) const institutions = - await this.InstitutionsGetter.promises.getCurrentInstitutionIds( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentInstitutionIds( + ctx.userId ) expect(institutions.length).to.equal(1) expect(institutions[0]).to.equal(456) }) - it('handles empty response', async function () { - this.UserGetter.promises.getUserFullEmails.resolves([]) + it('handles empty response', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([]) const institutions = - await this.InstitutionsGetter.promises.getCurrentInstitutionIds( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentInstitutionIds( + ctx.userId ) expect(institutions).to.deep.equal([]) }) - it('handles errors', async function () { - this.UserGetter.promises.getUserFullEmails.rejects(new Error('oops')) + it('handles errors', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.rejects(new Error('oops')) let e try { - await this.InstitutionsGetter.promises.getCurrentInstitutionIds( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentInstitutionIds( + ctx.userId ) } catch (error) { e = error @@ -168,37 +176,37 @@ describe('InstitutionsGetter', function () { }) describe('getCurrentAndPastAffiliationIds', function () { - it('filters unconfirmed affiliations, preserves those past reconfirmation, and returns only 1 result per institution', async function () { - this.UserGetter.promises.getUserFullEmails.resolves( - this.fullEmailCollection + it('filters unconfirmed affiliations, preserves those past reconfirmation, and returns only 1 result per institution', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves( + ctx.fullEmailCollection ) const institutions = - await this.InstitutionsGetter.promises.getCurrentAndPastAffiliationIds( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentAndPastAffiliationIds( + ctx.userId ) expect(institutions).to.deep.equal([777, 888, 456, 135]) }) - it('handles empty response', async function () { - this.UserGetter.promises.getUserFullEmails.resolves([]) + it('handles empty response', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([]) const institutions = - await this.InstitutionsGetter.promises.getCurrentInstitutionIds( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentInstitutionIds( + ctx.userId ) expect(institutions).to.deep.equal([]) }) }) describe('getCurrentInstitutionsWithLicence', function () { - it('returns one result per institution and filters out affiliations without license', async function () { - this.UserGetter.promises.getUserFullEmails.resolves( - this.fullEmailCollection + it('returns one result per institution and filters out affiliations without license', async function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves( + ctx.fullEmailCollection ) const institutions = - await this.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence( - this.userId + await ctx.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence( + ctx.userId ) expect(institutions.map(institution => institution.id)).to.deep.equal([ - this.licencedAffiliation.affiliation.institution.id, + ctx.licencedAffiliation.affiliation.institution.id, ]) }) }) diff --git a/services/web/test/unit/src/Institutions/InstitutionsManager.test.mjs b/services/web/test/unit/src/Institutions/InstitutionsManager.test.mjs index 4811962eba..77a0dea3f1 100644 --- a/services/web/test/unit/src/Institutions/InstitutionsManager.test.mjs +++ b/services/web/test/unit/src/Institutions/InstitutionsManager.test.mjs @@ -1,270 +1,311 @@ -const SandboxedModule = require('sandboxed-module') -const path = require('path') -const sinon = require('sinon') -const { expect } = require('chai') -const { ObjectId } = require('mongodb-legacy') +import { vi, expect } from 'vitest' +import path from 'path' +import sinon from 'sinon' +import mongodb from 'mongodb-legacy' +import Features from '../../../../app/src/infrastructure/Features.js' const modulePath = path.join( - __dirname, + import.meta.dirname, '../../../../app/src/Features/Institutions/InstitutionsManager' ) -const Features = require('../../../../app/src/infrastructure/Features') + +const { ObjectId } = mongodb describe('InstitutionsManager', function () { - beforeEach(function () { - this.institutionId = 123 - this.user = {} + beforeEach(async function (ctx) { + ctx.institutionId = 123 + ctx.user = {} const lapsedUser = { _id: '657300a08a14461b3d1aac3e', features: {}, } - this.users = [ + ctx.users = [ lapsedUser, { _id: '657300a08a14461b3d1aac3f', features: {} }, { _id: '657300a08a14461b3d1aac40', features: {} }, { _id: '657300a08a14461b3d1aac41', features: {} }, ] - this.ssoUsers = [ + ctx.ssoUsers = [ { _id: '657300a08a14461b3d1aac3f', - samlIdentifiers: [{ providerId: this.institutionId.toString() }], + samlIdentifiers: [{ providerId: ctx.institutionId.toString() }], }, { _id: '657300a08a14461b3d1aac40', samlIdentifiers: [ { - providerId: this.institutionId.toString(), + providerId: ctx.institutionId.toString(), hasEntitlement: true, }, ], }, { _id: '657300a08a14461b3d1aac3e', - samlIdentifiers: [{ providerId: this.institutionId.toString() }], + samlIdentifiers: [{ providerId: ctx.institutionId.toString() }], hasEntitlement: true, }, ] - this.UserGetter = { + ctx.UserGetter = { promises: { - getUser: sinon.stub().resolves(this.user), - getUsers: sinon.stub().resolves(this.users), + getUser: sinon.stub().resolves(ctx.user), + getUsers: sinon.stub().resolves(ctx.users), getUsersByAnyConfirmedEmail: sinon.stub().resolves(), - getSsoUsersAtInstitution: (this.getSsoUsersAtInstitution = sinon + getSsoUsersAtInstitution: (ctx.getSsoUsersAtInstitution = sinon .stub() - .resolves(this.ssoUsers)), + .resolves(ctx.ssoUsers)), }, } - this.creator = { create: sinon.stub().resolves() } - this.NotificationsBuilder = { + ctx.creator = { create: sinon.stub().resolves() } + ctx.NotificationsBuilder = { promises: { - featuresUpgradedByAffiliation: sinon.stub().returns(this.creator), - redundantPersonalSubscription: sinon.stub().returns(this.creator), + featuresUpgradedByAffiliation: sinon.stub().returns(ctx.creator), + redundantPersonalSubscription: sinon.stub().returns(ctx.creator), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves(), }, } - this.institutionWithV1Data = { name: 'Wombat University' } - this.institution = { - fetchV1DataPromise: sinon.stub().resolves(this.institutionWithV1Data), + ctx.institutionWithV1Data = { name: 'Wombat University' } + ctx.institution = { + fetchV1DataPromise: sinon.stub().resolves(ctx.institutionWithV1Data), } - this.InstitutionModel = { + ctx.InstitutionModel = { Institution: { findOne: sinon.stub().returns({ - exec: sinon.stub().resolves(this.institution), + exec: sinon.stub().resolves(ctx.institution), }), }, } - this.subscriptionExec = sinon.stub().resolves() + ctx.subscriptionExec = sinon.stub().resolves() const SubscriptionModel = { Subscription: { find: () => ({ populate: () => ({ - exec: this.subscriptionExec, + exec: ctx.subscriptionExec, }), }), }, } - this.Mongo = { + ctx.Mongo = { ObjectId, } - this.v1Counts = { - user_ids: this.users.map(user => user._id), + ctx.v1Counts = { + user_ids: ctx.users.map(user => user._id), current_users_count: 3, lapsed_user_ids: [lapsedUser._id], entitled_via_sso: 1, // 2 entitled, but 1 lapsed with_confirmed_email: 2, // 1 non entitled SSO + 1 email user } - this.InstitutionsManager = SandboxedModule.require(modulePath, { - requires: { - './InstitutionsAPI': { + vi.doMock( + '../../../../app/src/Features/Institutions/InstitutionsAPI', + () => ({ + default: { promises: { - addAffiliation: (this.addAffiliationPromise = sinon + addAffiliation: (ctx.addAffiliationPromise = sinon .stub() .resolves()), - getInstitutionAffiliations: - (this.getInstitutionAffiliationsPromise = sinon - .stub() - .resolves(this.affiliations)), + getInstitutionAffiliations: (ctx.getInstitutionAffiliationsPromise = + sinon.stub().resolves(ctx.affiliations)), getConfirmedInstitutionAffiliations: - (this.getConfirmedInstitutionAffiliationsPromise = sinon + (ctx.getConfirmedInstitutionAffiliationsPromise = sinon .stub() - .resolves(this.affiliations)), + .resolves(ctx.affiliations)), getInstitutionAffiliationsCounts: - (this.getInstitutionAffiliationsCounts = sinon + (ctx.getInstitutionAffiliationsCounts = sinon .stub() - .resolves(this.v1Counts)), + .resolves(ctx.v1Counts)), }, }, - '../Subscription/FeaturesUpdater': { + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/FeaturesUpdater', + () => ({ + default: { promises: { - refreshFeatures: (this.refreshFeaturesPromise = sinon + refreshFeatures: (ctx.refreshFeaturesPromise = sinon .stub() .resolves()), }, }, - '../Subscription/FeaturesHelper': { - isFeatureSetBetter: (this.isFeatureSetBetter = sinon.stub()), - }, - '../User/UserGetter': this.UserGetter, - '../Notifications/NotificationsBuilder': this.NotificationsBuilder, - '../Subscription/SubscriptionLocator': this.SubscriptionLocator, - '../../models/Institution': this.InstitutionModel, - '../../models/Subscription': SubscriptionModel, - 'mongodb-legacy': this.Mongo, - '@overleaf/settings': { - features: { professional: { 'test-feature': true } }, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/FeaturesHelper', + () => ({ + default: { + isFeatureSetBetter: (ctx.isFeatureSetBetter = sinon.stub()), }, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/models/Institution', + () => ctx.InstitutionModel + ) + + vi.doMock( + '../../../../app/src/models/Subscription', + () => SubscriptionModel + ) + + vi.doMock('mongodb-legacy', () => ({ + default: ctx.Mongo, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: { + features: { professional: { 'test-feature': true } }, }, - }) + })) + + ctx.InstitutionsManager = (await import(modulePath)).default }) describe('refreshInstitutionUsers', function () { - beforeEach(function () { - this.user1Id = '123abc123abc123abc123abc' - this.user2Id = '456def456def456def456def' - this.user3Id = '789abd789abd789abd789abd' - this.user4Id = '321cba321cba321cba321cba' - this.affiliations = [ - { user_id: this.user1Id }, - { user_id: this.user2Id }, - { user_id: this.user3Id }, - { user_id: this.user4Id }, + beforeEach(function (ctx) { + ctx.user1Id = '123abc123abc123abc123abc' + ctx.user2Id = '456def456def456def456def' + ctx.user3Id = '789abd789abd789abd789abd' + ctx.user4Id = '321cba321cba321cba321cba' + ctx.affiliations = [ + { user_id: ctx.user1Id }, + { user_id: ctx.user2Id }, + { user_id: ctx.user3Id }, + { user_id: ctx.user4Id }, ] - this.user1 = { _id: this.user1Id } - this.user2 = { _id: this.user2Id } - this.user3 = { _id: this.user3Id } - this.user4 = { _id: this.user4Id } + ctx.user1 = { _id: ctx.user1Id } + ctx.user2 = { _id: ctx.user2Id } + ctx.user3 = { _id: ctx.user3Id } + ctx.user4 = { _id: ctx.user4Id } - this.UserGetter.promises.getUser - .withArgs(new ObjectId(this.user1Id)) - .resolves(this.user1) - this.UserGetter.promises.getUser - .withArgs(new ObjectId(this.user2Id)) - .resolves(this.user2) - this.UserGetter.promises.getUser - .withArgs(new ObjectId(this.user3Id)) - .resolves(this.user3) - this.UserGetter.promises.getUser - .withArgs(new ObjectId(this.user4Id)) - .resolves(this.user4) + ctx.UserGetter.promises.getUser + .withArgs(new ObjectId(ctx.user1Id)) + .resolves(ctx.user1) + ctx.UserGetter.promises.getUser + .withArgs(new ObjectId(ctx.user2Id)) + .resolves(ctx.user2) + ctx.UserGetter.promises.getUser + .withArgs(new ObjectId(ctx.user3Id)) + .resolves(ctx.user3) + ctx.UserGetter.promises.getUser + .withArgs(new ObjectId(ctx.user4Id)) + .resolves(ctx.user4) - this.SubscriptionLocator.promises.getUsersSubscription - .withArgs(this.user2) + ctx.SubscriptionLocator.promises.getUsersSubscription + .withArgs(ctx.user2) .resolves({ planCode: 'pro', groupPlan: false, }) - this.SubscriptionLocator.promises.getUsersSubscription - .withArgs(this.user3) + ctx.SubscriptionLocator.promises.getUsersSubscription + .withArgs(ctx.user3) .resolves({ planCode: 'collaborator_free_trial_7_days', groupPlan: false, }) - this.SubscriptionLocator.promises.getUsersSubscription - .withArgs(this.user4) + ctx.SubscriptionLocator.promises.getUsersSubscription + .withArgs(ctx.user4) .resolves({ planCode: 'collaborator-annual', groupPlan: true, }) - this.refreshFeaturesPromise.resolves({ + ctx.refreshFeaturesPromise.resolves({ newFeatures: {}, featuresChanged: false, }) - this.refreshFeaturesPromise - .withArgs(new ObjectId(this.user1Id)) + ctx.refreshFeaturesPromise + .withArgs(new ObjectId(ctx.user1Id)) .resolves({ newFeatures: {}, featuresChanged: true }) - this.getInstitutionAffiliationsPromise.resolves(this.affiliations) - this.getConfirmedInstitutionAffiliationsPromise.resolves( - this.affiliations - ) + ctx.getInstitutionAffiliationsPromise.resolves(ctx.affiliations) + ctx.getConfirmedInstitutionAffiliationsPromise.resolves(ctx.affiliations) }) - it('refresh all users Features', async function () { - await this.InstitutionsManager.promises.refreshInstitutionUsers( - this.institutionId, + it('refresh all users Features', async function (ctx) { + await ctx.InstitutionsManager.promises.refreshInstitutionUsers( + ctx.institutionId, false ) - sinon.assert.callCount(this.refreshFeaturesPromise, 4) + sinon.assert.callCount(ctx.refreshFeaturesPromise, 4) // expect no notifications sinon.assert.notCalled( - this.NotificationsBuilder.promises.featuresUpgradedByAffiliation + ctx.NotificationsBuilder.promises.featuresUpgradedByAffiliation ) sinon.assert.notCalled( - this.NotificationsBuilder.promises.redundantPersonalSubscription + ctx.NotificationsBuilder.promises.redundantPersonalSubscription ) }) - it('notifies users if their features have been upgraded', async function () { - await this.InstitutionsManager.promises.refreshInstitutionUsers( - this.institutionId, + it('notifies users if their features have been upgraded', async function (ctx) { + await ctx.InstitutionsManager.promises.refreshInstitutionUsers( + ctx.institutionId, true ) sinon.assert.calledOnce( - this.NotificationsBuilder.promises.featuresUpgradedByAffiliation + ctx.NotificationsBuilder.promises.featuresUpgradedByAffiliation ) sinon.assert.calledWith( - this.NotificationsBuilder.promises.featuresUpgradedByAffiliation, - this.affiliations[0], - this.user1 + ctx.NotificationsBuilder.promises.featuresUpgradedByAffiliation, + ctx.affiliations[0], + ctx.user1 ) }) - it('notifies users if they have a subscription, or a trial subscription, that should be cancelled', async function () { - await this.InstitutionsManager.promises.refreshInstitutionUsers( - this.institutionId, + it('notifies users if they have a subscription, or a trial subscription, that should be cancelled', async function (ctx) { + await ctx.InstitutionsManager.promises.refreshInstitutionUsers( + ctx.institutionId, true ) sinon.assert.calledTwice( - this.NotificationsBuilder.promises.redundantPersonalSubscription + ctx.NotificationsBuilder.promises.redundantPersonalSubscription ) sinon.assert.calledWith( - this.NotificationsBuilder.promises.redundantPersonalSubscription, - this.affiliations[1], - this.user2 + ctx.NotificationsBuilder.promises.redundantPersonalSubscription, + ctx.affiliations[1], + ctx.user2 ) sinon.assert.calledWith( - this.NotificationsBuilder.promises.redundantPersonalSubscription, - this.affiliations[2], - this.user3 + ctx.NotificationsBuilder.promises.redundantPersonalSubscription, + ctx.affiliations[2], + ctx.user3 ) }) }) describe('checkInstitutionUsers', function () { - it('returns entitled/not, sso/not, lapsed/current, and pro counts', async function () { + it('returns entitled/not, sso/not, lapsed/current, and pro counts', async function (ctx) { if (Features.hasFeature('saas')) { - this.isFeatureSetBetter.returns(true) + ctx.isFeatureSetBetter.returns(true) const usersSummary = - await this.InstitutionsManager.promises.checkInstitutionUsers( - this.institutionId + await ctx.InstitutionsManager.promises.checkInstitutionUsers( + ctx.institutionId ) expect(usersSummary).to.deep.equal({ emailUsers: { @@ -300,13 +341,13 @@ describe('InstitutionsManager', function () { } }) - it('includes withConfirmedEmailMismatch when v1 and v2 counts do not add up', async function () { + it('includes withConfirmedEmailMismatch when v1 and v2 counts do not add up', async function (ctx) { if (Features.hasFeature('saas')) { - this.isFeatureSetBetter.returns(true) - this.v1Counts.with_confirmed_email = 100 + ctx.isFeatureSetBetter.returns(true) + ctx.v1Counts.with_confirmed_email = 100 const usersSummary = - await this.InstitutionsManager.promises.checkInstitutionUsers( - this.institutionId + await ctx.InstitutionsManager.promises.checkInstitutionUsers( + ctx.institutionId ) expect(usersSummary).to.deep.equal({ emailUsers: { @@ -350,116 +391,115 @@ describe('InstitutionsManager', function () { }) describe('getInstitutionUsersSubscriptions', function () { - it('returns all institution users subscriptions', async function () { + it('returns all institution users subscriptions', async function (ctx) { const stubbedUsers = [ { user_id: '123abc123abc123abc123abc' }, { user_id: '456def456def456def456def' }, { user_id: '789def789def789def789def' }, ] - this.getInstitutionAffiliationsPromise.resolves(stubbedUsers) - await this.InstitutionsManager.promises.getInstitutionUsersSubscriptions( - this.institutionId + ctx.getInstitutionAffiliationsPromise.resolves(stubbedUsers) + await ctx.InstitutionsManager.promises.getInstitutionUsersSubscriptions( + ctx.institutionId ) - sinon.assert.calledOnce(this.subscriptionExec) + sinon.assert.calledOnce(ctx.subscriptionExec) }) }) describe('addAffiliations', function () { - beforeEach(function () { - this.host = 'mit.edu'.split('').reverse().join('') - this.stubbedUser1 = { + beforeEach(function (ctx) { + ctx.host = 'mit.edu'.split('').reverse().join('') + ctx.stubbedUser1 = { _id: '6573014d8a14461b3d1aac3f', name: 'bob', email: 'hello@world.com', emails: [ - { email: 'stubb1@mit.edu', reversedHostname: this.host }, + { email: 'stubb1@mit.edu', reversedHostname: ctx.host }, { email: 'test@test.com', reversedHostname: 'test.com' }, - { email: 'another@mit.edu', reversedHostname: this.host }, + { email: 'another@mit.edu', reversedHostname: ctx.host }, ], } - this.stubbedUser1DecoratedEmails = [ + ctx.stubbedUser1DecoratedEmails = [ { email: 'stubb1@mit.edu', - reversedHostname: this.host, + reversedHostname: ctx.host, samlIdentifier: { hasEntitlement: false }, }, { email: 'test@test.com', reversedHostname: 'test.com' }, { email: 'another@mit.edu', - reversedHostname: this.host, + reversedHostname: ctx.host, samlIdentifier: { hasEntitlement: true }, }, ] - this.stubbedUser2 = { + ctx.stubbedUser2 = { _id: '6573014d8a14461b3d1aac40', name: 'test', email: 'hello2@world.com', - emails: [{ email: 'subb2@mit.edu', reversedHostname: this.host }], + emails: [{ email: 'subb2@mit.edu', reversedHostname: ctx.host }], } - this.stubbedUser2DecoratedEmails = [ + ctx.stubbedUser2DecoratedEmails = [ { email: 'subb2@mit.edu', - reversedHostname: this.host, + reversedHostname: ctx.host, }, ] - this.getInstitutionUsersByHostname = sinon.stub().resolves([ + ctx.getInstitutionUsersByHostname = sinon.stub().resolves([ { - _id: this.stubbedUser1._id, - emails: this.stubbedUser1DecoratedEmails, + _id: ctx.stubbedUser1._id, + emails: ctx.stubbedUser1DecoratedEmails, }, { - _id: this.stubbedUser2._id, - emails: this.stubbedUser2DecoratedEmails, + _id: ctx.stubbedUser2._id, + emails: ctx.stubbedUser2DecoratedEmails, }, ]) - this.UserGetter.promises.getInstitutionUsersByHostname = - this.getInstitutionUsersByHostname + ctx.UserGetter.promises.getInstitutionUsersByHostname = + ctx.getInstitutionUsersByHostname }) describe('affiliateUsers', function () { - it('should add affiliations for matching users', async function () { - await this.InstitutionsManager.promises.affiliateUsers('mit.edu') + it('should add affiliations for matching users', async function (ctx) { + await ctx.InstitutionsManager.promises.affiliateUsers('mit.edu') - this.getInstitutionUsersByHostname.calledOnce.should.equal(true) - this.addAffiliationPromise.calledThrice.should.equal(true) - this.addAffiliationPromise + ctx.getInstitutionUsersByHostname.calledOnce.should.equal(true) + ctx.addAffiliationPromise.calledThrice.should.equal(true) + ctx.addAffiliationPromise .calledWithMatch( - this.stubbedUser1._id, - this.stubbedUser1.emails[0].email, + ctx.stubbedUser1._id, + ctx.stubbedUser1.emails[0].email, { entitlement: false } ) .should.equal(true) - this.addAffiliationPromise + ctx.addAffiliationPromise .calledWithMatch( - this.stubbedUser1._id, - this.stubbedUser1.emails[2].email, + ctx.stubbedUser1._id, + ctx.stubbedUser1.emails[2].email, { entitlement: true } ) .should.equal(true) - this.addAffiliationPromise + ctx.addAffiliationPromise .calledWithMatch( - this.stubbedUser2._id, - this.stubbedUser2.emails[0].email, + ctx.stubbedUser2._id, + ctx.stubbedUser2.emails[0].email, { entitlement: undefined } ) .should.equal(true) - this.refreshFeaturesPromise - .calledWith(this.stubbedUser1._id) + ctx.refreshFeaturesPromise + .calledWith(ctx.stubbedUser1._id) .should.equal(true) - this.refreshFeaturesPromise - .calledWith(this.stubbedUser2._id) + ctx.refreshFeaturesPromise + .calledWith(ctx.stubbedUser2._id) .should.equal(true) - this.refreshFeaturesPromise.should.have.been.calledTwice + ctx.refreshFeaturesPromise.should.have.been.calledTwice }) - it('should return errors if last affiliation cannot be added', async function () { - this.addAffiliationPromise.onCall(2).rejects() - await expect( - this.InstitutionsManager.promises.affiliateUsers('mit.edu') - ).to.be.rejected + it('should return errors if last affiliation cannot be added', async function (ctx) { + ctx.addAffiliationPromise.onCall(2).rejects() + await expect(ctx.InstitutionsManager.promises.affiliateUsers('mit.edu')) + .to.be.rejected - this.getInstitutionUsersByHostname.calledOnce.should.equal(true) + ctx.getInstitutionUsersByHostname.calledOnce.should.equal(true) }) }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionEmailHandler.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionEmailHandler.test.mjs index c3fca396a9..d3208ba933 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionEmailHandler.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionEmailHandler.test.mjs @@ -1,69 +1,77 @@ -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') -const { expect } = require('chai') +import { vi, expect } from 'vitest' +import sinon from 'sinon' const modulePath = '../../../../app/src/Features/Subscription/SubscriptionEmailHandler' describe('SubscriptionEmailHandler', function () { - beforeEach(function () { - this.userId = '123456789abcde' - this.email = 'test@test.com' + beforeEach(async function (ctx) { + ctx.userId = '123456789abcde' + ctx.email = 'test@test.com' - this.SubscriptionEmailHandler = SandboxedModule.require(modulePath, { - requires: { - '../Email/EmailHandler': (this.EmailHandler = { - promises: { - sendEmail: sinon.stub().resolves({}), - }, + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: (ctx.EmailHandler = { + promises: { + sendEmail: sinon.stub().resolves({}), + }, + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { + promises: { + getUser: sinon + .stub() + .resolves({ _id: ctx.userId, email: 'test@test.com' }), + }, + }), + })) + + vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({ + default: (ctx.PlansLocator = { + findLocalPlanInSettings: sinon.stub().returns({ + name: 'foo', + features: { collaborators: 42 }, }), - '../User/UserGetter': (this.UserGetter = { - promises: { - getUser: sinon - .stub() - .resolves({ _id: this.userId, email: 'test@test.com' }), - }, - }), - './PlansLocator': (this.PlansLocator = { - findLocalPlanInSettings: sinon.stub().returns({ - name: 'foo', - features: { collaborators: 42 }, - }), - }), - '@overleaf/settings': (this.Settings = { - enableOnboardingEmails: true, - }), - }, - }) + }), + })) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.Settings = { + enableOnboardingEmails: true, + }), + })) + + ctx.SubscriptionEmailHandler = (await import(modulePath)).default }) describe('when onboarding emails are disabled', function () { - beforeEach(function () { - this.Settings.enableOnboardingEmails = false + beforeEach(function (ctx) { + ctx.Settings.enableOnboardingEmails = false }) - it('does not send a trial onboarding email', async function () { - await this.SubscriptionEmailHandler.sendTrialOnboardingEmail( - this.userId, + it('does not send a trial onboarding email', async function (ctx) { + await ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail( + ctx.userId, 'foo-plan-code' ) - expect(this.EmailHandler.promises.sendEmail).to.not.have.been.called + expect(ctx.EmailHandler.promises.sendEmail).to.not.have.been.called }) }) describe('when onboarding emails are enabled', function () { - it('sends trial onboarding email', async function () { - await this.SubscriptionEmailHandler.sendTrialOnboardingEmail( - this.userId, + it('sends trial onboarding email', async function (ctx) { + await ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail( + ctx.userId, 'foo-plan-code' ) - expect(this.PlansLocator.findLocalPlanInSettings).to.have.been.calledWith( + expect(ctx.PlansLocator.findLocalPlanInSettings).to.have.been.calledWith( 'foo-plan-code' ) - expect(this.EmailHandler.promises.sendEmail.lastCall.args).to.deep.equal([ + expect(ctx.EmailHandler.promises.sendEmail.lastCall.args).to.deep.equal([ 'trialOnboarding', { - to: this.email, - sendingUser_id: this.userId, + to: ctx.email, + sendingUser_id: ctx.userId, planName: 'foo', features: { collaborators: 42 }, }, diff --git a/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs index 0ee781d54f..d7a618c795 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs @@ -1,7 +1,5 @@ -const chai = require('chai') -const SubscriptionFormatters = require('../../../../app/src/Features/Subscription/SubscriptionFormatters') - -const { expect } = chai +import { expect } from 'vitest' +import SubscriptionFormatters from '../../../../app/src/Features/Subscription/SubscriptionFormatters.mjs' describe('SubscriptionFormatters', function () { describe('formatDateTime', function () {