UserGetter.test.mjs 48 KB

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