UserControllerTests.js 33 KB

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