Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci/runner/components/registration')
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js198
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js209
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js62
3 files changed, 469 insertions, 0 deletions
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
new file mode 100644
index 00000000000..cb46c668930
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -0,0 +1,198 @@
+import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
+import { mount, shallowMount, createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
+import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
+
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import {
+ mockGraphqlRunnerPlatforms,
+ mockGraphqlInstructions,
+} from 'jest/vue_shared/components/runner_instructions/mock_data';
+
+const mockToken = '0123456789';
+const maskToken = '**********';
+
+Vue.use(VueApollo);
+
+describe('RegistrationDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
+ const findRegistrationTokenInput = () =>
+ wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
+ const findTokenResetDropdownItem = () =>
+ wrapper.findComponent(RegistrationTokenResetDropdownItem);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findModalContent = () =>
+ createWrapper(document.body)
+ .find('[data-testid="runner-instructions-modal"]')
+ .text()
+ .replace(/[\n\t\s]+/g, ' ');
+
+ const openModal = async () => {
+ await findRegistrationInstructionsDropdownItem().trigger('click');
+ findModal().vm.$emit('shown');
+
+ await waitForPromises();
+ };
+
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
+ wrapper = extendedWrapper(
+ mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ }),
+ );
+ };
+
+ const createComponentWithModal = () => {
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ createComponent(
+ {
+ // Mock load modal contents from API
+ apolloProvider: createMockApollo(requestHandlers),
+ // Use `attachTo` to find the modal
+ attachTo: document.body,
+ },
+ mount,
+ );
+ };
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${'Register an instance runner'}
+ ${GROUP_TYPE} | ${'Register a group runner'}
+ ${PROJECT_TYPE} | ${'Register a project runner'}
+ `('Dropdown text for type $type is "$text"', () => {
+ createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+
+ expect(wrapper.text()).toContain('Register an instance runner');
+ });
+
+ it('Passes attributes to the dropdown component', () => {
+ createComponent({ attrs: { right: true } });
+
+ expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
+ });
+
+ describe('Instructions dropdown item', () => {
+ it('Displays "Show runner" dropdown item', () => {
+ createComponent();
+
+ expect(findRegistrationInstructionsDropdownItem().text()).toBe(
+ 'Show runner installation and registration instructions',
+ );
+ });
+
+ describe('When the dropdown item is clicked', () => {
+ beforeEach(async () => {
+ createComponentWithModal({}, mount);
+
+ await openModal();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('opens the modal with contents', () => {
+ const modalText = findModalContent();
+
+ expect(modalText).toContain('Install a runner');
+
+ // Environment selector
+ expect(modalText).toContain('Environment');
+ expect(modalText).toContain('Linux macOS Windows Docker Kubernetes');
+
+ // Architecture selector
+ expect(modalText).toContain('Architecture');
+ expect(modalText).toContain('amd64 amd64 386 arm arm64');
+
+ expect(modalText).toContain('Download and install binary');
+ });
+ });
+ });
+
+ describe('Registration token', () => {
+ it('Displays dropdown form for the registration token', () => {
+ createComponent();
+
+ expect(findTokenDropdownItem().exists()).toBe(true);
+ });
+
+ it('Displays masked value by default', () => {
+ createComponent({}, mount);
+
+ expect(findRegistrationTokenInput().element.value).toBe(maskToken);
+ });
+ });
+
+ describe('Reset token item', () => {
+ it('Displays registration token reset item', () => {
+ createComponent();
+
+ expect(findTokenResetDropdownItem().exists()).toBe(true);
+ });
+
+ it.each([INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE])('Set up token reset for %s', (type) => {
+ createComponent({ props: { type } });
+
+ expect(findTokenResetDropdownItem().props('type')).toBe(type);
+ });
+ });
+
+ describe('When token is reset', () => {
+ const newToken = 'mock1';
+
+ const resetToken = async () => {
+ findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
+ await nextTick();
+ };
+
+ it('Updates token input', async () => {
+ createComponent({}, mount);
+
+ expect(findRegistrationToken().props('value')).not.toBe(newToken);
+
+ await resetToken();
+
+ expect(findRegistrationToken().props('value')).toBe(newToken);
+ });
+
+ it('Updates token in modal', async () => {
+ createComponentWithModal({}, mount);
+
+ await openModal();
+
+ expect(findModalContent()).toContain(mockToken);
+
+ await resetToken();
+
+ expect(findModalContent()).toContain(newToken);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
new file mode 100644
index 00000000000..783a4d9252a
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -0,0 +1,209 @@
+import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+jest.mock('~/flash');
+jest.mock('~/ci/runner/sentry_utils');
+
+Vue.use(VueApollo);
+Vue.use(GlToast);
+
+const mockNewToken = 'NEW_TOKEN';
+const modalID = 'token-reset-modal';
+
+describe('RegistrationTokenResetDropdownItem', () => {
+ let wrapper;
+ let runnersRegistrationTokenResetMutationHandler;
+ let showToast;
+
+ const mockEvent = { preventDefault: jest.fn() };
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
+
+ const createComponent = ({ props, provide = {} } = {}) => {
+ wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
+ provide,
+ propsData: {
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ apolloProvider: createMockApollo([
+ [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
+ ]),
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ });
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
+ };
+
+ beforeEach(() => {
+ runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({
+ data: {
+ runnersRegistrationTokenReset: {
+ token: mockNewToken,
+ errors: [],
+ },
+ },
+ });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays reset button', () => {
+ expect(findDropdownItem().exists()).toBe(true);
+ });
+
+ describe('modal directive integration', () => {
+ it('has the correct ID on the dropdown', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
+ });
+
+ describe('On click and confirmation', () => {
+ const mockGroupId = '11';
+ const mockProjectId = '22';
+
+ describe.each`
+ type | provide | expectedInput
+ ${INSTANCE_TYPE} | ${{}} | ${{ type: INSTANCE_TYPE }}
+ ${GROUP_TYPE} | ${{ groupId: mockGroupId }} | ${{ type: GROUP_TYPE, id: `gid://gitlab/Group/${mockGroupId}` }}
+ ${PROJECT_TYPE} | ${{ projectId: mockProjectId }} | ${{ type: PROJECT_TYPE, id: `gid://gitlab/Project/${mockProjectId}` }}
+ `('Resets token of type $type', ({ type, provide, expectedInput }) => {
+ beforeEach(async () => {
+ createComponent({
+ provide,
+ props: { type },
+ });
+
+ findDropdownItem().trigger('click');
+ clickSubmit();
+ await waitForPromises();
+ });
+
+ it('resets token', () => {
+ expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledWith({
+ input: expectedInput,
+ });
+ });
+
+ it('emits result', () => {
+ expect(wrapper.emitted('tokenReset')).toHaveLength(1);
+ expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+ });
+
+ it('does not show a loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows confirmation', () => {
+ expect(showToast).toHaveBeenLastCalledWith(
+ expect.stringContaining('registration token generated'),
+ );
+ });
+ });
+ });
+
+ describe('On click without confirmation', () => {
+ beforeEach(async () => {
+ findDropdownItem().vm.$emit('click');
+ await waitForPromises();
+ });
+
+ it('does not reset token', () => {
+ expect(runnersRegistrationTokenResetMutationHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any result', () => {
+ expect(wrapper.emitted('tokenReset')).toBeUndefined();
+ });
+
+ it('does not show a loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('does not shows confirmation', () => {
+ expect(showToast).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('On error', () => {
+ it('On network error, error message is shown', async () => {
+ const mockErrorMsg = 'Token reset failed!';
+
+ runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ findDropdownItem().trigger('click');
+ clickSubmit();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: mockErrorMsg,
+ });
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerRegistrationTokenReset',
+ });
+ });
+
+ it('On validation error, error message is shown', async () => {
+ const mockErrorMsg = 'User not allowed!';
+ const mockErrorMsg2 = 'Type is not valid!';
+
+ runnersRegistrationTokenResetMutationHandler.mockResolvedValue({
+ data: {
+ runnersRegistrationTokenReset: {
+ token: null,
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ findDropdownItem().trigger('click');
+ clickSubmit();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerRegistrationTokenReset',
+ });
+ });
+ });
+
+ describe('Immediately after click', () => {
+ it('shows loading state', async () => {
+ findDropdownItem().trigger('click');
+ clickSubmit();
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
new file mode 100644
index 00000000000..d2a51c0d910
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -0,0 +1,62 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+const mockToken = '01234567890';
+const mockMasked = '***********';
+
+describe('RegistrationToken', () => {
+ let wrapper;
+ let showToast;
+
+ Vue.use(GlToast);
+
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RegistrationToken, {
+ propsData: {
+ value: mockToken,
+ inputId: 'token-value',
+ ...props,
+ },
+ });
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays value and copy button', () => {
+ createComponent();
+
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
+ 'Copy registration token',
+ );
+ });
+
+ // Component integration test to ensure secure masking
+ it('Displays masked value by default', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(wrapper.find('input').element.value).toBe(mockMasked);
+ });
+
+ describe('When the copy to clipboard button is clicked', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a copied message', () => {
+ findInputCopyToggleVisibility().vm.$emit('copy');
+
+ expect(showToast).toHaveBeenCalledTimes(1);
+ expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ });
+ });
+});