Skip to content

Commit 917cd29

Browse files
Add tests for PasswordChangeRequest handler
1 parent 6b39dcf commit 917cd29

File tree

4 files changed

+263
-2
lines changed

4 files changed

+263
-2
lines changed

lib/models/account-manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ class AccountManager {
413413
*
414414
* @throws {Error} If missing or invalid token
415415
*
416-
* @return {Object} Saved token data object
416+
* @return {Object|false} Saved token data object if verified, false otherwise
417417
*/
418418
validateResetToken (token) {
419419
let tokenValue = this.tokenService.verify(token)

lib/requests/password-change-request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class PasswordChangeRequest extends AuthRequest {
9090
validateToken () {
9191
return Promise.resolve()
9292
.then(() => {
93-
if (!this.token) { return }
93+
if (!this.token) { return false }
9494

9595
return this.accountManager.validateResetToken(this.token)
9696
})
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
'use strict'
2+
3+
const chai = require('chai')
4+
const sinon = require('sinon')
5+
const expect = chai.expect
6+
const dirtyChai = require('dirty-chai')
7+
chai.use(dirtyChai)
8+
const sinonChai = require('sinon-chai')
9+
chai.use(sinonChai)
10+
chai.should()
11+
12+
const HttpMocks = require('node-mocks-http')
13+
14+
const PasswordChangeRequest = require('../../lib/requests/password-change-request')
15+
const SolidHost = require('../../lib/models/solid-host')
16+
17+
describe('PasswordChangeRequest', () => {
18+
sinon.spy(PasswordChangeRequest.prototype, 'error')
19+
20+
describe('constructor()', () => {
21+
it('should initialize a request instance from options', () => {
22+
let res = HttpMocks.createResponse()
23+
24+
let accountManager = {}
25+
let userStore = {}
26+
27+
let options = {
28+
accountManager,
29+
userStore,
30+
returnToUrl: 'https://example.com/resource',
31+
response: res,
32+
token: '12345',
33+
newPassword: 'swordfish'
34+
}
35+
36+
let request = new PasswordChangeRequest(options)
37+
38+
expect(request.returnToUrl).to.equal(options.returnToUrl)
39+
expect(request.response).to.equal(res)
40+
expect(request.token).to.equal(options.token)
41+
expect(request.newPassword).to.equal(options.newPassword)
42+
expect(request.accountManager).to.equal(accountManager)
43+
expect(request.userStore).to.equal(userStore)
44+
})
45+
})
46+
47+
describe('fromParams()', () => {
48+
it('should return a request instance from options', () => {
49+
let returnToUrl = 'https://example.com/resource'
50+
let token = '12345'
51+
let newPassword = 'swordfish'
52+
let accountManager = {}
53+
let userStore = {}
54+
55+
let req = {
56+
app: { locals: { accountManager, oidc: { users: userStore } } },
57+
query: { returnToUrl, token },
58+
body: { newPassword }
59+
}
60+
let res = HttpMocks.createResponse()
61+
62+
let request = PasswordChangeRequest.fromParams(req, res)
63+
64+
expect(request.returnToUrl).to.equal(returnToUrl)
65+
expect(request.response).to.equal(res)
66+
expect(request.token).to.equal(token)
67+
expect(request.newPassword).to.equal(newPassword)
68+
expect(request.accountManager).to.equal(accountManager)
69+
expect(request.userStore).to.equal(userStore)
70+
})
71+
})
72+
73+
describe('get()', () => {
74+
let returnToUrl = 'https://example.com/resource'
75+
let token = '12345'
76+
let userStore = {}
77+
let res = HttpMocks.createResponse()
78+
sinon.spy(res, 'render')
79+
80+
it('should create an instance and render a change password form', () => {
81+
let accountManager = {
82+
validateResetToken: sinon.stub().resolves(true)
83+
}
84+
let req = {
85+
app: { locals: { accountManager, oidc: { users: userStore } } },
86+
query: { returnToUrl, token }
87+
}
88+
89+
return PasswordChangeRequest.get(req, res)
90+
.then(() => {
91+
expect(accountManager.validateResetToken)
92+
.to.have.been.called()
93+
expect(res.render).to.have.been.calledWith('auth/change-password',
94+
{ returnToUrl, token, validToken: true })
95+
})
96+
})
97+
98+
it('should display an error message on an invalid token', () => {
99+
let accountManager = {
100+
validateResetToken: sinon.stub().throws()
101+
}
102+
let req = {
103+
app: { locals: { accountManager, oidc: { users: userStore } } },
104+
query: { returnToUrl, token }
105+
}
106+
107+
return PasswordChangeRequest.get(req, res)
108+
.then(() => {
109+
expect(PasswordChangeRequest.prototype.error)
110+
.to.have.been.called()
111+
})
112+
})
113+
})
114+
115+
describe('post()', () => {
116+
it('creates a request instance and invokes handlePost()', () => {
117+
sinon.spy(PasswordChangeRequest, 'handlePost')
118+
119+
let returnToUrl = 'https://example.com/resource'
120+
let token = '12345'
121+
let newPassword = 'swordfish'
122+
let host = SolidHost.from({ serverUri: 'https://example.com' })
123+
let alice = {
124+
webId: 'https://alice.example.com/#me'
125+
}
126+
let storedToken = { webId: alice.webId }
127+
let store = {
128+
findUser: sinon.stub().resolves(alice),
129+
updatePassword: sinon.stub()
130+
}
131+
let accountManager = {
132+
host,
133+
store,
134+
userAccountFrom: sinon.stub().resolves(alice),
135+
validateResetToken: sinon.stub().resolves(storedToken)
136+
}
137+
138+
accountManager.accountExists = sinon.stub().resolves(true)
139+
accountManager.loadAccountRecoveryEmail = sinon.stub().resolves('alice@example.com')
140+
accountManager.sendPasswordResetEmail = sinon.stub().resolves()
141+
142+
let req = {
143+
app: { locals: { accountManager, oidc: { users: store } } },
144+
query: { returnToUrl },
145+
body: { token, newPassword }
146+
}
147+
let res = HttpMocks.createResponse()
148+
149+
return PasswordChangeRequest.post(req, res)
150+
.then(() => {
151+
expect(PasswordChangeRequest.handlePost).to.have.been.called()
152+
})
153+
})
154+
})
155+
156+
describe('handlePost()', () => {
157+
it('should display error message if validation error encountered', () => {
158+
let returnToUrl = 'https://example.com/resource'
159+
let token = '12345'
160+
let userStore = {}
161+
let res = HttpMocks.createResponse()
162+
let accountManager = {
163+
validateResetToken: sinon.stub().throws()
164+
}
165+
let req = {
166+
app: { locals: { accountManager, oidc: { users: userStore } } },
167+
query: { returnToUrl, token }
168+
}
169+
170+
let request = PasswordChangeRequest.fromParams(req, res)
171+
172+
return PasswordChangeRequest.handlePost(request)
173+
.then(() => {
174+
expect(PasswordChangeRequest.prototype.error)
175+
.to.have.been.called()
176+
})
177+
})
178+
})
179+
180+
describe('validateToken()', () => {
181+
it('should return false if no token is present', () => {
182+
let accountManager = {
183+
validateResetToken: sinon.stub()
184+
}
185+
let request = new PasswordChangeRequest({ accountManager, token: null })
186+
187+
return request.validateToken()
188+
.then(result => {
189+
expect(result).to.be.false()
190+
expect(accountManager.validateResetToken).to.not.have.been.called()
191+
})
192+
})
193+
})
194+
195+
describe('validatePost()', () => {
196+
it('should throw an error if no new password was entered', () => {
197+
let request = new PasswordChangeRequest({ newPassword: null })
198+
199+
expect(() => request.validatePost()).to.throw('Please enter a new password')
200+
})
201+
})
202+
203+
describe('error()', () => {
204+
it('should invoke renderForm() with the error', () => {
205+
let request = new PasswordChangeRequest({})
206+
request.renderForm = sinon.stub()
207+
let error = new Error('error message')
208+
209+
request.error(error)
210+
211+
expect(request.renderForm).to.have.been.calledWith(error)
212+
})
213+
})
214+
215+
describe('changePassword()', () => {
216+
it('should create a new user store entry if none exists', () => {
217+
// this would be the case for legacy pre-user-store accounts
218+
let webId = 'https://alice.example.com/#me'
219+
let user = { webId, id: webId }
220+
let accountManager = {
221+
userAccountFrom: sinon.stub().returns(user)
222+
}
223+
let userStore = {
224+
findUser: sinon.stub().resolves(null), // no user found
225+
createUser: sinon.stub().resolves(),
226+
updatePassword: sinon.stub().resolves()
227+
}
228+
229+
let options = {
230+
accountManager, userStore, newPassword: 'swordfish'
231+
}
232+
let request = new PasswordChangeRequest(options)
233+
234+
return request.changePassword(user)
235+
.then(() => {
236+
expect(userStore.createUser).to.have.been.calledWith(user, options.newPassword)
237+
})
238+
})
239+
})
240+
241+
describe('renderForm()', () => {
242+
it('should set response status to error status, if error exists', () => {
243+
let returnToUrl = 'https://example.com/resource'
244+
let token = '12345'
245+
let response = HttpMocks.createResponse()
246+
sinon.spy(response, 'render')
247+
248+
let options = { returnToUrl, token, response }
249+
250+
let request = new PasswordChangeRequest(options)
251+
252+
let error = new Error('error message')
253+
254+
request.renderForm(error)
255+
256+
expect(response.render).to.have.been.calledWith('auth/change-password',
257+
{ validToken: false, token, returnToUrl, error: 'error message' })
258+
expect(response.statusCode).to.equal(400)
259+
})
260+
})
261+
})

0 commit comments

Comments
 (0)