| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341 |
- 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,
- }),
- }))
- 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 = { aiErrorAssistant: true }
- ctx.fakeUser.features = { aiErrorAssistant: false }
- 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
- )
- })
- })
- })
|