Files
overleaf-cep/services/web/test/unit/src/User/UserGetter.test.mjs
Jimmy Domagala-Tang 72b7524094 Revert "Revert "Adding More Ai Quota Tiers"" (#32433)
* Revert "Revert "Adding More Ai Quota Tiers (#32128)" (#32431)"

This reverts commit f0ea19b418da6096c84b42024aea643807347649.

* fix: dont block workbench from users who have access via WF

* fix: simplify workbench access rules and ensure wf premium users can use workbench

GitOrigin-RevId: 7927248eadd906d7f802d55fa5c6bc7df162b141
2026-03-27 09:05:21 +00:00

1356 lines
45 KiB
JavaScript

import { vi, expect } from 'vitest'
import mongodb from 'mongodb-legacy'
import assert from 'node:assert'
import moment from 'moment'
import path from 'node:path'
import sinon from 'sinon'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import MongoHelpers from '../../../../app/src/Features/Helpers/Mongo.mjs'
const modulePath = path.join(
import.meta.dirname,
'../../../../app/src/Features/User/UserGetter'
)
const { normalizeQuery, normalizeMultiQuery } = MongoHelpers
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
const { ObjectId } = mongodb
describe('UserGetter', function () {
beforeEach(async function (ctx) {
const confirmedAt = new Date()
ctx.fakeUser = {
_id: new ObjectId(),
email: 'email2@foo.bar',
emails: [
{
email: 'email1@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt,
lastConfirmedAt: confirmedAt,
},
{ email: 'email2@foo.bar', reversedHostname: 'rab.oof' },
],
}
ctx.findOne = sinon.stub().resolves(ctx.fakeUser)
ctx.findToArrayStub = sinon.stub().resolves([ctx.fakeUser])
ctx.find = sinon.stub().returns({ toArray: ctx.findToArrayStub })
ctx.Mongo = {
db: {
users: {
findOne: ctx.findOne,
find: ctx.find,
},
},
ObjectId,
}
ctx.getUserAffiliations = sinon.stub().resolves([])
ctx.Modules = {
promises: { hooks: { fire: sinon.stub().resolves() } },
}
ctx.AsyncLocalStorage = {
storage: {
getStore: sinon.stub().returns(undefined),
},
}
vi.doMock('../../../../app/src/Features/Helpers/Mongo', () => ({
default: { normalizeQuery, normalizeMultiQuery },
}))
vi.doMock('../../../../app/src/infrastructure/mongodb', () => ctx.Mongo)
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
reconfirmNotificationDays: 14,
aiFeatures: {
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 5,
basic: 5,
standard: 10,
unlimited: 200,
},
},
}),
}))
vi.doMock(
'../../../../app/src/Features/Institutions/InstitutionsAPI',
() => ({
default: {
promises: {
getUserAffiliations: ctx.getUserAffiliations,
},
},
})
)
vi.doMock('../../../../app/src/infrastructure/Features', () => ({
default: {
hasFeature: sinon.stub().returns(true),
},
}))
vi.doMock('../../../../app/src/models/User', () => ({
User: (ctx.User = {}),
}))
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: ctx.Modules,
}))
vi.doMock('../../../../app/src/infrastructure/AsyncLocalStorage', () => ({
default: ctx.AsyncLocalStorage,
}))
ctx.UserGetter = (await import(modulePath)).default
})
describe('getSsoUsersAtInstitution', function () {
it('should throw an error when no projection is passed', async function (ctx) {
await expect(
ctx.UserGetter.promises.getSsoUsersAtInstitution(1, undefined)
).to.be.rejectedWith('missing projection')
})
})
describe('getUser', function () {
it('should get user', async function (ctx) {
const query = { _id: '000000000000000000000000' }
const projection = { email: 1 }
const user = await ctx.UserGetter.promises.getUser(query, projection)
ctx.findOne.called.should.equal(true)
ctx.findOne.calledWith(query, { projection }).should.equal(true)
expect(user).to.deep.equal(ctx.fakeUser)
})
it('should not allow null query', async function (ctx) {
await expect(
ctx.UserGetter.promises.getUser(null, {})
).to.be.rejectedWith('no query provided')
})
})
describe('getUsers', function () {
it('should get users with array of userIds', async function (ctx) {
const query = [new ObjectId()]
const projection = { email: 1 }
const users = await ctx.UserGetter.promises.getUsers(query, projection)
ctx.find.should.have.been.calledWithMatch(
{ _id: { $in: query } },
{ projection }
)
users.should.deep.equal([ctx.fakeUser])
})
it('should not call mongo with empty list', async function (ctx) {
const query = []
const projection = { email: 1 }
const users = await ctx.UserGetter.promises.getUsers(query, projection)
expect(users).to.deep.equal([])
expect(ctx.find).to.not.have.been.called
})
it('should not allow null query', async function (ctx) {
await expect(
ctx.UserGetter.promises.getUsers(null, {})
).to.be.rejectedWith('no query provided')
})
})
describe('getUserFullEmails', function () {
it('should get user', async function (ctx) {
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
await ctx.UserGetter.promises.getUserFullEmails(ctx.fakeUser._id)
ctx.UserGetter.promises.getUser.called.should.equal(true)
ctx.UserGetter.promises.getUser
.calledWith(ctx.fakeUser._id, projection)
.should.equal(true)
})
it('should fetch emails data', async function (ctx) {
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
assert.deepEqual(fullEmails, [
{
email: 'email1@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
emailHasInstitutionLicence: false,
default: false,
},
{
email: 'email2@foo.bar',
reversedHostname: 'rab.oof',
emailHasInstitutionLicence: false,
default: true,
lastConfirmedAt: null,
},
])
})
it('should merge affiliation data', async function (ctx) {
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
const affiliationsData = [
{
email: 'email1@foo.bar',
role: 'Prof',
cached_confirmed_at: '2019-07-11T18:25:01.639Z',
cached_reconfirmed_at: '2021-07-11T18:25:01.639Z',
department: 'Maths',
entitlement: false,
inferred: false,
licence: 'pro_plus',
institution: {
name: 'University Name',
isUniversity: true,
confirmed: true,
},
last_day_to_reconfirm: undefined,
past_reconfirm_date: false,
portal: undefined,
},
]
ctx.getUserAffiliations.resolves(affiliationsData)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
assert.deepEqual(fullEmails, [
{
email: 'email1@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
default: false,
emailHasInstitutionLicence: true,
affiliation: {
institution: affiliationsData[0].institution,
inferred: affiliationsData[0].inferred,
department: affiliationsData[0].department,
role: affiliationsData[0].role,
lastDayToReconfirm: undefined,
licence: affiliationsData[0].licence,
inReconfirmNotificationPeriod: false,
cachedConfirmedAt: '2019-07-11T18:25:01.639Z',
cachedReconfirmedAt: '2021-07-11T18:25:01.639Z',
cachedEntitlement: false,
cachedLastDayToReconfirm: undefined,
cachedPastReconfirmDate: false,
pastReconfirmDate: false,
portal: undefined,
},
},
{
email: 'email2@foo.bar',
reversedHostname: 'rab.oof',
emailHasInstitutionLicence: false,
default: true,
lastConfirmedAt: null,
},
])
})
it('should merge SAML identifier', async function (ctx) {
const fakeSamlIdentifiers = [
{ providerId: 'saml_id', externalUserId: 'whatever' },
]
const fakeUserWithSaml = ctx.fakeUser
fakeUserWithSaml.emails[0].samlProviderId = 'saml_id'
fakeUserWithSaml.samlIdentifiers = fakeSamlIdentifiers
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
ctx.getUserAffiliations.resolves([])
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
assert.deepEqual(fullEmails, [
{
email: 'email1@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
default: false,
emailHasInstitutionLicence: false,
samlProviderId: 'saml_id',
samlIdentifier: fakeSamlIdentifiers[0],
},
{
email: 'email2@foo.bar',
reversedHostname: 'rab.oof',
emailHasInstitutionLicence: false,
default: true,
lastConfirmedAt: null,
},
])
})
it('should get user when it has no emails field', async function (ctx) {
ctx.fakeUserNoEmails = {
_id: '12390i',
email: 'email2@foo.bar',
}
ctx.UserGetter.promises.getUser = sinon
.stub()
.resolves(ctx.fakeUserNoEmails)
const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUserNoEmails._id
)
ctx.UserGetter.promises.getUser.called.should.equal(true)
ctx.UserGetter.promises.getUser
.calledWith(ctx.fakeUserNoEmails._id, projection)
.should.equal(true)
assert.deepEqual(fullEmails, [])
})
describe('affiliation reconfirmation', function () {
const institutionNonSSO = {
id: 1,
name: 'University Name',
commonsAccount: true,
isUniversity: true,
confirmed: true,
ssoBeta: false,
ssoEnabled: false,
maxConfirmationMonths: 12,
}
const institutionSSO = {
id: 2,
name: 'SSO University Name',
isUniversity: true,
confirmed: true,
ssoBeta: false,
ssoEnabled: true,
maxConfirmationMonths: 12,
}
describe('non-SSO institutions', function () {
const email1 = 'leonard@example-affiliation.com'
const email2 = 'mccoy@example-affiliation.com'
const affiliationsData = [
{
email: email1,
role: 'Prof',
department: 'Medicine',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
},
{
email: email2,
role: 'Prof',
department: 'Medicine',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
},
]
it('should flag inReconfirmNotificationPeriod for all affiliations in period', async function (ctx) {
const { maxConfirmationMonths } = institutionNonSSO
const confirmed1 = moment()
.subtract(maxConfirmationMonths + 2, 'months')
.toDate()
const lastDayToReconfirm1 = moment(confirmed1)
.add(maxConfirmationMonths, 'months')
.toDate()
const confirmed2 = moment()
.subtract(maxConfirmationMonths + 1, 'months')
.toDate()
const lastDayToReconfirm2 = moment(confirmed2)
.add(maxConfirmationMonths, 'months')
.toDate()
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
reversedHostname: 'moc.noitailiffa-elpmaxe',
confirmedAt: confirmed1,
default: true,
},
{
email: email2,
reversedHostname: 'moc.noitailiffa-elpmaxe',
confirmedAt: confirmed2,
},
],
}
const affiliations = [...affiliationsData]
affiliations[0].last_day_to_reconfirm = lastDayToReconfirm1
affiliations[1].last_day_to_reconfirm = lastDayToReconfirm2
ctx.getUserAffiliations.resolves(affiliations)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
it('should not flag affiliations outside of notification period', async function (ctx) {
const { maxConfirmationMonths } = institutionNonSSO
const confirmed1 = new Date()
const lastDayToReconfirm1 = moment(confirmed1)
.add(maxConfirmationMonths, 'months')
.toDate()
const confirmed2 = moment()
.subtract(maxConfirmationMonths, 'months')
.add(30, 'days')
.toDate() // expires in 30 days and reconfirmNotificationDays is set to 14
const lastDayToReconfirm2 = moment(confirmed2)
.add(maxConfirmationMonths, 'months')
.toDate()
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
reversedHostname: 'moc.noitailiffa-elpmaxe',
confirmedAt: confirmed1,
default: true,
},
{
email: email2,
reversedHostname: 'moc.noitailiffa-elpmaxe',
confirmedAt: confirmed2,
},
],
}
const affiliations = [...affiliationsData]
affiliations[0].last_day_to_reconfirm = lastDayToReconfirm1
affiliations[1].last_day_to_reconfirm = lastDayToReconfirm2
ctx.getUserAffiliations.resolves(affiliations)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
})
})
describe('SSO institutions', function () {
it('should flag only linked email, if in notification period', async function (ctx) {
const { maxConfirmationMonths } = institutionSSO
const email1 = 'email1@sso.bar'
const email2 = 'email2@sso.bar'
const email3 = 'email3@sso.bar'
const reconfirmedAt = new Date('2019-09-24')
const lastDayToReconfirm = moment(reconfirmedAt)
.add(maxConfirmationMonths, 'months')
.toDate()
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
reversedHostname: 'rab.oss',
confirmedAt: new Date('2019-09-24'),
reconfirmedAt,
default: true,
},
{
email: email2,
reversedHostname: 'rab.oss',
confirmedAt: new Date('2019-09-24'),
reconfirmedAt,
samlProviderId: institutionSSO.id,
},
{
email: email3,
reversedHostname: 'rab.oss',
confirmedAt: new Date('2019-09-24'),
reconfirmedAt,
},
],
samlIdentifiers: [
{
providerId: institutionSSO.id,
externalUserId: 'abc123',
},
],
}
const affiliations = [
{
email: email1,
role: 'Prof',
department: 'Maths!',
inferred: false,
licence: 'pro_plus',
institution: institutionSSO,
last_day_to_reconfirm: lastDayToReconfirm,
},
{
email: email2,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionSSO,
last_day_to_reconfirm: lastDayToReconfirm,
},
{
email: email3,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionSSO,
last_day_to_reconfirm: lastDayToReconfirm,
},
]
ctx.getUserAffiliations.resolves(affiliations)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[2].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
})
})
describe('multiple institution affiliations', function () {
it('should flag each institution', async function (ctx) {
const { maxConfirmationMonths } = institutionSSO
const email1 = 'email1@sso.bar'
const email2 = 'email2@sso.bar'
const email3 = 'email3@foo.bar'
const email4 = 'email4@foo.bar'
const confirmed1 = new Date('2019-09-24T20:25:08.503Z')
const lastDayToReconfirm1 = moment(confirmed1)
.add(maxConfirmationMonths, 'months')
.toDate()
const confirmed2 = new Date('2019-10-24T20:25:08.503Z')
const lastDayToReconfirm2 = moment(confirmed2)
.add(maxConfirmationMonths, 'months')
.toDate()
const affiliationsData = [
{
email: email1,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionSSO,
last_day_to_reconfirm: lastDayToReconfirm1,
},
{
email: email2,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionSSO,
last_day_to_reconfirm: lastDayToReconfirm1,
},
{
email: email3,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: lastDayToReconfirm2,
},
{
email: email4,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: lastDayToReconfirm1,
},
]
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
reversedHostname: 'rab.oss',
confirmedAt: confirmed1,
default: true,
},
{
email: email2,
reversedHostname: 'rab.oss',
confirmedAt: confirmed1,
samlProviderId: institutionSSO.id,
},
{
email: email3,
reversedHostname: 'rab.oof',
confirmedAt: confirmed2,
},
{
email: email4,
reversedHostname: 'rab.oof',
confirmedAt: confirmed1,
},
],
samlIdentifiers: [
{
providerId: institutionSSO.id,
externalUserId: 'abc123',
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.to.equal(false)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[2].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[3].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
})
describe('reconfirmedAt', function () {
it('only use confirmedAt when no reconfirmedAt', async function (ctx) {
const { maxConfirmationMonths } = institutionSSO
const email1 = 'email1@foo.bar'
const reconfirmed1 = moment().subtract(
maxConfirmationMonths * 3,
'months'
)
const lastDayToReconfirm1 = moment(reconfirmed1)
.add(maxConfirmationMonths, 'months')
.toDate()
const email2 = 'email2@foo.bar'
const reconfirmed2 = moment().subtract(
maxConfirmationMonths * 2,
'months'
)
const lastDayToReconfirm2 = moment(reconfirmed2)
.add(maxConfirmationMonths, 'months')
.toDate()
const email3 = 'email3@foo.bar'
const reconfirmed3 = moment().subtract(
maxConfirmationMonths * 4,
'months'
)
const lastDayToReconfirm3 = moment(reconfirmed3)
.add(maxConfirmationMonths, 'months')
.toDate()
const affiliationsData = [
{
email: email1,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: lastDayToReconfirm1,
},
{
email: email2,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: lastDayToReconfirm2,
},
{
email: email3,
role: 'Prof',
department: 'Maths',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: lastDayToReconfirm3,
},
]
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
reversedHostname: 'rab.oof',
confirmedAt: moment().subtract(
institutionSSO.maxConfirmationMonths * 2,
'months'
),
reconfirmedAt: reconfirmed1,
default: true,
},
{
email: email2,
reversedHostname: 'rab.oof',
confirmedAt: moment().subtract(
institutionSSO.maxConfirmationMonths * 3,
'months'
),
reconfirmedAt: reconfirmed2,
},
{
email: email3,
reversedHostname: 'rab.oof',
confirmedAt: moment().subtract(
institutionSSO.maxConfirmationMonths * 4,
'months'
),
reconfirmedAt: reconfirmed3,
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
expect(
fullEmails[2].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
})
describe('before reconfirmation period expires and within reconfirmation notification period', function () {
const email = 'leonard@example-affiliation.com'
it('should flag the email', async function (ctx) {
const { maxConfirmationMonths } = institutionNonSSO
const confirmedAt = moment()
.subtract(maxConfirmationMonths, 'months')
.subtract(14, 'days')
.toDate() // expires in 14 days
const affiliationsData = [
{
email,
role: 'Prof',
department: 'Medicine',
inferred: false,
licence: 'pro_plus',
institution: institutionNonSSO,
last_day_to_reconfirm: moment(confirmedAt)
.add(maxConfirmationMonths, 'months')
.toDate(),
},
]
const user = {
_id: '12390i',
email,
emails: [
{
email,
confirmedAt,
default: true,
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
})
describe('when no Settings.reconfirmNotificationDays', function () {
it('should always return inReconfirmNotificationPeriod:false', async function (ctx) {
const email1 = 'email1@sso.bar'
const email2 = 'email2@foo.bar'
const email3 = 'email3@foo.bar'
const confirmedAtAboutToExpire = moment()
.subtract(institutionNonSSO.maxConfirmationMonths, 'months')
.subtract(14, 'days')
.toDate() // expires in 14 days
const affiliationsData = [
{
email: email1,
institution: institutionSSO,
},
{
email: email2,
institution: institutionNonSSO,
},
{
email: email3,
institution: institutionNonSSO,
},
]
const user = {
_id: '12390i',
email: email1,
emails: [
{
email: email1,
confirmedAt: confirmedAtAboutToExpire,
default: true,
samlProviderId: institutionSSO.id,
},
{
email: email2,
confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
},
{
email: email3,
confirmedAt: new Date('2019-10-24T20:25:08.503Z'),
},
],
samlIdentifiers: [
{
providerId: institutionSSO.id,
externalUserId: 'abc123',
},
],
}
ctx.settings.reconfirmNotificationDays = undefined
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
expect(
fullEmails[1].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
expect(
fullEmails[2].affiliation.inReconfirmNotificationPeriod
).to.equal(false)
})
})
it('should flag to show notification if v1 shows as past reconfirmation but v2 does not', async function (ctx) {
const email = 'abc123@test.com'
const confirmedAt = new Date()
const affiliationsData = [
{
email,
licence: 'free',
institution: institutionNonSSO,
last_day_to_reconfirm: '2020-07-11T18:25:01.639Z',
past_reconfirm_date: true,
},
]
const user = {
_id: '12390i',
email,
emails: [
{
email,
confirmedAt,
default: true,
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
it('should flag to show notification if v1 shows as reconfirmation upcoming but v2 does not', async function (ctx) {
const email = 'abc123@test.com'
const { maxConfirmationMonths } = institutionNonSSO
const affiliationsData = [
{
email,
licence: 'free',
institution: institutionNonSSO,
last_day_to_reconfirm: moment()
.subtract(maxConfirmationMonths, 'months')
.add(3, 'day')
.toDate(),
},
]
const user = {
_id: '12390i',
email,
emails: [
{
email,
confirmedAt: new Date(),
default: true,
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
it('should flag to show notification if v2 shows as reconfirmation upcoming but v1 does not', async function (ctx) {
const email = 'abc123@test.com'
const { maxConfirmationMonths } = institutionNonSSO
const datePastReconfirmation = moment()
.subtract(maxConfirmationMonths, 'months')
.add(3, 'day')
.toDate()
const dateNotPastReconfirmation = moment().add(1, 'month').toDate()
const affiliationsData = [
{
email,
licence: 'free',
institution: institutionNonSSO,
last_day_to_reconfirm: dateNotPastReconfirmation,
},
]
const user = {
_id: '12390i',
email,
emails: [
{
email,
confirmedAt: datePastReconfirmation,
default: true,
},
],
}
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(
fullEmails[0].affiliation.inReconfirmNotificationPeriod
).to.equal(true)
})
describe('cachedLastDayToReconfirm', function () {
const email = 'abc123@test.com'
const confirmedAt = new Date('2019-07-11T18:25:01.639Z')
const lastDay = '2020-07-11T18:25:01.639Z'
const affiliationsData = [
{
email,
licence: 'professional',
institution: institutionSSO,
last_day_to_reconfirm: lastDay,
past_reconfirm_date: true,
},
]
const user = {
_id: '12390i',
email,
emails: [
{
email,
confirmedAt,
default: true,
},
],
}
it('should set cachedLastDayToReconfirm for SSO institutions if email is linked to SSO', async function (ctx) {
const userLinked = Object.assign({}, user)
userLinked.emails[0].samlProviderId = institutionSSO.id.toString()
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(userLinked)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
lastDay
)
})
it('should NOT set cachedLastDayToReconfirm for SSO institutions if email is NOT linked to SSO', async function (ctx) {
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
lastDay
)
})
it('should set cachedLastDayToReconfirm for non-SSO institutions', async function (ctx) {
ctx.getUserAffiliations.resolves(affiliationsData)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
lastDay
)
})
})
})
describe('caching full emails data if run inside AsyncLocalStorage context', function () {
it('should store the data in the AsyncLocalStorage store', async function (ctx) {
ctx.store = {}
ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
ctx.getUserAffiliations.resolves([
{
email: 'email1@foo.bar',
licence: 'professional',
institution: {},
},
])
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id
)
expect(ctx.UserGetter.promises.getUser).to.have.been.calledOnce
expect(ctx.getUserAffiliations).to.have.been.calledOnce
expect(fullEmails).to.be.an('array')
expect(fullEmails.length).to.equal(2)
expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
fullEmails
)
})
it('should fetch data from the store if available', async function (ctx) {
ctx.store = {
userFullEmails: {
[ctx.fakeUser._id]: [{ email: '1' }, { email: '2' }],
},
}
ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id,
ctx.req
)
expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
expect(ctx.getUserAffiliations).to.not.have.been.called
expect(fullEmails).to.be.an('array')
expect(fullEmails.length).to.equal(2)
expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
fullEmails
)
})
it('should not return cached data for different user ids', async function (ctx) {
ctx.store = {}
ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
ctx.fakeUser._id,
ctx.req
)
expect(ctx.UserGetter.promises.getUser).to.have.been.calledOnce
expect(ctx.getUserAffiliations).to.have.been.calledOnce
expect(fullEmails).to.be.an('array')
expect(fullEmails.length).to.equal(2)
ctx.otherUser = {
_id: new ObjectId(),
email: 'other@foo.bar',
emails: [
{
email: 'other@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: new Date(),
lastConfirmedAt: new Date(),
},
],
}
ctx.UserGetter.promises.getUser.resolves(ctx.otherUser)
ctx.getUserAffiliations.resolves([
{
email: 'other@foo.bar',
licence: 'professional',
institution: {},
},
])
const fullEmailsOther = await ctx.UserGetter.promises.getUserFullEmails(
ctx.otherUser._id,
ctx.req
)
expect(ctx.UserGetter.promises.getUser).to.have.been.calledTwice
expect(ctx.getUserAffiliations).to.have.been.calledTwice
expect(fullEmailsOther).to.not.deep.equal(fullEmails)
expect(fullEmailsOther).to.be.an('array')
expect(fullEmailsOther.length).to.equal(1)
expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
fullEmails
)
expect(ctx.store.userFullEmails[ctx.otherUser._id]).to.deep.equal(
fullEmailsOther
)
})
})
})
describe('getUserConfirmedEmails', function () {
beforeEach(function (ctx) {
ctx.fakeUser = {
emails: [
{
email: 'email1@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: new Date(),
},
{ email: 'email2@foo.bar', reversedHostname: 'rab.oof' },
{
email: 'email3@foo.bar',
reversedHostname: 'rab.oof',
confirmedAt: new Date(),
},
],
}
ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
})
it('should get user', async function (ctx) {
const projection = { emails: 1 }
await ctx.UserGetter.promises.getUserConfirmedEmails(ctx.fakeUser._id)
ctx.UserGetter.promises.getUser
.calledWith(ctx.fakeUser._id, projection)
.should.equal(true)
})
it('should return only confirmed emails', async function (ctx) {
const confirmedEmails =
await ctx.UserGetter.promises.getUserConfirmedEmails(ctx.fakeUser._id)
expect(confirmedEmails.length).to.equal(2)
expect(confirmedEmails[0].email).to.equal('email1@foo.bar')
expect(confirmedEmails[1].email).to.equal('email3@foo.bar')
})
})
describe('getUserbyMainEmail', function () {
it('query user by main email', async function (ctx) {
const email = 'hello@world.com'
const projection = { emails: 1 }
await ctx.UserGetter.promises.getUserByMainEmail(email, projection)
ctx.findOne.called.should.equal(true)
ctx.findOne.calledWith({ email }, { projection }).should.equal(true)
})
it('return user if found', async function (ctx) {
const email = 'hello@world.com'
const user = await ctx.UserGetter.promises.getUserByMainEmail(email)
user.should.deep.equal(ctx.fakeUser)
})
it('trim email', async function (ctx) {
const email = 'hello@world.com'
await ctx.UserGetter.promises.getUserByMainEmail(` ${email} `)
ctx.findOne.called.should.equal(true)
ctx.findOne.calledWith({ email }).should.equal(true)
})
})
describe('getUserByAnyEmail', function () {
it('query user for any email', async function (ctx) {
const email = 'hello@world.com'
const expectedQuery = {
emails: { $exists: true },
'emails.email': email,
}
const projection = { emails: 1 }
const user = await ctx.UserGetter.promises.getUserByAnyEmail(
` ${email} `,
projection
)
ctx.findOne.calledWith(expectedQuery, { projection }).should.equal(true)
user.should.deep.equal(ctx.fakeUser)
})
it('query contains $exists:true so partial index is used', async function (ctx) {
const expectedQuery = {
emails: { $exists: true },
'emails.email': '',
}
await ctx.UserGetter.promises.getUserByAnyEmail('', {})
ctx.findOne
.calledWith(expectedQuery, { projection: {} })
.should.equal(true)
})
it('checks main email as well', async function (ctx) {
ctx.findOne.resolves(null)
const email = 'hello@world.com'
const projection = { emails: 1 }
await ctx.UserGetter.promises.getUserByAnyEmail(` ${email} `, projection)
ctx.findOne.calledTwice.should.equal(true)
ctx.findOne.calledWith({ email }, { projection }).should.equal(true)
})
})
describe('getUsersByHostname', function () {
it('should find user by hostname', async function (ctx) {
const hostname = 'bar.foo'
const expectedQuery = {
emails: { $exists: true },
'emails.reversedHostname': hostname.split('').reverse().join(''),
}
const projection = { emails: 1 }
await ctx.UserGetter.promises.getUsersByHostname(hostname, projection)
ctx.find.calledOnce.should.equal(true)
ctx.find.calledWith(expectedQuery, { projection }).should.equal(true)
})
})
describe('getUsersByAnyConfirmedEmail', function () {
it('should find users by confirmed email', async function (ctx) {
const emails = ['confirmed@example.com']
await ctx.UserGetter.promises.getUsersByAnyConfirmedEmail(emails)
expect(ctx.find).to.be.calledOnceWith(
{
'emails.email': { $in: emails }, // use the index on emails.email
emails: {
$exists: true,
$elemMatch: {
email: { $in: emails },
confirmedAt: { $exists: true },
},
},
},
{ projection: {} }
)
})
})
describe('getUsersByV1Id', function () {
it('should find users by list of v1 ids', async function (ctx) {
const v1Ids = [501]
const expectedQuery = {
'overleaf.id': { $in: v1Ids },
}
const projection = { emails: 1 }
await ctx.UserGetter.promises.getUsersByV1Ids(v1Ids, projection)
ctx.find.calledOnce.should.equal(true)
ctx.find.calledWith(expectedQuery, { projection }).should.equal(true)
})
})
describe('ensureUniqueEmailAddress', function () {
beforeEach(function (ctx) {
ctx.UserGetter.promises.getUserByAnyEmail = sinon.stub()
})
it('should return error if existing user is found', async function (ctx) {
ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.fakeUser)
await expect(
ctx.UserGetter.promises.ensureUniqueEmailAddress(ctx.newEmail)
).to.be.rejectedWith(Errors.EmailExistsError)
})
it('should return null if no user is found', async function (ctx) {
ctx.UserGetter.promises.getUserByAnyEmail.resolves(null)
await expect(
ctx.UserGetter.promises.ensureUniqueEmailAddress(ctx.newEmail)
).to.be.fulfilled
})
})
describe('getUserFeatures', function () {
beforeEach(function (ctx) {
ctx.Modules.promises.hooks.fire = sinon.stub().resolves()
ctx.fakeUser.features = {}
})
it('should return user features', async function (ctx) {
ctx.fakeUser.features = { feature1: true, feature2: false }
const features = await ctx.UserGetter.promises.getUserFeatures(
new ObjectId()
)
expect(features).to.deep.equal(ctx.fakeUser.features)
})
it('should return user features when using promises', async function (ctx) {
ctx.fakeUser.features = { feature1: true, feature2: false }
const features = await ctx.UserGetter.promises.getUserFeatures(
ctx.fakeUser._id
)
expect(features).to.deep.equal(ctx.fakeUser.features)
})
it('should take into account features overrides from modules', async function (ctx) {
// this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
const bundleFeatures = { aiUsageQuota: 'unlimited' }
ctx.fakeUser.features = { aiUsageQuota: 'basic' }
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures])
const features = await ctx.UserGetter.promises.getUserFeatures(
ctx.fakeUser._id
)
expect(features).to.deep.equal(bundleFeatures)
ctx.Modules.promises.hooks.fire.should.have.been.calledWith(
'getModuleProvidedFeatures',
ctx.fakeUser._id
)
})
it('should handle modules not returning any features', async function (ctx) {
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([])
ctx.fakeUser.features = { test: true }
const features = await ctx.UserGetter.promises.getUserFeatures(
ctx.fakeUser._id
)
expect(features).to.deep.equal({ test: true })
ctx.Modules.promises.hooks.fire.should.have.been.calledWith(
'getModuleProvidedFeatures',
ctx.fakeUser._id
)
})
})
})