UserControllerTests.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath = '../../../../app/src/Features/User/UserController.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. const OError = require('@overleaf/o-error')
  6. const Errors = require('../../../../app/src/Features/Errors/Errors')
  7. describe('UserController', function () {
  8. beforeEach(function () {
  9. this.user_id = '323123'
  10. this.user = {
  11. _id: this.user_id,
  12. email: 'email@overleaf.com',
  13. save: sinon.stub().callsArgWith(0),
  14. ace: {},
  15. }
  16. this.req = {
  17. user: {},
  18. session: {
  19. destroy() {},
  20. user: {
  21. _id: this.user_id,
  22. email: 'old@something.com',
  23. },
  24. analyticsId: this.user_id,
  25. },
  26. sessionID: '123',
  27. body: {},
  28. i18n: {
  29. translate: text => text,
  30. },
  31. ip: '0:0:0:0',
  32. query: {},
  33. headers: {},
  34. }
  35. this.UserDeleter = { deleteUser: sinon.stub().yields() }
  36. this.UserGetter = {
  37. getUser: sinon.stub().callsArgWith(1, null, this.user),
  38. promises: { getUser: sinon.stub().resolves(this.user) },
  39. }
  40. this.User = { findById: sinon.stub().callsArgWith(1, null, this.user) }
  41. this.NewsLetterManager = {
  42. subscribe: sinon.stub().yields(),
  43. unsubscribe: sinon.stub().yields(),
  44. }
  45. this.AuthenticationController = {
  46. establishUserSession: sinon.stub().callsArg(2),
  47. }
  48. this.SessionManager = {
  49. getLoggedInUserId: sinon.stub().returns(this.user._id),
  50. getSessionUser: sinon.stub().returns(this.req.session.user),
  51. setInSessionUser: sinon.stub(),
  52. }
  53. this.AuthenticationManager = {
  54. authenticate: sinon.stub(),
  55. validatePassword: sinon.stub(),
  56. promises: {
  57. authenticate: sinon.stub(),
  58. setUserPassword: sinon.stub(),
  59. },
  60. getMessageForInvalidPasswordError: sinon
  61. .stub()
  62. .returns({ type: 'error', key: 'some-key' }),
  63. }
  64. this.UserUpdater = {
  65. changeEmailAddress: sinon.stub(),
  66. promises: {
  67. confirmEmail: sinon.stub().resolves(),
  68. addAffiliationForNewUser: sinon.stub().resolves(),
  69. },
  70. }
  71. this.settings = { siteUrl: 'sharelatex.example.com' }
  72. this.UserHandler = { populateTeamInvites: sinon.stub().callsArgWith(1) }
  73. this.UserSessionsManager = {
  74. trackSession: sinon.stub(),
  75. untrackSession: sinon.stub(),
  76. revokeAllUserSessions: sinon.stub().callsArgWith(2, null),
  77. promises: {
  78. getAllUserSessions: sinon.stub().resolves(),
  79. revokeAllUserSessions: sinon.stub().resolves(),
  80. },
  81. }
  82. this.HttpErrorHandler = {
  83. badRequest: sinon.stub(),
  84. conflict: sinon.stub(),
  85. unprocessableEntity: sinon.stub(),
  86. legacyInternal: sinon.stub(),
  87. }
  88. this.UrlHelper = {
  89. getSafeRedirectPath: sinon.stub(),
  90. }
  91. this.UrlHelper.getSafeRedirectPath
  92. .withArgs('https://evil.com')
  93. .returns(undefined)
  94. this.UrlHelper.getSafeRedirectPath.returnsArg(0)
  95. this.acceptsJson = sinon.stub().returns(false)
  96. this.UserController = SandboxedModule.require(modulePath, {
  97. requires: {
  98. '../Helpers/UrlHelper': this.UrlHelper,
  99. './UserGetter': this.UserGetter,
  100. './UserDeleter': this.UserDeleter,
  101. './UserUpdater': this.UserUpdater,
  102. '../../models/User': {
  103. User: this.User,
  104. },
  105. '../Newsletter/NewsletterManager': this.NewsLetterManager,
  106. '../Authentication/AuthenticationController':
  107. this.AuthenticationController,
  108. '../Authentication/SessionManager': this.SessionManager,
  109. '../Authentication/AuthenticationManager': this.AuthenticationManager,
  110. '../../infrastructure/Features': (this.Features = {
  111. hasFeature: sinon.stub(),
  112. }),
  113. './UserAuditLogHandler': (this.UserAuditLogHandler = {
  114. promises: {
  115. addEntry: sinon.stub().resolves(),
  116. },
  117. }),
  118. './UserHandler': this.UserHandler,
  119. './UserSessionsManager': this.UserSessionsManager,
  120. '../Errors/HttpErrorHandler': this.HttpErrorHandler,
  121. '@overleaf/settings': this.settings,
  122. '@overleaf/metrics': {
  123. inc() {},
  124. },
  125. '@overleaf/o-error': OError,
  126. '../Email/EmailHandler': (this.EmailHandler = {
  127. sendEmail: sinon.stub(),
  128. promises: { sendEmail: sinon.stub().resolves() },
  129. }),
  130. '../../infrastructure/RequestContentTypeDetection': {
  131. acceptsJson: this.acceptsJson,
  132. },
  133. },
  134. })
  135. this.res = {
  136. send: sinon.stub(),
  137. status: sinon.stub(),
  138. sendStatus: sinon.stub(),
  139. json: sinon.stub(),
  140. }
  141. this.res.status.returns(this.res)
  142. this.next = sinon.stub()
  143. this.callback = sinon.stub()
  144. })
  145. describe('tryDeleteUser', function () {
  146. beforeEach(function () {
  147. this.req.body.password = 'wat'
  148. this.req.logout = sinon.stub()
  149. this.req.session.destroy = sinon.stub().callsArgWith(0, null)
  150. this.SessionManager.getLoggedInUserId = sinon
  151. .stub()
  152. .returns(this.user._id)
  153. this.AuthenticationManager.authenticate = sinon
  154. .stub()
  155. .callsArgWith(2, null, this.user)
  156. })
  157. it('should send 200', function (done) {
  158. this.res.sendStatus = code => {
  159. code.should.equal(200)
  160. done()
  161. }
  162. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  163. })
  164. it('should try to authenticate user', function (done) {
  165. this.res.sendStatus = code => {
  166. this.AuthenticationManager.authenticate.callCount.should.equal(1)
  167. this.AuthenticationManager.authenticate
  168. .calledWith({ _id: this.user._id }, this.req.body.password)
  169. .should.equal(true)
  170. done()
  171. }
  172. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  173. })
  174. it('should delete the user', function (done) {
  175. this.res.sendStatus = code => {
  176. this.UserDeleter.deleteUser.callCount.should.equal(1)
  177. this.UserDeleter.deleteUser.calledWith(this.user._id).should.equal(true)
  178. done()
  179. }
  180. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  181. })
  182. describe('when no password is supplied', function () {
  183. beforeEach(function () {
  184. this.req.body.password = ''
  185. })
  186. it('should return 403', function (done) {
  187. this.res.sendStatus = code => {
  188. code.should.equal(403)
  189. done()
  190. }
  191. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  192. })
  193. })
  194. describe('when authenticate produces an error', function () {
  195. beforeEach(function () {
  196. this.AuthenticationManager.authenticate = sinon
  197. .stub()
  198. .callsArgWith(2, new Error('woops'))
  199. })
  200. it('should call next with an error', function (done) {
  201. this.next = err => {
  202. expect(err).to.not.equal(null)
  203. expect(err).to.be.instanceof(Error)
  204. done()
  205. }
  206. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  207. })
  208. })
  209. describe('when authenticate does not produce a user', function () {
  210. beforeEach(function () {
  211. this.AuthenticationManager.authenticate = sinon
  212. .stub()
  213. .callsArgWith(2, null, null)
  214. })
  215. it('should return 403', function (done) {
  216. this.res.sendStatus = code => {
  217. code.should.equal(403)
  218. done()
  219. }
  220. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  221. })
  222. })
  223. describe('when deleteUser produces an error', function () {
  224. beforeEach(function () {
  225. this.UserDeleter.deleteUser = sinon.stub().yields(new Error('woops'))
  226. })
  227. it('should call next with an error', function (done) {
  228. this.next = err => {
  229. expect(err).to.not.equal(null)
  230. expect(err).to.be.instanceof(Error)
  231. done()
  232. }
  233. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  234. })
  235. })
  236. describe('when deleteUser produces a known error', function () {
  237. beforeEach(function () {
  238. this.UserDeleter.deleteUser = sinon
  239. .stub()
  240. .yields(new Errors.SubscriptionAdminDeletionError())
  241. })
  242. it('should return a HTTP Unprocessable Entity error', function (done) {
  243. this.HttpErrorHandler.unprocessableEntity = sinon.spy(
  244. (req, res, message, info) => {
  245. expect(req).to.exist
  246. expect(res).to.exist
  247. expect(message).to.equal('error while deleting user account')
  248. expect(info).to.deep.equal({
  249. error: 'SubscriptionAdminDeletionError',
  250. })
  251. done()
  252. }
  253. )
  254. this.UserController.tryDeleteUser(this.req, this.res)
  255. })
  256. })
  257. describe('when session.destroy produces an error', function () {
  258. beforeEach(function () {
  259. this.req.session.destroy = sinon
  260. .stub()
  261. .callsArgWith(0, new Error('woops'))
  262. })
  263. it('should call next with an error', function (done) {
  264. this.next = err => {
  265. expect(err).to.not.equal(null)
  266. expect(err).to.be.instanceof(Error)
  267. done()
  268. }
  269. this.UserController.tryDeleteUser(this.req, this.res, this.next)
  270. })
  271. })
  272. })
  273. describe('subscribe', function () {
  274. it('should send the user to subscribe', function (done) {
  275. this.res.json = data => {
  276. expect(data.message).to.equal('thanks_settings_updated')
  277. this.NewsLetterManager.subscribe
  278. .calledWith(this.user)
  279. .should.equal(true)
  280. done()
  281. }
  282. this.UserController.subscribe(this.req, this.res)
  283. })
  284. })
  285. describe('unsubscribe', function () {
  286. it('should send the user to unsubscribe', function (done) {
  287. this.res.json = data => {
  288. expect(data.message).to.equal('thanks_settings_updated')
  289. this.NewsLetterManager.unsubscribe
  290. .calledWith(this.user)
  291. .should.equal(true)
  292. done()
  293. }
  294. this.UserController.unsubscribe(this.req, this.res)
  295. })
  296. })
  297. describe('updateUserSettings', function () {
  298. beforeEach(function () {
  299. this.auditLog = { initiatorId: this.user_id, ipAddress: this.req.ip }
  300. this.newEmail = 'hello@world.com'
  301. this.req.externalAuthenticationSystemUsed = sinon.stub().returns(false)
  302. })
  303. it('should call save', function (done) {
  304. this.req.body = {}
  305. this.res.sendStatus = code => {
  306. this.user.save.called.should.equal(true)
  307. done()
  308. }
  309. this.UserController.updateUserSettings(this.req, this.res)
  310. })
  311. it('should set the first name', function (done) {
  312. this.req.body = { first_name: 'bobby ' }
  313. this.res.sendStatus = code => {
  314. this.user.first_name.should.equal('bobby')
  315. done()
  316. }
  317. this.UserController.updateUserSettings(this.req, this.res)
  318. })
  319. it('should set the role', function (done) {
  320. this.req.body = { role: 'student' }
  321. this.res.sendStatus = code => {
  322. this.user.role.should.equal('student')
  323. done()
  324. }
  325. this.UserController.updateUserSettings(this.req, this.res)
  326. })
  327. it('should set the institution', function (done) {
  328. this.req.body = { institution: 'MIT' }
  329. this.res.sendStatus = code => {
  330. this.user.institution.should.equal('MIT')
  331. done()
  332. }
  333. this.UserController.updateUserSettings(this.req, this.res)
  334. })
  335. it('should set some props on ace', function (done) {
  336. this.req.body = { editorTheme: 'something' }
  337. this.res.sendStatus = code => {
  338. this.user.ace.theme.should.equal('something')
  339. done()
  340. }
  341. this.UserController.updateUserSettings(this.req, this.res)
  342. })
  343. it('should set the overall theme', function (done) {
  344. this.req.body = { overallTheme: 'green-ish' }
  345. this.res.sendStatus = code => {
  346. this.user.ace.overallTheme.should.equal('green-ish')
  347. done()
  348. }
  349. this.UserController.updateUserSettings(this.req, this.res)
  350. })
  351. it('should send an error if the email is 0 len', function (done) {
  352. this.req.body.email = ''
  353. this.res.sendStatus = function (code) {
  354. code.should.equal(400)
  355. done()
  356. }
  357. this.UserController.updateUserSettings(this.req, this.res)
  358. })
  359. it('should send an error if the email does not contain an @', function (done) {
  360. this.req.body.email = 'bob at something dot com'
  361. this.res.sendStatus = function (code) {
  362. code.should.equal(400)
  363. done()
  364. }
  365. this.UserController.updateUserSettings(this.req, this.res)
  366. })
  367. it('should call the user updater with the new email and user _id', function (done) {
  368. this.req.body.email = this.newEmail.toUpperCase()
  369. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  370. this.res.sendStatus = code => {
  371. code.should.equal(200)
  372. this.UserUpdater.changeEmailAddress
  373. .calledWith(this.user_id, this.newEmail, this.auditLog)
  374. .should.equal(true)
  375. done()
  376. }
  377. this.UserController.updateUserSettings(this.req, this.res)
  378. })
  379. it('should update the email on the session', function (done) {
  380. this.req.body.email = this.newEmail.toUpperCase()
  381. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  382. let callcount = 0
  383. this.User.findById = (id, cb) => {
  384. if (++callcount === 2) {
  385. this.user.email = this.newEmail
  386. }
  387. cb(null, this.user)
  388. }
  389. this.res.sendStatus = code => {
  390. code.should.equal(200)
  391. this.SessionManager.setInSessionUser
  392. .calledWith(this.req.session, {
  393. email: this.newEmail,
  394. first_name: undefined,
  395. last_name: undefined,
  396. })
  397. .should.equal(true)
  398. done()
  399. }
  400. this.UserController.updateUserSettings(this.req, this.res)
  401. })
  402. it('should call populateTeamInvites', function (done) {
  403. this.req.body.email = this.newEmail.toUpperCase()
  404. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  405. this.res.sendStatus = code => {
  406. code.should.equal(200)
  407. this.UserHandler.populateTeamInvites
  408. .calledWith(this.user)
  409. .should.equal(true)
  410. done()
  411. }
  412. this.UserController.updateUserSettings(this.req, this.res)
  413. })
  414. describe('when changeEmailAddress yields an error', function () {
  415. it('should pass on an error and not send a success status', function (done) {
  416. this.req.body.email = this.newEmail.toUpperCase()
  417. this.UserUpdater.changeEmailAddress.callsArgWith(3, new OError())
  418. this.HttpErrorHandler.legacyInternal = sinon.spy(
  419. (req, res, message, error) => {
  420. expect(req).to.exist
  421. expect(req).to.exist
  422. message.should.equal('problem_changing_email_address')
  423. expect(error).to.be.instanceof(OError)
  424. done()
  425. }
  426. )
  427. this.UserController.updateUserSettings(this.req, this.res, this.next)
  428. })
  429. it('should call the HTTP conflict error handler when the email already exists', function (done) {
  430. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  431. expect(req).to.exist
  432. expect(req).to.exist
  433. message.should.equal('email_already_registered')
  434. done()
  435. })
  436. this.req.body.email = this.newEmail.toUpperCase()
  437. this.UserUpdater.changeEmailAddress.callsArgWith(
  438. 3,
  439. new Errors.EmailExistsError()
  440. )
  441. this.UserController.updateUserSettings(this.req, this.res)
  442. })
  443. })
  444. describe('when using an external auth source', function () {
  445. beforeEach(function () {
  446. this.UserUpdater.changeEmailAddress.callsArgWith(2)
  447. this.newEmail = 'someone23@example.com'
  448. this.req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
  449. })
  450. it('should not set a new email', function (done) {
  451. this.req.body.email = this.newEmail
  452. this.res.sendStatus = code => {
  453. code.should.equal(200)
  454. this.UserUpdater.changeEmailAddress
  455. .calledWith(this.user_id, this.newEmail)
  456. .should.equal(false)
  457. done()
  458. }
  459. this.UserController.updateUserSettings(this.req, this.res)
  460. })
  461. })
  462. })
  463. describe('logout', function () {
  464. beforeEach(function () {
  465. this.acceptsJson.returns(false)
  466. })
  467. it('should destroy the session', function (done) {
  468. this.req.session.destroy = sinon.stub().callsArgWith(0)
  469. this.res.redirect = url => {
  470. url.should.equal('/login')
  471. this.req.session.destroy.called.should.equal(true)
  472. done()
  473. }
  474. this.UserController.logout(this.req, this.res)
  475. })
  476. it('should untrack session', function (done) {
  477. this.req.session.destroy = sinon.stub().callsArgWith(0)
  478. this.res.redirect = url => {
  479. url.should.equal('/login')
  480. this.UserSessionsManager.untrackSession.callCount.should.equal(1)
  481. this.UserSessionsManager.untrackSession
  482. .calledWith(sinon.match(this.req.user), this.req.sessionID)
  483. .should.equal(true)
  484. done()
  485. }
  486. this.UserController.logout(this.req, this.res)
  487. })
  488. it('should redirect after logout', function (done) {
  489. this.req.body.redirect = '/institutional-login'
  490. this.req.session.destroy = sinon.stub().callsArgWith(0)
  491. this.res.redirect = url => {
  492. url.should.equal(this.req.body.redirect)
  493. done()
  494. }
  495. this.UserController.logout(this.req, this.res)
  496. })
  497. it('should redirect after logout, but not to evil.com', function (done) {
  498. this.req.body.redirect = 'https://evil.com'
  499. this.req.session.destroy = sinon.stub().callsArgWith(0)
  500. this.res.redirect = url => {
  501. url.should.equal('/login')
  502. done()
  503. }
  504. this.UserController.logout(this.req, this.res)
  505. })
  506. it('should redirect to login after logout when no redirect set', function (done) {
  507. this.req.session.destroy = sinon.stub().callsArgWith(0)
  508. this.res.redirect = url => {
  509. url.should.equal('/login')
  510. done()
  511. }
  512. this.UserController.logout(this.req, this.res)
  513. })
  514. it('should send json with redir property for json request', function (done) {
  515. this.acceptsJson.returns(true)
  516. this.req.session.destroy = sinon.stub().callsArgWith(0)
  517. this.res.status = code => {
  518. code.should.equal(200)
  519. return this.res
  520. }
  521. this.res.json = data => {
  522. data.redir.should.equal('/login')
  523. done()
  524. }
  525. this.UserController.logout(this.req, this.res)
  526. })
  527. })
  528. describe('clearSessions', function () {
  529. describe('success', function () {
  530. it('should call revokeAllUserSessions', function (done) {
  531. this.res.sendStatus.callsFake(() => {
  532. this.UserSessionsManager.promises.revokeAllUserSessions.callCount.should.equal(
  533. 1
  534. )
  535. done()
  536. })
  537. this.UserController.clearSessions(this.req, this.res)
  538. })
  539. it('send a 201 response', function (done) {
  540. this.res.sendStatus.callsFake(status => {
  541. status.should.equal(201)
  542. done()
  543. })
  544. this.UserController.clearSessions(this.req, this.res)
  545. })
  546. it('sends a security alert email', function (done) {
  547. this.res.sendStatus.callsFake(status => {
  548. this.EmailHandler.promises.sendEmail.callCount.should.equal(1)
  549. const expectedArg = {
  550. to: this.user.email,
  551. actionDescribed: `active sessions were cleared on your account ${this.user.email}`,
  552. action: 'active sessions cleared',
  553. }
  554. const emailCall = this.EmailHandler.promises.sendEmail.lastCall
  555. expect(emailCall.args[0]).to.equal('securityAlert')
  556. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  557. done()
  558. })
  559. this.UserController.clearSessions(this.req, this.res)
  560. })
  561. })
  562. describe('errors', function () {
  563. describe('when getAllUserSessions produces an error', function () {
  564. it('should return an error', function (done) {
  565. this.UserSessionsManager.promises.getAllUserSessions.rejects(
  566. new Error('woops')
  567. )
  568. this.UserController.clearSessions(this.req, this.res, error => {
  569. expect(error).to.be.instanceof(Error)
  570. done()
  571. })
  572. })
  573. })
  574. describe('when audit log addEntry produces an error', function () {
  575. it('should call next with an error', function (done) {
  576. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('woops'))
  577. this.UserController.clearSessions(this.req, this.res, error => {
  578. expect(error).to.be.instanceof(Error)
  579. done()
  580. })
  581. })
  582. })
  583. describe('when revokeAllUserSessions produces an error', function () {
  584. it('should call next with an error', function (done) {
  585. this.UserSessionsManager.promises.revokeAllUserSessions.rejects(
  586. new Error('woops')
  587. )
  588. this.UserController.clearSessions(this.req, this.res, error => {
  589. expect(error).to.be.instanceof(Error)
  590. done()
  591. })
  592. })
  593. })
  594. describe('when EmailHandler produces an error', function () {
  595. const anError = new Error('oops')
  596. it('send a 201 response but log error', function (done) {
  597. this.EmailHandler.promises.sendEmail.rejects(anError)
  598. this.res.sendStatus.callsFake(status => {
  599. status.should.equal(201)
  600. this.logger.error.callCount.should.equal(1)
  601. const loggerCall = this.logger.error.getCall(0)
  602. expect(loggerCall.args[0]).to.deep.equal({
  603. error: anError,
  604. userId: this.user_id,
  605. })
  606. expect(loggerCall.args[1]).to.contain(
  607. 'could not send security alert email when sessions cleared'
  608. )
  609. done()
  610. })
  611. this.UserController.clearSessions(this.req, this.res)
  612. })
  613. })
  614. })
  615. })
  616. describe('changePassword', function () {
  617. describe('success', function () {
  618. beforeEach(function () {
  619. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  620. this.AuthenticationManager.promises.setUserPassword.resolves()
  621. this.req.body = {
  622. newPassword1: 'newpass',
  623. newPassword2: 'newpass',
  624. }
  625. })
  626. it('should set the new password if they do match', function (done) {
  627. this.res.json.callsFake(() => {
  628. this.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith(
  629. this.user,
  630. 'newpass'
  631. )
  632. done()
  633. })
  634. this.UserController.changePassword(this.req, this.res)
  635. })
  636. it('should log the update', function (done) {
  637. this.res.json.callsFake(() => {
  638. this.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
  639. this.user._id,
  640. 'update-password',
  641. this.user._id,
  642. this.req.ip
  643. )
  644. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  645. 1
  646. )
  647. done()
  648. })
  649. this.UserController.changePassword(this.req, this.res)
  650. })
  651. it('should send security alert email', function (done) {
  652. this.res.json.callsFake(() => {
  653. const expectedArg = {
  654. to: this.user.email,
  655. actionDescribed: `your password has been changed on your account ${this.user.email}`,
  656. action: 'password changed',
  657. }
  658. const emailCall = this.EmailHandler.sendEmail.lastCall
  659. expect(emailCall.args[0]).to.equal('securityAlert')
  660. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  661. done()
  662. })
  663. this.UserController.changePassword(this.req, this.res)
  664. })
  665. })
  666. describe('errors', function () {
  667. it('should check the old password is the current one at the moment', function (done) {
  668. this.AuthenticationManager.promises.authenticate.resolves()
  669. this.req.body = { currentPassword: 'oldpasshere' }
  670. this.HttpErrorHandler.badRequest.callsFake(() => {
  671. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  672. this.req,
  673. this.res,
  674. 'password_change_old_password_wrong'
  675. )
  676. this.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  677. { _id: this.user._id },
  678. 'oldpasshere'
  679. )
  680. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  681. 0
  682. )
  683. done()
  684. })
  685. this.UserController.changePassword(this.req, this.res)
  686. })
  687. it('it should not set the new password if they do not match', function (done) {
  688. this.AuthenticationManager.promises.authenticate.resolves({})
  689. this.req.body = {
  690. newPassword1: '1',
  691. newPassword2: '2',
  692. }
  693. this.HttpErrorHandler.badRequest.callsFake(() => {
  694. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  695. this.req,
  696. this.res,
  697. 'password_change_passwords_do_not_match'
  698. )
  699. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  700. 0
  701. )
  702. done()
  703. })
  704. this.UserController.changePassword(this.req, this.res)
  705. })
  706. it('it should not set the new password if it is invalid', function (done) {
  707. // this.AuthenticationManager.validatePassword = sinon
  708. // .stub()
  709. // .returns({ message: 'validation-error' })
  710. const err = new Error('bad')
  711. err.name = 'InvalidPasswordError'
  712. const message = {
  713. type: 'error',
  714. key: 'some-message-key',
  715. }
  716. this.AuthenticationManager.getMessageForInvalidPasswordError.returns(
  717. message
  718. )
  719. this.AuthenticationManager.promises.setUserPassword.rejects(err)
  720. this.AuthenticationManager.promises.authenticate.resolves({})
  721. this.req.body = {
  722. newPassword1: 'newpass',
  723. newPassword2: 'newpass',
  724. }
  725. this.res.json.callsFake(result => {
  726. expect(result.message).to.deep.equal(message)
  727. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  728. 1
  729. )
  730. done()
  731. })
  732. this.UserController.changePassword(this.req, this.res)
  733. })
  734. describe('UserAuditLogHandler error', function () {
  735. it('should return error and not update password', function (done) {
  736. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops'))
  737. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  738. this.AuthenticationManager.promises.setUserPassword.resolves()
  739. this.req.body = {
  740. newPassword1: 'newpass',
  741. newPassword2: 'newpass',
  742. }
  743. this.UserController.changePassword(this.req, this.res, error => {
  744. expect(error).to.be.instanceof(Error)
  745. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  746. 1
  747. )
  748. done()
  749. })
  750. })
  751. })
  752. describe('EmailHandler error', function () {
  753. const anError = new Error('oops')
  754. beforeEach(function () {
  755. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  756. this.AuthenticationManager.promises.setUserPassword.resolves()
  757. this.req.body = {
  758. newPassword1: 'newpass',
  759. newPassword2: 'newpass',
  760. }
  761. this.EmailHandler.sendEmail.yields(anError)
  762. })
  763. it('should not return error but should log it', function (done) {
  764. this.res.json.callsFake(result => {
  765. expect(result.message.type).to.equal('success')
  766. this.logger.error.callCount.should.equal(1)
  767. expect(this.logger.error).to.have.been.calledWithExactly(
  768. {
  769. error: anError,
  770. userId: this.user_id,
  771. },
  772. 'could not send security alert email when password changed'
  773. )
  774. done()
  775. })
  776. this.UserController.changePassword(this.req, this.res)
  777. })
  778. })
  779. })
  780. })
  781. describe('ensureAffiliationMiddleware', function () {
  782. describe('without affiliations feature', function () {
  783. beforeEach(async function () {
  784. await this.UserController.promises.ensureAffiliationMiddleware(
  785. this.req,
  786. this.res,
  787. this.next
  788. )
  789. })
  790. it('should not run affiliation check', function () {
  791. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  792. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  793. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  794. .been.called
  795. })
  796. it('should not return an error', function () {
  797. expect(this.next).to.be.calledWith()
  798. })
  799. })
  800. describe('without ensureAffiliation query parameter', function () {
  801. beforeEach(async function () {
  802. this.Features.hasFeature.withArgs('affiliations').returns(true)
  803. await this.UserController.promises.ensureAffiliationMiddleware(
  804. this.req,
  805. this.res,
  806. this.next
  807. )
  808. })
  809. it('should not run middleware', function () {
  810. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  811. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  812. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  813. .been.called
  814. })
  815. it('should not return an error', function () {
  816. expect(this.next).to.be.calledWith()
  817. })
  818. })
  819. describe('no flagged email', function () {
  820. beforeEach(async function () {
  821. const email = 'unit-test@overleaf.com'
  822. this.user.email = email
  823. this.user.emails = [
  824. {
  825. email,
  826. },
  827. ]
  828. this.Features.hasFeature.withArgs('affiliations').returns(true)
  829. this.req.query.ensureAffiliation = true
  830. await this.UserController.promises.ensureAffiliationMiddleware(
  831. this.req,
  832. this.res,
  833. this.next
  834. )
  835. })
  836. it('should get the user', function () {
  837. expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
  838. this.user._id
  839. )
  840. })
  841. it('should not try to add affiliation or update user', function () {
  842. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  843. .been.called
  844. })
  845. it('should not return an error', function () {
  846. expect(this.next).to.be.calledWith()
  847. })
  848. })
  849. describe('flagged non-SSO email', function () {
  850. let emailFlagged
  851. beforeEach(async function () {
  852. emailFlagged = 'flagged@overleaf.com'
  853. this.user.email = emailFlagged
  854. this.user.emails = [
  855. {
  856. email: emailFlagged,
  857. affiliationUnchecked: true,
  858. },
  859. ]
  860. this.Features.hasFeature.withArgs('affiliations').returns(true)
  861. this.req.query.ensureAffiliation = true
  862. this.req.assertPermission = sinon.stub()
  863. await this.UserController.promises.ensureAffiliationMiddleware(
  864. this.req,
  865. this.res,
  866. this.next
  867. )
  868. })
  869. it('should check the user has permission', function () {
  870. expect(this.req.assertPermission).to.have.been.calledWith(
  871. 'add-affiliation'
  872. )
  873. })
  874. it('should unflag the emails but not confirm', function () {
  875. expect(
  876. this.UserUpdater.promises.addAffiliationForNewUser
  877. ).to.have.been.calledWith(this.user._id, emailFlagged)
  878. expect(
  879. this.UserUpdater.promises.confirmEmail
  880. ).to.not.have.been.calledWith(this.user._id, emailFlagged)
  881. })
  882. it('should not return an error', function () {
  883. expect(this.next).to.be.calledWith()
  884. })
  885. })
  886. describe('flagged SSO email', function () {
  887. let emailFlagged
  888. beforeEach(async function () {
  889. emailFlagged = 'flagged@overleaf.com'
  890. this.user.email = emailFlagged
  891. this.user.emails = [
  892. {
  893. email: emailFlagged,
  894. affiliationUnchecked: true,
  895. samlProviderId: '123',
  896. },
  897. ]
  898. this.Features.hasFeature.withArgs('affiliations').returns(true)
  899. this.req.query.ensureAffiliation = true
  900. this.req.assertPermission = sinon.stub()
  901. await this.UserController.promises.ensureAffiliationMiddleware(
  902. this.req,
  903. this.res,
  904. this.next
  905. )
  906. })
  907. it('should check the user has permission', function () {
  908. expect(this.req.assertPermission).to.have.been.calledWith(
  909. 'add-affiliation'
  910. )
  911. })
  912. it('should add affiliation to v1, unflag and confirm on v2', function () {
  913. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not
  914. .been.called
  915. expect(this.UserUpdater.promises.confirmEmail).to.have.been.calledWith(
  916. this.user._id,
  917. emailFlagged
  918. )
  919. })
  920. it('should not return an error', function () {
  921. expect(this.next).to.be.calledWith()
  922. })
  923. })
  924. describe('when v1 returns an error', function () {
  925. let emailFlagged
  926. beforeEach(async function () {
  927. this.UserUpdater.promises.addAffiliationForNewUser.rejects()
  928. emailFlagged = 'flagged@overleaf.com'
  929. this.user.email = emailFlagged
  930. this.user.emails = [
  931. {
  932. email: emailFlagged,
  933. affiliationUnchecked: true,
  934. },
  935. ]
  936. this.Features.hasFeature.withArgs('affiliations').returns(true)
  937. this.req.query.ensureAffiliation = true
  938. this.req.assertPermission = sinon.stub()
  939. await this.UserController.promises.ensureAffiliationMiddleware(
  940. this.req,
  941. this.res,
  942. this.next
  943. )
  944. })
  945. it('should check the user has permission', function () {
  946. expect(this.req.assertPermission).to.have.been.calledWith(
  947. 'add-affiliation'
  948. )
  949. })
  950. it('should return the error', function () {
  951. expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
  952. })
  953. })
  954. })
  955. })