UserControllerTests.js 34 KB

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