UserController.test.mjs 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  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 set referencesSearchMode to advanced', function (done) {
  379. this.req.body = { referencesSearchMode: 'advanced' }
  380. this.res.sendStatus = code => {
  381. this.user.ace.referencesSearchMode.should.equal('advanced')
  382. done()
  383. }
  384. this.UserController.updateUserSettings(this.req, this.res)
  385. })
  386. it('should set referencesSearchMode to simple', function (done) {
  387. this.req.body = { referencesSearchMode: 'simple' }
  388. this.res.sendStatus = code => {
  389. this.user.ace.referencesSearchMode.should.equal('simple')
  390. done()
  391. }
  392. this.UserController.updateUserSettings(this.req, this.res)
  393. })
  394. it('should not allow arbitrary referencesSearchMode', function (done) {
  395. this.req.body = { referencesSearchMode: 'foobar' }
  396. this.res.sendStatus = code => {
  397. this.user.ace.referencesSearchMode.should.equal('advanced')
  398. done()
  399. }
  400. this.UserController.updateUserSettings(this.req, this.res)
  401. })
  402. it('should set enableNewEditor to true', function (done) {
  403. this.req.body = { enableNewEditor: true }
  404. this.res.sendStatus = code => {
  405. this.user.ace.enableNewEditor.should.equal(true)
  406. done()
  407. }
  408. this.UserController.updateUserSettings(this.req, this.res)
  409. })
  410. it('should set enableNewEditor to false', function (done) {
  411. this.req.body = { enableNewEditor: false }
  412. this.res.sendStatus = code => {
  413. this.user.ace.enableNewEditor.should.equal(false)
  414. done()
  415. }
  416. this.UserController.updateUserSettings(this.req, this.res)
  417. })
  418. it('should keep enableNewEditor a boolean', function (done) {
  419. this.req.body = { enableNewEditor: 'foobar' }
  420. this.res.sendStatus = code => {
  421. this.user.ace.enableNewEditor.should.equal(true)
  422. done()
  423. }
  424. this.UserController.updateUserSettings(this.req, this.res)
  425. })
  426. it('should send an error if the email is 0 len', function (done) {
  427. this.req.body.email = ''
  428. this.res.sendStatus = function (code) {
  429. code.should.equal(400)
  430. done()
  431. }
  432. this.UserController.updateUserSettings(this.req, this.res)
  433. })
  434. it('should send an error if the email does not contain an @', function (done) {
  435. this.req.body.email = 'bob at something dot com'
  436. this.res.sendStatus = function (code) {
  437. code.should.equal(400)
  438. done()
  439. }
  440. this.UserController.updateUserSettings(this.req, this.res)
  441. })
  442. it('should call the user updater with the new email and user _id', function (done) {
  443. this.req.body.email = this.newEmail.toUpperCase()
  444. this.res.sendStatus = code => {
  445. code.should.equal(200)
  446. this.UserUpdater.promises.changeEmailAddress.should.have.been.calledWith(
  447. this.user_id,
  448. this.newEmail,
  449. this.auditLog
  450. )
  451. done()
  452. }
  453. this.UserController.updateUserSettings(this.req, this.res)
  454. })
  455. it('should update the email on the session', function (done) {
  456. this.req.body.email = this.newEmail.toUpperCase()
  457. let callcount = 0
  458. this.User.findById = id => ({
  459. exec: async () => {
  460. if (++callcount === 2) {
  461. this.user.email = this.newEmail
  462. }
  463. return this.user
  464. },
  465. })
  466. this.res.sendStatus = code => {
  467. code.should.equal(200)
  468. this.SessionManager.setInSessionUser
  469. .calledWith(this.req.session, {
  470. email: this.newEmail,
  471. first_name: undefined,
  472. last_name: undefined,
  473. })
  474. .should.equal(true)
  475. done()
  476. }
  477. this.UserController.updateUserSettings(this.req, this.res)
  478. })
  479. it('should call populateTeamInvites', function (done) {
  480. this.req.body.email = this.newEmail.toUpperCase()
  481. this.res.sendStatus = code => {
  482. code.should.equal(200)
  483. this.UserHandler.promises.populateTeamInvites.should.have.been.calledWith(
  484. this.user
  485. )
  486. done()
  487. }
  488. this.UserController.updateUserSettings(this.req, this.res)
  489. })
  490. describe('when changeEmailAddress yields an error', function () {
  491. it('should pass on an error and not send a success status', function (done) {
  492. this.req.body.email = this.newEmail.toUpperCase()
  493. this.UserUpdater.promises.changeEmailAddress.rejects(new OError())
  494. this.HttpErrorHandler.legacyInternal = sinon.spy(
  495. (req, res, message, error) => {
  496. expect(req).to.exist
  497. expect(req).to.exist
  498. message.should.equal('problem_changing_email_address')
  499. expect(error).to.be.instanceof(OError)
  500. done()
  501. }
  502. )
  503. this.UserController.updateUserSettings(this.req, this.res, this.next)
  504. })
  505. it('should call the HTTP conflict error handler when the email already exists', function (done) {
  506. this.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  507. expect(req).to.exist
  508. expect(req).to.exist
  509. message.should.equal('email_already_registered')
  510. done()
  511. })
  512. this.req.body.email = this.newEmail.toUpperCase()
  513. this.UserUpdater.promises.changeEmailAddress.rejects(
  514. new Errors.EmailExistsError()
  515. )
  516. this.UserController.updateUserSettings(this.req, this.res)
  517. })
  518. })
  519. describe('when using an external auth source', function () {
  520. beforeEach(function () {
  521. this.newEmail = 'someone23@example.com'
  522. this.req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
  523. })
  524. it('should not set a new email', function (done) {
  525. this.req.body.email = this.newEmail
  526. this.res.sendStatus = code => {
  527. code.should.equal(200)
  528. this.UserUpdater.promises.changeEmailAddress
  529. .calledWith(this.user_id, this.newEmail)
  530. .should.equal(false)
  531. done()
  532. }
  533. this.UserController.updateUserSettings(this.req, this.res)
  534. })
  535. })
  536. })
  537. describe('logout', function () {
  538. beforeEach(function () {
  539. this.RequestContentTypeDetection.acceptsJson.returns(false)
  540. })
  541. it('should destroy the session', function (done) {
  542. this.req.session.destroy = sinon.stub().callsArgWith(0)
  543. this.res.redirect = url => {
  544. url.should.equal('/login')
  545. this.req.session.destroy.called.should.equal(true)
  546. done()
  547. }
  548. this.UserController.logout(this.req, this.res)
  549. })
  550. it('should untrack session', function (done) {
  551. this.req.session.destroy = sinon.stub().callsArgWith(0)
  552. this.res.redirect = url => {
  553. url.should.equal('/login')
  554. this.UserSessionsManager.promises.untrackSession.should.have.been
  555. .calledOnce
  556. this.UserSessionsManager.promises.untrackSession.should.have.been.calledWith(
  557. sinon.match(this.req.user),
  558. this.req.sessionID
  559. )
  560. done()
  561. }
  562. this.UserController.logout(this.req, this.res)
  563. })
  564. it('should redirect after logout', function (done) {
  565. this.req.body.redirect = '/sso-login'
  566. this.req.session.destroy = sinon.stub().callsArgWith(0)
  567. this.res.redirect = url => {
  568. url.should.equal(this.req.body.redirect)
  569. done()
  570. }
  571. this.UserController.logout(this.req, this.res)
  572. })
  573. it('should redirect after logout, but not to evil.com', function (done) {
  574. this.req.body.redirect = 'https://evil.com'
  575. this.req.session.destroy = sinon.stub().callsArgWith(0)
  576. this.res.redirect = url => {
  577. url.should.equal('/login')
  578. done()
  579. }
  580. this.UserController.logout(this.req, this.res)
  581. })
  582. it('should redirect to login after logout when no redirect set', function (done) {
  583. this.req.session.destroy = sinon.stub().callsArgWith(0)
  584. this.res.redirect = url => {
  585. url.should.equal('/login')
  586. done()
  587. }
  588. this.UserController.logout(this.req, this.res)
  589. })
  590. it('should send json with redir property for json request', function (done) {
  591. this.RequestContentTypeDetection.acceptsJson.returns(true)
  592. this.req.session.destroy = sinon.stub().callsArgWith(0)
  593. this.res.status = code => {
  594. code.should.equal(200)
  595. return this.res
  596. }
  597. this.res.json = data => {
  598. data.redir.should.equal('/login')
  599. done()
  600. }
  601. this.UserController.logout(this.req, this.res)
  602. })
  603. })
  604. describe('clearSessions', function () {
  605. describe('success', function () {
  606. it('should call removeSessionsFromRedis', function (done) {
  607. this.res.sendStatus.callsFake(() => {
  608. this.UserSessionsManager.promises.removeSessionsFromRedis.should.have
  609. .been.calledOnce
  610. done()
  611. })
  612. this.UserController.clearSessions(this.req, this.res)
  613. })
  614. it('send a 201 response', function (done) {
  615. this.res.sendStatus.callsFake(status => {
  616. status.should.equal(201)
  617. done()
  618. })
  619. this.UserController.clearSessions(this.req, this.res)
  620. })
  621. it('sends a security alert email', function (done) {
  622. this.res.sendStatus.callsFake(status => {
  623. this.EmailHandler.promises.sendEmail.callCount.should.equal(1)
  624. const expectedArg = {
  625. to: this.user.email,
  626. actionDescribed: `active sessions were cleared on your account ${this.user.email}`,
  627. action: 'active sessions cleared',
  628. }
  629. const emailCall = this.EmailHandler.promises.sendEmail.lastCall
  630. expect(emailCall.args[0]).to.equal('securityAlert')
  631. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  632. done()
  633. })
  634. this.UserController.clearSessions(this.req, this.res)
  635. })
  636. })
  637. describe('errors', function () {
  638. describe('when getAllUserSessions produces an error', function () {
  639. it('should return an error', function (done) {
  640. this.UserSessionsManager.promises.getAllUserSessions.rejects(
  641. new Error('woops')
  642. )
  643. this.UserController.clearSessions(this.req, this.res, error => {
  644. expect(error).to.be.instanceof(Error)
  645. done()
  646. })
  647. })
  648. })
  649. describe('when audit log addEntry produces an error', function () {
  650. it('should call next with an error', function (done) {
  651. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('woops'))
  652. this.UserController.clearSessions(this.req, this.res, error => {
  653. expect(error).to.be.instanceof(Error)
  654. done()
  655. })
  656. })
  657. })
  658. describe('when removeSessionsFromRedis produces an error', function () {
  659. it('should call next with an error', function (done) {
  660. this.UserSessionsManager.promises.removeSessionsFromRedis.rejects(
  661. new Error('woops')
  662. )
  663. this.UserController.clearSessions(this.req, this.res, error => {
  664. expect(error).to.be.instanceof(Error)
  665. done()
  666. })
  667. })
  668. })
  669. describe('when EmailHandler produces an error', function () {
  670. const anError = new Error('oops')
  671. it('send a 201 response but log error', function (done) {
  672. this.EmailHandler.promises.sendEmail.rejects(anError)
  673. this.res.sendStatus.callsFake(status => {
  674. status.should.equal(201)
  675. this.logger.error.callCount.should.equal(1)
  676. const loggerCall = this.logger.error.getCall(0)
  677. expect(loggerCall.args[0]).to.deep.equal({
  678. error: anError,
  679. userId: this.user_id,
  680. })
  681. expect(loggerCall.args[1]).to.contain(
  682. 'could not send security alert email when sessions cleared'
  683. )
  684. done()
  685. })
  686. this.UserController.clearSessions(this.req, this.res)
  687. })
  688. })
  689. })
  690. })
  691. describe('changePassword', function () {
  692. describe('success', function () {
  693. beforeEach(function () {
  694. this.AuthenticationManager.promises.authenticate.resolves({
  695. user: this.user,
  696. })
  697. this.AuthenticationManager.promises.setUserPassword.resolves()
  698. this.req.body = {
  699. newPassword1: 'newpass',
  700. newPassword2: 'newpass',
  701. }
  702. })
  703. it('should set the new password if they do match', function (done) {
  704. this.res.json.callsFake(() => {
  705. this.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith(
  706. this.user,
  707. 'newpass'
  708. )
  709. done()
  710. })
  711. this.UserController.changePassword(this.req, this.res)
  712. })
  713. it('should log the update', function (done) {
  714. this.res.json.callsFake(() => {
  715. this.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
  716. this.user._id,
  717. 'update-password',
  718. this.user._id,
  719. this.req.ip
  720. )
  721. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  722. 1
  723. )
  724. done()
  725. })
  726. this.UserController.changePassword(this.req, this.res)
  727. })
  728. it('should send security alert email', function (done) {
  729. this.res.json.callsFake(() => {
  730. const expectedArg = {
  731. to: this.user.email,
  732. actionDescribed: `your password has been changed on your account ${this.user.email}`,
  733. action: 'password changed',
  734. }
  735. const emailCall = this.EmailHandler.promises.sendEmail.lastCall
  736. expect(emailCall.args[0]).to.equal('securityAlert')
  737. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  738. done()
  739. })
  740. this.UserController.changePassword(this.req, this.res)
  741. })
  742. it('should expire password reset tokens', function (done) {
  743. this.res.json.callsFake(() => {
  744. this.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith(
  745. this.user._id,
  746. 'password'
  747. )
  748. done()
  749. })
  750. this.UserController.changePassword(this.req, this.res)
  751. })
  752. })
  753. describe('errors', function () {
  754. it('should check the old password is the current one at the moment', function (done) {
  755. this.AuthenticationManager.promises.authenticate.resolves({})
  756. this.req.body = { currentPassword: 'oldpasshere' }
  757. this.HttpErrorHandler.badRequest.callsFake(() => {
  758. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  759. this.req,
  760. this.res,
  761. 'password_change_old_password_wrong'
  762. )
  763. this.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  764. { _id: this.user._id },
  765. 'oldpasshere'
  766. )
  767. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  768. 0
  769. )
  770. done()
  771. })
  772. this.UserController.changePassword(this.req, this.res)
  773. })
  774. it('it should not set the new password if they do not match', function (done) {
  775. this.AuthenticationManager.promises.authenticate.resolves({
  776. user: this.user,
  777. })
  778. this.req.body = {
  779. newPassword1: '1',
  780. newPassword2: '2',
  781. }
  782. this.HttpErrorHandler.badRequest.callsFake(() => {
  783. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  784. this.req,
  785. this.res,
  786. 'password_change_passwords_do_not_match'
  787. )
  788. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  789. 0
  790. )
  791. done()
  792. })
  793. this.UserController.changePassword(this.req, this.res)
  794. })
  795. it('it should not set the new password if it is invalid', function (done) {
  796. // this.AuthenticationManager.validatePassword = sinon
  797. // .stub()
  798. // .returns({ message: 'validation-error' })
  799. const err = new Error('bad')
  800. err.name = 'InvalidPasswordError'
  801. const message = {
  802. type: 'error',
  803. key: 'some-message-key',
  804. }
  805. this.AuthenticationManager.getMessageForInvalidPasswordError.returns(
  806. message
  807. )
  808. this.AuthenticationManager.promises.setUserPassword.rejects(err)
  809. this.AuthenticationManager.promises.authenticate.resolves({
  810. user: this.user,
  811. })
  812. this.req.body = {
  813. newPassword1: 'newpass',
  814. newPassword2: 'newpass',
  815. }
  816. this.res.json.callsFake(result => {
  817. expect(result.message).to.deep.equal(message)
  818. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  819. 1
  820. )
  821. done()
  822. })
  823. this.UserController.changePassword(this.req, this.res)
  824. })
  825. describe('UserAuditLogHandler error', function () {
  826. it('should return error and not update password', function (done) {
  827. this.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops'))
  828. this.AuthenticationManager.promises.authenticate.resolves({
  829. user: this.user,
  830. })
  831. this.AuthenticationManager.promises.setUserPassword.resolves()
  832. this.req.body = {
  833. newPassword1: 'newpass',
  834. newPassword2: 'newpass',
  835. }
  836. this.UserController.changePassword(this.req, this.res, error => {
  837. expect(error).to.be.instanceof(Error)
  838. this.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  839. 1
  840. )
  841. done()
  842. })
  843. })
  844. })
  845. describe('EmailHandler error', function () {
  846. const anError = new Error('oops')
  847. beforeEach(function () {
  848. this.AuthenticationManager.promises.authenticate.resolves({
  849. user: this.user,
  850. })
  851. this.AuthenticationManager.promises.setUserPassword.resolves()
  852. this.req.body = {
  853. newPassword1: 'newpass',
  854. newPassword2: 'newpass',
  855. }
  856. this.EmailHandler.promises.sendEmail.rejects(anError)
  857. })
  858. it('should not return error but should log it', function (done) {
  859. this.res.json.callsFake(result => {
  860. expect(result.message.type).to.equal('success')
  861. this.logger.error.callCount.should.equal(1)
  862. expect(this.logger.error).to.have.been.calledWithExactly(
  863. {
  864. error: anError,
  865. userId: this.user_id,
  866. },
  867. 'could not send security alert email when password changed'
  868. )
  869. done()
  870. })
  871. this.UserController.changePassword(this.req, this.res)
  872. })
  873. })
  874. })
  875. })
  876. describe('ensureAffiliationMiddleware', function () {
  877. describe('without affiliations feature', function () {
  878. beforeEach(async function () {
  879. await this.UserController.ensureAffiliationMiddleware(
  880. this.req,
  881. this.res,
  882. this.next
  883. )
  884. })
  885. it('should not run affiliation check', function () {
  886. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  887. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  888. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  889. .been.called
  890. })
  891. it('should not return an error', function () {
  892. expect(this.next).to.be.calledWith()
  893. })
  894. })
  895. describe('without ensureAffiliation query parameter', function () {
  896. beforeEach(async function () {
  897. this.Features.hasFeature.withArgs('affiliations').returns(true)
  898. await this.UserController.ensureAffiliationMiddleware(
  899. this.req,
  900. this.res,
  901. this.next
  902. )
  903. })
  904. it('should not run middleware', function () {
  905. expect(this.UserGetter.promises.getUser).to.not.have.been.called
  906. expect(this.UserUpdater.promises.confirmEmail).to.not.have.been.called
  907. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  908. .been.called
  909. })
  910. it('should not return an error', function () {
  911. expect(this.next).to.be.calledWith()
  912. })
  913. })
  914. describe('no flagged email', function () {
  915. beforeEach(async function () {
  916. const email = 'unit-test@overleaf.com'
  917. this.user.email = email
  918. this.user.emails = [
  919. {
  920. email,
  921. },
  922. ]
  923. this.Features.hasFeature.withArgs('affiliations').returns(true)
  924. this.req.query.ensureAffiliation = true
  925. await this.UserController.ensureAffiliationMiddleware(
  926. this.req,
  927. this.res,
  928. this.next
  929. )
  930. })
  931. it('should get the user', function () {
  932. expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
  933. this.user._id
  934. )
  935. })
  936. it('should not try to add affiliation or update user', function () {
  937. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  938. .been.called
  939. })
  940. it('should not return an error', function () {
  941. expect(this.next).to.be.calledWith()
  942. })
  943. })
  944. describe('flagged non-SSO email', function () {
  945. let emailFlagged
  946. beforeEach(async function () {
  947. emailFlagged = 'flagged@overleaf.com'
  948. this.user.email = emailFlagged
  949. this.user.emails = [
  950. {
  951. email: emailFlagged,
  952. affiliationUnchecked: true,
  953. },
  954. ]
  955. this.Features.hasFeature.withArgs('affiliations').returns(true)
  956. this.req.query.ensureAffiliation = true
  957. this.req.assertPermission = sinon.stub()
  958. await this.UserController.ensureAffiliationMiddleware(
  959. this.req,
  960. this.res,
  961. this.next
  962. )
  963. })
  964. it('should check the user has permission', function () {
  965. expect(this.req.assertPermission).to.have.been.calledWith(
  966. 'add-affiliation'
  967. )
  968. })
  969. it('should unflag the emails but not confirm', function () {
  970. expect(
  971. this.UserUpdater.promises.addAffiliationForNewUser
  972. ).to.have.been.calledWith(this.user._id, emailFlagged)
  973. expect(
  974. this.UserUpdater.promises.confirmEmail
  975. ).to.not.have.been.calledWith(this.user._id, emailFlagged)
  976. })
  977. it('should not return an error', function () {
  978. expect(this.next).to.be.calledWith()
  979. })
  980. })
  981. describe('flagged SSO email', function () {
  982. let emailFlagged
  983. beforeEach(async function () {
  984. emailFlagged = 'flagged@overleaf.com'
  985. this.user.email = emailFlagged
  986. this.user.emails = [
  987. {
  988. email: emailFlagged,
  989. affiliationUnchecked: true,
  990. samlProviderId: '123',
  991. },
  992. ]
  993. this.Features.hasFeature.withArgs('affiliations').returns(true)
  994. this.req.query.ensureAffiliation = true
  995. this.req.assertPermission = sinon.stub()
  996. await this.UserController.ensureAffiliationMiddleware(
  997. this.req,
  998. this.res,
  999. this.next
  1000. )
  1001. })
  1002. it('should check the user has permission', function () {
  1003. expect(this.req.assertPermission).to.have.been.calledWith(
  1004. 'add-affiliation'
  1005. )
  1006. })
  1007. it('should add affiliation to v1, unflag and confirm on v2', function () {
  1008. expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not
  1009. .been.called
  1010. expect(this.UserUpdater.promises.confirmEmail).to.have.been.calledWith(
  1011. this.user._id,
  1012. emailFlagged
  1013. )
  1014. })
  1015. it('should not return an error', function () {
  1016. expect(this.next).to.be.calledWith()
  1017. })
  1018. })
  1019. describe('when v1 returns an error', function () {
  1020. let emailFlagged
  1021. beforeEach(async function () {
  1022. this.UserUpdater.promises.addAffiliationForNewUser.rejects()
  1023. emailFlagged = 'flagged@overleaf.com'
  1024. this.user.email = emailFlagged
  1025. this.user.emails = [
  1026. {
  1027. email: emailFlagged,
  1028. affiliationUnchecked: true,
  1029. },
  1030. ]
  1031. this.Features.hasFeature.withArgs('affiliations').returns(true)
  1032. this.req.query.ensureAffiliation = true
  1033. this.req.assertPermission = sinon.stub()
  1034. await this.UserController.ensureAffiliationMiddleware(
  1035. this.req,
  1036. this.res,
  1037. this.next
  1038. )
  1039. })
  1040. it('should check the user has permission', function () {
  1041. expect(this.req.assertPermission).to.have.been.calledWith(
  1042. 'add-affiliation'
  1043. )
  1044. })
  1045. it('should return the error', function () {
  1046. expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1047. })
  1048. })
  1049. describe('when user is not found', function () {
  1050. beforeEach(async function () {
  1051. this.UserGetter.promises.getUser.rejects(new Error('not found'))
  1052. this.Features.hasFeature.withArgs('affiliations').returns(true)
  1053. this.req.query.ensureAffiliation = true
  1054. await this.UserController.ensureAffiliationMiddleware(
  1055. this.req,
  1056. this.res,
  1057. this.next
  1058. )
  1059. })
  1060. it('should return the error', function () {
  1061. expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1062. })
  1063. })
  1064. })
  1065. })