UserControllerTests.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const modulePath = '../../../../app/src/Features/User/UserController.js'
  5. const SandboxedModule = require('sandboxed-module')
  6. const OError = require('@overleaf/o-error')
  7. const Errors = require('../../../../app/src/Features/Errors/Errors')
  8. describe('UserController', function() {
  9. beforeEach(function() {
  10. this.user_id = '323123'
  11. this.user = {
  12. _id: this.user_id,
  13. email: 'email@overleaf.com',
  14. save: sinon.stub().callsArgWith(0),
  15. ace: {}
  16. }
  17. this.req = {
  18. user: {},
  19. session: {
  20. destroy() {},
  21. user: {
  22. _id: this.user_id,
  23. email: 'old@something.com'
  24. }
  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. getLoggedInUserId: sinon.stub().returns(this.user._id),
  45. getSessionUser: sinon.stub().returns(this.req.session.user),
  46. setInSessionUser: sinon.stub()
  47. }
  48. this.AuthenticationManager = {
  49. authenticate: sinon.stub(),
  50. validatePassword: sinon.stub(),
  51. promises: {
  52. authenticate: sinon.stub(),
  53. setUserPassword: sinon.stub()
  54. }
  55. }
  56. this.UserUpdater = {
  57. changeEmailAddress: sinon.stub(),
  58. promises: {
  59. confirmEmail: sinon.stub().resolves(),
  60. addAffiliationForNewUser: sinon.stub().resolves()
  61. }
  62. }
  63. this.settings = { siteUrl: 'sharelatex.example.com' }
  64. this.UserHandler = { populateTeamInvites: sinon.stub().callsArgWith(1) }
  65. this.UserSessionsManager = {
  66. trackSession: sinon.stub(),
  67. untrackSession: sinon.stub(),
  68. revokeAllUserSessions: sinon.stub().callsArgWith(2, null),
  69. promises: {
  70. getAllUserSessions: sinon.stub().resolves(),
  71. revokeAllUserSessions: sinon.stub().resolves()
  72. }
  73. }
  74. this.HttpErrorHandler = {
  75. badRequest: sinon.stub(),
  76. conflict: sinon.stub(),
  77. unprocessableEntity: sinon.stub(),
  78. legacyInternal: sinon.stub()
  79. }
  80. this.UrlHelper = {
  81. getSafeRedirectPath: sinon.stub()
  82. }
  83. this.UrlHelper.getSafeRedirectPath
  84. .withArgs('https://evil.com')
  85. .returns(undefined)
  86. this.UrlHelper.getSafeRedirectPath.returnsArg(0)
  87. this.UserController = SandboxedModule.require(modulePath, {
  88. globals: {
  89. console: console
  90. },
  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. './UserRegistrationHandler': this.UserRegistrationHandler,
  101. '../Authentication/AuthenticationController': this
  102. .AuthenticationController,
  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. 'settings-sharelatex': this.settings,
  116. 'logger-sharelatex': (this.logger = {
  117. log() {},
  118. warn() {},
  119. err() {},
  120. error: sinon.stub()
  121. }),
  122. '@overleaf/metrics': {
  123. inc() {}
  124. },
  125. '@overleaf/o-error': OError,
  126. '../Email/EmailHandler': (this.EmailHandler = {
  127. sendEmail: sinon.stub(),
  128. promises: { sendEmail: sinon.stub().resolves() }
  129. })
  130. }
  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.AuthenticationController.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('unsubscribe', function() {
  271. it('should send the user to unsubscribe', function(done) {
  272. this.res.sendStatus = () => {
  273. this.NewsLetterManager.unsubscribe
  274. .calledWith(this.user)
  275. .should.equal(true)
  276. done()
  277. }
  278. this.UserController.unsubscribe(this.req, this.res)
  279. })
  280. })
  281. describe('updateUserSettings', function() {
  282. beforeEach(function() {
  283. this.auditLog = { initiatorId: this.user_id, ipAddress: this.req.ip }
  284. this.newEmail = 'hello@world.com'
  285. this.req.externalAuthenticationSystemUsed = sinon.stub().returns(false)
  286. })
  287. it('should call save', function(done) {
  288. this.req.body = {}
  289. this.res.sendStatus = code => {
  290. this.user.save.called.should.equal(true)
  291. done()
  292. }
  293. this.UserController.updateUserSettings(this.req, this.res)
  294. })
  295. it('should set the first name', function(done) {
  296. this.req.body = { first_name: 'bobby ' }
  297. this.res.sendStatus = code => {
  298. this.user.first_name.should.equal('bobby')
  299. done()
  300. }
  301. this.UserController.updateUserSettings(this.req, this.res)
  302. })
  303. it('should set the role', function(done) {
  304. this.req.body = { role: 'student' }
  305. this.res.sendStatus = code => {
  306. this.user.role.should.equal('student')
  307. done()
  308. }
  309. this.UserController.updateUserSettings(this.req, this.res)
  310. })
  311. it('should set the institution', function(done) {
  312. this.req.body = { institution: 'MIT' }
  313. this.res.sendStatus = code => {
  314. this.user.institution.should.equal('MIT')
  315. done()
  316. }
  317. this.UserController.updateUserSettings(this.req, this.res)
  318. })
  319. it('should set some props on ace', function(done) {
  320. this.req.body = { editorTheme: 'something' }
  321. this.res.sendStatus = code => {
  322. this.user.ace.theme.should.equal('something')
  323. done()
  324. }
  325. this.UserController.updateUserSettings(this.req, this.res)
  326. })
  327. it('should set the overall theme', function(done) {
  328. this.req.body = { overallTheme: 'green-ish' }
  329. this.res.sendStatus = code => {
  330. this.user.ace.overallTheme.should.equal('green-ish')
  331. done()
  332. }
  333. this.UserController.updateUserSettings(this.req, this.res)
  334. })
  335. it('should send an error if the email is 0 len', function(done) {
  336. this.req.body.email = ''
  337. this.res.sendStatus = function(code) {
  338. code.should.equal(400)
  339. done()
  340. }
  341. this.UserController.updateUserSettings(this.req, this.res)
  342. })
  343. it('should send an error if the email does not contain an @', function(done) {
  344. this.req.body.email = 'bob at something dot com'
  345. this.res.sendStatus = function(code) {
  346. code.should.equal(400)
  347. done()
  348. }
  349. this.UserController.updateUserSettings(this.req, this.res)
  350. })
  351. it('should call the user updater with the new email and user _id', function(done) {
  352. this.req.body.email = this.newEmail.toUpperCase()
  353. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  354. this.res.sendStatus = code => {
  355. code.should.equal(200)
  356. this.UserUpdater.changeEmailAddress
  357. .calledWith(this.user_id, this.newEmail, this.auditLog)
  358. .should.equal(true)
  359. done()
  360. }
  361. this.UserController.updateUserSettings(this.req, this.res)
  362. })
  363. it('should update the email on the session', function(done) {
  364. this.req.body.email = this.newEmail.toUpperCase()
  365. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  366. let callcount = 0
  367. this.User.findById = (id, cb) => {
  368. if (++callcount === 2) {
  369. this.user.email = this.newEmail
  370. }
  371. cb(null, this.user)
  372. }
  373. this.res.sendStatus = code => {
  374. code.should.equal(200)
  375. this.AuthenticationController.setInSessionUser
  376. .calledWith(this.req, {
  377. email: this.newEmail,
  378. first_name: undefined,
  379. last_name: undefined
  380. })
  381. .should.equal(true)
  382. done()
  383. }
  384. this.UserController.updateUserSettings(this.req, this.res)
  385. })
  386. it('should call populateTeamInvites', function(done) {
  387. this.req.body.email = this.newEmail.toUpperCase()
  388. this.UserUpdater.changeEmailAddress.callsArgWith(3)
  389. this.res.sendStatus = code => {
  390. code.should.equal(200)
  391. this.UserHandler.populateTeamInvites
  392. .calledWith(this.user)
  393. .should.equal(true)
  394. done()
  395. }
  396. this.UserController.updateUserSettings(this.req, this.res)
  397. })
  398. describe('when changeEmailAddress yields an error', function() {
  399. it('should pass on an error and not send a success status', function(done) {
  400. this.req.body.email = this.newEmail.toUpperCase()
  401. this.UserUpdater.changeEmailAddress.callsArgWith(3, new OError())
  402. this.HttpErrorHandler.legacyInternal = sinon.spy(
  403. (req, res, message, error) => {
  404. expect(req).to.exist
  405. expect(req).to.exist
  406. message.should.equal('problem_changing_email_address')
  407. expect(error).to.be.instanceof(OError)
  408. done()
  409. }
  410. )
  411. this.UserController.updateUserSettings(this.req, this.res, this.next)
  412. })
  413. it('should call the HTTP conflict error handler when the email already exists', function(done) {
  414. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  415. expect(req).to.exist
  416. expect(req).to.exist
  417. message.should.equal('email_already_registered')
  418. done()
  419. })
  420. this.req.body.email = this.newEmail.toUpperCase()
  421. this.UserUpdater.changeEmailAddress.callsArgWith(
  422. 3,
  423. new Errors.EmailExistsError()
  424. )
  425. this.UserController.updateUserSettings(this.req, this.res)
  426. })
  427. })
  428. describe('when using an external auth source', function() {
  429. beforeEach(function() {
  430. this.UserUpdater.changeEmailAddress.callsArgWith(2)
  431. this.newEmail = 'someone23@example.com'
  432. this.req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
  433. })
  434. it('should not set a new email', function(done) {
  435. this.req.body.email = this.newEmail
  436. this.res.sendStatus = code => {
  437. code.should.equal(200)
  438. this.UserUpdater.changeEmailAddress
  439. .calledWith(this.user_id, this.newEmail)
  440. .should.equal(false)
  441. done()
  442. }
  443. this.UserController.updateUserSettings(this.req, this.res)
  444. })
  445. })
  446. })
  447. describe('logout', function() {
  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. })
  496. describe('register', function() {
  497. beforeEach(function() {
  498. this.UserRegistrationHandler.registerNewUserAndSendActivationEmail = sinon
  499. .stub()
  500. .callsArgWith(1, null, this.user, (this.url = 'mock/url'))
  501. this.req.body.email = this.user.email = this.email = 'email@example.com'
  502. this.UserController.register(this.req, this.res)
  503. })
  504. it('should register the user and send them an email', function() {
  505. this.UserRegistrationHandler.registerNewUserAndSendActivationEmail
  506. .calledWith(this.email)
  507. .should.equal(true)
  508. })
  509. it('should return the user and activation url', function() {
  510. this.res.json
  511. .calledWith({
  512. email: this.email,
  513. setNewPasswordUrl: this.url
  514. })
  515. .should.equal(true)
  516. })
  517. })
  518. describe('clearSessions', function() {
  519. describe('success', function() {
  520. it('should call revokeAllUserSessions', function(done) {
  521. this.res.sendStatus.callsFake(() => {
  522. this.UserSessionsManager.promises.revokeAllUserSessions.callCount.should.equal(
  523. 1
  524. )
  525. done()
  526. })
  527. this.UserController.clearSessions(this.req, this.res)
  528. })
  529. it('send a 201 response', function(done) {
  530. this.res.sendStatus.callsFake(status => {
  531. status.should.equal(201)
  532. done()
  533. })
  534. this.UserController.clearSessions(this.req, this.res)
  535. })
  536. it('sends a security alert email', function(done) {
  537. this.res.sendStatus.callsFake(status => {
  538. this.EmailHandler.promises.sendEmail.callCount.should.equal(1)
  539. const expectedArg = {
  540. to: this.user.email,
  541. actionDescribed: `active sessions were cleared on your account ${this.user.email}`,
  542. action: 'active sessions cleared'
  543. }
  544. const emailCall = this.EmailHandler.promises.sendEmail.lastCall
  545. expect(emailCall.args[0]).to.equal('securityAlert')
  546. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  547. done()
  548. })
  549. this.UserController.clearSessions(this.req, this.res)
  550. })
  551. })
  552. describe('errors', function() {
  553. describe('when getAllUserSessions produces an error', function() {
  554. it('should return an error', function(done) {
  555. this.UserSessionsManager.promises.getAllUserSessions.rejects(
  556. new Error('woops')
  557. )
  558. this.UserController.clearSessions(this.req, this.res, error => {
  559. expect(error).to.be.instanceof(Error)
  560. done()
  561. })
  562. })
  563. })
  564. describe('when audit log addEntry produces an error', function() {
  565. it('should call next with an error', function(done) {
  566. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('woops'))
  567. this.UserController.clearSessions(this.req, this.res, error => {
  568. expect(error).to.be.instanceof(Error)
  569. done()
  570. })
  571. })
  572. })
  573. describe('when revokeAllUserSessions produces an error', function() {
  574. it('should call next with an error', function(done) {
  575. this.UserSessionsManager.promises.revokeAllUserSessions.rejects(
  576. new Error('woops')
  577. )
  578. this.UserController.clearSessions(this.req, this.res, error => {
  579. expect(error).to.be.instanceof(Error)
  580. done()
  581. })
  582. })
  583. })
  584. describe('when EmailHandler produces an error', function() {
  585. const anError = new Error('oops')
  586. it('send a 201 response but log error', function(done) {
  587. this.EmailHandler.promises.sendEmail.rejects(anError)
  588. this.res.sendStatus.callsFake(status => {
  589. status.should.equal(201)
  590. this.logger.error.callCount.should.equal(1)
  591. const loggerCall = this.logger.error.getCall(0)
  592. expect(loggerCall.args[0]).to.deep.equal({
  593. error: anError,
  594. userId: this.user_id
  595. })
  596. expect(loggerCall.args[1]).to.contain(
  597. 'could not send security alert email when sessions cleared'
  598. )
  599. done()
  600. })
  601. this.UserController.clearSessions(this.req, this.res)
  602. })
  603. })
  604. })
  605. })
  606. describe('changePassword', function() {
  607. describe('success', function() {
  608. beforeEach(function() {
  609. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  610. this.AuthenticationManager.promises.setUserPassword.resolves()
  611. this.req.body = {
  612. newPassword1: 'newpass',
  613. newPassword2: 'newpass'
  614. }
  615. })
  616. it('should set the new password if they do match', function(done) {
  617. this.res.json.callsFake(() => {
  618. this.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith(
  619. this.user,
  620. 'newpass'
  621. )
  622. done()
  623. })
  624. this.UserController.changePassword(this.req, this.res)
  625. })
  626. it('should log the update', function(done) {
  627. this.res.json.callsFake(() => {
  628. this.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
  629. this.user._id,
  630. 'update-password',
  631. this.user._id,
  632. this.req.ip
  633. )
  634. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  635. 1
  636. )
  637. done()
  638. })
  639. this.UserController.changePassword(this.req, this.res)
  640. })
  641. it('should send security alert email', function(done) {
  642. this.res.json.callsFake(() => {
  643. const expectedArg = {
  644. to: this.user.email,
  645. actionDescribed: `your password has been changed on your account ${this.user.email}`,
  646. action: 'password changed'
  647. }
  648. const emailCall = this.EmailHandler.sendEmail.lastCall
  649. expect(emailCall.args[0]).to.equal('securityAlert')
  650. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  651. done()
  652. })
  653. this.UserController.changePassword(this.req, this.res)
  654. })
  655. })
  656. describe('errors', function() {
  657. it('should check the old password is the current one at the moment', function(done) {
  658. this.AuthenticationManager.promises.authenticate.resolves()
  659. this.req.body = { currentPassword: 'oldpasshere' }
  660. this.HttpErrorHandler.badRequest.callsFake(() => {
  661. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  662. this.req,
  663. this.res,
  664. 'Your old password is wrong'
  665. )
  666. this.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  667. { _id: this.user._id },
  668. 'oldpasshere'
  669. )
  670. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  671. 0
  672. )
  673. done()
  674. })
  675. this.UserController.changePassword(this.req, this.res)
  676. })
  677. it('it should not set the new password if they do not match', function(done) {
  678. this.AuthenticationManager.promises.authenticate.resolves({})
  679. this.req.body = {
  680. newPassword1: '1',
  681. newPassword2: '2'
  682. }
  683. this.HttpErrorHandler.badRequest.callsFake(() => {
  684. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  685. this.req,
  686. this.res,
  687. 'password_change_passwords_do_not_match'
  688. )
  689. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  690. 0
  691. )
  692. done()
  693. })
  694. this.UserController.changePassword(this.req, this.res)
  695. })
  696. it('it should not set the new password if it is invalid', function(done) {
  697. // this.AuthenticationManager.validatePassword = sinon
  698. // .stub()
  699. // .returns({ message: 'validation-error' })
  700. const err = new Error('bad')
  701. err.name = 'InvalidPasswordError'
  702. this.AuthenticationManager.promises.setUserPassword.rejects(err)
  703. this.AuthenticationManager.promises.authenticate.resolves({})
  704. this.req.body = {
  705. newPassword1: 'newpass',
  706. newPassword2: 'newpass'
  707. }
  708. this.HttpErrorHandler.badRequest.callsFake(() => {
  709. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  710. this.req,
  711. this.res,
  712. err.message
  713. )
  714. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  715. 1
  716. )
  717. done()
  718. })
  719. this.UserController.changePassword(this.req, this.res)
  720. })
  721. describe('UserAuditLogHandler error', function() {
  722. it('should return error and not update password', function(done) {
  723. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops'))
  724. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  725. this.AuthenticationManager.promises.setUserPassword.resolves()
  726. this.req.body = {
  727. newPassword1: 'newpass',
  728. newPassword2: 'newpass'
  729. }
  730. this.UserController.changePassword(this.req, this.res, error => {
  731. expect(error).to.be.instanceof(Error)
  732. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  733. 1
  734. )
  735. done()
  736. })
  737. })
  738. })
  739. describe('EmailHandler error', function() {
  740. const anError = new Error('oops')
  741. beforeEach(function() {
  742. this.AuthenticationManager.promises.authenticate.resolves(this.user)
  743. this.AuthenticationManager.promises.setUserPassword.resolves()
  744. this.req.body = {
  745. newPassword1: 'newpass',
  746. newPassword2: 'newpass'
  747. }
  748. this.EmailHandler.sendEmail.yields(anError)
  749. })
  750. it('should not return error but should log it', function(done) {
  751. this.res.json.callsFake(result => {
  752. expect(result.message.type).to.equal('success')
  753. this.logger.error.callCount.should.equal(1)
  754. expect(this.logger.error).to.have.been.calledWithExactly(
  755. {
  756. error: anError,
  757. userId: this.user_id
  758. },
  759. 'could not send security alert email when password changed'
  760. )
  761. done()
  762. })
  763. this.UserController.changePassword(this.req, this.res)
  764. })
  765. })
  766. })
  767. })
  768. describe('ensureAffiliationMiddleware', function() {
  769. describe('without affiliations feature', function() {
  770. beforeEach(async function() {
  771. await this.UserController.promises.ensureAffiliationMiddleware(
  772. this.req,
  773. this.res,
  774. this.next
  775. )
  776. })
  777. it('should not run affiliation check', function() {
  778. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  779. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  780. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  781. .been.called
  782. })
  783. it('should not return an error', function() {
  784. expect(this.next).to.be.calledWith()
  785. })
  786. })
  787. describe('without ensureAffiliation query parameter', function() {
  788. beforeEach(async function() {
  789. this.Features.hasFeature.withArgs('affiliations').returns(true)
  790. await this.UserController.promises.ensureAffiliationMiddleware(
  791. this.req,
  792. this.res,
  793. this.next
  794. )
  795. })
  796. it('should not run middleware', function() {
  797. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  798. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  799. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  800. .been.called
  801. })
  802. it('should not return an error', function() {
  803. expect(this.next).to.be.calledWith()
  804. })
  805. })
  806. describe('no flagged email', function() {
  807. beforeEach(async function() {
  808. const email = 'unit-test@overleaf.com'
  809. this.user.email = email
  810. this.user.emails = [
  811. {
  812. email
  813. }
  814. ]
  815. this.Features.hasFeature.withArgs('affiliations').returns(true)
  816. this.req.query.ensureAffiliation = true
  817. await this.UserController.promises.ensureAffiliationMiddleware(
  818. this.req,
  819. this.res,
  820. this.next
  821. )
  822. })
  823. it('should get the user', function() {
  824. expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
  825. this.user._id
  826. )
  827. })
  828. it('should not try to add affiliation or update user', function() {
  829. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  830. .been.called
  831. })
  832. it('should not return an error', function() {
  833. expect(this.next).to.be.calledWith()
  834. })
  835. })
  836. describe('flagged non-SSO email', function() {
  837. let emailFlagged
  838. beforeEach(async function() {
  839. emailFlagged = 'flagged@overleaf.com'
  840. this.user.email = emailFlagged
  841. this.user.emails = [
  842. {
  843. email: emailFlagged,
  844. affiliationUnchecked: true
  845. }
  846. ]
  847. this.Features.hasFeature.withArgs('affiliations').returns(true)
  848. this.req.query.ensureAffiliation = true
  849. await this.UserController.promises.ensureAffiliationMiddleware(
  850. this.req,
  851. this.res,
  852. this.next
  853. )
  854. })
  855. it('should unflag the emails but not confirm', function() {
  856. expect(
  857. this.UserUpdater.promises.addAffiliationForNewUser
  858. ).to.have.been.calledWith(this.user._id, emailFlagged)
  859. expect(
  860. this.UserUpdater.promises.confirmEmail
  861. ).to.not.have.been.calledWith(this.user._id, emailFlagged)
  862. })
  863. it('should not return an error', function() {
  864. expect(this.next).to.be.calledWith()
  865. })
  866. })
  867. describe('flagged SSO email', function() {
  868. let emailFlagged
  869. beforeEach(async function() {
  870. emailFlagged = 'flagged@overleaf.com'
  871. this.user.email = emailFlagged
  872. this.user.emails = [
  873. {
  874. email: emailFlagged,
  875. affiliationUnchecked: true,
  876. samlProviderId: '123'
  877. }
  878. ]
  879. this.Features.hasFeature.withArgs('affiliations').returns(true)
  880. this.req.query.ensureAffiliation = true
  881. await this.UserController.promises.ensureAffiliationMiddleware(
  882. this.req,
  883. this.res,
  884. this.next
  885. )
  886. })
  887. it('should add affiliation to v1, unflag and confirm on v2', function() {
  888. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not
  889. .been.called
  890. expect(this.UserUpdater.promises.confirmEmail).to.have.been.calledWith(
  891. this.user._id,
  892. emailFlagged
  893. )
  894. })
  895. it('should not return an error', function() {
  896. expect(this.next).to.be.calledWith()
  897. })
  898. })
  899. describe('when v1 returns an error', function() {
  900. let emailFlagged
  901. beforeEach(async function() {
  902. this.UserUpdater.promises.addAffiliationForNewUser.rejects()
  903. emailFlagged = 'flagged@overleaf.com'
  904. this.user.email = emailFlagged
  905. this.user.emails = [
  906. {
  907. email: emailFlagged,
  908. affiliationUnchecked: true
  909. }
  910. ]
  911. this.Features.hasFeature.withArgs('affiliations').returns(true)
  912. this.req.query.ensureAffiliation = true
  913. await this.UserController.promises.ensureAffiliationMiddleware(
  914. this.req,
  915. this.res,
  916. this.next
  917. )
  918. })
  919. it('should return the error', function() {
  920. expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
  921. })
  922. })
  923. })
  924. })