UserEmailsControllerTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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 = { _id: 'mock-user-id', email: 'example@overleaf.com' }
  17. this.UserGetter = {
  18. getUserFullEmails: sinon.stub(),
  19. getUserByAnyEmail: sinon.stub(),
  20. promises: {
  21. getUser: sinon.stub().resolves(this.user),
  22. },
  23. }
  24. this.SessionManager = {
  25. getSessionUser: sinon.stub().returns(this.user),
  26. getLoggedInUserId: sinon.stub().returns(this.user._id),
  27. setInSessionUser: sinon.stub(),
  28. }
  29. this.Features = {
  30. hasFeature: sinon.stub(),
  31. }
  32. this.UserSessionsManager = {
  33. revokeAllUserSessions: sinon.stub().yields(),
  34. }
  35. this.UserUpdater = {
  36. addEmailAddress: sinon.stub(),
  37. removeEmailAddress: sinon.stub(),
  38. setDefaultEmailAddress: sinon.stub(),
  39. updateV1AndSetDefaultEmailAddress: sinon.stub(),
  40. promises: {
  41. addEmailAddress: sinon.stub().resolves(),
  42. },
  43. }
  44. this.EmailHelper = { parseEmail: sinon.stub() }
  45. this.endorseAffiliation = sinon.stub().yields()
  46. this.InstitutionsAPI = {
  47. endorseAffiliation: this.endorseAffiliation,
  48. }
  49. this.HttpErrorHandler = { conflict: sinon.stub() }
  50. this.AnalyticsManager = {
  51. recordEventForUser: sinon.stub(),
  52. }
  53. this.UserEmailsController = SandboxedModule.require(modulePath, {
  54. requires: {
  55. '../Authentication/SessionManager': this.SessionManager,
  56. '../../infrastructure/Features': this.Features,
  57. './UserSessionsManager': this.UserSessionsManager,
  58. './UserGetter': this.UserGetter,
  59. './UserUpdater': this.UserUpdater,
  60. '../Email/EmailHandler': (this.EmailHandler = {
  61. promises: {
  62. sendEmail: sinon.stub().resolves(),
  63. },
  64. }),
  65. '../Helpers/EmailHelper': this.EmailHelper,
  66. './UserEmailsConfirmationHandler': (this.UserEmailsConfirmationHandler =
  67. {
  68. sendReconfirmationEmail: sinon.stub(),
  69. promises: {
  70. sendConfirmationEmail: sinon.stub().resolves(),
  71. },
  72. }),
  73. '../Institutions/InstitutionsAPI': this.InstitutionsAPI,
  74. '../Errors/HttpErrorHandler': this.HttpErrorHandler,
  75. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  76. },
  77. })
  78. })
  79. describe('List', function () {
  80. beforeEach(function () {})
  81. it('lists emails', function (done) {
  82. const fullEmails = [{ some: 'data' }]
  83. this.UserGetter.getUserFullEmails.callsArgWith(1, null, fullEmails)
  84. this.UserEmailsController.list(this.req, {
  85. json: response => {
  86. assert.deepEqual(response, fullEmails)
  87. assertCalledWith(this.UserGetter.getUserFullEmails, this.user._id)
  88. done()
  89. },
  90. })
  91. })
  92. })
  93. describe('Add', function () {
  94. beforeEach(function () {
  95. this.newEmail = 'new_email@baz.com'
  96. this.req.body = {
  97. email: this.newEmail,
  98. university: { name: 'University Name' },
  99. department: 'Department',
  100. role: 'Role',
  101. }
  102. this.EmailHelper.parseEmail.returns(this.newEmail)
  103. this.UserEmailsConfirmationHandler.sendConfirmationEmail = sinon
  104. .stub()
  105. .yields()
  106. })
  107. it('passed audit log to addEmailAddress', function (done) {
  108. this.res.sendStatus = sinon.stub()
  109. this.res.sendStatus.callsFake(() => {
  110. const addCall = this.UserUpdater.promises.addEmailAddress.lastCall
  111. expect(addCall.args[3]).to.deep.equal({
  112. initiatorId: this.user._id,
  113. ipAddress: this.req.ip,
  114. })
  115. done()
  116. })
  117. this.UserEmailsController.add(this.req, this.res)
  118. })
  119. it('adds new email', function (done) {
  120. this.UserEmailsController.add(
  121. this.req,
  122. {
  123. sendStatus: code => {
  124. code.should.equal(204)
  125. assertCalledWith(this.EmailHelper.parseEmail, this.newEmail)
  126. assertCalledWith(
  127. this.UserUpdater.promises.addEmailAddress,
  128. this.user._id,
  129. this.newEmail
  130. )
  131. const affiliationOptions =
  132. this.UserUpdater.promises.addEmailAddress.lastCall.args[2]
  133. Object.keys(affiliationOptions).length.should.equal(3)
  134. affiliationOptions.university.should.equal(this.req.body.university)
  135. affiliationOptions.department.should.equal(this.req.body.department)
  136. affiliationOptions.role.should.equal(this.req.body.role)
  137. done()
  138. },
  139. },
  140. this.next
  141. )
  142. })
  143. it('sends a security alert email', function (done) {
  144. this.res.sendStatus = sinon.stub()
  145. this.res.sendStatus.callsFake(() => {
  146. const emailCall = this.EmailHandler.promises.sendEmail.getCall(0)
  147. emailCall.args[0].should.to.equal('securityAlert')
  148. emailCall.args[1].to.should.equal(this.user.email)
  149. emailCall.args[1].actionDescribed.should.contain(
  150. 'a secondary email address'
  151. )
  152. emailCall.args[1].to.should.equal(this.user.email)
  153. emailCall.args[1].message[0].should.contain(this.newEmail)
  154. done()
  155. })
  156. this.UserEmailsController.add(this.req, this.res)
  157. })
  158. it('sends an email confirmation', function (done) {
  159. this.UserEmailsController.add(
  160. this.req,
  161. {
  162. sendStatus: code => {
  163. code.should.equal(204)
  164. assertCalledWith(
  165. this.UserEmailsConfirmationHandler.promises.sendConfirmationEmail,
  166. this.user._id,
  167. this.newEmail
  168. )
  169. done()
  170. },
  171. },
  172. this.next
  173. )
  174. })
  175. it('handles email parse error', function (done) {
  176. this.EmailHelper.parseEmail.returns(null)
  177. this.UserEmailsController.add(
  178. this.req,
  179. {
  180. sendStatus: code => {
  181. code.should.equal(422)
  182. assertNotCalled(this.UserUpdater.promises.addEmailAddress)
  183. done()
  184. },
  185. },
  186. this.next
  187. )
  188. })
  189. it('should pass the error to the next handler when adding the email fails', function (done) {
  190. this.UserUpdater.promises.addEmailAddress.rejects(new Error())
  191. this.UserEmailsController.add(this.req, this.res, error => {
  192. expect(error).to.be.instanceof(Error)
  193. done()
  194. })
  195. })
  196. it('should call the HTTP conflict handler when the email already exists', function (done) {
  197. this.UserUpdater.promises.addEmailAddress.rejects(
  198. new Errors.EmailExistsError()
  199. )
  200. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  201. req.should.exist
  202. res.should.exist
  203. message.should.equal('email_already_registered')
  204. done()
  205. })
  206. this.UserEmailsController.add(this.req, this.res, this.next)
  207. })
  208. it("should call the HTTP conflict handler when there's a domain matching error", function (done) {
  209. this.UserUpdater.promises.addEmailAddress.rejects(
  210. new Error('422: Email does not belong to university')
  211. )
  212. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  213. req.should.exist
  214. res.should.exist
  215. message.should.equal('email_does_not_belong_to_university')
  216. done()
  217. })
  218. this.UserEmailsController.add(this.req, this.res, this.next)
  219. })
  220. })
  221. describe('remove', function () {
  222. beforeEach(function () {
  223. this.email = 'email_to_remove@bar.com'
  224. this.req.body.email = this.email
  225. this.EmailHelper.parseEmail.returns(this.email)
  226. })
  227. it('removes email', function (done) {
  228. const auditLog = {
  229. initiatorId: this.user._id,
  230. ipAddress: this.req.ip,
  231. }
  232. this.UserUpdater.removeEmailAddress.callsArgWith(3, null)
  233. this.UserEmailsController.remove(this.req, {
  234. sendStatus: code => {
  235. code.should.equal(200)
  236. assertCalledWith(this.EmailHelper.parseEmail, this.email)
  237. assertCalledWith(
  238. this.UserUpdater.removeEmailAddress,
  239. this.user._id,
  240. this.email,
  241. auditLog
  242. )
  243. done()
  244. },
  245. })
  246. })
  247. it('handles email parse error', function (done) {
  248. this.EmailHelper.parseEmail.returns(null)
  249. this.UserEmailsController.remove(this.req, {
  250. sendStatus: code => {
  251. code.should.equal(422)
  252. assertNotCalled(this.UserUpdater.removeEmailAddress)
  253. done()
  254. },
  255. })
  256. })
  257. })
  258. describe('setDefault', function () {
  259. beforeEach(function () {
  260. this.email = 'email_to_set_default@bar.com'
  261. this.req.body.email = this.email
  262. this.EmailHelper.parseEmail.returns(this.email)
  263. this.SessionManager.setInSessionUser.returns(null)
  264. })
  265. it('sets default email', function (done) {
  266. this.UserUpdater.setDefaultEmailAddress.yields()
  267. this.UserEmailsController.setDefault(this.req, {
  268. sendStatus: code => {
  269. code.should.equal(200)
  270. assertCalledWith(this.EmailHelper.parseEmail, this.email)
  271. assertCalledWith(
  272. this.SessionManager.setInSessionUser,
  273. this.req.session,
  274. {
  275. email: this.email,
  276. }
  277. )
  278. assertCalledWith(
  279. this.UserUpdater.setDefaultEmailAddress,
  280. this.user._id,
  281. this.email
  282. )
  283. done()
  284. },
  285. })
  286. })
  287. it('handles email parse error', function (done) {
  288. this.EmailHelper.parseEmail.returns(null)
  289. this.UserEmailsController.setDefault(this.req, {
  290. sendStatus: code => {
  291. code.should.equal(422)
  292. assertNotCalled(this.UserUpdater.setDefaultEmailAddress)
  293. done()
  294. },
  295. })
  296. })
  297. it('should reset the users other sessions', function (done) {
  298. this.UserUpdater.setDefaultEmailAddress.yields()
  299. this.res.callback = () => {
  300. expect(
  301. this.UserSessionsManager.revokeAllUserSessions
  302. ).to.have.been.calledWith(this.user, [this.req.sessionID])
  303. done()
  304. }
  305. this.UserEmailsController.setDefault(this.req, this.res, done)
  306. })
  307. it('handles error from revoking sessions and returns 200', function (done) {
  308. this.UserUpdater.setDefaultEmailAddress.yields()
  309. const redisError = new Error('redis error')
  310. this.UserSessionsManager.revokeAllUserSessions = sinon
  311. .stub()
  312. .yields(redisError)
  313. this.res.callback = () => {
  314. expect(this.res.statusCode).to.equal(200)
  315. // give revoke process time to run
  316. setTimeout(() => {
  317. expect(this.logger.warn).to.have.been.calledWith(
  318. sinon.match({ err: redisError }),
  319. 'failed revoking secondary sessions after changing default email'
  320. )
  321. done()
  322. })
  323. }
  324. this.UserEmailsController.setDefault(this.req, this.res, done)
  325. })
  326. })
  327. describe('endorse', function () {
  328. beforeEach(function () {
  329. this.email = 'email_to_endorse@bar.com'
  330. this.req.body.email = this.email
  331. this.EmailHelper.parseEmail.returns(this.email)
  332. })
  333. it('endorses affiliation', function (done) {
  334. this.req.body.role = 'Role'
  335. this.req.body.department = 'Department'
  336. this.UserEmailsController.endorse(this.req, {
  337. sendStatus: code => {
  338. code.should.equal(204)
  339. assertCalledWith(
  340. this.endorseAffiliation,
  341. this.user._id,
  342. this.email,
  343. 'Role',
  344. 'Department'
  345. )
  346. done()
  347. },
  348. })
  349. })
  350. })
  351. describe('confirm', function () {
  352. beforeEach(function () {
  353. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  354. .stub()
  355. .yields()
  356. this.res = {
  357. sendStatus: sinon.stub(),
  358. json: sinon.stub(),
  359. }
  360. this.res.status = sinon.stub().returns(this.res)
  361. this.next = sinon.stub()
  362. this.token = 'mock-token'
  363. this.req.body = { token: this.token }
  364. })
  365. describe('successfully', function () {
  366. beforeEach(function () {
  367. this.UserEmailsController.confirm(this.req, this.res, this.next)
  368. })
  369. it('should confirm the email from the token', function () {
  370. this.UserEmailsConfirmationHandler.confirmEmailFromToken
  371. .calledWith(this.token)
  372. .should.equal(true)
  373. })
  374. it('should return a 200 status', function () {
  375. this.res.sendStatus.calledWith(200).should.equal(true)
  376. })
  377. })
  378. describe('without a token', function () {
  379. beforeEach(function () {
  380. this.req.body.token = null
  381. this.UserEmailsController.confirm(this.req, this.res, this.next)
  382. })
  383. it('should return a 422 status', function () {
  384. this.res.status.calledWith(422).should.equal(true)
  385. })
  386. })
  387. describe('when confirming fails', function () {
  388. beforeEach(function () {
  389. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  390. .stub()
  391. .yields(new Errors.NotFoundError('not found'))
  392. this.UserEmailsController.confirm(this.req, this.res, this.next)
  393. })
  394. it('should return a 404 error code with a message', function () {
  395. this.res.status.calledWith(404).should.equal(true)
  396. this.res.json
  397. .calledWith({
  398. message: this.req.i18n.translate('confirmation_token_invalid'),
  399. })
  400. .should.equal(true)
  401. })
  402. })
  403. })
  404. describe('resendConfirmation', function () {
  405. beforeEach(function () {
  406. this.EmailHelper.parseEmail.returnsArg(0)
  407. this.UserGetter.getUserByAnyEmail.yields(undefined, {
  408. _id: this.user._id,
  409. })
  410. this.req = {
  411. body: {},
  412. }
  413. this.res = {
  414. sendStatus: sinon.stub(),
  415. }
  416. this.next = sinon.stub()
  417. this.UserEmailsConfirmationHandler.sendConfirmationEmail = sinon
  418. .stub()
  419. .yields()
  420. })
  421. it('should send the email', function (done) {
  422. this.req = {
  423. body: {
  424. email: 'test@example.com',
  425. },
  426. }
  427. this.UserEmailsController.sendReconfirmation(
  428. this.req,
  429. this.res,
  430. this.next
  431. )
  432. expect(this.UserEmailsConfirmationHandler.sendReconfirmationEmail).to.have
  433. .been.calledOnce
  434. done()
  435. })
  436. it('should return 422 if email not valid', function (done) {
  437. this.req = {
  438. body: {},
  439. }
  440. this.UserEmailsController.resendConfirmation(
  441. this.req,
  442. this.res,
  443. this.next
  444. )
  445. expect(this.UserEmailsConfirmationHandler.sendConfirmationEmail).to.not
  446. .have.been.called
  447. expect(this.res.sendStatus.lastCall.args[0]).to.equal(422)
  448. done()
  449. })
  450. describe('email on another user account', function () {
  451. beforeEach(function () {
  452. this.UserGetter.getUserByAnyEmail.yields(undefined, {
  453. _id: 'another-user-id',
  454. })
  455. })
  456. it('should return 422', function (done) {
  457. this.req = {
  458. body: {
  459. email: 'test@example.com',
  460. },
  461. }
  462. this.UserEmailsController.resendConfirmation(
  463. this.req,
  464. this.res,
  465. this.next
  466. )
  467. expect(this.UserEmailsConfirmationHandler.sendConfirmationEmail).to.not
  468. .have.been.called
  469. expect(this.res.sendStatus.lastCall.args[0]).to.equal(422)
  470. done()
  471. })
  472. })
  473. })
  474. describe('sendReconfirmation', function () {
  475. beforeEach(function () {
  476. this.res.sendStatus = sinon.stub()
  477. this.UserGetter.getUserByAnyEmail.yields(undefined, {
  478. _id: this.user._id,
  479. })
  480. this.EmailHelper.parseEmail.returnsArg(0)
  481. })
  482. it('should send the email', function (done) {
  483. this.req = {
  484. body: {
  485. email: 'test@example.com',
  486. },
  487. }
  488. this.UserEmailsController.sendReconfirmation(
  489. this.req,
  490. this.res,
  491. this.next
  492. )
  493. expect(this.UserEmailsConfirmationHandler.sendReconfirmationEmail).to.have
  494. .been.calledOnce
  495. done()
  496. })
  497. it('should return 400 if email not valid', function (done) {
  498. this.req = {
  499. body: {},
  500. }
  501. this.UserEmailsController.sendReconfirmation(
  502. this.req,
  503. this.res,
  504. this.next
  505. )
  506. expect(this.UserEmailsConfirmationHandler.sendReconfirmationEmail).to.not
  507. .have.been.called
  508. expect(this.res.sendStatus.lastCall.args[0]).to.equal(400)
  509. done()
  510. })
  511. describe('email on another user account', function () {
  512. beforeEach(function () {
  513. this.UserGetter.getUserByAnyEmail.yields(undefined, {
  514. _id: 'another-user-id',
  515. })
  516. })
  517. it('should return 422', function (done) {
  518. this.req = {
  519. body: {
  520. email: 'test@example.com',
  521. },
  522. }
  523. this.UserEmailsController.sendReconfirmation(
  524. this.req,
  525. this.res,
  526. this.next
  527. )
  528. expect(this.UserEmailsConfirmationHandler.sendReconfirmationEmail).to
  529. .not.have.been.called
  530. expect(this.res.sendStatus.lastCall.args[0]).to.equal(422)
  531. done()
  532. })
  533. })
  534. })
  535. })