import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
import MockWebAuthnDevice from './mock_webauthn_device';
import { useMockNavigatorCredentials } from './util';
const mockResponse = {
type: 'public-key',
id: '',
rawId: '',
response: { clientDataJSON: '', authenticatorData: '', signature: '', userHandle: '' },
getClientExtensionResults: () => {},
};
describe('WebAuthnAuthenticate', () => {
useMockNavigatorCredentials();
let fallbackElement;
let webAuthnDevice;
let container;
let component;
let submitSpy;
const findDeviceResponseInput = () => container[0].querySelector('#js-device-response');
const findDeviceResponseInputValue = () => findDeviceResponseInput().value;
const findMessage = () => container[0].querySelector('p');
const findRetryButton = () => container[0].querySelector('#js-token-2fa-try-again');
const expectAuthenticated = () => {
expect(container.text()).toMatchInterpolatedText(
'We heard back from your device. You have been authenticated.',
);
expect(findDeviceResponseInputValue()).toBe(JSON.stringify(mockResponse));
expect(submitSpy).toHaveBeenCalled();
};
beforeEach(() => {
loadHTMLFixture('webauthn/authenticate.html');
fallbackElement = document.createElement('div');
fallbackElement.classList.add('js-2fa-form');
webAuthnDevice = new MockWebAuthnDevice();
container = $('#js-authenticate-token-2fa');
component = new WebAuthnAuthenticate(
container,
'#js-login-token-2fa-form',
{
options:
// we need some valid base64 for base64ToBuffer
// so we use "YQ==" = base64("a")
JSON.stringify({
challenge: 'YQ==',
timeout: 120000,
allowCredentials: [
{ type: 'public-key', id: 'YQ==' },
{ type: 'public-key', id: 'YQ==' },
],
userVerification: 'discouraged',
}),
},
document.querySelector('#js-login-2fa-device'),
fallbackElement,
);
submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
});
afterEach(() => {
resetHTMLFixture();
});
describe('with webauthn unavailable', () => {
let oldGetCredentials;
beforeEach(() => {
oldGetCredentials = window.navigator.credentials.get;
window.navigator.credentials.get = null;
});
afterEach(() => {
window.navigator.credentials.get = oldGetCredentials;
});
it('falls back to normal 2fa', () => {
component.start();
expect(container.html()).toBe('');
expect(container[0]).toHaveClass('hidden');
expect(fallbackElement).not.toHaveClass('hidden');
});
});
describe('with webauthn available', () => {
beforeEach(() => {
component.start();
});
it('shows in progress', () => {
const inProgressMessage = container.find('p');
expect(inProgressMessage.text()).toMatchInterpolatedText(
"Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.",
);
});
it('allows authenticating via a WebAuthn device', () => {
webAuthnDevice.respondToAuthenticateRequest(mockResponse);
return waitForPromises().then(() => {
expectAuthenticated();
});
});
describe('errors', () => {
beforeEach(() => {
webAuthnDevice.rejectAuthenticateRequest(new DOMException());
return waitForPromises();
});
it('displays an error message', () => {
expect(submitSpy).not.toHaveBeenCalled();
expect(findMessage().textContent).toMatchInterpolatedText(
'There was a problem communicating with your device. (Error)',
);
});
it('allows retrying authentication after an error', () => {
findRetryButton().click();
webAuthnDevice.respondToAuthenticateRequest(mockResponse);
return waitForPromises().then(() => {
expectAuthenticated();
});
});
});
});
});