UserControllerTests.js 33 KB

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