UserEmailsControllerTests.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. const sinon = require('sinon')
  2. const assertCalledWith = sinon.assert.calledWith
  3. const assertNotCalled = sinon.assert.notCalled
  4. const { assert, expect } = require('chai')
  5. const modulePath = '../../../../app/src/Features/User/UserEmailsController.js'
  6. const SandboxedModule = require('sandboxed-module')
  7. const MockRequest = require('../helpers/MockRequest')
  8. const MockResponse = require('../helpers/MockResponse')
  9. const Errors = require('../../../../app/src/Features/Errors/Errors')
  10. describe('UserEmailsController', function () {
  11. beforeEach(function () {
  12. this.req = new MockRequest()
  13. this.req.sessionID = Math.random().toString()
  14. this.res = new MockResponse()
  15. this.next = sinon.stub()
  16. this.user = {
  17. _id: 'mock-user-id',
  18. email: 'example@overleaf.com',
  19. emails: [],
  20. }
  21. this.UserGetter = {
  22. getUser: sinon.stub().yields(),
  23. getUserFullEmails: sinon.stub(),
  24. promises: {
  25. ensureUniqueEmailAddress: sinon.stub().resolves(),
  26. getUser: sinon.stub().resolves(this.user),
  27. getUserByAnyEmail: sinon.stub(),
  28. },
  29. }
  30. this.SessionManager = {
  31. getSessionUser: sinon.stub().returns(this.user),
  32. getLoggedInUserId: sinon.stub().returns(this.user._id),
  33. setInSessionUser: sinon.stub(),
  34. }
  35. this.Features = {
  36. hasFeature: sinon.stub(),
  37. }
  38. this.UserSessionsManager = {
  39. promises: { removeSessionsFromRedis: sinon.stub().resolves() },
  40. }
  41. this.UserUpdater = {
  42. addEmailAddress: sinon.stub(),
  43. updateV1AndSetDefaultEmailAddress: sinon.stub(),
  44. promises: {
  45. addEmailAddress: sinon.stub().resolves(),
  46. confirmEmail: sinon.stub().resolves(),
  47. removeEmailAddress: sinon.stub(),
  48. setDefaultEmailAddress: sinon.stub().resolves(),
  49. },
  50. }
  51. this.EmailHelper = { parseEmail: sinon.stub() }
  52. this.endorseAffiliation = sinon.stub().yields()
  53. this.InstitutionsAPI = {
  54. endorseAffiliation: this.endorseAffiliation,
  55. }
  56. this.HttpErrorHandler = { conflict: sinon.stub() }
  57. this.AnalyticsManager = {
  58. recordEventForUserInBackground: sinon.stub(),
  59. }
  60. this.UserAuditLogHandler = {
  61. addEntry: sinon.stub().yields(),
  62. promises: {
  63. addEntry: sinon.stub().resolves(),
  64. },
  65. }
  66. this.rateLimiter = {
  67. consume: sinon.stub().resolves(),
  68. }
  69. this.RateLimiter = {
  70. RateLimiter: sinon.stub().returns(this.rateLimiter),
  71. }
  72. this.AuthenticationController = {
  73. getRedirectFromSession: sinon.stub().returns(null),
  74. }
  75. this.UserEmailsController = SandboxedModule.require(modulePath, {
  76. requires: {
  77. '../Authentication/AuthenticationController':
  78. this.AuthenticationController,
  79. '../Authentication/SessionManager': this.SessionManager,
  80. '../../infrastructure/Features': this.Features,
  81. './UserSessionsManager': this.UserSessionsManager,
  82. './UserGetter': this.UserGetter,
  83. './UserUpdater': this.UserUpdater,
  84. '../Email/EmailHandler': (this.EmailHandler = {
  85. promises: {
  86. sendEmail: sinon.stub().resolves(),
  87. },
  88. }),
  89. '../Helpers/EmailHelper': this.EmailHelper,
  90. './UserEmailsConfirmationHandler': (this.UserEmailsConfirmationHandler =
  91. {
  92. promises: {
  93. sendConfirmationEmail: sinon.stub().resolves(),
  94. },
  95. }),
  96. '../Institutions/InstitutionsAPI': this.InstitutionsAPI,
  97. '../Errors/HttpErrorHandler': this.HttpErrorHandler,
  98. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  99. './UserAuditLogHandler': this.UserAuditLogHandler,
  100. '../../infrastructure/RateLimiter': this.RateLimiter,
  101. },
  102. })
  103. })
  104. describe('List', function () {
  105. beforeEach(function () {})
  106. it('lists emails', function (done) {
  107. const fullEmails = [{ some: 'data' }]
  108. this.UserGetter.getUserFullEmails.callsArgWith(1, null, fullEmails)
  109. this.UserEmailsController.list(this.req, {
  110. json: response => {
  111. assert.deepEqual(response, fullEmails)
  112. assertCalledWith(this.UserGetter.getUserFullEmails, this.user._id)
  113. done()
  114. },
  115. })
  116. })
  117. })
  118. describe('addWithConfirmationCode', function () {
  119. beforeEach(function () {
  120. this.newEmail = 'new_email@baz.com'
  121. this.req.body = {
  122. email: this.newEmail,
  123. }
  124. this.EmailHelper.parseEmail.returns(this.newEmail)
  125. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
  126. .stub()
  127. .resolves({
  128. confirmCode: '123456',
  129. confirmCodeExpiresTimestamp: new Date(),
  130. })
  131. })
  132. it('sends an email confirmation', function (done) {
  133. this.UserEmailsController.addWithConfirmationCode(this.req, {
  134. sendStatus: code => {
  135. code.should.equal(200)
  136. assertCalledWith(
  137. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
  138. this.newEmail,
  139. false
  140. )
  141. done()
  142. },
  143. })
  144. })
  145. it('handles email parse error', function (done) {
  146. this.EmailHelper.parseEmail.returns(null)
  147. this.UserEmailsController.addWithConfirmationCode(this.req, {
  148. sendStatus: code => {
  149. code.should.equal(422)
  150. done()
  151. },
  152. })
  153. })
  154. it('handles when the email already exists', function (done) {
  155. this.UserGetter.promises.ensureUniqueEmailAddress.rejects(
  156. new Errors.EmailExistsError()
  157. )
  158. this.UserEmailsController.addWithConfirmationCode(this.req, {
  159. status: code => {
  160. code.should.equal(409)
  161. return { json: () => done() }
  162. },
  163. })
  164. })
  165. it('should fail to add new emails when the limit has been reached', function (done) {
  166. this.user.emails = []
  167. for (let i = 0; i < 10; i++) {
  168. this.user.emails.push({ email: `example${i}@overleaf.com` })
  169. }
  170. this.UserEmailsController.addWithConfirmationCode(this.req, {
  171. status: code => {
  172. expect(code).to.equal(422)
  173. return {
  174. json: error => {
  175. expect(error.message).to.equal('secondary email limit exceeded')
  176. done()
  177. },
  178. }
  179. },
  180. })
  181. })
  182. })
  183. describe('checkNewSecondaryEmailConfirmationCode', function () {
  184. beforeEach(function () {
  185. this.newEmail = 'new_email@baz.com'
  186. this.req.session.pendingSecondaryEmail = {
  187. confirmCode: '123456',
  188. email: this.newEmail,
  189. confirmCodeExpiresTimestamp: new Date(Math.max),
  190. }
  191. })
  192. describe('with a valid confirmation code', function () {
  193. beforeEach(function () {
  194. this.req.body = {
  195. code: '123456',
  196. }
  197. })
  198. it('adds the email', function (done) {
  199. this.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
  200. this.req,
  201. {
  202. json: () => {
  203. assertCalledWith(
  204. this.UserUpdater.promises.addEmailAddress,
  205. this.user._id,
  206. this.newEmail
  207. )
  208. assertCalledWith(
  209. this.UserUpdater.promises.confirmEmail,
  210. this.user._id,
  211. this.newEmail
  212. )
  213. done()
  214. },
  215. }
  216. )
  217. })
  218. it('redirects to /project', function (done) {
  219. this.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
  220. this.req,
  221. {
  222. json: ({ redir }) => {
  223. redir.should.equal('/project')
  224. done()
  225. },
  226. }
  227. )
  228. })
  229. it('sends a security alert email', async function () {
  230. this.req.session.pendingSecondaryEmail = {
  231. confirmCode: '123456',
  232. email: this.newEmail,
  233. confirmCodeExpiresTimestamp: new Date(Math.max),
  234. affiliationOptions: {},
  235. }
  236. this.req.body.code = '123456'
  237. await this.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
  238. this.req,
  239. {
  240. json: sinon.stub().resolves(),
  241. }
  242. )
  243. const emailCall = this.EmailHandler.promises.sendEmail.getCall(0)
  244. expect(emailCall.args[0]).to.equal('securityAlert')
  245. expect(emailCall.args[1].to).to.equal(this.user.email)
  246. expect(emailCall.args[1].actionDescribed).to.contain(
  247. 'a secondary email address'
  248. )
  249. expect(emailCall.args[1].message[0]).to.contain(this.newEmail)
  250. })
  251. })
  252. describe('with an invalid confirmation code', function () {
  253. beforeEach(function () {
  254. this.req.body = {
  255. code: '999999',
  256. }
  257. })
  258. it('does not add the email', function (done) {
  259. this.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
  260. this.req,
  261. {
  262. status: () => {
  263. assertNotCalled(this.UserUpdater.promises.addEmailAddress)
  264. assertNotCalled(this.UserUpdater.promises.confirmEmail)
  265. done()
  266. return { json: this.next }
  267. },
  268. }
  269. )
  270. })
  271. it('responds with a 403', function (done) {
  272. this.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
  273. this.req,
  274. {
  275. status: code => {
  276. code.should.equal(403)
  277. done()
  278. return { json: this.next }
  279. },
  280. }
  281. )
  282. })
  283. })
  284. })
  285. describe('resendNewSecondaryEmailConfirmationCode', function () {
  286. beforeEach(function () {
  287. this.newEmail = 'new_email@baz.com'
  288. this.req.session.pendingSecondaryEmail = {
  289. confirmCode: '123456',
  290. email: this.newEmail,
  291. confirmCodeExpiresTimestamp: new Date(Math.max),
  292. }
  293. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
  294. .stub()
  295. .resolves({
  296. confirmCode: '123456',
  297. confirmCodeExpiresTimestamp: new Date(),
  298. })
  299. })
  300. it('should send the email', function (done) {
  301. this.UserEmailsController.resendNewSecondaryEmailConfirmationCode(
  302. this.req,
  303. {
  304. status: code => {
  305. code.should.equal(200)
  306. assertCalledWith(
  307. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
  308. this.newEmail,
  309. false
  310. )
  311. done()
  312. return { json: this.next }
  313. },
  314. }
  315. )
  316. })
  317. })
  318. describe('remove', function () {
  319. beforeEach(function () {
  320. this.email = 'email_to_remove@bar.com'
  321. this.req.body.email = this.email
  322. this.EmailHelper.parseEmail.returns(this.email)
  323. })
  324. it('removes email', function (done) {
  325. const auditLog = {
  326. initiatorId: this.user._id,
  327. ipAddress: this.req.ip,
  328. }
  329. this.UserUpdater.promises.removeEmailAddress.resolves()
  330. this.UserEmailsController.remove(this.req, {
  331. sendStatus: code => {
  332. code.should.equal(200)
  333. assertCalledWith(this.EmailHelper.parseEmail, this.email)
  334. assertCalledWith(
  335. this.UserUpdater.promises.removeEmailAddress,
  336. this.user._id,
  337. this.email,
  338. auditLog
  339. )
  340. done()
  341. },
  342. })
  343. })
  344. it('handles email parse error', function (done) {
  345. this.EmailHelper.parseEmail.returns(null)
  346. this.UserEmailsController.remove(this.req, {
  347. sendStatus: code => {
  348. code.should.equal(422)
  349. assertNotCalled(this.UserUpdater.promises.removeEmailAddress)
  350. done()
  351. },
  352. })
  353. })
  354. })
  355. describe('setDefault', function () {
  356. beforeEach(function () {
  357. this.email = 'email_to_set_default@bar.com'
  358. this.req.body.email = this.email
  359. this.EmailHelper.parseEmail.returns(this.email)
  360. this.SessionManager.setInSessionUser.returns(null)
  361. })
  362. it('sets default email', function (done) {
  363. this.UserEmailsController.setDefault(this.req, {
  364. sendStatus: code => {
  365. code.should.equal(200)
  366. assertCalledWith(this.EmailHelper.parseEmail, this.email)
  367. assertCalledWith(
  368. this.SessionManager.setInSessionUser,
  369. this.req.session,
  370. {
  371. email: this.email,
  372. }
  373. )
  374. assertCalledWith(
  375. this.UserUpdater.promises.setDefaultEmailAddress,
  376. this.user._id,
  377. this.email
  378. )
  379. done()
  380. },
  381. })
  382. })
  383. it('deletes unconfirmed primary if delete-unconfirmed-primary is set', function (done) {
  384. this.user.emails = [{ email: 'example@overleaf.com' }]
  385. this.req.query['delete-unconfirmed-primary'] = ''
  386. this.UserEmailsController.setDefault(this.req, {
  387. sendStatus: () => {
  388. assertCalledWith(
  389. this.UserUpdater.promises.removeEmailAddress,
  390. this.user._id,
  391. 'example@overleaf.com',
  392. {
  393. initiatorId: this.user._id,
  394. ipAddress: this.req.ip,
  395. extraInfo: {
  396. info: 'removed unconfirmed email after setting new primary',
  397. },
  398. }
  399. )
  400. done()
  401. },
  402. })
  403. })
  404. it('doesnt delete a confirmed primary', function (done) {
  405. this.user.emails = [
  406. { email: 'example@overleaf.com', confirmedAt: '2000-01-01' },
  407. ]
  408. this.req.query['delete-unconfirmed-primary'] = ''
  409. this.UserEmailsController.setDefault(this.req, {
  410. sendStatus: () => {
  411. assertNotCalled(this.UserUpdater.promises.removeEmailAddress)
  412. done()
  413. },
  414. })
  415. })
  416. it('doesnt delete primary if delete-unconfirmed-primary is not set', function (done) {
  417. this.UserEmailsController.setDefault(this.req, {
  418. sendStatus: () => {
  419. assertNotCalled(this.UserUpdater.promises.removeEmailAddress)
  420. done()
  421. },
  422. })
  423. })
  424. it('handles email parse error', function (done) {
  425. this.EmailHelper.parseEmail.returns(null)
  426. this.UserEmailsController.setDefault(this.req, {
  427. sendStatus: code => {
  428. code.should.equal(422)
  429. assertNotCalled(this.UserUpdater.promises.setDefaultEmailAddress)
  430. done()
  431. },
  432. })
  433. })
  434. it('should reset the users other sessions', function (done) {
  435. this.res.callback = () => {
  436. expect(
  437. this.UserSessionsManager.promises.removeSessionsFromRedis
  438. ).to.have.been.calledWith(this.user, this.req.sessionID)
  439. done()
  440. }
  441. this.UserEmailsController.setDefault(this.req, this.res, done)
  442. })
  443. it('handles error from revoking sessions and returns 200', function (done) {
  444. const redisError = new Error('redis error')
  445. this.UserSessionsManager.promises.removeSessionsFromRedis = sinon
  446. .stub()
  447. .rejects(redisError)
  448. this.res.callback = () => {
  449. expect(this.res.statusCode).to.equal(200)
  450. // give revoke process time to run
  451. setTimeout(() => {
  452. expect(this.logger.warn).to.have.been.calledWith(
  453. sinon.match({ err: redisError }),
  454. 'failed revoking secondary sessions after changing default email'
  455. )
  456. done()
  457. })
  458. }
  459. this.UserEmailsController.setDefault(this.req, this.res, done)
  460. })
  461. })
  462. describe('endorse', function () {
  463. beforeEach(function () {
  464. this.email = 'email_to_endorse@bar.com'
  465. this.req.body.email = this.email
  466. this.EmailHelper.parseEmail.returns(this.email)
  467. })
  468. it('endorses affiliation', function (done) {
  469. this.req.body.role = 'Role'
  470. this.req.body.department = 'Department'
  471. this.UserEmailsController.endorse(this.req, {
  472. sendStatus: code => {
  473. code.should.equal(204)
  474. assertCalledWith(
  475. this.endorseAffiliation,
  476. this.user._id,
  477. this.email,
  478. 'Role',
  479. 'Department'
  480. )
  481. done()
  482. },
  483. })
  484. })
  485. })
  486. describe('confirm', function () {
  487. beforeEach(function () {
  488. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  489. .stub()
  490. .yields(null, { userId: this.user._id, email: this.user.email })
  491. this.res = {
  492. sendStatus: sinon.stub(),
  493. json: sinon.stub(),
  494. }
  495. this.res.status = sinon.stub().returns(this.res)
  496. this.next = sinon.stub()
  497. this.token = 'mock-token'
  498. this.req.body = { token: this.token }
  499. this.req.ip = '0.0.0.0'
  500. })
  501. describe('successfully', function () {
  502. beforeEach(function () {
  503. this.UserEmailsController.confirm(this.req, this.res, this.next)
  504. })
  505. it('should confirm the email from the token', function () {
  506. this.UserEmailsConfirmationHandler.confirmEmailFromToken
  507. .calledWith(this.req, this.token)
  508. .should.equal(true)
  509. })
  510. it('should return a 200 status', function () {
  511. this.res.sendStatus.calledWith(200).should.equal(true)
  512. })
  513. it('should log the confirmation to the audit log', function () {
  514. sinon.assert.calledWith(
  515. this.UserAuditLogHandler.addEntry,
  516. this.user._id,
  517. 'confirm-email',
  518. this.user._id,
  519. this.req.ip,
  520. {
  521. token: this.token.substring(0, 10),
  522. email: this.user.email,
  523. }
  524. )
  525. })
  526. })
  527. describe('without a token', function () {
  528. beforeEach(function () {
  529. this.req.body.token = null
  530. this.UserEmailsController.confirm(this.req, this.res, this.next)
  531. })
  532. it('should return a 422 status', function () {
  533. this.res.status.calledWith(422).should.equal(true)
  534. })
  535. })
  536. describe('when confirming fails', function () {
  537. beforeEach(function () {
  538. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  539. .stub()
  540. .yields(new Errors.NotFoundError('not found'))
  541. this.UserEmailsController.confirm(this.req, this.res, this.next)
  542. })
  543. it('should return a 404 error code with a message', function () {
  544. this.res.status.calledWith(404).should.equal(true)
  545. this.res.json
  546. .calledWith({
  547. message: this.req.i18n.translate('confirmation_token_invalid'),
  548. })
  549. .should.equal(true)
  550. })
  551. })
  552. })
  553. describe('sendExistingEmailConfirmationCode', function () {
  554. beforeEach(function () {
  555. this.email = 'existing-email@example.com'
  556. this.req.body.email = this.email
  557. this.EmailHelper.parseEmail.returns(this.email)
  558. this.UserGetter.promises.getUserByAnyEmail.resolves({
  559. _id: this.user._id,
  560. email: this.email,
  561. })
  562. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
  563. .stub()
  564. .resolves({
  565. confirmCode: '123456',
  566. confirmCodeExpiresTimestamp: new Date(),
  567. })
  568. })
  569. it('should send confirmation code for existing email', async function () {
  570. await this.UserEmailsController.sendExistingEmailConfirmationCode(
  571. this.req,
  572. {
  573. sendStatus: code => {
  574. code.should.equal(204)
  575. assertCalledWith(
  576. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
  577. this.email,
  578. false
  579. )
  580. },
  581. }
  582. )
  583. })
  584. it('should store confirmation code in session', async function () {
  585. const confirmCode = '123456'
  586. const confirmCodeExpiresTimestamp = new Date()
  587. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode.resolves(
  588. { confirmCode, confirmCodeExpiresTimestamp }
  589. )
  590. await this.UserEmailsController.sendExistingEmailConfirmationCode(
  591. this.req,
  592. { sendStatus: sinon.stub() }
  593. )
  594. expect(this.req.session.pendingExistingEmail).to.deep.equal({
  595. email: this.email,
  596. confirmCode,
  597. confirmCodeExpiresTimestamp,
  598. affiliationOptions: undefined,
  599. })
  600. })
  601. it('should handle invalid email', async function () {
  602. this.EmailHelper.parseEmail.returns(null)
  603. await this.UserEmailsController.sendExistingEmailConfirmationCode(
  604. this.req,
  605. {
  606. sendStatus: code => {
  607. code.should.equal(400)
  608. assertNotCalled(
  609. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode
  610. )
  611. },
  612. }
  613. )
  614. })
  615. it('should handle email not belonging to user', async function () {
  616. this.UserGetter.promises.getUserByAnyEmail.resolves({
  617. _id: 'another-user-id',
  618. })
  619. await this.UserEmailsController.sendExistingEmailConfirmationCode(
  620. this.req,
  621. {
  622. sendStatus: code => {
  623. code.should.equal(422)
  624. assertNotCalled(
  625. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode
  626. )
  627. },
  628. }
  629. )
  630. })
  631. })
  632. describe('checkExistingEmailConfirmationCode', function () {
  633. beforeEach(function () {
  634. this.email = 'existing-email@example.com'
  635. this.req.session.pendingExistingEmail = {
  636. confirmCode: '123456',
  637. email: this.email,
  638. confirmCodeExpiresTimestamp: new Date(Math.max),
  639. }
  640. this.UserUpdater.promises.confirmEmail.resolves()
  641. this.res = {
  642. json: sinon.stub(),
  643. status: sinon.stub().returns({ json: sinon.stub() }),
  644. }
  645. })
  646. describe('with a valid confirmation code', function () {
  647. beforeEach(function () {
  648. this.req.body = { code: '123456' }
  649. })
  650. it('confirms the email', async function () {
  651. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  652. this.req,
  653. {
  654. json: () => {
  655. assertCalledWith(
  656. this.UserUpdater.promises.confirmEmail,
  657. this.user._id,
  658. this.email
  659. )
  660. },
  661. }
  662. )
  663. })
  664. it('adds audit log entry', async function () {
  665. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  666. this.req,
  667. { json: sinon.stub() }
  668. )
  669. assertCalledWith(
  670. this.UserAuditLogHandler.promises.addEntry,
  671. this.user._id,
  672. 'confirm-email-via-code',
  673. this.user._id,
  674. this.req.ip,
  675. { email: this.email }
  676. )
  677. })
  678. it('records analytics event', async function () {
  679. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  680. this.req,
  681. { json: sinon.stub() }
  682. )
  683. assertCalledWith(
  684. this.AnalyticsManager.recordEventForUserInBackground,
  685. this.user._id,
  686. 'email-verified',
  687. {
  688. provider: 'email',
  689. verification_type: 'token',
  690. isPrimary: this.user.email === this.email,
  691. }
  692. )
  693. })
  694. it('removes pendingExistingEmail from session', async function () {
  695. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  696. this.req,
  697. { json: sinon.stub() }
  698. )
  699. expect(this.req.session.pendingExistingEmail).to.be.undefined
  700. })
  701. })
  702. describe('with an invalid confirmation code', function () {
  703. beforeEach(function () {
  704. this.req.body = { code: '999999' }
  705. })
  706. it('does not confirm the email', async function () {
  707. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  708. this.req,
  709. {
  710. status: () => {
  711. assertNotCalled(this.UserUpdater.promises.confirmEmail)
  712. return { json: this.next }
  713. },
  714. }
  715. )
  716. })
  717. it('responds with a 403', async function () {
  718. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  719. this.req,
  720. {
  721. status: code => {
  722. code.should.equal(403)
  723. return { json: this.next }
  724. },
  725. }
  726. )
  727. })
  728. })
  729. describe('with an expired confirmation code', function () {
  730. beforeEach(function () {
  731. this.req.session.pendingExistingEmail.confirmCodeExpiresTimestamp =
  732. new Date(0)
  733. this.req.body = { code: '123456' }
  734. })
  735. it('responds with a 403', async function () {
  736. await this.UserEmailsController.checkExistingEmailConfirmationCode(
  737. this.req,
  738. {
  739. status: code => {
  740. code.should.equal(403)
  741. return { json: this.next }
  742. },
  743. }
  744. )
  745. })
  746. })
  747. })
  748. describe('resendExistingSecondaryEmailConfirmationCode', function () {
  749. beforeEach(function () {
  750. this.email = 'existing-email@example.com'
  751. this.req.session.pendingExistingEmail = {
  752. confirmCode: '123456',
  753. email: this.email,
  754. confirmCodeExpiresTimestamp: new Date(Math.max),
  755. }
  756. this.res.status = sinon.stub().returns({ json: sinon.stub() })
  757. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
  758. .stub()
  759. .resolves({
  760. confirmCode: '654321',
  761. confirmCodeExpiresTimestamp: new Date(),
  762. })
  763. })
  764. it('should resend confirmation code', async function () {
  765. await this.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
  766. this.req,
  767. {
  768. status: code => {
  769. code.should.equal(200)
  770. assertCalledWith(
  771. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
  772. this.email,
  773. false
  774. )
  775. return { json: sinon.stub() }
  776. },
  777. }
  778. )
  779. })
  780. it('should update session with new code', async function () {
  781. const newCode = '654321'
  782. const newExpiryTime = new Date()
  783. this.UserEmailsConfirmationHandler.promises.sendConfirmationCode.resolves(
  784. {
  785. confirmCode: newCode,
  786. confirmCodeExpiresTimestamp: newExpiryTime,
  787. }
  788. )
  789. await this.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
  790. this.req,
  791. { status: () => ({ json: sinon.stub() }) }
  792. )
  793. expect(this.req.session.pendingExistingEmail.confirmCode).to.equal(
  794. newCode
  795. )
  796. expect(
  797. this.req.session.pendingExistingEmail.confirmCodeExpiresTimestamp
  798. ).to.equal(newExpiryTime)
  799. })
  800. it('should add audit log entry', async function () {
  801. await this.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
  802. this.req,
  803. { status: () => ({ json: sinon.stub() }) }
  804. )
  805. assertCalledWith(
  806. this.UserAuditLogHandler.promises.addEntry,
  807. this.user._id,
  808. 'resend-confirm-email-code',
  809. this.user._id,
  810. this.req.ip,
  811. { email: this.email }
  812. )
  813. })
  814. it('should handle rate limiting', async function () {
  815. this.rateLimiter.consume.rejects({ remainingPoints: 0 })
  816. await this.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
  817. this.req,
  818. {
  819. status: code => {
  820. code.should.equal(429)
  821. return { json: sinon.stub() }
  822. },
  823. }
  824. )
  825. })
  826. })
  827. })