UserGetterTests.js 39 KB

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