Features ESM conversion

GitOrigin-RevId: d659326723a90ac0789f4f7acc7f00aa9eaa63e2
This commit is contained in:
Andrew Rumble
2025-10-16 09:15:03 +01:00
committed by Copybot
parent e909995ce0
commit 3d3be18f57
14 changed files with 715 additions and 685 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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'

View File

@@ -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,
};
}

View File

@@ -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' }

View File

@@ -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
})
})

View File

@@ -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)
})
})
})

View File

@@ -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,
])
})
})

View File

@@ -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)
})
})
})

View File

@@ -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 },
},

View File

@@ -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 () {