gitlab-ce/spec/frontend/token_access/inbound_token_access_spec.js

980 lines
36 KiB
JavaScript

import {
GlAlert,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
GlFormRadioGroup,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
import {
JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
} from '~/token_access/constants';
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import RemoveAutopopulatedEntriesModal from '~/token_access/components/remove_autopopulated_entries_modal.vue';
import autopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
import inboundGetCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import getAuthLogCountQuery from '~/token_access/graphql/queries/get_auth_log_count.query.graphql';
import getCiJobTokenScopeAllowlistQuery from '~/token_access/graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
import removeAutopopulatedEntriesMutation from '~/token_access/graphql/mutations/remove_autopopulated_entries.mutation.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ConfirmActionModal from '~/vue_shared/components/confirm_action_modal.vue';
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { stubComponent } from 'helpers/stub_component';
import {
inboundJobTokenScopeEnabledResponse,
inboundJobTokenScopeDisabledResponse,
inboundGroupsAndProjectsWithScopeResponse,
inboundRemoveNamespaceSuccess,
inboundUpdateScopeSuccessResponse,
mockAuthLogsCountResponse,
mockCiJobTokenScopeAllowlistResponse,
mockAutopopulateAllowlistResponse,
mockRemoveAutopopulatedEntriesResponse,
} from './mock_data';
const projectPath = 'root/my-repo';
const message = 'An error occurred';
const error = new Error(message);
Vue.use(VueApollo);
jest.mock('~/alert');
describe('TokenAccess component', () => {
let wrapper;
const authLogCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(4));
const ciJobTokenScopeAllowlistResponseHandler = jest
.fn()
.mockResolvedValue(mockCiJobTokenScopeAllowlistResponse);
const authLogZeroCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(0));
const autopopulateAllowlistResponseHandler = jest
.fn()
.mockResolvedValue(mockAutopopulateAllowlistResponse());
const autopopulateAllowlistResponseErrorHandler = jest
.fn()
.mockResolvedValue(mockAutopopulateAllowlistResponse({ errorMessage: message }));
const inboundJobTokenScopeEnabledResponseHandler = jest
.fn()
.mockResolvedValue(inboundJobTokenScopeEnabledResponse);
const inboundJobTokenScopeDisabledResponseHandler = jest
.fn()
.mockResolvedValue(inboundJobTokenScopeDisabledResponse);
const inboundGroupsAndProjectsWithScopeResponseHandler = jest
.fn()
.mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(true));
const inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler = jest
.fn()
.mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(false));
const inboundRemoveGroupSuccessHandler = jest
.fn()
.mockResolvedValue(inboundRemoveNamespaceSuccess);
const inboundRemoveProjectSuccessHandler = jest
.fn()
.mockResolvedValue(inboundRemoveNamespaceSuccess);
const inboundUpdateScopeSuccessResponseHandler = jest
.fn()
.mockResolvedValue(inboundUpdateScopeSuccessResponse);
const removeAutopopulatedEntriesMutationHandler = jest
.fn()
.mockResolvedValue(mockRemoveAutopopulatedEntriesResponse());
const removeAutopopulatedEntriesMutationErrorHandler = jest
.fn()
.mockResolvedValue(mockRemoveAutopopulatedEntriesResponse({ errorMessage: message }));
const failureHandler = jest.fn().mockRejectedValue(error);
const mockToastShow = jest.fn();
const findAutopopulateAllowlistModal = () => wrapper.findComponent(AutopopulateAllowlistModal);
const findAutopopulationAlert = () => wrapper.findByTestId('autopopulation-alert');
const findAllowlistOptions = () => wrapper.findComponent(GlDisclosureDropdown);
const findAllowlistOption = (index) =>
wrapper.findAllComponents(GlDisclosureDropdownItem).at(index).find('button');
const findFormSelector = () => wrapper.findByTestId('form-selector');
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
const findNamespaceForm = () => wrapper.findComponent(NamespaceForm);
const findSaveChangesBtn = () => wrapper.findByTestId('save-ci-job-token-scope-changes-btn');
const findCountLoadingIcon = () => wrapper.findByTestId('count-loading-icon');
const findGroupCount = () => wrapper.findByTestId('group-count');
const findProjectCount = () => wrapper.findByTestId('project-count');
const findConfirmActionModal = () => wrapper.findComponent(ConfirmActionModal);
const findRemoveAutopopulatedEntriesModal = () =>
wrapper.findComponent(RemoveAutopopulatedEntriesModal);
const findTokenAccessTable = () => wrapper.findComponent(TokenAccessTable);
const createComponent = (
requestHandlers,
{
isJobTokenPoliciesEnabled = false,
enforceAllowlist = false,
projectAllowlistLimit = 2,
stubs = {},
isLoading = false,
} = {},
) => {
wrapper = shallowMountExtended(InboundTokenAccess, {
provide: {
fullPath: projectPath,
enforceAllowlist,
projectAllowlistLimit,
isJobTokenPoliciesEnabled,
},
apolloProvider: createMockApollo(requestHandlers),
mocks: {
$toast: { show: mockToastShow },
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
CrudComponent: stubComponent(CrudComponent),
...stubs,
},
});
if (!isLoading) {
return waitForPromises();
}
return Promise.resolve();
};
describe('loading state', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
],
{ isLoading: true },
);
await nextTick();
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
});
describe('setting allowlist limit', () => {
// in the following tests, group count is 1, project count is 1, and auth log count is 4.
describe('when fetching auth log count is successful', () => {
const createComponentWithAllowlistLimit = async (projectAllowlistLimit) => {
await createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{ projectAllowlistLimit },
);
};
it('calls the query with the expected variables', async () => {
await createComponentWithAllowlistLimit(5);
expect(authLogCountResponseHandler).toHaveBeenCalledWith({ fullPath: 'root/my-repo' });
});
it('passes the correct limit values to the autopopulation modal when allowlist limit is not exceeded', async () => {
await createComponentWithAllowlistLimit(5);
expect(findAutopopulateAllowlistModal().props('authLogExceedsLimit')).toBe(true);
expect(findAutopopulateAllowlistModal().props('projectAllowlistLimit')).toBe(5);
});
it('passes the correct limit values to the autopopulation modal when allowlist limit is exceeded', async () => {
await createComponentWithAllowlistLimit(10);
expect(findAutopopulateAllowlistModal().props('authLogExceedsLimit')).toBe(false);
expect(findAutopopulateAllowlistModal().props('projectAllowlistLimit')).toBe(10);
});
});
it('handles fetches auth log count error correctly', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[getAuthLogCountQuery, failureHandler],
]);
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching authorization logs count.',
});
});
});
describe('fetching groups and projects and scope', () => {
it('fetches groups and projects and scope correctly', () => {
const expectedVariables = {
fullPath: 'root/my-repo',
};
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
]);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledWith(
expectedVariables,
);
});
it('handles fetch groups and projects error correctly', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, failureHandler],
]);
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the projects',
});
});
it('handles fetch scope error correctly', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, failureHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
]);
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the job token scope value',
});
});
it('adds the current project at the top of the list', async () => {
await createComponent(
[[getCiJobTokenScopeAllowlistQuery, ciJobTokenScopeAllowlistResponseHandler]],
{ isJobTokenPoliciesEnabled: true },
);
expect(findTokenAccessTable().props('items')[0].fullPath).toBe('root/my-repo');
expect(findTokenAccessTable().props('items')[1].fullPath).toBe('abc/123');
expect(findTokenAccessTable().props('items')[2].fullPath).toBe('root/your-repo');
expect(findTokenAccessTable().props('items')[3].fullPath).toBe('zed');
});
});
describe('inbound CI job token scope', () => {
it('is on and the alert is hidden', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
]);
expect(findRadioGroup().attributes('checked')).toBe('true');
expect(findTokenDisabledAlert().exists()).toBe(false);
});
it('is off and the alert is visible', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
]);
expect(findRadioGroup().attributes('checked')).toBeUndefined();
expect(findTokenDisabledAlert().exists()).toBe(true);
});
describe('radio group', () => {
it('uses the correct "options" prop', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
]);
const expectedOptions = [
{
value: false,
text: 'All groups and projects',
},
{
value: true,
text: 'Only this project and any groups and projects in the allowlist',
},
];
expect(findRadioGroup().props('options')).toEqual(expectedOptions);
});
});
describe('on update', () => {
it('calls inboundUpdateCIJobTokenScopeMutation mutation', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
]);
const radioGroup = findRadioGroup();
expect(radioGroup.attributes('checked')).toBe('true');
await radioGroup.vm.$emit('input', false);
expect(radioGroup.attributes('checked')).toBeUndefined();
findSaveChangesBtn().vm.$emit('click');
await waitForPromises();
expect(inboundUpdateScopeSuccessResponseHandler).toHaveBeenCalledWith({
input: {
fullPath: 'root/my-repo',
inboundJobTokenScopeEnabled: false,
},
});
});
it('when mutation is successful, renders toast message', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
]);
findSaveChangesBtn().vm.$emit('click');
await waitForPromises();
expect(mockToastShow).toHaveBeenCalledWith(
`CI/CD job token permissions for 'Test project' were successfully updated.`,
);
});
it('handles an update error correctly', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
[inboundUpdateCIJobTokenScopeMutation, failureHandler],
]);
const radioGroup = findRadioGroup();
expect(radioGroup.attributes('checked')).toBeUndefined();
await radioGroup.vm.$emit('input', true);
expect(radioGroup.attributes('checked')).toBe('true');
findSaveChangesBtn().vm.$emit('click');
await waitForPromises();
expect(radioGroup.attributes('checked')).toBeUndefined();
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
describe('save changes button', () => {
it('shows a loading state on click', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
]);
const button = findSaveChangesBtn();
expect(button.props('loading')).toBe(false);
await button.vm.$emit('click');
expect(button.props('loading')).toBe(true);
await waitForPromises();
expect(button.props('loading')).toBe(false);
});
it('has a correct title', async () => {
await createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
]);
expect(findSaveChangesBtn().text()).toBe('Save Changes');
});
});
});
describe('namespace form', () => {
beforeEach(() =>
createComponent(
[
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
],
{ stubs: { CrudComponent } },
),
);
it('does not show form on page load', () => {
expect(findNamespaceForm().exists()).toBe(false);
});
describe('when Add group or project button is clicked', () => {
it('renders the namespace form when clicking "Add group or project option"', async () => {
expect(findNamespaceForm().exists()).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
await nextTick();
expect(findNamespaceForm().exists()).toBe(true);
});
it('closes form when form emits close event', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
await nextTick();
findNamespaceForm().vm.$emit('close');
await nextTick();
expect(findNamespaceForm().exists()).toBe(false);
});
it('refetches groups and projects when form emits saved event', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
await nextTick();
findNamespaceForm().vm.$emit('saved');
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
});
});
});
describe('when importing entries from authorization log', () => {
beforeEach(() =>
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, autopopulateAllowlistResponseHandler],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
),
);
describe('autopopulate entries', () => {
it('renders the autopopulate allowlist modal when clicking "All projects in authentication log"', async () => {
expect(findAutopopulateAllowlistModal().props('showModal')).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
await nextTick();
expect(findAutopopulateAllowlistModal().props('showModal')).toBe(true);
});
it('unselects dropdown option when autopopulate allowlist modal is hidden', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('hide');
await nextTick();
expect(findFormSelector().props('selected')).toBe(null);
});
it('shows loading state while autopopulating entries', async () => {
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await nextTick();
expect(findCountLoadingIcon().exists()).toBe(true);
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Auto-populating allowlist entries. Please wait while the action completes.',
);
});
it('resets loading state after autopopulating entries', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await nextTick();
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Auto-populating allowlist entries. Please wait while the action completes.',
);
await waitForPromises();
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
it('calls the autopopulate allowlist mutation and refetches allowlist and job token setting', async () => {
expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(1);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(1);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(2);
});
it('shows error alert when mutation returns an error', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, autopopulateAllowlistResponseErrorHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe('An error occurred');
});
it('shows error alert when mutation fails', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, failureHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe(
'An error occurred while adding the authentication log entries. Please try again.',
);
});
});
describe('remove autopopulated entries', () => {
const triggerRemoveEntries = () => {
findAllowlistOption(0).trigger('click');
findRemoveAutopopulatedEntriesModal().vm.$emit('remove-entries');
};
it('additional actions are available in the disclosure dropdown', () => {
expect(findAllowlistOptions().exists()).toBe(true);
});
it('"Remove only entries auto-added" renders the remove autopopulated entries modal', async () => {
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(false);
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
it('shows loading state while remove autopopulated entries mutation is processing', async () => {
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
triggerRemoveEntries();
await nextTick();
expect(findCountLoadingIcon().exists()).toBe(true);
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Removing auto-added allowlist entries. Please wait while the action completes.',
);
});
it('resets loading state after removing autopopulated entries', async () => {
triggerRemoveEntries();
await nextTick();
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Removing auto-added allowlist entries. Please wait while the action completes.',
);
await waitForPromises();
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
it('calls the remove autopopulated entries mutation and refetches allowlist', async () => {
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(1);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
});
it('shows toast message when mutation is successful', async () => {
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(mockToastShow).toHaveBeenCalledWith(
'Authentication log entries were successfully removed from the allowlist.',
);
});
it('shows error alert when mutation returns an error', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationErrorHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe('An error occurred');
});
it('shows error alert when mutation fails', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, failureHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe(
'An error occurred while removing the auto-added log entries. Please try again.',
);
});
it('modal can be re-opened again after it closes', async () => {
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
findRemoveAutopopulatedEntriesModal().vm.$emit('hide');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(false);
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
});
describe('allowlist actions', () => {
beforeEach(async () => {
await createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler,
],
[getAuthLogCountQuery, authLogZeroCountResponseHandler],
],
{
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await nextTick();
});
it('hides add auth log entries option if auth log count is zero', () => {
expect(findFormSelector().props('items')).toMatchObject([
{
text: 'Group or project',
value: 'JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT',
},
]);
});
it('hides remove auth log entries option if there are no autopopulated entries', () => {
expect(findAllowlistOptions().exists()).toBe(false);
});
});
});
describe.each`
type | mutation | handler
${'CiJobTokenAccessibleGroup'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
${'Project'} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler}
`('remove $type', ({ type, mutation, handler }) => {
describe('when remove button is clicked', () => {
beforeEach(async () => {
await createComponent([[mutation, handler]]);
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path' });
});
it('shows remove confirmation modal', () => {
expect(findConfirmActionModal().props()).toMatchObject({
title: `Remove full/path`,
actionFn: wrapper.vm.removeItem,
actionText: 'Remove group or project',
});
});
describe('after confirmation modal closes', () => {
beforeEach(() => findConfirmActionModal().vm.$emit('close'));
it('hides remove confirmation modal', () => {
expect(findConfirmActionModal().exists()).toBe(false);
});
});
});
describe('when there is a mutation error', () => {
beforeEach(async () => {
await createComponent([[mutation, failureHandler]]);
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path', __typename: type });
});
it('returns an error', async () => {
await expect(wrapper.vm.removeItem()).rejects.toThrow(error);
});
});
});
describe('when allowlist is enforced by admin', () => {
beforeEach(() => {
const requestHandlers = [
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
];
return createComponent(requestHandlers, { enforceAllowlist: true });
});
it('hides alert, options, and submit button', () => {
expect(findTokenDisabledAlert().exists()).toBe(false);
expect(findRadioGroup().exists()).toBe(false);
expect(findSaveChangesBtn().exists()).toBe(false);
});
});
describe('allowlist counts', () => {
beforeEach(() => {
const requestHandlers = [
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
];
return createComponent(requestHandlers, { stubs: { CrudComponent } });
});
describe('when allowlist query is loaded', () => {
it('does not show loading icon', () => {
expect(findCountLoadingIcon().exists()).toBe(false);
});
it('shows group count', () => {
expect(findGroupCount().text()).toBe('1');
});
it('has group count tooltip', () => {
const tooltip = getBinding(findGroupCount().element, 'gl-tooltip');
expect(tooltip).toMatchObject({ modifiers: { d0: true }, value: '1 group has access' });
});
it('shows project count', () => {
expect(findProjectCount().text()).toBe('1');
});
it('has project count tooltip', () => {
const tooltip = getBinding(findProjectCount().element, 'gl-tooltip');
expect(tooltip).toMatchObject({ modifiers: { d0: true }, value: '1 project has access' });
});
});
describe('when allowlist query is loading', () => {
beforeEach(async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
await nextTick();
findNamespaceForm().vm.$emit('saved');
});
it('shows loading icon', () => {
expect(findCountLoadingIcon().exists()).toBe(true);
});
it('does not show group count', () => {
expect(findGroupCount().exists()).toBe(false);
});
it('does not show project count', () => {
expect(findProjectCount().exists()).toBe(false);
});
});
});
describe.each`
isJobTokenPoliciesEnabled | oldQueryCallCount | newQueryCallCount
${true} | ${0} | ${1}
${false} | ${1} | ${0}
`(
'when isJobTokenPoliciesEnabled is $isJobTokenPoliciesEnabled',
({ isJobTokenPoliciesEnabled, oldQueryCallCount, newQueryCallCount }) => {
const oldQueryHandler = jest.fn();
const newQueryHandler = jest.fn();
beforeEach(() => {
createComponent(
[
[inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, oldQueryHandler],
[getCiJobTokenScopeAllowlistQuery, newQueryHandler],
],
{ isJobTokenPoliciesEnabled },
);
});
it(`calls the old query ${oldQueryCallCount} times`, () => {
expect(oldQueryHandler).toHaveBeenCalledTimes(oldQueryCallCount);
});
it(`calls the new query ${newQueryCallCount} times`, () => {
expect(newQueryHandler).toHaveBeenCalledTimes(newQueryCallCount);
});
},
);
describe('editing an allowlist item', () => {
const item = {};
beforeEach(async () => {
await createComponent([], { stubs: { CrudComponent } });
findTokenAccessTable().vm.$emit('editItem', item);
});
it('shows the form with the namespace', () => {
expect(findNamespaceForm().props('namespace')).toBe(item);
});
describe('when form is closed', () => {
beforeEach(() => findNamespaceForm().vm.$emit('close'));
it('clears the selected namespace', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
await nextTick();
expect(findNamespaceForm().props('namespace')).toBe(null);
});
});
});
});