UserControllerTests.js 35 KB

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