UserControllerTests.js 34 KB

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