UserEmailsControllerTests.js 16 KB

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