mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-06 11:23:48 -05:00
the next url comes from sign in via a query param, and needs to go to the POST /webauthn/authenticate endpoint. That endpoint logs the user in and returns the redirect to the browser, and will take the next from the request query params to get there. also moving the window mocks to beforeEach/afterEach ensures that promise callbacks from previous tests aren't still associated in future tests to ensure good test isolation. unfortunately i couldn't get mocking location for a single js test to work, but by changing the global config i was able to add some query params that i can expect to be passed through. Don't love this at all but not quite sure of a good way round this. I think we're not practicing very good hygiene and best practices with our mocking and it's really confounding me here.
289 lines
8.9 KiB
JavaScript
289 lines
8.9 KiB
JavaScript
beforeAll(() => {
|
|
window.CBOR = require('../../node_modules/cbor-js/cbor.js')
|
|
require('../../app/assets/javascripts/authenticateSecurityKey.js')
|
|
|
|
// disable console.error() so we don't see it in test output
|
|
// you might need to comment this out to debug some failures
|
|
jest.spyOn(console, 'error').mockImplementation(() => { })
|
|
|
|
// ensure window.alert() is implemented to simplify errors
|
|
jest.spyOn(window, 'alert').mockImplementation(() => { })
|
|
})
|
|
|
|
afterAll(() => {
|
|
require('./support/teardown.js')
|
|
})
|
|
|
|
describe('Authenticate with security key', () => {
|
|
let button
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `
|
|
<button type="submit" data-module="authenticate-security-key" data-csrf-token="abc123"></button>
|
|
`
|
|
button = document.querySelector('[data-module="authenticate-security-key"]')
|
|
|
|
// populate missing values to allow consistent jest.spyOn()
|
|
window.fetch = () => { }
|
|
window.navigator.credentials = { get: () => { } }
|
|
window.alert = () => { }
|
|
|
|
window.GOVUK.modules.start()
|
|
})
|
|
|
|
afterEach(() => {
|
|
// restore window attributes to their original undefined state
|
|
delete window.fetch
|
|
delete window.navigator.credentials
|
|
delete window.alert
|
|
})
|
|
|
|
test('authenticates a credential and redirects based on the admin app response', (done) => {
|
|
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url) => {
|
|
// initial fetch of options from the server
|
|
// fetch defaults to GET
|
|
// options from the server are CBOR-encoded
|
|
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => webauthnOptions
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
|
|
expect(options).toEqual('someArbitraryOptions')
|
|
|
|
// fake PublicKeyCredential response from WebAuthn API
|
|
// all of the array properties represent Array(Buffer) objects
|
|
const credentialsGetResponse = {
|
|
response: {
|
|
authenticatorData: [2, 2, 2],
|
|
signature: [3, 3, 3],
|
|
clientDataJSON: [4, 4, 4]
|
|
},
|
|
rawId: [1, 1, 1],
|
|
type: "public-key",
|
|
}
|
|
return Promise.resolve(credentialsGetResponse)
|
|
})
|
|
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url, options = {}) => {
|
|
// subsequent POST of credential data to server
|
|
const decodedData = window.CBOR.decode(options.body)
|
|
expect(decodedData.credentialId).toEqual(new Uint8Array([1, 1, 1]))
|
|
expect(decodedData.authenticatorData).toEqual(new Uint8Array([2, 2, 2]))
|
|
expect(decodedData.signature).toEqual(new Uint8Array([3, 3, 3]))
|
|
expect(decodedData.clientDataJSON).toEqual(new Uint8Array([4, 4, 4]))
|
|
expect(options.headers['X-CSRFToken']).toBe('abc123')
|
|
const loginResponse = window.CBOR.encode({ redirect_url: '/foo' })
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => Promise.resolve(loginResponse)
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.location, 'assign').mockImplementation((href) => {
|
|
expect(href).toEqual("/foo")
|
|
done();
|
|
})
|
|
|
|
// this will make the test fail if the alert is called
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
done(msg)
|
|
})
|
|
|
|
button.click()
|
|
});
|
|
|
|
test('authenticates and passes a redirect url through to the authenticate admin endpoint', (done) => {
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url) => {
|
|
// initial fetch of options from the server
|
|
// fetch defaults to GET
|
|
// options from the server are CBOR-encoded
|
|
let webauthnOptions = window.CBOR.encode('someArbitraryOptions')
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => webauthnOptions
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
|
|
let credentialsGetResponse = {
|
|
response: {
|
|
authenticatorData: [],
|
|
signature: [],
|
|
clientDataJSON: []
|
|
},
|
|
rawId: [],
|
|
type: "public-key",
|
|
}
|
|
return Promise.resolve(credentialsGetResponse)
|
|
})
|
|
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((url, options = {}) => {
|
|
// subsequent POST of credential data to server
|
|
expect(url.toString()).toEqual(
|
|
'https://www.notifications.service.gov.uk/webauthn/authenticate?next=%2Ffoo%3Fbar%3Dbaz'
|
|
);
|
|
|
|
// mark the test as done here as we've finished all our asserts - if something goes wrong later and
|
|
// we end up in the alert mock, that `done(msg)` will override this and mark the test as failed
|
|
done();
|
|
|
|
const loginResponse = window.CBOR.encode({ redirect_url: '/foo' })
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => Promise.resolve(loginResponse)
|
|
})
|
|
})
|
|
|
|
// make sure we error out if alert is called
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
done(msg)
|
|
})
|
|
|
|
button.click()
|
|
});
|
|
|
|
test.each([
|
|
['network'],
|
|
['server'],
|
|
])('alerts if fetching WebAuthn fails (%s error)', (errorType, done) => {
|
|
jest.spyOn(window, 'fetch').mockImplementation((_url) => {
|
|
if (errorType == 'network') {
|
|
return Promise.reject('error')
|
|
} else {
|
|
return Promise.resolve({ ok: false, statusText: 'error' })
|
|
}
|
|
})
|
|
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
expect(msg).toEqual('Error during authentication.\n\nerror')
|
|
done()
|
|
})
|
|
|
|
button.click()
|
|
})
|
|
|
|
test('alerts if comms with the authenticator fails', (done) => {
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url) => {
|
|
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => webauthnOptions
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.navigator.credentials, 'get').mockImplementation(() => {
|
|
return Promise.reject(new DOMException('error'))
|
|
})
|
|
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
expect(msg).toEqual('Error during authentication.\n\nerror')
|
|
done()
|
|
})
|
|
|
|
button.click()
|
|
});
|
|
|
|
test.each([
|
|
['network error'],
|
|
['internal server error'],
|
|
])('alerts if POSTing WebAuthn credentials fails (%s)', (errorType, done) => {
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url) => {
|
|
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => webauthnOptions
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
|
|
expect(options).toEqual('someArbitraryOptions')
|
|
const credentialsGetResponse = {
|
|
response: {
|
|
authenticatorData: [2, 2, 2],
|
|
signature: [3, 3, 3],
|
|
clientDataJSON: [4, 4, 4]
|
|
},
|
|
rawId: [1, 1, 1],
|
|
type: "public-key",
|
|
}
|
|
return Promise.resolve(credentialsGetResponse)
|
|
})
|
|
|
|
jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
|
|
// subsequent POST of credential data to server
|
|
switch (errorType) {
|
|
case 'network error':
|
|
return Promise.reject('error')
|
|
case 'internal server error':
|
|
// dont need this becuase we dont cbor return errors
|
|
const message = Promise.reject('encoding error')
|
|
return Promise.resolve({ ok: false, arrayBuffer: () => message, statusText: 'error' })
|
|
}
|
|
})
|
|
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
expect(msg).toEqual('Error during authentication.\n\nerror')
|
|
done()
|
|
})
|
|
|
|
button.click()
|
|
});
|
|
|
|
|
|
test('reloads page if POSTing WebAuthn credentials returns 403', (done) => {
|
|
jest.spyOn(window, 'fetch')
|
|
.mockImplementationOnce((_url) => {
|
|
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
|
|
|
|
return Promise.resolve({
|
|
ok: true, arrayBuffer: () => webauthnOptions
|
|
})
|
|
})
|
|
|
|
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
|
|
expect(options).toEqual('someArbitraryOptions')
|
|
const credentialsGetResponse = {
|
|
response: {
|
|
authenticatorData: [2, 2, 2],
|
|
signature: [3, 3, 3],
|
|
clientDataJSON: [4, 4, 4]
|
|
},
|
|
rawId: [1, 1, 1],
|
|
type: "public-key",
|
|
}
|
|
return Promise.resolve(credentialsGetResponse)
|
|
})
|
|
|
|
jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
|
|
return Promise.resolve(
|
|
{
|
|
ok: false,
|
|
status: 403,
|
|
})
|
|
})
|
|
|
|
// assert that reload is called and the page is refreshed
|
|
jest.spyOn(window.location, 'reload').mockImplementation(() => {
|
|
done();
|
|
})
|
|
|
|
// this will make the test fail if the alert is called
|
|
jest.spyOn(window, 'alert').mockImplementation((msg) => {
|
|
done(msg)
|
|
})
|
|
|
|
button.click()
|
|
});
|
|
|
|
|
|
});
|