PasswordResetController.test.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import { beforeEach, describe, expect, it, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import MockResponse from '../helpers/MockResponse.mjs'
  4. const MODULE_PATH =
  5. '../../../../app/src/Features/PasswordReset/PasswordResetController.mjs'
  6. describe('PasswordResetController', function () {
  7. beforeEach(async function (ctx) {
  8. ctx.email = 'bob@bob.com'
  9. ctx.user_id = '507f1f77bcf86cd799439011'
  10. ctx.token = 'my security token that was emailed to me'
  11. ctx.password = 'my new password'
  12. ctx.req = {
  13. body: {
  14. email: ctx.email,
  15. passwordResetToken: ctx.token,
  16. password: ctx.password,
  17. },
  18. i18n: {
  19. translate() {
  20. return '.'
  21. },
  22. },
  23. session: {},
  24. query: {},
  25. }
  26. ctx.res = new MockResponse(vi)
  27. ctx.settings = {}
  28. ctx.PasswordResetHandler = {
  29. generateAndEmailResetToken: sinon.stub(),
  30. promises: {
  31. generateAndEmailResetToken: sinon.stub(),
  32. setNewUserPassword: sinon.stub().resolves({
  33. found: true,
  34. reset: true,
  35. userID: ctx.user_id,
  36. mustReconfirm: true,
  37. }),
  38. getUserForPasswordResetToken: sinon
  39. .stub()
  40. .withArgs(ctx.token)
  41. .resolves({
  42. user: { _id: ctx.user_id },
  43. remainingPeeks: 1,
  44. }),
  45. },
  46. }
  47. ctx.UserSessionsManager = {
  48. promises: {
  49. removeSessionsFromRedis: sinon.stub().resolves(),
  50. },
  51. }
  52. ctx.UserUpdater = {
  53. promises: {
  54. removeReconfirmFlag: sinon.stub().resolves(),
  55. },
  56. }
  57. vi.doMock('@overleaf/settings', () => ({
  58. default: ctx.settings,
  59. }))
  60. vi.doMock(
  61. '../../../../app/src/Features/PasswordReset/PasswordResetHandler',
  62. () => ({
  63. default: ctx.PasswordResetHandler,
  64. })
  65. )
  66. vi.doMock(
  67. '../../../../app/src/Features/Authentication/AuthenticationManager',
  68. () => ({
  69. default: {
  70. validatePassword: sinon.stub().returns(null),
  71. },
  72. })
  73. )
  74. vi.doMock(
  75. '../../../../app/src/Features/Authentication/AuthenticationController',
  76. () => ({
  77. default: (ctx.AuthenticationController = {
  78. getLoggedInUserId: sinon.stub(),
  79. finishLogin: sinon.stub(),
  80. setAuditInfo: sinon.stub(),
  81. }),
  82. })
  83. )
  84. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  85. default: (ctx.UserGetter = {
  86. promises: {
  87. getUser: sinon.stub(),
  88. },
  89. }),
  90. }))
  91. vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({
  92. default: ctx.UserSessionsManager,
  93. }))
  94. vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
  95. default: ctx.UserUpdater,
  96. }))
  97. ctx.PasswordResetController = (await import(MODULE_PATH)).default
  98. })
  99. describe('requestReset', function () {
  100. it('should tell the handler to process that email', async function (ctx) {
  101. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves(
  102. 'primary'
  103. )
  104. await ctx.PasswordResetController.requestReset(ctx.req, ctx.res)
  105. expect(ctx.res.statusCode).to.equal(200)
  106. expect(ctx.res.json).toHaveBeenCalledWith(
  107. expect.objectContaining({ message: expect.anything() })
  108. )
  109. expect(
  110. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.lastCall
  111. .args[0]
  112. ).equal(ctx.email)
  113. })
  114. it('should send a 500 if there is an error', async function (ctx) {
  115. await new Promise(resolve => {
  116. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.rejects(
  117. new Error('error')
  118. )
  119. ctx.PasswordResetController.requestReset(ctx.req, ctx.res, error => {
  120. expect(error).to.exist
  121. resolve()
  122. })
  123. })
  124. })
  125. it("should send a 404 if the email doesn't exist", async function (ctx) {
  126. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves(
  127. null
  128. )
  129. await ctx.PasswordResetController.requestReset(ctx.req, ctx.res)
  130. expect(ctx.res.statusCode).to.equal(404)
  131. expect(ctx.res.json).toHaveBeenCalledWith(
  132. expect.objectContaining({ message: expect.anything() })
  133. )
  134. })
  135. it('should send a 404 if the email is registered as a secondard email', async function (ctx) {
  136. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves(
  137. 'secondary'
  138. )
  139. await ctx.PasswordResetController.requestReset(ctx.req, ctx.res)
  140. expect(ctx.res.statusCode).to.equal(404)
  141. expect(ctx.res.json).toHaveBeenCalledWith(
  142. expect.objectContaining({ message: expect.anything() })
  143. )
  144. })
  145. it('should normalize the email address', async function (ctx) {
  146. ctx.email = ' UPperCaseEMAILWithSpacesAround@example.Com '
  147. ctx.req.body.email = ctx.email
  148. ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves(
  149. 'primary'
  150. )
  151. await ctx.PasswordResetController.requestReset(ctx.req, ctx.res)
  152. expect(ctx.res.statusCode).to.equal(200)
  153. expect(ctx.res.json).toHaveBeenCalledWith(
  154. expect.objectContaining({ message: expect.anything() })
  155. )
  156. })
  157. })
  158. describe('setNewUserPassword', function () {
  159. beforeEach(function (ctx) {
  160. ctx.req.session.resetToken = ctx.token
  161. })
  162. it('should tell the user handler to reset the password', async function (ctx) {
  163. await new Promise(resolve => {
  164. ctx.res.sendStatus = code => {
  165. code.should.equal(200)
  166. ctx.PasswordResetHandler.promises.setNewUserPassword
  167. .calledWith(ctx.token, ctx.password)
  168. .should.equal(true)
  169. resolve()
  170. }
  171. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  172. })
  173. })
  174. it('should preserve spaces in the password', async function (ctx) {
  175. await new Promise(resolve => {
  176. ctx.password = ctx.req.body.password = ' oh! clever! spaces around! '
  177. ctx.res.sendStatus = code => {
  178. code.should.equal(200)
  179. ctx.PasswordResetHandler.promises.setNewUserPassword.should.have.been.calledWith(
  180. ctx.token,
  181. ctx.password
  182. )
  183. resolve()
  184. }
  185. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  186. })
  187. })
  188. it('should send 404 if the token was not found', async function (ctx) {
  189. await new Promise(resolve => {
  190. ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({
  191. found: false,
  192. reset: false,
  193. userId: ctx.user_id,
  194. })
  195. ctx.res.status = code => {
  196. code.should.equal(404)
  197. return ctx.res
  198. }
  199. ctx.res.json = data => {
  200. data.message.key.should.equal('token-expired')
  201. resolve()
  202. }
  203. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  204. })
  205. })
  206. it('should return 500 if not reset', async function (ctx) {
  207. await new Promise(resolve => {
  208. ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({
  209. found: true,
  210. reset: false,
  211. userId: ctx.user_id,
  212. })
  213. ctx.res.status = code => {
  214. code.should.equal(500)
  215. return ctx.res
  216. }
  217. ctx.res.json = data => {
  218. expect(data.message).to.exist
  219. resolve()
  220. }
  221. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  222. })
  223. })
  224. it('should return 400 (Bad Request) if there is no password', async function (ctx) {
  225. await new Promise(resolve => {
  226. ctx.req.body.password = ''
  227. ctx.res.status = code => {
  228. code.should.equal(400)
  229. return ctx.res
  230. }
  231. ctx.res.json = data => {
  232. data.message.key.should.equal('invalid-password')
  233. ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal(
  234. false
  235. )
  236. resolve()
  237. }
  238. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  239. })
  240. })
  241. it('should return 400 (Bad Request) if there is no passwordResetToken', async function (ctx) {
  242. await new Promise(resolve => {
  243. ctx.req.body.passwordResetToken = ''
  244. ctx.res.status = code => {
  245. code.should.equal(400)
  246. return ctx.res
  247. }
  248. ctx.res.json = data => {
  249. data.message.key.should.equal('invalid-password')
  250. ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal(
  251. false
  252. )
  253. resolve()
  254. }
  255. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  256. })
  257. })
  258. it('should return 400 (Bad Request) if the password is invalid', async function (ctx) {
  259. await new Promise(resolve => {
  260. ctx.req.body.password = 'correct horse battery staple'
  261. const err = new Error('bad')
  262. err.name = 'InvalidPasswordError'
  263. ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(err)
  264. ctx.res.status = code => {
  265. code.should.equal(400)
  266. return ctx.res
  267. }
  268. ctx.res.json = data => {
  269. data.message.key.should.equal('invalid-password')
  270. ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal(
  271. true
  272. )
  273. resolve()
  274. }
  275. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  276. })
  277. })
  278. it('should clear sessions', async function (ctx) {
  279. await new Promise(resolve => {
  280. ctx.res.sendStatus = code => {
  281. ctx.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal(
  282. 1
  283. )
  284. resolve()
  285. }
  286. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  287. })
  288. })
  289. it('should call removeReconfirmFlag if user.must_reconfirm', async function (ctx) {
  290. await new Promise(resolve => {
  291. ctx.res.sendStatus = code => {
  292. ctx.UserUpdater.promises.removeReconfirmFlag.callCount.should.equal(1)
  293. resolve()
  294. }
  295. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  296. })
  297. })
  298. describe('catch errors', function () {
  299. it('should return 404 for NotFoundError', async function (ctx) {
  300. await new Promise(resolve => {
  301. const anError = new Error('oops')
  302. anError.name = 'NotFoundError'
  303. ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError)
  304. ctx.res.status = code => {
  305. code.should.equal(404)
  306. return ctx.res
  307. }
  308. ctx.res.json = data => {
  309. data.message.key.should.equal('token-expired')
  310. resolve()
  311. }
  312. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  313. })
  314. })
  315. it('should return 400 for InvalidPasswordError', async function (ctx) {
  316. await new Promise(resolve => {
  317. const anError = new Error('oops')
  318. anError.name = 'InvalidPasswordError'
  319. ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError)
  320. ctx.res.status = code => {
  321. code.should.equal(400)
  322. return ctx.res
  323. }
  324. ctx.res.json = data => {
  325. data.message.key.should.equal('invalid-password')
  326. resolve()
  327. }
  328. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  329. })
  330. })
  331. it('should return 500 for other errors', async function (ctx) {
  332. await new Promise(resolve => {
  333. const anError = new Error('oops')
  334. ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError)
  335. ctx.res.status = code => {
  336. code.should.equal(500)
  337. return ctx.res
  338. }
  339. ctx.res.json = data => {
  340. expect(data.message).to.exist
  341. resolve()
  342. }
  343. ctx.res.sendStatus = code => {
  344. code.should.equal(500)
  345. resolve()
  346. }
  347. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  348. })
  349. })
  350. })
  351. describe('when doLoginAfterPasswordReset is set', function () {
  352. beforeEach(function (ctx) {
  353. ctx.user = {
  354. _id: ctx.userId,
  355. email: 'joe@example.com',
  356. }
  357. ctx.UserGetter.promises.getUser.resolves(ctx.user)
  358. ctx.req.session.doLoginAfterPasswordReset = 'true'
  359. })
  360. it('should login user', async function (ctx) {
  361. await new Promise(resolve => {
  362. ctx.AuthenticationController.finishLogin.callsFake((...args) => {
  363. expect(args[0]).to.equal(ctx.user)
  364. resolve()
  365. })
  366. ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res)
  367. })
  368. })
  369. })
  370. })
  371. describe('renderSetPasswordForm', function () {
  372. describe('with token in query-string', function () {
  373. beforeEach(function (ctx) {
  374. ctx.req.query.passwordResetToken = ctx.token
  375. ctx.req.query.email = 'test@example.com'
  376. })
  377. it('should set session.resetToken and redirect', async function (ctx) {
  378. await new Promise(resolve => {
  379. ctx.req.session.should.not.have.property('resetToken')
  380. ctx.res.redirect = path => {
  381. path.should.equal('/user/password/set?email=test%40example.com')
  382. ctx.req.session.resetToken.should.equal(ctx.token)
  383. resolve()
  384. }
  385. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  386. })
  387. })
  388. })
  389. describe('with expired token in query', function () {
  390. beforeEach(function (ctx) {
  391. ctx.req.query.email = 'test@example.com'
  392. ctx.req.query.passwordResetToken = ctx.token
  393. ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = sinon
  394. .stub()
  395. .withArgs(ctx.token)
  396. .resolves({ user: { _id: ctx.user_id }, remainingPeeks: 0 })
  397. })
  398. it('should redirect to the reset request page with an error message', async function (ctx) {
  399. await new Promise((resolve, reject) => {
  400. ctx.res.redirect = path => {
  401. path.should.equal('/user/password/reset?error=token_expired')
  402. ctx.req.session.should.not.have.property('resetToken')
  403. resolve()
  404. }
  405. ctx.res.render = (templatePath, options) => {
  406. reject(new Error('should not render'))
  407. }
  408. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  409. })
  410. })
  411. })
  412. describe('with token and email in query-string', function () {
  413. beforeEach(function (ctx) {
  414. ctx.req.query.passwordResetToken = ctx.token
  415. ctx.req.query.email = 'foo@bar.com'
  416. })
  417. it('should set session.resetToken and redirect with email', async function (ctx) {
  418. await new Promise(resolve => {
  419. ctx.req.session.should.not.have.property('resetToken')
  420. ctx.res.redirect = path => {
  421. path.should.equal('/user/password/set?email=foo%40bar.com')
  422. ctx.req.session.resetToken.should.equal(ctx.token)
  423. resolve()
  424. }
  425. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  426. })
  427. })
  428. })
  429. describe('with token and invalid email in query-string', function () {
  430. beforeEach(function (ctx) {
  431. ctx.req.query.passwordResetToken = ctx.token
  432. ctx.req.query.email = 'not-an-email'
  433. })
  434. it('should set session.resetToken and redirect without email', async function (ctx) {
  435. await new Promise(resolve => {
  436. ctx.req.session.should.not.have.property('resetToken')
  437. ctx.res.redirect = path => {
  438. path.should.equal('/user/password/set')
  439. ctx.req.session.resetToken.should.equal(ctx.token)
  440. resolve()
  441. }
  442. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  443. })
  444. })
  445. })
  446. describe('with token and non-string email in query-string', function () {
  447. beforeEach(function (ctx) {
  448. ctx.req.query.passwordResetToken = ctx.token
  449. ctx.req.query.email = { foo: 'bar' }
  450. })
  451. it('should call next with an error', async function (ctx) {
  452. await new Promise(resolve => {
  453. ctx.req.session.should.not.have.property('resetToken')
  454. const next = error => {
  455. expect(error).to.exist
  456. resolve()
  457. }
  458. ctx.PasswordResetController.renderSetPasswordForm(
  459. ctx.req,
  460. ctx.res,
  461. next
  462. )
  463. })
  464. })
  465. })
  466. describe('without a token in query-string', function () {
  467. describe('with token in session', function () {
  468. beforeEach(function (ctx) {
  469. ctx.req.session.resetToken = ctx.token
  470. ctx.req.query.email = 'test@example.com'
  471. })
  472. it('should render the page, passing the reset token', async function (ctx) {
  473. await new Promise(resolve => {
  474. ctx.res.render = (templatePath, options) => {
  475. options.passwordResetToken.should.equal(ctx.token)
  476. resolve()
  477. }
  478. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  479. })
  480. })
  481. it('should clear the req.session.resetToken', async function (ctx) {
  482. await new Promise(resolve => {
  483. ctx.res.render = (templatePath, options) => {
  484. ctx.req.session.should.not.have.property('resetToken')
  485. resolve()
  486. }
  487. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  488. })
  489. })
  490. })
  491. describe('without a token in session', function () {
  492. it('should redirect to the reset request page', async function (ctx) {
  493. await new Promise(resolve => {
  494. ctx.res.redirect = path => {
  495. path.should.equal('/user/password/reset')
  496. ctx.req.session.should.not.have.property('resetToken')
  497. resolve()
  498. }
  499. ctx.req.query.email = 'test@example.com'
  500. ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res)
  501. })
  502. })
  503. })
  504. })
  505. })
  506. })