UserEmailsControllerTests.js 17 KB

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