UserEmailsControllerTests.js 17 KB

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