PasswordResetHandler.test.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import { expect, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. const modulePath = new URL(
  4. '../../../../app/src/Features/PasswordReset/PasswordResetHandler',
  5. import.meta.url
  6. ).pathname
  7. describe('PasswordResetHandler', function () {
  8. beforeEach(async function (ctx) {
  9. ctx.settings = { siteUrl: 'https://www.overleaf.com' }
  10. ctx.OneTimeTokenHandler = {
  11. promises: {
  12. getNewToken: sinon.stub(),
  13. peekValueFromToken: sinon.stub(),
  14. },
  15. peekValueFromToken: sinon.stub(),
  16. expireToken: sinon.stub(),
  17. }
  18. ctx.UserGetter = {
  19. getUserByMainEmail: sinon.stub(),
  20. getUser: sinon.stub(),
  21. promises: {
  22. getUserByAnyEmail: sinon.stub(),
  23. getUserByMainEmail: sinon.stub(),
  24. },
  25. }
  26. ctx.EmailHandler = { promises: { sendEmail: sinon.stub() } }
  27. ctx.AuthenticationManager = {
  28. setUserPasswordInV2: sinon.stub(),
  29. promises: {
  30. setUserPassword: sinon.stub().resolves(),
  31. },
  32. }
  33. vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
  34. default: (ctx.UserAuditLogHandler = {
  35. promises: {
  36. addEntry: sinon.stub().resolves(),
  37. },
  38. }),
  39. }))
  40. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  41. default: ctx.UserGetter,
  42. }))
  43. vi.doMock(
  44. '../../../../app/src/Features/Security/OneTimeTokenHandler',
  45. () => ({
  46. default: ctx.OneTimeTokenHandler,
  47. })
  48. )
  49. vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
  50. default: ctx.EmailHandler,
  51. }))
  52. vi.doMock(
  53. '../../../../app/src/Features/Authentication/AuthenticationManager',
  54. () => ({
  55. default: ctx.AuthenticationManager,
  56. })
  57. )
  58. vi.doMock('@overleaf/settings', () => ({
  59. default: ctx.settings,
  60. }))
  61. vi.doMock(
  62. '../../../../app/src/Features/Authorization/PermissionsManager',
  63. () => ({
  64. default: (ctx.PermissionsManager = {
  65. promises: {
  66. assertUserPermissions: sinon.stub(),
  67. },
  68. }),
  69. })
  70. )
  71. ctx.PasswordResetHandler = (await import(modulePath)).default
  72. ctx.token = '12312321i'
  73. ctx.user_id = 'user_id_here'
  74. ctx.user = { email: (ctx.email = 'bob@bob.com'), _id: ctx.user_id }
  75. ctx.password = 'my great secret password'
  76. ctx.callback = sinon.stub()
  77. // this should not have any effect now
  78. ctx.settings.overleaf = true
  79. })
  80. afterEach(function (ctx) {
  81. ctx.settings.overleaf = false
  82. })
  83. describe('generateAndEmailResetToken', function () {
  84. it('should check the user exists', function (ctx) {
  85. ctx.UserGetter.promises.getUserByAnyEmail.resolves()
  86. ctx.PasswordResetHandler.generateAndEmailResetToken(
  87. ctx.user.email,
  88. ctx.callback
  89. )
  90. ctx.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith(
  91. ctx.user.email
  92. )
  93. })
  94. it('should send the email with the token', async function (ctx) {
  95. await new Promise(resolve => {
  96. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user)
  97. ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token)
  98. ctx.EmailHandler.promises.sendEmail.resolves()
  99. ctx.PasswordResetHandler.generateAndEmailResetToken(
  100. ctx.user.email,
  101. (err, status) => {
  102. expect(err).to.not.exist
  103. ctx.EmailHandler.promises.sendEmail.called.should.equal(true)
  104. status.should.equal('primary')
  105. const args = ctx.EmailHandler.promises.sendEmail.args[0]
  106. args[0].should.equal('passwordResetRequested')
  107. args[1].setNewPasswordUrl.should.equal(
  108. `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${
  109. ctx.token
  110. }&email=${encodeURIComponent(ctx.user.email)}`
  111. )
  112. resolve()
  113. }
  114. )
  115. })
  116. })
  117. it('should return errors from getUserByAnyEmail', async function (ctx) {
  118. await new Promise(resolve => {
  119. const err = new Error('oops')
  120. ctx.UserGetter.promises.getUserByAnyEmail.rejects(err)
  121. ctx.PasswordResetHandler.generateAndEmailResetToken(
  122. ctx.user.email,
  123. err => {
  124. expect(err).to.equal(err)
  125. resolve()
  126. }
  127. )
  128. })
  129. })
  130. describe('when the email exists', function () {
  131. let result
  132. beforeEach(async function (ctx) {
  133. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user)
  134. ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token)
  135. ctx.EmailHandler.promises.sendEmail.resolves()
  136. result =
  137. await ctx.PasswordResetHandler.promises.generateAndEmailResetToken(
  138. ctx.email
  139. )
  140. })
  141. it('should set the password token data to the user id and email', function (ctx) {
  142. ctx.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith(
  143. 'password',
  144. {
  145. email: ctx.email,
  146. user_id: ctx.user._id,
  147. }
  148. )
  149. })
  150. it('should send an email with the token', function (ctx) {
  151. ctx.EmailHandler.promises.sendEmail.called.should.equal(true)
  152. const args = ctx.EmailHandler.promises.sendEmail.args[0]
  153. args[0].should.equal('passwordResetRequested')
  154. args[1].setNewPasswordUrl.should.equal(
  155. `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${
  156. ctx.token
  157. }&email=${encodeURIComponent(ctx.user.email)}`
  158. )
  159. })
  160. it('should return status == true', async function () {
  161. expect(result).to.equal('primary')
  162. })
  163. })
  164. describe("when the email doesn't exist", function () {
  165. let result
  166. beforeEach(async function (ctx) {
  167. ctx.UserGetter.promises.getUserByAnyEmail.resolves(null)
  168. result =
  169. await ctx.PasswordResetHandler.promises.generateAndEmailResetToken(
  170. ctx.email
  171. )
  172. })
  173. it('should not set the password token data', function (ctx) {
  174. ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false)
  175. })
  176. it('should send an email with the token', function (ctx) {
  177. ctx.EmailHandler.promises.sendEmail.called.should.equal(false)
  178. })
  179. it('should return status == null', function () {
  180. expect(result).to.equal(null)
  181. })
  182. })
  183. describe('when the email is a secondary email', function () {
  184. let result
  185. beforeEach(async function (ctx) {
  186. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user)
  187. result =
  188. await ctx.PasswordResetHandler.promises.generateAndEmailResetToken(
  189. 'secondary@email.com'
  190. )
  191. })
  192. it('should not set the password token data', function (ctx) {
  193. ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false)
  194. })
  195. it('should not send an email with the token', function (ctx) {
  196. ctx.EmailHandler.promises.sendEmail.called.should.equal(false)
  197. })
  198. it('should return status == secondary', function () {
  199. expect(result).to.equal('secondary')
  200. })
  201. })
  202. })
  203. describe('setNewUserPassword', function () {
  204. beforeEach(function (ctx) {
  205. ctx.auditLog = { ip: '0:0:0:0' }
  206. })
  207. describe('when no data is found', function () {
  208. beforeEach(function (ctx) {
  209. ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null)
  210. })
  211. it('should return found == false and reset == false', function (ctx) {
  212. ctx.PasswordResetHandler.setNewUserPassword(
  213. ctx.token,
  214. ctx.password,
  215. ctx.auditLog,
  216. (error, result) => {
  217. expect(error).to.not.exist
  218. expect(result).to.deep.equal({
  219. found: false,
  220. reset: false,
  221. userId: null,
  222. })
  223. }
  224. )
  225. })
  226. })
  227. describe('when the token has a user_id and email', function () {
  228. beforeEach(function (ctx) {
  229. ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  230. data: {
  231. user_id: ctx.user._id,
  232. email: ctx.email,
  233. },
  234. })
  235. ctx.AuthenticationManager.promises.setUserPassword
  236. .withArgs(ctx.user, ctx.password)
  237. .resolves(true)
  238. ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null)
  239. })
  240. describe('when no user is found with this email', function () {
  241. beforeEach(function (ctx) {
  242. ctx.UserGetter.getUserByMainEmail
  243. .withArgs(ctx.email)
  244. .yields(null, null)
  245. })
  246. it('should return found == false and reset == false', async function (ctx) {
  247. await new Promise(resolve => {
  248. ctx.PasswordResetHandler.setNewUserPassword(
  249. ctx.token,
  250. ctx.password,
  251. ctx.auditLog,
  252. (err, result) => {
  253. const { found, reset } = result
  254. expect(err).to.not.exist
  255. expect(found).to.be.false
  256. expect(reset).to.be.false
  257. expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal(
  258. 0
  259. )
  260. resolve()
  261. }
  262. )
  263. })
  264. })
  265. })
  266. describe("when the email and user don't match", function () {
  267. beforeEach(function (ctx) {
  268. ctx.UserGetter.getUserByMainEmail
  269. .withArgs(ctx.email)
  270. .yields(null, { _id: 'not-the-same', email: ctx.email })
  271. ctx.OneTimeTokenHandler.expireToken.callsArgWith(2, null)
  272. })
  273. it('should return found == false and reset == false', async function (ctx) {
  274. await new Promise(resolve => {
  275. ctx.PasswordResetHandler.setNewUserPassword(
  276. ctx.token,
  277. ctx.password,
  278. ctx.auditLog,
  279. (err, result) => {
  280. const { found, reset } = result
  281. expect(err).to.not.exist
  282. expect(found).to.be.false
  283. expect(reset).to.be.false
  284. expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal(
  285. 0
  286. )
  287. resolve()
  288. }
  289. )
  290. })
  291. })
  292. })
  293. describe('when the email and user match', function () {
  294. describe('success', function () {
  295. beforeEach(function (ctx) {
  296. ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user)
  297. ctx.OneTimeTokenHandler.expireToken = sinon
  298. .stub()
  299. .callsArgWith(2, null)
  300. })
  301. it('should update the user audit log', async function (ctx) {
  302. await new Promise(resolve => {
  303. ctx.PasswordResetHandler.setNewUserPassword(
  304. ctx.token,
  305. ctx.password,
  306. ctx.auditLog,
  307. (error, result) => {
  308. sinon.assert.calledWith(
  309. ctx.UserAuditLogHandler.promises.addEntry,
  310. ctx.user_id,
  311. 'reset-password',
  312. undefined,
  313. ctx.auditLog.ip,
  314. { token: ctx.token.substring(0, 10) }
  315. )
  316. expect(error).to.not.exist
  317. resolve()
  318. }
  319. )
  320. })
  321. })
  322. it('should return reset == true and the user id', async function (ctx) {
  323. await new Promise(resolve => {
  324. ctx.PasswordResetHandler.setNewUserPassword(
  325. ctx.token,
  326. ctx.password,
  327. ctx.auditLog,
  328. (err, result) => {
  329. const { reset, userId } = result
  330. expect(err).to.not.exist
  331. expect(reset).to.be.true
  332. expect(userId).to.equal(ctx.user._id)
  333. resolve()
  334. }
  335. )
  336. })
  337. })
  338. it('should expire the token', async function (ctx) {
  339. await new Promise(resolve => {
  340. ctx.PasswordResetHandler.setNewUserPassword(
  341. ctx.token,
  342. ctx.password,
  343. ctx.auditLog,
  344. (_err, _result) => {
  345. expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal(
  346. true
  347. )
  348. resolve()
  349. }
  350. )
  351. })
  352. })
  353. describe('when logged in', function () {
  354. beforeEach(function (ctx) {
  355. ctx.auditLog.initiatorId = ctx.user_id
  356. })
  357. it('should update the user audit log with initiatorId', async function (ctx) {
  358. await new Promise(resolve => {
  359. ctx.PasswordResetHandler.setNewUserPassword(
  360. ctx.token,
  361. ctx.password,
  362. ctx.auditLog,
  363. (error, result) => {
  364. expect(error).to.not.exist
  365. sinon.assert.calledWith(
  366. ctx.UserAuditLogHandler.promises.addEntry,
  367. ctx.user_id,
  368. 'reset-password',
  369. ctx.user_id,
  370. ctx.auditLog.ip,
  371. { token: ctx.token.substring(0, 10) }
  372. )
  373. resolve()
  374. }
  375. )
  376. })
  377. })
  378. })
  379. })
  380. describe('errors', function () {
  381. describe('via setUserPassword', function () {
  382. beforeEach(function (ctx) {
  383. ctx.PasswordResetHandler.promises.getUserForPasswordResetToken =
  384. sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user })
  385. ctx.AuthenticationManager.promises.setUserPassword
  386. .withArgs(ctx.user, ctx.password)
  387. .rejects()
  388. })
  389. it('should return the error', async function (ctx) {
  390. await new Promise(resolve => {
  391. ctx.PasswordResetHandler.setNewUserPassword(
  392. ctx.token,
  393. ctx.password,
  394. ctx.auditLog,
  395. (error, _result) => {
  396. expect(error).to.exist
  397. expect(
  398. ctx.UserAuditLogHandler.promises.addEntry.callCount
  399. ).to.equal(1)
  400. resolve()
  401. }
  402. )
  403. })
  404. })
  405. })
  406. describe('via UserAuditLogHandler', function () {
  407. beforeEach(function (ctx) {
  408. ctx.PasswordResetHandler.promises.getUserForPasswordResetToken =
  409. sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user })
  410. ctx.UserAuditLogHandler.promises.addEntry.rejects(
  411. new Error('oops')
  412. )
  413. })
  414. it('should return the error', async function (ctx) {
  415. await new Promise(resolve => {
  416. ctx.PasswordResetHandler.setNewUserPassword(
  417. ctx.token,
  418. ctx.password,
  419. ctx.auditLog,
  420. (error, _result) => {
  421. expect(error).to.exist
  422. expect(
  423. ctx.UserAuditLogHandler.promises.addEntry.callCount
  424. ).to.equal(1)
  425. expect(ctx.AuthenticationManager.promises.setUserPassword)
  426. .to.not.have.been.called
  427. resolve()
  428. }
  429. )
  430. })
  431. })
  432. })
  433. })
  434. })
  435. })
  436. describe('when the token has a v1_user_id and email', function () {
  437. beforeEach(function (ctx) {
  438. ctx.user.overleaf = { id: 184 }
  439. ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  440. data: {
  441. v1_user_id: ctx.user.overleaf.id,
  442. email: ctx.email,
  443. },
  444. })
  445. ctx.AuthenticationManager.promises.setUserPassword
  446. .withArgs(ctx.user, ctx.password)
  447. .resolves(true)
  448. ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null)
  449. })
  450. describe('when no user is reset with this email', function () {
  451. beforeEach(function (ctx) {
  452. ctx.UserGetter.getUserByMainEmail
  453. .withArgs(ctx.email)
  454. .yields(null, null)
  455. })
  456. it('should return reset == false', async function (ctx) {
  457. await new Promise(resolve => {
  458. ctx.PasswordResetHandler.setNewUserPassword(
  459. ctx.token,
  460. ctx.password,
  461. ctx.auditLog,
  462. (err, result) => {
  463. const { reset } = result
  464. expect(err).to.not.exist
  465. expect(reset).to.be.false
  466. expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal(
  467. false
  468. )
  469. resolve()
  470. }
  471. )
  472. })
  473. })
  474. })
  475. describe("when the email and user don't match", function () {
  476. beforeEach(function (ctx) {
  477. ctx.UserGetter.getUserByMainEmail.withArgs(ctx.email).yields(null, {
  478. _id: ctx.user._id,
  479. email: ctx.email,
  480. overleaf: { id: 'not-the-same' },
  481. })
  482. })
  483. it('should return reset == false', async function (ctx) {
  484. await new Promise(resolve => {
  485. ctx.PasswordResetHandler.setNewUserPassword(
  486. ctx.token,
  487. ctx.password,
  488. ctx.auditLog,
  489. (err, result) => {
  490. const { reset } = result
  491. expect(err).to.not.exist
  492. expect(reset).to.be.false
  493. expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal(
  494. false
  495. )
  496. resolve()
  497. }
  498. )
  499. })
  500. })
  501. })
  502. describe('when the email and user match', function () {
  503. beforeEach(function (ctx) {
  504. ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user)
  505. })
  506. it('should return reset == true and the user id', async function (ctx) {
  507. await new Promise(resolve => {
  508. ctx.PasswordResetHandler.setNewUserPassword(
  509. ctx.token,
  510. ctx.password,
  511. ctx.auditLog,
  512. (err, result) => {
  513. const { reset, userId } = result
  514. expect(err).to.not.exist
  515. expect(reset).to.be.true
  516. expect(userId).to.equal(ctx.user._id)
  517. expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal(
  518. true
  519. )
  520. resolve()
  521. }
  522. )
  523. })
  524. })
  525. })
  526. })
  527. })
  528. describe('getUserForPasswordResetToken', function () {
  529. beforeEach(function (ctx) {
  530. ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  531. data: {
  532. user_id: ctx.user._id,
  533. email: ctx.email,
  534. },
  535. remainingPeeks: 1,
  536. })
  537. ctx.UserGetter.promises.getUserByMainEmail.resolves({
  538. _id: ctx.user._id,
  539. email: ctx.email,
  540. })
  541. })
  542. it('should returns errors from user permissions', async function (ctx) {
  543. let error
  544. const err = new Error('nope')
  545. ctx.PermissionsManager.promises.assertUserPermissions.rejects(err)
  546. try {
  547. await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken(
  548. 'abc123'
  549. )
  550. } catch (e) {
  551. error = e
  552. }
  553. expect(error).to.deep.equal(error)
  554. })
  555. it('returns user when user has permissions and remaining peaks', async function (ctx) {
  556. const result =
  557. await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken(
  558. 'abc123'
  559. )
  560. expect(result).to.deep.equal({
  561. user: { _id: ctx.user._id, email: ctx.email },
  562. remainingPeeks: 1,
  563. })
  564. })
  565. })
  566. })