PasswordResetControllerTests.js 15 KB

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