PasswordResetControllerTests.js 14 KB

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