From 8b4c9a2c4889f58f2296081984b04d6e0b01843a Mon Sep 17 00:00:00 2001 From: Mireya Andres Date: Fri, 21 Feb 2025 23:50:25 +0800 Subject: [PATCH 1/2] Update UX behavior when autopopulating allowlist entries This is developed under the `authentication_logs_migration_for_allowlist` feature flag. Changes include: - Moving loading behavior to page-level instead of modal-level - Update copy text on modal - Refetch allowlist setting after auto-populating - Hide actions if there are no entries to be added/removed --- .../autopopulate_allowlist_modal.vue | 58 +------ .../components/inbound_token_access.vue | 73 +++++++-- doc/ci/jobs/ci_job_token.md | 6 + locale/gitlab.pot | 7 +- .../autopopulate_allowlist_modal_spec.js | 87 +---------- .../token_access/inbound_token_access_spec.js | 141 +++++++++++++++++- spec/frontend/token_access/mock_data.js | 29 ++-- 7 files changed, 231 insertions(+), 170 deletions(-) diff --git a/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue b/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue index 3b6df8c2e5e4ab..87077cbf949ce1 100644 --- a/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue +++ b/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue @@ -2,7 +2,6 @@ import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { __, s__, sprintf } from '~/locale'; -import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql'; export default { name: 'AutopopulateAllowlistModal', @@ -33,12 +32,6 @@ export default { }, }, apollo: {}, - data() { - return { - errorMessage: false, - isAutopopulating: false, - }; - }, computed: { authLogExceedsLimitMessage() { return sprintf( @@ -56,14 +49,12 @@ export default { text: __('Add entries'), attributes: { variant: 'confirm', - loading: this.isAutopopulating, }, }, actionSecondary: { text: __('Cancel'), attributes: { variant: 'default', - disabled: this.isAutopopulating, }, }, }; @@ -77,48 +68,15 @@ export default { }, }, methods: { - async autopopulateAllowlist() { - this.isAutopopulating = true; - this.errorMessage = null; - - try { - const { - data: { - ciJobTokenScopeAutopopulateAllowlist: { errors }, - }, - } = await this.$apollo.mutate({ - mutation: autopopulateAllowlistMutation, - variables: { - projectPath: this.fullPath, - }, - }); - - if (errors.length) { - throw new Error(errors[0]); - } - - this.$emit('refetch-allowlist'); - this.hideModal(); - this.$toast.show( - s__('CICD|Authentication log entries were successfully added to the allowlist.'), - ); - } catch (error) { - this.errorMessage = - error?.message || - s__( - 'CICD|An error occurred while adding the authentication log entries. Please try again.', - ); - } finally { - this.isAutopopulating = false; - } + autopopulateAllowlist() { + this.$emit('autopopulate-allowlist'); }, hideModal() { - this.errorMessage = null; this.$emit('hide'); }, }, compactionAlgorithmHelpPage: helpPagePath('ci/jobs/ci_job_token', { - anchor: 'auto-populate-a-projects-allowlist', + anchor: 'allowlist-compaction', }), }; @@ -135,9 +93,6 @@ export default { @canceled="hideModal" @hidden="hideModal" > - - {{ errorMessage }} -
{{ authLogExceedsLimitMessage }} @@ -163,13 +118,16 @@ export default { +

@@ -182,7 +140,7 @@ export default {

{{ s__( - 'CICD|The process to add entries could take a moment to complete with large logs or allowlists.', + 'CICD|The process might take a moment to complete for large authentication logs or allowlists.', ) }}

diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue index 3f81fbc7a7bfb1..5cb81182a05fc7 100644 --- a/app/assets/javascripts/token_access/components/inbound_token_access.vue +++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue @@ -23,6 +23,7 @@ import inboundRemoveGroupCIJobTokenScopeMutation from '../graphql/mutations/inbo import inboundUpdateCIJobTokenScopeMutation from '../graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql'; import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_job_token_scope.query.graphql'; import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql'; +import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql'; import getCiJobTokenScopeAllowlistQuery from '../graphql/queries/get_ci_job_token_scope_allowlist.query.graphql'; import getAuthLogCountQuery from '../graphql/queries/get_auth_log_count.query.graphql'; import removeAutopopulatedEntriesMutation from '../graphql/mutations/remove_autopopulated_entries.mutation.graphql'; @@ -71,16 +72,6 @@ export default { text: s__('CICD|Only this project and any groups and projects in the allowlist'), }, ], - crudFormActions: [ - { - text: __('Group or project'), - value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT, - }, - { - text: __('All projects in authentication log'), - value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG, - }, - ], components: { AutopopulateAllowlistModal, GlAlert, @@ -206,6 +197,23 @@ export default { anchor: 'control-job-token-access-to-your-project', }); }, + crudFormActions() { + const actions = [ + { + text: __('Group or project'), + value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT, + }, + ]; + + if (this.authLogCount > 0) { + actions.push({ + text: __('All projects in authentication log'), + value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG, + }); + } + + return actions; + }, allowlist() { const { groups, projects } = this.groupsAndProjectsWithAccess; return [...groups, ...projects]; @@ -224,6 +232,9 @@ export default { }, ]; }, + hasAutopopulatedEntries() { + return this.allowlist.filter((entry) => entry.autopopulated).length > 0; + }, groupCount() { return this.groupsAndProjectsWithAccess.groups.length; }, @@ -317,6 +328,43 @@ export default { this.refetchGroupsAndProjects(); return Promise.resolve(); }, + async autopopulateAllowlist() { + this.hideSelectedAction(); + this.autopopulationErrorMessage = null; + this.allowlistLoadingMessage = s__( + 'CICD|Auto-populating allowlist entries. Please wait while the action completes.', + ); + + try { + const { + data: { + ciJobTokenScopeAutopopulateAllowlist: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: autopopulateAllowlistMutation, + variables: { + projectPath: this.fullPath, + }, + }); + + if (errors.length) { + this.autopopulationErrorMessage = errors[0].message; + return; + } + + this.$apollo.queries.inboundJobTokenScopeEnabled.refetch(); + this.refetchAllowlist(); + this.$toast.show( + s__('CICD|Authentication log entries were successfully added to the allowlist.'), + ); + } catch (error) { + this.autopopulationErrorMessage = s__( + 'CICD|An error occurred while adding the authentication log entries. Please try again.', + ); + } finally { + this.allowlistLoadingMessage = ''; + } + }, async removeAutopopulatedEntries() { this.hideSelectedAction(); this.autopopulationErrorMessage = null; @@ -388,7 +436,7 @@ export default { :project-name="projectName" :show-modal="showAutopopulateModal" @hide="hideSelectedAction" - @refetch-allowlist="refetchAllowlist" + @autopopulate-allowlist="autopopulateAllowlist" /> }} + +- Introduced in [GitLab 17.10](https://gitlab.com/gitlab-org/gitlab/-/issues/498125). [Deployed behind the `:authentication_logs_migration_for_allowlist` feature flag](../../user/feature_flags.md), disabled by default. + +{{< /history >}} + To auto-populate the allowlist through the UI: 1. On the left sidebar, select **Search or go** to and find your project. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8df79e3e57e799..fd9ca4e65b9829 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11251,6 +11251,9 @@ msgstr "" msgid "CICD|Auto DevOps" msgstr "" +msgid "CICD|Auto-populating allowlist entries. Please wait while the action completes." +msgstr "" + msgid "CICD|Automatic deployment to staging, manual deployment to production" msgstr "" @@ -11371,7 +11374,7 @@ msgstr "" msgid "CICD|The allowlist can contain a maximum of %{projectAllowlistLimit} groups and projects." msgstr "" -msgid "CICD|The process to add entries could take a moment to complete with large logs or allowlists." +msgid "CICD|The process might take a moment to complete for large authentication logs or allowlists." msgstr "" msgid "CICD|There are several CI/CD limits in place." @@ -11398,7 +11401,7 @@ msgstr "" msgid "CICD|When enabled, all projects must use their allowlist to control CI/CD job token access between projects. The option to allow access from all groups and projects is hidden. %{link_start}Learn More.%{link_end}" msgstr "" -msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. Duplicate entries will be ignored." +msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. This will also update the Job Token setting to %{codeStart}This project and any groups and projects in the allowlist%{codeEnd}, if not already set. Duplicate entries will be ignored." msgstr "" msgid "CICD|group enabled" diff --git a/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js index e0eac4f7236fd9..6e460c96ccbb0d 100644 --- a/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js +++ b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js @@ -1,44 +1,22 @@ import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import { createMockDirective } from 'helpers/vue_mock_directive'; -import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import AutopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql'; import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue'; -import { mockAutopopulateAllowlistResponse, mockAutopopulateAllowlistError } from './mock_data'; const projectName = 'My project'; const fullPath = 'root/my-repo'; -Vue.use(VueApollo); -const mockToastShow = jest.fn(); - describe('AutopopulateAllowlistModal component', () => { let wrapper; - let mockApollo; - let mockAutopopulateMutation; const findAlert = () => wrapper.findComponent(GlAlert); const findModal = () => wrapper.findComponent(GlModal); const findLink = () => wrapper.findComponent(GlLink); const createComponent = ({ props } = {}) => { - const handlers = [[AutopopulateAllowlistMutation, mockAutopopulateMutation]]; - mockApollo = createMockApollo(handlers); - wrapper = shallowMountExtended(AutopopulateAllowlistModal, { - apolloProvider: mockApollo, provide: { fullPath, }, - mocks: { - $toast: { show: mockToastShow }, - }, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, propsData: { authLogExceedsLimit: false, projectAllowlistLimit: 4, @@ -52,10 +30,6 @@ describe('AutopopulateAllowlistModal component', () => { }); }; - beforeEach(() => { - mockAutopopulateMutation = jest.fn(); - }); - describe('template', () => { beforeEach(() => { createComponent(); @@ -90,7 +64,7 @@ describe('AutopopulateAllowlistModal component', () => { it('renders help link', () => { expect(findLink().text()).toBe('What is the compaction algorithm?'); expect(findLink().attributes('href')).toBe( - '/help/ci/jobs/ci_job_token#auto-populate-a-projects-allowlist', + '/help/ci/jobs/ci_job_token#allowlist-compaction', ); }); }); @@ -112,66 +86,15 @@ describe('AutopopulateAllowlistModal component', () => { ); }); - describe('when mutation is running', () => { - beforeEach(() => { - mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse); + describe('when clicking on the primary button', () => { + it('emits the remove-entries event', () => { createComponent(); - }); - it('shows loading state for confirm button and disables cancel button', async () => { - expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: false }); - expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: false }); + expect(wrapper.emitted('autopopulate-allowlist')).toBeUndefined(); findModal().vm.$emit('primary', { preventDefault: jest.fn() }); - await nextTick(); - - expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: true }); - expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: true }); - }); - }); - - describe('when mutation is successful', () => { - beforeEach(async () => { - mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse); - - createComponent(); - findModal().vm.$emit('primary', { preventDefault: jest.fn() }); - await waitForPromises(); - }); - - it('calls the mutation', () => { - expect(mockAutopopulateMutation).toHaveBeenCalledWith({ projectPath: fullPath }); - }); - - it('shows toast message', () => { - expect(mockToastShow).toHaveBeenCalledWith( - 'Authentication log entries were successfully added to the allowlist.', - ); - }); - - it('emits events for refetching data and hiding modal', () => { - expect(wrapper.emitted('refetch-allowlist')).toHaveLength(1); - expect(wrapper.emitted('hide')).toHaveLength(1); - }); - }); - - describe('when mutation fails', () => { - beforeEach(async () => { - createComponent(); - findModal().vm.$emit('primary', { preventDefault: jest.fn() }); - await waitForPromises(); - - mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistError); - }); - - it('renders alert', () => { - expect(findAlert().exists()).toBe(true); - }); - it('does not render toast message or emit events', () => { - expect(mockToastShow).not.toHaveBeenCalledWith(); - expect(wrapper.emitted('refetch-allowlist')).toBeUndefined(); - expect(wrapper.emitted('hide')).toBeUndefined(); + expect(wrapper.emitted('autopopulate-allowlist')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js index b3c9aabcc8c052..ba8e4ca2da5d18 100644 --- a/spec/frontend/token_access/inbound_token_access_spec.js +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -18,6 +18,7 @@ import { 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'; @@ -38,6 +39,7 @@ import { inboundRemoveNamespaceSuccess, inboundUpdateScopeSuccessResponse, mockAuthLogsCountResponse, + mockAutopopulateAllowlistResponse, mockRemoveAutopopulatedEntriesResponse, } from './mock_data'; @@ -53,6 +55,13 @@ describe('TokenAccess component', () => { let wrapper; const authLogCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(4)); + 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); @@ -61,7 +70,10 @@ describe('TokenAccess component', () => { .mockResolvedValue(inboundJobTokenScopeDisabledResponse); const inboundGroupsAndProjectsWithScopeResponseHandler = jest .fn() - .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse); + .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(true)); + const inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler = jest + .fn() + .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(false)); const inboundRemoveGroupSuccessHandler = jest .fn() .mockResolvedValue(inboundRemoveNamespaceSuccess); @@ -474,7 +486,9 @@ describe('TokenAccess component', () => { inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, inboundGroupsAndProjectsWithScopeResponseHandler, ], + [autopopulateAllowlistMutation, autopopulateAllowlistResponseHandler], [removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationHandler], + [getAuthLogCountQuery, authLogCountResponseHandler], ], { authenticationLogsMigrationForAllowlist: true, @@ -515,15 +529,95 @@ describe('TokenAccess component', () => { expect(findFormSelector().props('selected')).toBe(null); }); - it('refetches allowlist when autopopulate mutation is successful', async () => { + 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('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('refetch-allowlist'); + findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist'); + await waitForPromises(); await nextTick(); + expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(1); expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2); - expect(findFormSelector().props('selected')).toBe(null); + expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(2); + }); + + it('shows error alert when mutation returns an error', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + [autopopulateAllowlistMutation, autopopulateAllowlistResponseErrorHandler], + [getAuthLogCountQuery, authLogCountResponseHandler], + ], + { + authenticationLogsMigrationForAllowlist: true, + 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], + ], + { + authenticationLogsMigrationForAllowlist: true, + 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.', + ); }); }); @@ -592,6 +686,7 @@ describe('TokenAccess component', () => { inboundGroupsAndProjectsWithScopeResponseHandler, ], [removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationErrorHandler], + [getAuthLogCountQuery, authLogCountResponseHandler], ], { authenticationLogsMigrationForAllowlist: true, @@ -599,6 +694,8 @@ describe('TokenAccess component', () => { }, ); + await waitForPromises(); + expect(findAutopopulationAlert().exists()).toBe(false); triggerRemoveEntries(); @@ -617,6 +714,7 @@ describe('TokenAccess component', () => { inboundGroupsAndProjectsWithScopeResponseHandler, ], [removeAutopopulatedEntriesMutation, failureHandler], + [getAuthLogCountQuery, authLogCountResponseHandler], ], { authenticationLogsMigrationForAllowlist: true, @@ -624,6 +722,8 @@ describe('TokenAccess component', () => { }, ); + await waitForPromises(); + expect(findAutopopulationAlert().exists()).toBe(false); triggerRemoveEntries(); @@ -652,6 +752,39 @@ describe('TokenAccess component', () => { expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true); }); }); + + describe('allowlist actions', () => { + beforeEach(async () => { + await createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler, + ], + [getAuthLogCountQuery, authLogZeroCountResponseHandler], + ], + { + authenticationLogsMigrationForAllowlist: true, + 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` diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js index b7fd38afb6a7c5..d1e107b2265ff3 100644 --- a/spec/frontend/token_access/mock_data.js +++ b/spec/frontend/token_access/mock_data.js @@ -164,7 +164,7 @@ export const inboundJobTokenScopeDisabledResponse = { }, }; -export const inboundGroupsAndProjectsWithScopeResponse = { +export const inboundGroupsAndProjectsWithScopeResponse = (hasAutopopulatedEntries = true) => ({ data: { project: { __typename: 'Project', @@ -197,12 +197,14 @@ export const inboundGroupsAndProjectsWithScopeResponse = { }, ], }, - groupAllowlistAutopopulatedIds: ['gid://gitlab/Group/45'], - inboundAllowlistAutopopulatedIds: ['gid://gitlab/Project/23'], + groupAllowlistAutopopulatedIds: hasAutopopulatedEntries ? ['gid://gitlab/Group/45'] : [], + inboundAllowlistAutopopulatedIds: hasAutopopulatedEntries + ? ['gid://gitlab/Project/23'] + : [], }, }, }, -}; +}); export const getSaveNamespaceHandler = (error) => jest.fn().mockResolvedValue({ @@ -301,28 +303,15 @@ export const mockAuthLogsResponse = (hasNextPage = false) => ({ }, }); -export const mockAutopopulateAllowlistResponse = { +export const mockAutopopulateAllowlistResponse = ({ errorMessage } = {}) => ({ data: { ciJobTokenScopeAutopopulateAllowlist: { status: 'complete', - errors: [], - __typename: 'CiJobTokenScopeAutopopulateAllowlistPayload', - }, - }, -}; - -export const mockAutopopulateAllowlistError = { - data: { - ciJobTokenScopeAutopopulateAllowlist: { - errors: [ - { - message: 'An error occurred', - }, - ], + errors: errorMessage ? [{ message: errorMessage }] : [], __typename: 'CiJobTokenScopeAutopopulateAllowlistPayload', }, }, -}; +}); export const mockRemoveAutopopulatedEntriesResponse = ({ errorMessage } = {}) => ({ data: { -- GitLab From e46ac15b1681304e0b5aff7269db92bbbf1944db Mon Sep 17 00:00:00 2001 From: Mireya Andres Date: Tue, 4 Mar 2025 11:55:56 +0800 Subject: [PATCH 2/2] Add test for removing loading state --- .../components/inbound_token_access.vue | 6 ++-- .../token_access/inbound_token_access_spec.js | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue index 5cb81182a05fc7..dbc9d0be3ced02 100644 --- a/app/assets/javascripts/token_access/components/inbound_token_access.vue +++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue @@ -232,7 +232,7 @@ export default { }, ]; }, - hasAutopopulatedEntries() { + hasAutoPopulatedEntries() { return this.allowlist.filter((entry) => entry.autopopulated).length > 0; }, groupCount() { @@ -357,7 +357,7 @@ export default { this.$toast.show( s__('CICD|Authentication log entries were successfully added to the allowlist.'), ); - } catch (error) { + } catch { this.autopopulationErrorMessage = s__( 'CICD|An error occurred while adding the authentication log entries. Please try again.', ); @@ -506,7 +506,7 @@ export default { @select="selectAction($event, showForm)" /> { ); }); + 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); @@ -655,6 +672,21 @@ describe('TokenAccess component', () => { ); }); + 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); -- GitLab