UserEmailsControllerTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. './UserEmailsConfirmationHandler': (this.UserEmailsConfirmationHandler = {
  68. promises: {
  69. sendConfirmationEmail: sinon.stub().resolves()
  70. }
  71. }),
  72. '../Institutions/InstitutionsAPI': this.InstitutionsAPI,
  73. '../Errors/HttpErrorHandler': this.HttpErrorHandler,
  74. '../Errors/Errors': Errors,
  75. 'logger-sharelatex': {
  76. log() {
  77. console.log(arguments)
  78. },
  79. err() {}
  80. }
  81. }
  82. })
  83. })
  84. describe('List', function() {
  85. beforeEach(function() {})
  86. it('lists emails', function(done) {
  87. const fullEmails = [{ some: 'data' }]
  88. this.UserGetter.getUserFullEmails.callsArgWith(1, null, fullEmails)
  89. this.UserEmailsController.list(this.req, {
  90. json: response => {
  91. assert.deepEqual(response, fullEmails)
  92. assertCalledWith(this.UserGetter.getUserFullEmails, this.user._id)
  93. done()
  94. }
  95. })
  96. })
  97. })
  98. describe('Add', function() {
  99. beforeEach(function() {
  100. this.newEmail = 'new_email@baz.com'
  101. this.req.body = {
  102. email: this.newEmail,
  103. university: { name: 'University Name' },
  104. department: 'Department',
  105. role: 'Role'
  106. }
  107. this.EmailHelper.parseEmail.returns(this.newEmail)
  108. this.UserEmailsConfirmationHandler.sendConfirmationEmail = sinon
  109. .stub()
  110. .yields()
  111. })
  112. it('passed audit log to addEmailAddress', function(done) {
  113. this.res.sendStatus = sinon.stub()
  114. this.res.sendStatus.callsFake(() => {
  115. const addCall = this.UserUpdater.promises.addEmailAddress.lastCall
  116. expect(addCall.args[3]).to.deep.equal({
  117. initiatorId: this.user._id,
  118. ipAddress: this.req.ip
  119. })
  120. done()
  121. })
  122. this.UserEmailsController.add(this.req, this.res)
  123. })
  124. it('adds new email', function(done) {
  125. this.UserEmailsController.add(
  126. this.req,
  127. {
  128. sendStatus: code => {
  129. code.should.equal(204)
  130. assertCalledWith(this.EmailHelper.parseEmail, this.newEmail)
  131. assertCalledWith(
  132. this.UserUpdater.promises.addEmailAddress,
  133. this.user._id,
  134. this.newEmail
  135. )
  136. const affiliationOptions = this.UserUpdater.promises.addEmailAddress
  137. .lastCall.args[2]
  138. Object.keys(affiliationOptions).length.should.equal(3)
  139. affiliationOptions.university.should.equal(this.req.body.university)
  140. affiliationOptions.department.should.equal(this.req.body.department)
  141. affiliationOptions.role.should.equal(this.req.body.role)
  142. done()
  143. }
  144. },
  145. this.next
  146. )
  147. })
  148. it('sends a security alert email', function(done) {
  149. this.res.sendStatus = sinon.stub()
  150. this.res.sendStatus.callsFake(() => {
  151. const emailCall = this.EmailHandler.promises.sendEmail.getCall(0)
  152. emailCall.args[0].should.to.equal('securityAlert')
  153. emailCall.args[1].to.should.equal(this.user.email)
  154. emailCall.args[1].actionDescribed.should.contain(
  155. 'a secondary email address'
  156. )
  157. emailCall.args[1].to.should.equal(this.user.email)
  158. emailCall.args[1].message[0].should.contain(this.newEmail)
  159. done()
  160. })
  161. this.UserEmailsController.add(this.req, this.res)
  162. })
  163. it('sends an email confirmation', function(done) {
  164. this.UserEmailsController.add(
  165. this.req,
  166. {
  167. sendStatus: code => {
  168. code.should.equal(204)
  169. assertCalledWith(
  170. this.UserEmailsConfirmationHandler.promises.sendConfirmationEmail,
  171. this.user._id,
  172. this.newEmail
  173. )
  174. done()
  175. }
  176. },
  177. this.next
  178. )
  179. })
  180. it('handles email parse error', function(done) {
  181. this.EmailHelper.parseEmail.returns(null)
  182. this.UserEmailsController.add(
  183. this.req,
  184. {
  185. sendStatus: code => {
  186. code.should.equal(422)
  187. assertNotCalled(this.UserUpdater.promises.addEmailAddress)
  188. done()
  189. }
  190. },
  191. this.next
  192. )
  193. })
  194. it('should pass the error to the next handler when adding the email fails', function(done) {
  195. this.UserUpdater.promises.addEmailAddress.rejects(new Error())
  196. this.UserEmailsController.add(this.req, this.res, error => {
  197. expect(error).to.be.instanceof(Error)
  198. done()
  199. })
  200. })
  201. it('should call the HTTP conflict handler when the email already exists', function(done) {
  202. this.UserUpdater.promises.addEmailAddress.rejects(
  203. new Errors.EmailExistsError()
  204. )
  205. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  206. req.should.exist
  207. res.should.exist
  208. message.should.equal('email_already_registered')
  209. done()
  210. })
  211. this.UserEmailsController.add(this.req, this.res, this.next)
  212. })
  213. it("should call the HTTP conflict handler when there's a domain matching error", function(done) {
  214. this.UserUpdater.promises.addEmailAddress.rejects(
  215. new Error('422: Email does not belong to university')
  216. )
  217. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  218. req.should.exist
  219. res.should.exist
  220. message.should.equal('email_does_not_belong_to_university')
  221. done()
  222. })
  223. this.UserEmailsController.add(this.req, this.res, this.next)
  224. })
  225. })
  226. describe('remove', function() {
  227. beforeEach(function() {
  228. this.email = 'email_to_remove@bar.com'
  229. this.req.body.email = this.email
  230. this.EmailHelper.parseEmail.returns(this.email)
  231. })
  232. it('removes email', function(done) {
  233. this.UserUpdater.removeEmailAddress.callsArgWith(2, null)
  234. this.UserEmailsController.remove(this.req, {
  235. sendStatus: code => {
  236. code.should.equal(200)
  237. assertCalledWith(this.EmailHelper.parseEmail, this.email)
  238. assertCalledWith(
  239. this.UserUpdater.removeEmailAddress,
  240. this.user._id,
  241. this.email
  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.AuthenticationController.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.AuthenticationController.setInSessionUser,
  273. this.req,
  274. { email: this.email }
  275. )
  276. assertCalledWith(
  277. this.UserUpdater.setDefaultEmailAddress,
  278. this.user._id,
  279. this.email
  280. )
  281. done()
  282. }
  283. })
  284. })
  285. it('handles email parse error', function(done) {
  286. this.EmailHelper.parseEmail.returns(null)
  287. this.UserEmailsController.setDefault(this.req, {
  288. sendStatus: code => {
  289. code.should.equal(422)
  290. assertNotCalled(this.UserUpdater.setDefaultEmailAddress)
  291. done()
  292. }
  293. })
  294. })
  295. })
  296. describe('endorse', function() {
  297. beforeEach(function() {
  298. this.email = 'email_to_endorse@bar.com'
  299. this.req.body.email = this.email
  300. this.EmailHelper.parseEmail.returns(this.email)
  301. })
  302. it('endorses affiliation', function(done) {
  303. this.req.body.role = 'Role'
  304. this.req.body.department = 'Department'
  305. this.UserEmailsController.endorse(this.req, {
  306. sendStatus: code => {
  307. code.should.equal(204)
  308. assertCalledWith(
  309. this.endorseAffiliation,
  310. this.user._id,
  311. this.email,
  312. 'Role',
  313. 'Department'
  314. )
  315. done()
  316. }
  317. })
  318. })
  319. })
  320. describe('confirm', function() {
  321. beforeEach(function() {
  322. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  323. .stub()
  324. .yields()
  325. this.res = {
  326. sendStatus: sinon.stub(),
  327. json: sinon.stub()
  328. }
  329. this.res.status = sinon.stub().returns(this.res)
  330. this.next = sinon.stub()
  331. this.token = 'mock-token'
  332. this.req.body = { token: this.token }
  333. })
  334. describe('successfully', function() {
  335. beforeEach(function() {
  336. this.UserEmailsController.confirm(this.req, this.res, this.next)
  337. })
  338. it('should confirm the email from the token', function() {
  339. this.UserEmailsConfirmationHandler.confirmEmailFromToken
  340. .calledWith(this.token)
  341. .should.equal(true)
  342. })
  343. it('should return a 200 status', function() {
  344. this.res.sendStatus.calledWith(200).should.equal(true)
  345. })
  346. })
  347. describe('without a token', function() {
  348. beforeEach(function() {
  349. this.req.body.token = null
  350. this.UserEmailsController.confirm(this.req, this.res, this.next)
  351. })
  352. it('should return a 422 status', function() {
  353. this.res.status.calledWith(422).should.equal(true)
  354. })
  355. })
  356. describe('when confirming fails', function() {
  357. beforeEach(function() {
  358. this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon
  359. .stub()
  360. .yields(new Errors.NotFoundError('not found'))
  361. this.UserEmailsController.confirm(this.req, this.res, this.next)
  362. })
  363. it('should return a 404 error code with a message', function() {
  364. this.res.status.calledWith(404).should.equal(true)
  365. this.res.json
  366. .calledWith({
  367. message: this.req.i18n.translate('confirmation_token_invalid')
  368. })
  369. .should.equal(true)
  370. })
  371. })
  372. })
  373. describe('resendConfirmation', function() {
  374. beforeEach(function() {
  375. this.req = {
  376. body: {}
  377. }
  378. this.res = {
  379. sendStatus: sinon.stub()
  380. }
  381. this.next = sinon.stub()
  382. this.UserEmailsConfirmationHandler.sendConfirmationEmail = sinon
  383. .stub()
  384. .yields()
  385. })
  386. describe('when institution SSO is released', function() {
  387. beforeEach(function() {
  388. this.Features.hasFeature.withArgs('saml').returns(true)
  389. })
  390. describe('for an institution SSO email', function() {
  391. beforeEach(function() {
  392. this.req.body.email = 'with-sso@overleaf.com'
  393. })
  394. it('should not send the email', function() {
  395. this.UserEmailsController.resendConfirmation(
  396. this.req,
  397. this.res,
  398. () => {
  399. this.UserEmailsConfirmationHandler.sendConfirmationEmail.should
  400. .not.have.been.called.once
  401. }
  402. )
  403. })
  404. })
  405. describe('for a non-institution SSO email', function() {
  406. beforeEach(function() {
  407. this.req.body.email = 'without-sso@example.com'
  408. })
  409. it('should send the email', function() {
  410. this.UserEmailsController.resendConfirmation(
  411. this.req,
  412. this.res,
  413. () => {
  414. this.UserEmailsConfirmationHandler.sendConfirmationEmail.should
  415. .have.been.called.once
  416. }
  417. )
  418. })
  419. })
  420. })
  421. describe('when institution SSO is not released', function() {
  422. beforeEach(function() {
  423. this.Features.hasFeature.withArgs('saml').returns(false)
  424. })
  425. describe('for an institution SSO email', function() {
  426. beforeEach(function() {
  427. this.req.body.email = 'with-sso@overleaf.com'
  428. })
  429. it('should send the email', function() {
  430. this.UserEmailsController.resendConfirmation(
  431. this.req,
  432. this.res,
  433. () => {
  434. this.UserEmailsConfirmationHandler.sendConfirmationEmail.should
  435. .have.been.called.once
  436. }
  437. )
  438. })
  439. })
  440. describe('for a non-institution SSO email', function() {
  441. beforeEach(function() {
  442. this.req.body.email = 'without-sso@example.com'
  443. })
  444. it('should send the email', function() {
  445. this.UserEmailsController.resendConfirmation(
  446. this.req,
  447. this.res,
  448. () => {
  449. this.UserEmailsConfirmationHandler.sendConfirmationEmail.should
  450. .have.been.called.once
  451. }
  452. )
  453. })
  454. })
  455. })
  456. })
  457. })