UserGetter.test.mjs 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341
  1. import { vi, expect } from 'vitest'
  2. import mongodb from 'mongodb-legacy'
  3. import assert from 'node:assert'
  4. import moment from 'moment'
  5. import path from 'node:path'
  6. import sinon from 'sinon'
  7. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  8. import MongoHelpers from '../../../../app/src/Features/Helpers/Mongo.mjs'
  9. const modulePath = path.join(
  10. import.meta.dirname,
  11. '../../../../app/src/Features/User/UserGetter'
  12. )
  13. const { normalizeQuery, normalizeMultiQuery } = MongoHelpers
  14. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  15. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  16. )
  17. const { ObjectId } = mongodb
  18. describe('UserGetter', function () {
  19. beforeEach(async function (ctx) {
  20. const confirmedAt = new Date()
  21. ctx.fakeUser = {
  22. _id: new ObjectId(),
  23. email: 'email2@foo.bar',
  24. emails: [
  25. {
  26. email: 'email1@foo.bar',
  27. reversedHostname: 'rab.oof',
  28. confirmedAt,
  29. lastConfirmedAt: confirmedAt,
  30. },
  31. { email: 'email2@foo.bar', reversedHostname: 'rab.oof' },
  32. ],
  33. }
  34. ctx.findOne = sinon.stub().resolves(ctx.fakeUser)
  35. ctx.findToArrayStub = sinon.stub().resolves([ctx.fakeUser])
  36. ctx.find = sinon.stub().returns({ toArray: ctx.findToArrayStub })
  37. ctx.Mongo = {
  38. db: {
  39. users: {
  40. findOne: ctx.findOne,
  41. find: ctx.find,
  42. },
  43. },
  44. ObjectId,
  45. }
  46. ctx.getUserAffiliations = sinon.stub().resolves([])
  47. ctx.Modules = {
  48. promises: { hooks: { fire: sinon.stub().resolves() } },
  49. }
  50. ctx.AsyncLocalStorage = {
  51. storage: {
  52. getStore: sinon.stub().returns(undefined),
  53. },
  54. }
  55. vi.doMock('../../../../app/src/Features/Helpers/Mongo', () => ({
  56. default: { normalizeQuery, normalizeMultiQuery },
  57. }))
  58. vi.doMock('../../../../app/src/infrastructure/mongodb', () => ctx.Mongo)
  59. vi.doMock('@overleaf/settings', () => ({
  60. default: (ctx.settings = {
  61. reconfirmNotificationDays: 14,
  62. }),
  63. }))
  64. vi.doMock(
  65. '../../../../app/src/Features/Institutions/InstitutionsAPI',
  66. () => ({
  67. default: {
  68. promises: {
  69. getUserAffiliations: ctx.getUserAffiliations,
  70. },
  71. },
  72. })
  73. )
  74. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  75. default: {
  76. hasFeature: sinon.stub().returns(true),
  77. },
  78. }))
  79. vi.doMock('../../../../app/src/models/User', () => ({
  80. User: (ctx.User = {}),
  81. }))
  82. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  83. default: ctx.Modules,
  84. }))
  85. vi.doMock('../../../../app/src/infrastructure/AsyncLocalStorage', () => ({
  86. default: ctx.AsyncLocalStorage,
  87. }))
  88. ctx.UserGetter = (await import(modulePath)).default
  89. })
  90. describe('getSsoUsersAtInstitution', function () {
  91. it('should throw an error when no projection is passed', async function (ctx) {
  92. await expect(
  93. ctx.UserGetter.promises.getSsoUsersAtInstitution(1, undefined)
  94. ).to.be.rejectedWith('missing projection')
  95. })
  96. })
  97. describe('getUser', function () {
  98. it('should get user', async function (ctx) {
  99. const query = { _id: '000000000000000000000000' }
  100. const projection = { email: 1 }
  101. const user = await ctx.UserGetter.promises.getUser(query, projection)
  102. ctx.findOne.called.should.equal(true)
  103. ctx.findOne.calledWith(query, { projection }).should.equal(true)
  104. expect(user).to.deep.equal(ctx.fakeUser)
  105. })
  106. it('should not allow null query', async function (ctx) {
  107. await expect(
  108. ctx.UserGetter.promises.getUser(null, {})
  109. ).to.be.rejectedWith('no query provided')
  110. })
  111. })
  112. describe('getUsers', function () {
  113. it('should get users with array of userIds', async function (ctx) {
  114. const query = [new ObjectId()]
  115. const projection = { email: 1 }
  116. const users = await ctx.UserGetter.promises.getUsers(query, projection)
  117. ctx.find.should.have.been.calledWithMatch(
  118. { _id: { $in: query } },
  119. { projection }
  120. )
  121. users.should.deep.equal([ctx.fakeUser])
  122. })
  123. it('should not call mongo with empty list', async function (ctx) {
  124. const query = []
  125. const projection = { email: 1 }
  126. const users = await ctx.UserGetter.promises.getUsers(query, projection)
  127. expect(users).to.deep.equal([])
  128. expect(ctx.find).to.not.have.been.called
  129. })
  130. it('should not allow null query', async function (ctx) {
  131. await expect(
  132. ctx.UserGetter.promises.getUsers(null, {})
  133. ).to.be.rejectedWith('no query provided')
  134. })
  135. })
  136. describe('getUserFullEmails', function () {
  137. it('should get user', async function (ctx) {
  138. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  139. const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
  140. await ctx.UserGetter.promises.getUserFullEmails(ctx.fakeUser._id)
  141. ctx.UserGetter.promises.getUser.called.should.equal(true)
  142. ctx.UserGetter.promises.getUser
  143. .calledWith(ctx.fakeUser._id, projection)
  144. .should.equal(true)
  145. })
  146. it('should fetch emails data', async function (ctx) {
  147. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  148. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  149. ctx.fakeUser._id
  150. )
  151. assert.deepEqual(fullEmails, [
  152. {
  153. email: 'email1@foo.bar',
  154. reversedHostname: 'rab.oof',
  155. confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
  156. lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
  157. emailHasInstitutionLicence: false,
  158. default: false,
  159. },
  160. {
  161. email: 'email2@foo.bar',
  162. reversedHostname: 'rab.oof',
  163. emailHasInstitutionLicence: false,
  164. default: true,
  165. lastConfirmedAt: null,
  166. },
  167. ])
  168. })
  169. it('should merge affiliation data', async function (ctx) {
  170. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  171. const affiliationsData = [
  172. {
  173. email: 'email1@foo.bar',
  174. role: 'Prof',
  175. cached_confirmed_at: '2019-07-11T18:25:01.639Z',
  176. cached_reconfirmed_at: '2021-07-11T18:25:01.639Z',
  177. department: 'Maths',
  178. entitlement: false,
  179. inferred: false,
  180. licence: 'pro_plus',
  181. institution: {
  182. name: 'University Name',
  183. isUniversity: true,
  184. confirmed: true,
  185. },
  186. last_day_to_reconfirm: undefined,
  187. past_reconfirm_date: false,
  188. portal: undefined,
  189. },
  190. ]
  191. ctx.getUserAffiliations.resolves(affiliationsData)
  192. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  193. ctx.fakeUser._id
  194. )
  195. assert.deepEqual(fullEmails, [
  196. {
  197. email: 'email1@foo.bar',
  198. reversedHostname: 'rab.oof',
  199. confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
  200. lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
  201. default: false,
  202. emailHasInstitutionLicence: true,
  203. affiliation: {
  204. institution: affiliationsData[0].institution,
  205. inferred: affiliationsData[0].inferred,
  206. department: affiliationsData[0].department,
  207. role: affiliationsData[0].role,
  208. lastDayToReconfirm: undefined,
  209. licence: affiliationsData[0].licence,
  210. inReconfirmNotificationPeriod: false,
  211. cachedConfirmedAt: '2019-07-11T18:25:01.639Z',
  212. cachedReconfirmedAt: '2021-07-11T18:25:01.639Z',
  213. cachedEntitlement: false,
  214. cachedLastDayToReconfirm: undefined,
  215. cachedPastReconfirmDate: false,
  216. pastReconfirmDate: false,
  217. portal: undefined,
  218. },
  219. },
  220. {
  221. email: 'email2@foo.bar',
  222. reversedHostname: 'rab.oof',
  223. emailHasInstitutionLicence: false,
  224. default: true,
  225. lastConfirmedAt: null,
  226. },
  227. ])
  228. })
  229. it('should merge SAML identifier', async function (ctx) {
  230. const fakeSamlIdentifiers = [
  231. { providerId: 'saml_id', externalUserId: 'whatever' },
  232. ]
  233. const fakeUserWithSaml = ctx.fakeUser
  234. fakeUserWithSaml.emails[0].samlProviderId = 'saml_id'
  235. fakeUserWithSaml.samlIdentifiers = fakeSamlIdentifiers
  236. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  237. ctx.getUserAffiliations.resolves([])
  238. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  239. ctx.fakeUser._id
  240. )
  241. assert.deepEqual(fullEmails, [
  242. {
  243. email: 'email1@foo.bar',
  244. reversedHostname: 'rab.oof',
  245. confirmedAt: ctx.fakeUser.emails[0].confirmedAt,
  246. lastConfirmedAt: ctx.fakeUser.emails[0].lastConfirmedAt,
  247. default: false,
  248. emailHasInstitutionLicence: false,
  249. samlProviderId: 'saml_id',
  250. samlIdentifier: fakeSamlIdentifiers[0],
  251. },
  252. {
  253. email: 'email2@foo.bar',
  254. reversedHostname: 'rab.oof',
  255. emailHasInstitutionLicence: false,
  256. default: true,
  257. lastConfirmedAt: null,
  258. },
  259. ])
  260. })
  261. it('should get user when it has no emails field', async function (ctx) {
  262. ctx.fakeUserNoEmails = {
  263. _id: '12390i',
  264. email: 'email2@foo.bar',
  265. }
  266. ctx.UserGetter.promises.getUser = sinon
  267. .stub()
  268. .resolves(ctx.fakeUserNoEmails)
  269. const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
  270. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  271. ctx.fakeUserNoEmails._id
  272. )
  273. ctx.UserGetter.promises.getUser.called.should.equal(true)
  274. ctx.UserGetter.promises.getUser
  275. .calledWith(ctx.fakeUserNoEmails._id, projection)
  276. .should.equal(true)
  277. assert.deepEqual(fullEmails, [])
  278. })
  279. describe('affiliation reconfirmation', function () {
  280. const institutionNonSSO = {
  281. id: 1,
  282. name: 'University Name',
  283. commonsAccount: true,
  284. isUniversity: true,
  285. confirmed: true,
  286. ssoBeta: false,
  287. ssoEnabled: false,
  288. maxConfirmationMonths: 12,
  289. }
  290. const institutionSSO = {
  291. id: 2,
  292. name: 'SSO University Name',
  293. isUniversity: true,
  294. confirmed: true,
  295. ssoBeta: false,
  296. ssoEnabled: true,
  297. maxConfirmationMonths: 12,
  298. }
  299. describe('non-SSO institutions', function () {
  300. const email1 = 'leonard@example-affiliation.com'
  301. const email2 = 'mccoy@example-affiliation.com'
  302. const affiliationsData = [
  303. {
  304. email: email1,
  305. role: 'Prof',
  306. department: 'Medicine',
  307. inferred: false,
  308. licence: 'pro_plus',
  309. institution: institutionNonSSO,
  310. },
  311. {
  312. email: email2,
  313. role: 'Prof',
  314. department: 'Medicine',
  315. inferred: false,
  316. licence: 'pro_plus',
  317. institution: institutionNonSSO,
  318. },
  319. ]
  320. it('should flag inReconfirmNotificationPeriod for all affiliations in period', async function (ctx) {
  321. const { maxConfirmationMonths } = institutionNonSSO
  322. const confirmed1 = moment()
  323. .subtract(maxConfirmationMonths + 2, 'months')
  324. .toDate()
  325. const lastDayToReconfirm1 = moment(confirmed1)
  326. .add(maxConfirmationMonths, 'months')
  327. .toDate()
  328. const confirmed2 = moment()
  329. .subtract(maxConfirmationMonths + 1, 'months')
  330. .toDate()
  331. const lastDayToReconfirm2 = moment(confirmed2)
  332. .add(maxConfirmationMonths, 'months')
  333. .toDate()
  334. const user = {
  335. _id: '12390i',
  336. email: email1,
  337. emails: [
  338. {
  339. email: email1,
  340. reversedHostname: 'moc.noitailiffa-elpmaxe',
  341. confirmedAt: confirmed1,
  342. default: true,
  343. },
  344. {
  345. email: email2,
  346. reversedHostname: 'moc.noitailiffa-elpmaxe',
  347. confirmedAt: confirmed2,
  348. },
  349. ],
  350. }
  351. const affiliations = [...affiliationsData]
  352. affiliations[0].last_day_to_reconfirm = lastDayToReconfirm1
  353. affiliations[1].last_day_to_reconfirm = lastDayToReconfirm2
  354. ctx.getUserAffiliations.resolves(affiliations)
  355. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  356. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  357. ctx.fakeUser._id
  358. )
  359. expect(
  360. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  361. ).to.equal(true)
  362. expect(
  363. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  364. ).to.equal(true)
  365. })
  366. it('should not flag affiliations outside of notification period', async function (ctx) {
  367. const { maxConfirmationMonths } = institutionNonSSO
  368. const confirmed1 = new Date()
  369. const lastDayToReconfirm1 = moment(confirmed1)
  370. .add(maxConfirmationMonths, 'months')
  371. .toDate()
  372. const confirmed2 = moment()
  373. .subtract(maxConfirmationMonths, 'months')
  374. .add(30, 'days')
  375. .toDate() // expires in 30 days and reconfirmNotificationDays is set to 14
  376. const lastDayToReconfirm2 = moment(confirmed2)
  377. .add(maxConfirmationMonths, 'months')
  378. .toDate()
  379. const user = {
  380. _id: '12390i',
  381. email: email1,
  382. emails: [
  383. {
  384. email: email1,
  385. reversedHostname: 'moc.noitailiffa-elpmaxe',
  386. confirmedAt: confirmed1,
  387. default: true,
  388. },
  389. {
  390. email: email2,
  391. reversedHostname: 'moc.noitailiffa-elpmaxe',
  392. confirmedAt: confirmed2,
  393. },
  394. ],
  395. }
  396. const affiliations = [...affiliationsData]
  397. affiliations[0].last_day_to_reconfirm = lastDayToReconfirm1
  398. affiliations[1].last_day_to_reconfirm = lastDayToReconfirm2
  399. ctx.getUserAffiliations.resolves(affiliations)
  400. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  401. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  402. ctx.fakeUser._id
  403. )
  404. expect(
  405. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  406. ).to.equal(false)
  407. expect(
  408. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  409. ).to.equal(false)
  410. })
  411. })
  412. describe('SSO institutions', function () {
  413. it('should flag only linked email, if in notification period', async function (ctx) {
  414. const { maxConfirmationMonths } = institutionSSO
  415. const email1 = 'email1@sso.bar'
  416. const email2 = 'email2@sso.bar'
  417. const email3 = 'email3@sso.bar'
  418. const reconfirmedAt = new Date('2019-09-24')
  419. const lastDayToReconfirm = moment(reconfirmedAt)
  420. .add(maxConfirmationMonths, 'months')
  421. .toDate()
  422. const user = {
  423. _id: '12390i',
  424. email: email1,
  425. emails: [
  426. {
  427. email: email1,
  428. reversedHostname: 'rab.oss',
  429. confirmedAt: new Date('2019-09-24'),
  430. reconfirmedAt,
  431. default: true,
  432. },
  433. {
  434. email: email2,
  435. reversedHostname: 'rab.oss',
  436. confirmedAt: new Date('2019-09-24'),
  437. reconfirmedAt,
  438. samlProviderId: institutionSSO.id,
  439. },
  440. {
  441. email: email3,
  442. reversedHostname: 'rab.oss',
  443. confirmedAt: new Date('2019-09-24'),
  444. reconfirmedAt,
  445. },
  446. ],
  447. samlIdentifiers: [
  448. {
  449. providerId: institutionSSO.id,
  450. externalUserId: 'abc123',
  451. },
  452. ],
  453. }
  454. const affiliations = [
  455. {
  456. email: email1,
  457. role: 'Prof',
  458. department: 'Maths!',
  459. inferred: false,
  460. licence: 'pro_plus',
  461. institution: institutionSSO,
  462. last_day_to_reconfirm: lastDayToReconfirm,
  463. },
  464. {
  465. email: email2,
  466. role: 'Prof',
  467. department: 'Maths',
  468. inferred: false,
  469. licence: 'pro_plus',
  470. institution: institutionSSO,
  471. last_day_to_reconfirm: lastDayToReconfirm,
  472. },
  473. {
  474. email: email3,
  475. role: 'Prof',
  476. department: 'Maths',
  477. inferred: false,
  478. licence: 'pro_plus',
  479. institution: institutionSSO,
  480. last_day_to_reconfirm: lastDayToReconfirm,
  481. },
  482. ]
  483. ctx.getUserAffiliations.resolves(affiliations)
  484. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  485. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  486. ctx.fakeUser._id
  487. )
  488. expect(
  489. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  490. ).to.equal(false)
  491. expect(
  492. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  493. ).to.equal(true)
  494. expect(
  495. fullEmails[2].affiliation.inReconfirmNotificationPeriod
  496. ).to.equal(false)
  497. })
  498. })
  499. describe('multiple institution affiliations', function () {
  500. it('should flag each institution', async function (ctx) {
  501. const { maxConfirmationMonths } = institutionSSO
  502. const email1 = 'email1@sso.bar'
  503. const email2 = 'email2@sso.bar'
  504. const email3 = 'email3@foo.bar'
  505. const email4 = 'email4@foo.bar'
  506. const confirmed1 = new Date('2019-09-24T20:25:08.503Z')
  507. const lastDayToReconfirm1 = moment(confirmed1)
  508. .add(maxConfirmationMonths, 'months')
  509. .toDate()
  510. const confirmed2 = new Date('2019-10-24T20:25:08.503Z')
  511. const lastDayToReconfirm2 = moment(confirmed2)
  512. .add(maxConfirmationMonths, 'months')
  513. .toDate()
  514. const affiliationsData = [
  515. {
  516. email: email1,
  517. role: 'Prof',
  518. department: 'Maths',
  519. inferred: false,
  520. licence: 'pro_plus',
  521. institution: institutionSSO,
  522. last_day_to_reconfirm: lastDayToReconfirm1,
  523. },
  524. {
  525. email: email2,
  526. role: 'Prof',
  527. department: 'Maths',
  528. inferred: false,
  529. licence: 'pro_plus',
  530. institution: institutionSSO,
  531. last_day_to_reconfirm: lastDayToReconfirm1,
  532. },
  533. {
  534. email: email3,
  535. role: 'Prof',
  536. department: 'Maths',
  537. inferred: false,
  538. licence: 'pro_plus',
  539. institution: institutionNonSSO,
  540. last_day_to_reconfirm: lastDayToReconfirm2,
  541. },
  542. {
  543. email: email4,
  544. role: 'Prof',
  545. department: 'Maths',
  546. inferred: false,
  547. licence: 'pro_plus',
  548. institution: institutionNonSSO,
  549. last_day_to_reconfirm: lastDayToReconfirm1,
  550. },
  551. ]
  552. const user = {
  553. _id: '12390i',
  554. email: email1,
  555. emails: [
  556. {
  557. email: email1,
  558. reversedHostname: 'rab.oss',
  559. confirmedAt: confirmed1,
  560. default: true,
  561. },
  562. {
  563. email: email2,
  564. reversedHostname: 'rab.oss',
  565. confirmedAt: confirmed1,
  566. samlProviderId: institutionSSO.id,
  567. },
  568. {
  569. email: email3,
  570. reversedHostname: 'rab.oof',
  571. confirmedAt: confirmed2,
  572. },
  573. {
  574. email: email4,
  575. reversedHostname: 'rab.oof',
  576. confirmedAt: confirmed1,
  577. },
  578. ],
  579. samlIdentifiers: [
  580. {
  581. providerId: institutionSSO.id,
  582. externalUserId: 'abc123',
  583. },
  584. ],
  585. }
  586. ctx.getUserAffiliations.resolves(affiliationsData)
  587. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  588. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  589. ctx.fakeUser._id
  590. )
  591. expect(
  592. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  593. ).to.to.equal(false)
  594. expect(
  595. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  596. ).to.equal(true)
  597. expect(
  598. fullEmails[2].affiliation.inReconfirmNotificationPeriod
  599. ).to.equal(true)
  600. expect(
  601. fullEmails[3].affiliation.inReconfirmNotificationPeriod
  602. ).to.equal(true)
  603. })
  604. })
  605. describe('reconfirmedAt', function () {
  606. it('only use confirmedAt when no reconfirmedAt', async function (ctx) {
  607. const { maxConfirmationMonths } = institutionSSO
  608. const email1 = 'email1@foo.bar'
  609. const reconfirmed1 = moment().subtract(
  610. maxConfirmationMonths * 3,
  611. 'months'
  612. )
  613. const lastDayToReconfirm1 = moment(reconfirmed1)
  614. .add(maxConfirmationMonths, 'months')
  615. .toDate()
  616. const email2 = 'email2@foo.bar'
  617. const reconfirmed2 = moment().subtract(
  618. maxConfirmationMonths * 2,
  619. 'months'
  620. )
  621. const lastDayToReconfirm2 = moment(reconfirmed2)
  622. .add(maxConfirmationMonths, 'months')
  623. .toDate()
  624. const email3 = 'email3@foo.bar'
  625. const reconfirmed3 = moment().subtract(
  626. maxConfirmationMonths * 4,
  627. 'months'
  628. )
  629. const lastDayToReconfirm3 = moment(reconfirmed3)
  630. .add(maxConfirmationMonths, 'months')
  631. .toDate()
  632. const affiliationsData = [
  633. {
  634. email: email1,
  635. role: 'Prof',
  636. department: 'Maths',
  637. inferred: false,
  638. licence: 'pro_plus',
  639. institution: institutionNonSSO,
  640. last_day_to_reconfirm: lastDayToReconfirm1,
  641. },
  642. {
  643. email: email2,
  644. role: 'Prof',
  645. department: 'Maths',
  646. inferred: false,
  647. licence: 'pro_plus',
  648. institution: institutionNonSSO,
  649. last_day_to_reconfirm: lastDayToReconfirm2,
  650. },
  651. {
  652. email: email3,
  653. role: 'Prof',
  654. department: 'Maths',
  655. inferred: false,
  656. licence: 'pro_plus',
  657. institution: institutionNonSSO,
  658. last_day_to_reconfirm: lastDayToReconfirm3,
  659. },
  660. ]
  661. const user = {
  662. _id: '12390i',
  663. email: email1,
  664. emails: [
  665. {
  666. email: email1,
  667. reversedHostname: 'rab.oof',
  668. confirmedAt: moment().subtract(
  669. institutionSSO.maxConfirmationMonths * 2,
  670. 'months'
  671. ),
  672. reconfirmedAt: reconfirmed1,
  673. default: true,
  674. },
  675. {
  676. email: email2,
  677. reversedHostname: 'rab.oof',
  678. confirmedAt: moment().subtract(
  679. institutionSSO.maxConfirmationMonths * 3,
  680. 'months'
  681. ),
  682. reconfirmedAt: reconfirmed2,
  683. },
  684. {
  685. email: email3,
  686. reversedHostname: 'rab.oof',
  687. confirmedAt: moment().subtract(
  688. institutionSSO.maxConfirmationMonths * 4,
  689. 'months'
  690. ),
  691. reconfirmedAt: reconfirmed3,
  692. },
  693. ],
  694. }
  695. ctx.getUserAffiliations.resolves(affiliationsData)
  696. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  697. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  698. ctx.fakeUser._id
  699. )
  700. expect(
  701. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  702. ).to.equal(true)
  703. expect(
  704. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  705. ).to.equal(true)
  706. expect(
  707. fullEmails[2].affiliation.inReconfirmNotificationPeriod
  708. ).to.equal(true)
  709. })
  710. })
  711. describe('before reconfirmation period expires and within reconfirmation notification period', function () {
  712. const email = 'leonard@example-affiliation.com'
  713. it('should flag the email', async function (ctx) {
  714. const { maxConfirmationMonths } = institutionNonSSO
  715. const confirmedAt = moment()
  716. .subtract(maxConfirmationMonths, 'months')
  717. .subtract(14, 'days')
  718. .toDate() // expires in 14 days
  719. const affiliationsData = [
  720. {
  721. email,
  722. role: 'Prof',
  723. department: 'Medicine',
  724. inferred: false,
  725. licence: 'pro_plus',
  726. institution: institutionNonSSO,
  727. last_day_to_reconfirm: moment(confirmedAt)
  728. .add(maxConfirmationMonths, 'months')
  729. .toDate(),
  730. },
  731. ]
  732. const user = {
  733. _id: '12390i',
  734. email,
  735. emails: [
  736. {
  737. email,
  738. confirmedAt,
  739. default: true,
  740. },
  741. ],
  742. }
  743. ctx.getUserAffiliations.resolves(affiliationsData)
  744. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  745. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  746. ctx.fakeUser._id
  747. )
  748. expect(
  749. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  750. ).to.equal(true)
  751. })
  752. })
  753. describe('when no Settings.reconfirmNotificationDays', function () {
  754. it('should always return inReconfirmNotificationPeriod:false', async function (ctx) {
  755. const email1 = 'email1@sso.bar'
  756. const email2 = 'email2@foo.bar'
  757. const email3 = 'email3@foo.bar'
  758. const confirmedAtAboutToExpire = moment()
  759. .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
  760. .subtract(14, 'days')
  761. .toDate() // expires in 14 days
  762. const affiliationsData = [
  763. {
  764. email: email1,
  765. institution: institutionSSO,
  766. },
  767. {
  768. email: email2,
  769. institution: institutionNonSSO,
  770. },
  771. {
  772. email: email3,
  773. institution: institutionNonSSO,
  774. },
  775. ]
  776. const user = {
  777. _id: '12390i',
  778. email: email1,
  779. emails: [
  780. {
  781. email: email1,
  782. confirmedAt: confirmedAtAboutToExpire,
  783. default: true,
  784. samlProviderId: institutionSSO.id,
  785. },
  786. {
  787. email: email2,
  788. confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
  789. },
  790. {
  791. email: email3,
  792. confirmedAt: new Date('2019-10-24T20:25:08.503Z'),
  793. },
  794. ],
  795. samlIdentifiers: [
  796. {
  797. providerId: institutionSSO.id,
  798. externalUserId: 'abc123',
  799. },
  800. ],
  801. }
  802. ctx.settings.reconfirmNotificationDays = undefined
  803. ctx.getUserAffiliations.resolves(affiliationsData)
  804. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  805. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  806. ctx.fakeUser._id
  807. )
  808. expect(
  809. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  810. ).to.equal(false)
  811. expect(
  812. fullEmails[1].affiliation.inReconfirmNotificationPeriod
  813. ).to.equal(false)
  814. expect(
  815. fullEmails[2].affiliation.inReconfirmNotificationPeriod
  816. ).to.equal(false)
  817. })
  818. })
  819. it('should flag to show notification if v1 shows as past reconfirmation but v2 does not', async function (ctx) {
  820. const email = 'abc123@test.com'
  821. const confirmedAt = new Date()
  822. const affiliationsData = [
  823. {
  824. email,
  825. licence: 'free',
  826. institution: institutionNonSSO,
  827. last_day_to_reconfirm: '2020-07-11T18:25:01.639Z',
  828. past_reconfirm_date: true,
  829. },
  830. ]
  831. const user = {
  832. _id: '12390i',
  833. email,
  834. emails: [
  835. {
  836. email,
  837. confirmedAt,
  838. default: true,
  839. },
  840. ],
  841. }
  842. ctx.getUserAffiliations.resolves(affiliationsData)
  843. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  844. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  845. ctx.fakeUser._id
  846. )
  847. expect(
  848. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  849. ).to.equal(true)
  850. })
  851. it('should flag to show notification if v1 shows as reconfirmation upcoming but v2 does not', async function (ctx) {
  852. const email = 'abc123@test.com'
  853. const { maxConfirmationMonths } = institutionNonSSO
  854. const affiliationsData = [
  855. {
  856. email,
  857. licence: 'free',
  858. institution: institutionNonSSO,
  859. last_day_to_reconfirm: moment()
  860. .subtract(maxConfirmationMonths, 'months')
  861. .add(3, 'day')
  862. .toDate(),
  863. },
  864. ]
  865. const user = {
  866. _id: '12390i',
  867. email,
  868. emails: [
  869. {
  870. email,
  871. confirmedAt: new Date(),
  872. default: true,
  873. },
  874. ],
  875. }
  876. ctx.getUserAffiliations.resolves(affiliationsData)
  877. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  878. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  879. ctx.fakeUser._id
  880. )
  881. expect(
  882. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  883. ).to.equal(true)
  884. })
  885. it('should flag to show notification if v2 shows as reconfirmation upcoming but v1 does not', async function (ctx) {
  886. const email = 'abc123@test.com'
  887. const { maxConfirmationMonths } = institutionNonSSO
  888. const datePastReconfirmation = moment()
  889. .subtract(maxConfirmationMonths, 'months')
  890. .add(3, 'day')
  891. .toDate()
  892. const dateNotPastReconfirmation = moment().add(1, 'month').toDate()
  893. const affiliationsData = [
  894. {
  895. email,
  896. licence: 'free',
  897. institution: institutionNonSSO,
  898. last_day_to_reconfirm: dateNotPastReconfirmation,
  899. },
  900. ]
  901. const user = {
  902. _id: '12390i',
  903. email,
  904. emails: [
  905. {
  906. email,
  907. confirmedAt: datePastReconfirmation,
  908. default: true,
  909. },
  910. ],
  911. }
  912. ctx.getUserAffiliations.resolves(affiliationsData)
  913. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  914. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  915. ctx.fakeUser._id
  916. )
  917. expect(
  918. fullEmails[0].affiliation.inReconfirmNotificationPeriod
  919. ).to.equal(true)
  920. })
  921. describe('cachedLastDayToReconfirm', function () {
  922. const email = 'abc123@test.com'
  923. const confirmedAt = new Date('2019-07-11T18:25:01.639Z')
  924. const lastDay = '2020-07-11T18:25:01.639Z'
  925. const affiliationsData = [
  926. {
  927. email,
  928. licence: 'professional',
  929. institution: institutionSSO,
  930. last_day_to_reconfirm: lastDay,
  931. past_reconfirm_date: true,
  932. },
  933. ]
  934. const user = {
  935. _id: '12390i',
  936. email,
  937. emails: [
  938. {
  939. email,
  940. confirmedAt,
  941. default: true,
  942. },
  943. ],
  944. }
  945. it('should set cachedLastDayToReconfirm for SSO institutions if email is linked to SSO', async function (ctx) {
  946. const userLinked = Object.assign({}, user)
  947. userLinked.emails[0].samlProviderId = institutionSSO.id.toString()
  948. ctx.getUserAffiliations.resolves(affiliationsData)
  949. ctx.UserGetter.promises.getUser = sinon.stub().resolves(userLinked)
  950. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  951. ctx.fakeUser._id
  952. )
  953. expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
  954. lastDay
  955. )
  956. })
  957. it('should NOT set cachedLastDayToReconfirm for SSO institutions if email is NOT linked to SSO', async function (ctx) {
  958. ctx.getUserAffiliations.resolves(affiliationsData)
  959. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  960. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  961. ctx.fakeUser._id
  962. )
  963. expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
  964. lastDay
  965. )
  966. })
  967. it('should set cachedLastDayToReconfirm for non-SSO institutions', async function (ctx) {
  968. ctx.getUserAffiliations.resolves(affiliationsData)
  969. ctx.UserGetter.promises.getUser = sinon.stub().resolves(user)
  970. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  971. ctx.fakeUser._id
  972. )
  973. expect(fullEmails[0].affiliation.cachedLastDayToReconfirm).to.equal(
  974. lastDay
  975. )
  976. })
  977. })
  978. })
  979. describe('caching full emails data if run inside AsyncLocalStorage context', function () {
  980. it('should store the data in the AsyncLocalStorage store', async function (ctx) {
  981. ctx.store = {}
  982. ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
  983. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  984. ctx.getUserAffiliations.resolves([
  985. {
  986. email: 'email1@foo.bar',
  987. licence: 'professional',
  988. institution: {},
  989. },
  990. ])
  991. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  992. ctx.fakeUser._id
  993. )
  994. expect(ctx.UserGetter.promises.getUser).to.have.been.calledOnce
  995. expect(ctx.getUserAffiliations).to.have.been.calledOnce
  996. expect(fullEmails).to.be.an('array')
  997. expect(fullEmails.length).to.equal(2)
  998. expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
  999. fullEmails
  1000. )
  1001. })
  1002. it('should fetch data from the store if available', async function (ctx) {
  1003. ctx.store = {
  1004. userFullEmails: {
  1005. [ctx.fakeUser._id]: [{ email: '1' }, { email: '2' }],
  1006. },
  1007. }
  1008. ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
  1009. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  1010. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  1011. ctx.fakeUser._id,
  1012. ctx.req
  1013. )
  1014. expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
  1015. expect(ctx.getUserAffiliations).to.not.have.been.called
  1016. expect(fullEmails).to.be.an('array')
  1017. expect(fullEmails.length).to.equal(2)
  1018. expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
  1019. fullEmails
  1020. )
  1021. })
  1022. it('should not return cached data for different user ids', async function (ctx) {
  1023. ctx.store = {}
  1024. ctx.AsyncLocalStorage.storage.getStore.returns(ctx.store)
  1025. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  1026. const fullEmails = await ctx.UserGetter.promises.getUserFullEmails(
  1027. ctx.fakeUser._id,
  1028. ctx.req
  1029. )
  1030. expect(ctx.UserGetter.promises.getUser).to.have.been.calledOnce
  1031. expect(ctx.getUserAffiliations).to.have.been.calledOnce
  1032. expect(fullEmails).to.be.an('array')
  1033. expect(fullEmails.length).to.equal(2)
  1034. ctx.otherUser = {
  1035. _id: new ObjectId(),
  1036. email: 'other@foo.bar',
  1037. emails: [
  1038. {
  1039. email: 'other@foo.bar',
  1040. reversedHostname: 'rab.oof',
  1041. confirmedAt: new Date(),
  1042. lastConfirmedAt: new Date(),
  1043. },
  1044. ],
  1045. }
  1046. ctx.UserGetter.promises.getUser.resolves(ctx.otherUser)
  1047. ctx.getUserAffiliations.resolves([
  1048. {
  1049. email: 'other@foo.bar',
  1050. licence: 'professional',
  1051. institution: {},
  1052. },
  1053. ])
  1054. const fullEmailsOther = await ctx.UserGetter.promises.getUserFullEmails(
  1055. ctx.otherUser._id,
  1056. ctx.req
  1057. )
  1058. expect(ctx.UserGetter.promises.getUser).to.have.been.calledTwice
  1059. expect(ctx.getUserAffiliations).to.have.been.calledTwice
  1060. expect(fullEmailsOther).to.not.deep.equal(fullEmails)
  1061. expect(fullEmailsOther).to.be.an('array')
  1062. expect(fullEmailsOther.length).to.equal(1)
  1063. expect(ctx.store.userFullEmails[ctx.fakeUser._id]).to.deep.equal(
  1064. fullEmails
  1065. )
  1066. expect(ctx.store.userFullEmails[ctx.otherUser._id]).to.deep.equal(
  1067. fullEmailsOther
  1068. )
  1069. })
  1070. })
  1071. })
  1072. describe('getUserConfirmedEmails', function () {
  1073. beforeEach(function (ctx) {
  1074. ctx.fakeUser = {
  1075. emails: [
  1076. {
  1077. email: 'email1@foo.bar',
  1078. reversedHostname: 'rab.oof',
  1079. confirmedAt: new Date(),
  1080. },
  1081. { email: 'email2@foo.bar', reversedHostname: 'rab.oof' },
  1082. {
  1083. email: 'email3@foo.bar',
  1084. reversedHostname: 'rab.oof',
  1085. confirmedAt: new Date(),
  1086. },
  1087. ],
  1088. }
  1089. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.fakeUser)
  1090. })
  1091. it('should get user', async function (ctx) {
  1092. const projection = { emails: 1 }
  1093. await ctx.UserGetter.promises.getUserConfirmedEmails(ctx.fakeUser._id)
  1094. ctx.UserGetter.promises.getUser
  1095. .calledWith(ctx.fakeUser._id, projection)
  1096. .should.equal(true)
  1097. })
  1098. it('should return only confirmed emails', async function (ctx) {
  1099. const confirmedEmails =
  1100. await ctx.UserGetter.promises.getUserConfirmedEmails(ctx.fakeUser._id)
  1101. expect(confirmedEmails.length).to.equal(2)
  1102. expect(confirmedEmails[0].email).to.equal('email1@foo.bar')
  1103. expect(confirmedEmails[1].email).to.equal('email3@foo.bar')
  1104. })
  1105. })
  1106. describe('getUserbyMainEmail', function () {
  1107. it('query user by main email', async function (ctx) {
  1108. const email = 'hello@world.com'
  1109. const projection = { emails: 1 }
  1110. await ctx.UserGetter.promises.getUserByMainEmail(email, projection)
  1111. ctx.findOne.called.should.equal(true)
  1112. ctx.findOne.calledWith({ email }, { projection }).should.equal(true)
  1113. })
  1114. it('return user if found', async function (ctx) {
  1115. const email = 'hello@world.com'
  1116. const user = await ctx.UserGetter.promises.getUserByMainEmail(email)
  1117. user.should.deep.equal(ctx.fakeUser)
  1118. })
  1119. it('trim email', async function (ctx) {
  1120. const email = 'hello@world.com'
  1121. await ctx.UserGetter.promises.getUserByMainEmail(` ${email} `)
  1122. ctx.findOne.called.should.equal(true)
  1123. ctx.findOne.calledWith({ email }).should.equal(true)
  1124. })
  1125. })
  1126. describe('getUserByAnyEmail', function () {
  1127. it('query user for any email', async function (ctx) {
  1128. const email = 'hello@world.com'
  1129. const expectedQuery = {
  1130. emails: { $exists: true },
  1131. 'emails.email': email,
  1132. }
  1133. const projection = { emails: 1 }
  1134. const user = await ctx.UserGetter.promises.getUserByAnyEmail(
  1135. ` ${email} `,
  1136. projection
  1137. )
  1138. ctx.findOne.calledWith(expectedQuery, { projection }).should.equal(true)
  1139. user.should.deep.equal(ctx.fakeUser)
  1140. })
  1141. it('query contains $exists:true so partial index is used', async function (ctx) {
  1142. const expectedQuery = {
  1143. emails: { $exists: true },
  1144. 'emails.email': '',
  1145. }
  1146. await ctx.UserGetter.promises.getUserByAnyEmail('', {})
  1147. ctx.findOne
  1148. .calledWith(expectedQuery, { projection: {} })
  1149. .should.equal(true)
  1150. })
  1151. it('checks main email as well', async function (ctx) {
  1152. ctx.findOne.resolves(null)
  1153. const email = 'hello@world.com'
  1154. const projection = { emails: 1 }
  1155. await ctx.UserGetter.promises.getUserByAnyEmail(` ${email} `, projection)
  1156. ctx.findOne.calledTwice.should.equal(true)
  1157. ctx.findOne.calledWith({ email }, { projection }).should.equal(true)
  1158. })
  1159. })
  1160. describe('getUsersByHostname', function () {
  1161. it('should find user by hostname', async function (ctx) {
  1162. const hostname = 'bar.foo'
  1163. const expectedQuery = {
  1164. emails: { $exists: true },
  1165. 'emails.reversedHostname': hostname.split('').reverse().join(''),
  1166. }
  1167. const projection = { emails: 1 }
  1168. await ctx.UserGetter.promises.getUsersByHostname(hostname, projection)
  1169. ctx.find.calledOnce.should.equal(true)
  1170. ctx.find.calledWith(expectedQuery, { projection }).should.equal(true)
  1171. })
  1172. })
  1173. describe('getUsersByAnyConfirmedEmail', function () {
  1174. it('should find users by confirmed email', async function (ctx) {
  1175. const emails = ['confirmed@example.com']
  1176. await ctx.UserGetter.promises.getUsersByAnyConfirmedEmail(emails)
  1177. expect(ctx.find).to.be.calledOnceWith(
  1178. {
  1179. 'emails.email': { $in: emails }, // use the index on emails.email
  1180. emails: {
  1181. $exists: true,
  1182. $elemMatch: {
  1183. email: { $in: emails },
  1184. confirmedAt: { $exists: true },
  1185. },
  1186. },
  1187. },
  1188. { projection: {} }
  1189. )
  1190. })
  1191. })
  1192. describe('getUsersByV1Id', function () {
  1193. it('should find users by list of v1 ids', async function (ctx) {
  1194. const v1Ids = [501]
  1195. const expectedQuery = {
  1196. 'overleaf.id': { $in: v1Ids },
  1197. }
  1198. const projection = { emails: 1 }
  1199. await ctx.UserGetter.promises.getUsersByV1Ids(v1Ids, projection)
  1200. ctx.find.calledOnce.should.equal(true)
  1201. ctx.find.calledWith(expectedQuery, { projection }).should.equal(true)
  1202. })
  1203. })
  1204. describe('ensureUniqueEmailAddress', function () {
  1205. beforeEach(function (ctx) {
  1206. ctx.UserGetter.promises.getUserByAnyEmail = sinon.stub()
  1207. })
  1208. it('should return error if existing user is found', async function (ctx) {
  1209. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.fakeUser)
  1210. await expect(
  1211. ctx.UserGetter.promises.ensureUniqueEmailAddress(ctx.newEmail)
  1212. ).to.be.rejectedWith(Errors.EmailExistsError)
  1213. })
  1214. it('should return null if no user is found', async function (ctx) {
  1215. ctx.UserGetter.promises.getUserByAnyEmail.resolves(null)
  1216. await expect(
  1217. ctx.UserGetter.promises.ensureUniqueEmailAddress(ctx.newEmail)
  1218. ).to.be.fulfilled
  1219. })
  1220. })
  1221. describe('getUserFeatures', function () {
  1222. beforeEach(function (ctx) {
  1223. ctx.Modules.promises.hooks.fire = sinon.stub().resolves()
  1224. ctx.fakeUser.features = {}
  1225. })
  1226. it('should return user features', async function (ctx) {
  1227. ctx.fakeUser.features = { feature1: true, feature2: false }
  1228. const features = await ctx.UserGetter.promises.getUserFeatures(
  1229. new ObjectId()
  1230. )
  1231. expect(features).to.deep.equal(ctx.fakeUser.features)
  1232. })
  1233. it('should return user features when using promises', async function (ctx) {
  1234. ctx.fakeUser.features = { feature1: true, feature2: false }
  1235. const features = await ctx.UserGetter.promises.getUserFeatures(
  1236. ctx.fakeUser._id
  1237. )
  1238. expect(features).to.deep.equal(ctx.fakeUser.features)
  1239. })
  1240. it('should take into account features overrides from modules', async function (ctx) {
  1241. // this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
  1242. const bundleFeatures = { aiErrorAssistant: true }
  1243. ctx.fakeUser.features = { aiErrorAssistant: false }
  1244. ctx.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures])
  1245. const features = await ctx.UserGetter.promises.getUserFeatures(
  1246. ctx.fakeUser._id
  1247. )
  1248. expect(features).to.deep.equal(bundleFeatures)
  1249. ctx.Modules.promises.hooks.fire.should.have.been.calledWith(
  1250. 'getModuleProvidedFeatures',
  1251. ctx.fakeUser._id
  1252. )
  1253. })
  1254. it('should handle modules not returning any features', async function (ctx) {
  1255. ctx.Modules.promises.hooks.fire = sinon.stub().resolves([])
  1256. ctx.fakeUser.features = { test: true }
  1257. const features = await ctx.UserGetter.promises.getUserFeatures(
  1258. ctx.fakeUser._id
  1259. )
  1260. expect(features).to.deep.equal({ test: true })
  1261. ctx.Modules.promises.hooks.fire.should.have.been.calledWith(
  1262. 'getModuleProvidedFeatures',
  1263. ctx.fakeUser._id
  1264. )
  1265. })
  1266. })
  1267. })