UserControllerTests.js 33 KB

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