From 8b41647242282982279e88e0f863738e18b818ed Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 25 Feb 2023 03:12:22 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../webauthn/components/registration_spec.js | 249 +++++++++++++++++++++ spec/frontend/fixtures/webauthn.rb | 1 + spec/helpers/device_registration_helper_spec.rb | 37 +++ spec/models/oauth_access_token_spec.rb | 4 +- 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 spec/frontend/authentication/webauthn/components/registration_spec.js create mode 100644 spec/helpers/device_registration_helper_spec.rb (limited to 'spec') diff --git a/spec/frontend/authentication/webauthn/components/registration_spec.js b/spec/frontend/authentication/webauthn/components/registration_spec.js new file mode 100644 index 00000000000..56185c59b5a --- /dev/null +++ b/spec/frontend/authentication/webauthn/components/registration_spec.js @@ -0,0 +1,249 @@ +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, +} from '~/authentication/webauthn/constants'; +import * as WebAuthnUtils from '~/authentication/webauthn/util'; + +const csrfToken = 'mock-csrf-token'; +jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken })); +jest.mock('~/authentication/webauthn/util'); + +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)', () => { + WebAuthnUtils.isHTTPS.mockReturnValue(false); + WebAuthnUtils.supported.mockReturnValue(true); + 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.isHTTPS.mockReturnValue(true); + WebAuthnUtils.supported.mockReturnValue(false); + 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', async () => { + 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(); + mockCreate.mockRejectedValueOnce(new Error()); + + await setupDevice(); + + 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'); + }); + }); + }); +}); diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb index c6e9b41b584..ed6180118f0 100644 --- a/spec/frontend/fixtures/webauthn.rb +++ b/spec/frontend/fixtures/webauthn.rb @@ -32,6 +32,7 @@ RSpec.context 'WebAuthn' do allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance| allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end + stub_feature_flags(webauthn_without_totp: false) end it 'webauthn/register.html' do diff --git a/spec/helpers/device_registration_helper_spec.rb b/spec/helpers/device_registration_helper_spec.rb new file mode 100644 index 00000000000..a8222cddca9 --- /dev/null +++ b/spec/helpers/device_registration_helper_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe DeviceRegistrationHelper, feature_category: :authentication_and_authorization do + describe "#device_registration_data" do + it "returns a hash with device registration properties without initial error" do + device_registration_data = helper.device_registration_data( + current_password_required: false, + target_path: "/my/path", + webauthn_error: nil + ) + + expect(device_registration_data).to eq( + { + initial_error: nil, + target_path: "/my/path", + password_required: "false" + }) + end + + it "returns a hash with device registration properties with initial error" do + device_registration_data = helper.device_registration_data( + current_password_required: true, + target_path: "/my/path", + webauthn_error: { message: "my error" } + ) + + expect(device_registration_data).to eq( + { + initial_error: "my error", + target_path: "/my/path", + password_required: "true" + }) + end + end +end diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb index fc53d926dd6..5fa590eab58 100644 --- a/spec/models/oauth_access_token_spec.rb +++ b/spec/models/oauth_access_token_spec.rb @@ -59,7 +59,7 @@ RSpec.describe OauthAccessToken do it 'uses the expires_in value' do token = OauthAccessToken.new(expires_in: 1.minute) - expect(token.expires_in).to eq 1.minute + expect(token).to be_valid end end @@ -67,7 +67,7 @@ RSpec.describe OauthAccessToken do it 'uses default value' do token = OauthAccessToken.new(expires_in: nil) - expect(token.expires_in).to eq 2.hours + expect(token).to be_invalid end end end -- cgit v1.2.3