255 lines
8.3 KiB
JavaScript
255 lines
8.3 KiB
JavaScript
import { nextTick } from 'vue';
|
|
import { GlAlert, GlButton, GlForm, GlLoadingIcon } from '@gitlab/ui';
|
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|
import waitForPromises from 'helpers/wait_for_promises';
|
|
import Registration from '~/authentication/webauthn/components/registration.vue';
|
|
import {
|
|
I18N_BUTTON_REGISTER,
|
|
I18N_BUTTON_SETUP,
|
|
I18N_BUTTON_TRY_AGAIN,
|
|
I18N_ERROR_HTTP,
|
|
I18N_ERROR_UNSUPPORTED_BROWSER,
|
|
I18N_INFO_TEXT,
|
|
I18N_STATUS_SUCCESS,
|
|
I18N_STATUS_WAITING,
|
|
STATE_ERROR,
|
|
STATE_READY,
|
|
STATE_SUCCESS,
|
|
STATE_UNSUPPORTED,
|
|
STATE_WAITING,
|
|
WEBAUTHN_REGISTER,
|
|
} from '~/authentication/webauthn/constants';
|
|
import * as WebAuthnUtils from '~/authentication/webauthn/util';
|
|
import WebAuthnError from '~/authentication/webauthn/error';
|
|
|
|
const csrfToken = 'mock-csrf-token';
|
|
jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
|
|
jest.mock('~/authentication/webauthn/util');
|
|
jest.mock('~/authentication/webauthn/error');
|
|
|
|
describe('Registration', () => {
|
|
const initialError = null;
|
|
const passwordRequired = true;
|
|
const targetPath = '/-/profile/two_factor_auth/create_webauthn';
|
|
let wrapper;
|
|
|
|
const createComponent = (provide = {}) => {
|
|
wrapper = shallowMountExtended(Registration, {
|
|
provide: { initialError, passwordRequired, targetPath, ...provide },
|
|
});
|
|
};
|
|
|
|
const findButton = () => wrapper.findComponent(GlButton);
|
|
|
|
describe(`when ${STATE_UNSUPPORTED} state`, () => {
|
|
it('shows an error if using unsecure scheme (HTTP)', () => {
|
|
// `supported` function returns false for HTTP because `navigator.credentials` is undefined.
|
|
WebAuthnUtils.supported.mockReturnValue(false);
|
|
WebAuthnUtils.isHTTPS.mockReturnValue(false);
|
|
createComponent();
|
|
|
|
const alert = wrapper.findComponent(GlAlert);
|
|
expect(alert.props('variant')).toBe('danger');
|
|
expect(alert.text()).toBe(I18N_ERROR_HTTP);
|
|
});
|
|
|
|
it('shows an error if using unsupported browser', () => {
|
|
WebAuthnUtils.supported.mockReturnValue(false);
|
|
WebAuthnUtils.isHTTPS.mockReturnValue(true);
|
|
createComponent();
|
|
|
|
const alert = wrapper.findComponent(GlAlert);
|
|
expect(alert.props('variant')).toBe('danger');
|
|
expect(alert.text()).toBe(I18N_ERROR_UNSUPPORTED_BROWSER);
|
|
});
|
|
});
|
|
|
|
describe('when scheme or browser are supported', () => {
|
|
const mockCreate = jest.fn();
|
|
|
|
const clickSetupDeviceButton = () => {
|
|
findButton().vm.$emit('click');
|
|
return nextTick();
|
|
};
|
|
|
|
const setupDevice = () => {
|
|
clickSetupDeviceButton();
|
|
return waitForPromises();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
WebAuthnUtils.isHTTPS.mockReturnValue(true);
|
|
WebAuthnUtils.supported.mockReturnValue(true);
|
|
global.navigator.credentials = { create: mockCreate };
|
|
gon.webauthn = { options: {} };
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.navigator.credentials = undefined;
|
|
});
|
|
|
|
describe(`when ${STATE_READY} state`, () => {
|
|
it('shows button and explanation text', () => {
|
|
createComponent();
|
|
|
|
expect(findButton().text()).toBe(I18N_BUTTON_SETUP);
|
|
expect(wrapper.text()).toContain(I18N_INFO_TEXT);
|
|
});
|
|
});
|
|
|
|
describe(`when ${STATE_WAITING} state`, () => {
|
|
it('shows loading icon and message after pressing the button', async () => {
|
|
createComponent();
|
|
|
|
await clickSetupDeviceButton();
|
|
|
|
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
|
expect(wrapper.text()).toContain(I18N_STATUS_WAITING);
|
|
});
|
|
});
|
|
|
|
describe(`when ${STATE_SUCCESS} state`, () => {
|
|
const credentials = 1;
|
|
|
|
const findCurrentPasswordInput = () => wrapper.findByTestId('current-password-input');
|
|
const findDeviceNameInput = () => wrapper.findByTestId('device-name-input');
|
|
|
|
beforeEach(() => {
|
|
mockCreate.mockResolvedValueOnce(true);
|
|
WebAuthnUtils.convertCreateResponse.mockReturnValue(credentials);
|
|
});
|
|
|
|
describe('registration form', () => {
|
|
it('has correct action', async () => {
|
|
createComponent();
|
|
|
|
await setupDevice();
|
|
|
|
expect(wrapper.findComponent(GlForm).attributes('action')).toBe(targetPath);
|
|
});
|
|
|
|
describe('when password is required', () => {
|
|
it('shows device name and password fields', async () => {
|
|
createComponent();
|
|
|
|
await setupDevice();
|
|
|
|
expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
|
|
|
|
// Visible inputs
|
|
expect(findCurrentPasswordInput().attributes('name')).toBe('current_password');
|
|
expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
|
|
|
|
// Hidden inputs
|
|
expect(
|
|
wrapper
|
|
.find('input[name="device_registration[device_response]"]')
|
|
.attributes('value'),
|
|
).toBe(`${credentials}`);
|
|
expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
|
|
csrfToken,
|
|
);
|
|
|
|
expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
|
|
});
|
|
|
|
it('enables the register device button when device name and password are filled', async () => {
|
|
createComponent();
|
|
|
|
await setupDevice();
|
|
|
|
expect(findButton().props('disabled')).toBe(true);
|
|
|
|
// Visible inputs
|
|
findCurrentPasswordInput().vm.$emit('input', 'my current password');
|
|
findDeviceNameInput().vm.$emit('input', 'my device name');
|
|
await nextTick();
|
|
|
|
expect(findButton().props('disabled')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when password is not required', () => {
|
|
it('shows a device name field', async () => {
|
|
createComponent({ passwordRequired: false });
|
|
|
|
await setupDevice();
|
|
|
|
expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
|
|
|
|
// Visible inputs
|
|
expect(findCurrentPasswordInput().exists()).toBe(false);
|
|
expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
|
|
|
|
// Hidden inputs
|
|
expect(
|
|
wrapper
|
|
.find('input[name="device_registration[device_response]"]')
|
|
.attributes('value'),
|
|
).toBe(`${credentials}`);
|
|
expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
|
|
csrfToken,
|
|
);
|
|
|
|
expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
|
|
});
|
|
|
|
it('enables the register device button when device name is filled', async () => {
|
|
createComponent({ passwordRequired: false });
|
|
|
|
await setupDevice();
|
|
|
|
expect(findButton().props('disabled')).toBe(true);
|
|
|
|
findDeviceNameInput().vm.$emit('input', 'my device name');
|
|
await nextTick();
|
|
|
|
expect(findButton().props('disabled')).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe(`when ${STATE_ERROR} state`, () => {
|
|
it('shows an initial error message and a retry button', () => {
|
|
const myError = 'my error';
|
|
createComponent({ initialError: myError });
|
|
|
|
const alert = wrapper.findComponent(GlAlert);
|
|
expect(alert.props()).toMatchObject({
|
|
variant: 'danger',
|
|
secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
|
|
});
|
|
expect(alert.text()).toContain(myError);
|
|
});
|
|
|
|
it('shows an error message and a retry button', async () => {
|
|
createComponent();
|
|
const error = new Error();
|
|
mockCreate.mockRejectedValueOnce(error);
|
|
|
|
await setupDevice();
|
|
|
|
expect(WebAuthnError).toHaveBeenCalledWith(error, WEBAUTHN_REGISTER);
|
|
expect(wrapper.findComponent(GlAlert).props()).toMatchObject({
|
|
variant: 'danger',
|
|
secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
|
|
});
|
|
});
|
|
|
|
it('recovers after an error (error to success state)', async () => {
|
|
createComponent();
|
|
mockCreate.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(true);
|
|
|
|
await setupDevice();
|
|
|
|
expect(wrapper.findComponent(GlAlert).props('variant')).toBe('danger');
|
|
|
|
wrapper.findComponent(GlAlert).vm.$emit('secondaryAction');
|
|
await waitForPromises();
|
|
|
|
expect(wrapper.findComponent(GlAlert).props('variant')).toBe('info');
|
|
});
|
|
});
|
|
});
|
|
});
|