From 84d8663540bfd7517081ae903d482d5e0e1e4ca6 Mon Sep 17 00:00:00 2001 From: Miranda Fluharty Date: Wed, 1 May 2024 13:42:32 -0600 Subject: [PATCH] Allow groups to be added to the inbound allowlist Modify table to be usable for both groups and projects Accept group paths in the form, use the mutation that can handle both When removing, call either the group or project mutation Add group count to table/card header Update UI text that refers to projects to refer to groups and projects Update tests Changelog: added --- .../components/inbound_token_access.vue | 165 +++++++++++------- .../components/outbound_token_access.vue | 18 +- .../components/token_access_table.vue | 67 +++++++ .../components/token_projects_table.vue | 64 ------- ...roject_ci_job_token_scope.mutation.graphql | 5 + ...roject_ci_job_token_scope.mutation.graphql | 7 - ..._group_ci_job_token_scope.mutation.graphql | 7 + ...cts_with_ci_job_token_scope.query.graphql} | 13 +- locale/gitlab.pot | 21 ++- .../token_access/inbound_token_access_spec.js | 134 +++++++++----- spec/frontend/token_access/mock_data.js | 57 ++++-- .../token_access/token_access_table_spec.js | 56 ++++++ .../token_access/token_projects_table_spec.js | 64 ------- 13 files changed, 399 insertions(+), 279 deletions(-) create mode 100644 app/assets/javascripts/token_access/components/token_access_table.vue delete mode 100644 app/assets/javascripts/token_access/components/token_projects_table.vue create mode 100644 app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql delete mode 100644 app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql create mode 100644 app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql rename app/assets/javascripts/token_access/graphql/queries/{inbound_get_projects_with_ci_job_token_scope.query.graphql => inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql} (50%) create mode 100644 spec/frontend/token_access/token_access_table_spec.js delete mode 100644 spec/frontend/token_access/token_projects_table_spec.js 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 02605dfb4fb3af..31575cd36b7184 100644 --- a/app/assets/javascripts/token_access/components/inbound_token_access.vue +++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue @@ -13,42 +13,43 @@ import { import { createAlert } from '~/alert'; import { __, s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; -import inboundAddProjectCIJobTokenScopeMutation from '../graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql'; +import { TYPENAME_GROUP } from '~/graphql_shared/constants'; +import inboundAddGroupOrProjectCIJobTokenScope from '../graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql'; import inboundRemoveProjectCIJobTokenScopeMutation from '../graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql'; +import inboundRemoveGroupCIJobTokenScopeMutation from '../graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql'; 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 inboundGetProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql'; -import TokenProjectsTable from './token_projects_table.vue'; +import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql'; +import TokenAccessTable from './token_access_table.vue'; export default { i18n: { toggleLabelTitle: s__('CICD|Limit access %{italicStart}to%{italicEnd} this project'), - toggleHelpText: s__( - `CICD|Prevent access to this project from other project CI/CD job tokens, unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}.`, - ), - cardHeaderTitle: s__( - 'CICD|Allow CI job tokens from the following projects to access this project', + toggleDescription: s__( + `CICD|Allow access to this project from authorized groups or projects by adding them to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}.`, ), + cardHeaderTitle: s__('CICD|Groups and projects with access'), settingDisabledMessage: s__( - 'CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token.', + 'CICD|No access is currently allowed to this project. Enable feature to authorize access from groups or projects in the allowlist below.', ), - addProject: __('Add project'), + addGroupOrProject: __('Add group or project'), + add: __('Add'), cancel: __('Cancel'), - addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'), + addProjectPlaceholder: __( + 'Paste group path (i.e. gitlab-org) or project path (i.e. gitlab-org/gitlab)', + ), projectsFetchError: __('There was a problem fetching the projects'), scopeFetchError: __('There was a problem fetching the job token scope value'), }, fields: [ { - key: 'project', - label: __('Project with access'), - thClass: 'gl-border-t-none!', + key: 'fullPath', + label: '', }, { key: 'actions', label: '', tdClass: 'gl-text-right', - thClass: 'gl-border-t-none!', }, ], components: { @@ -61,7 +62,7 @@ export default { GlLoadingIcon, GlSprintf, GlToggle, - TokenProjectsTable, + TokenAccessTable, }, inject: { fullPath: { @@ -83,15 +84,16 @@ export default { createAlert({ message: this.$options.i18n.scopeFetchError }); }, }, - projects: { - query: inboundGetProjectsWithCIJobTokenScopeQuery, + groupsAndProjectsWithAccess: { + query: inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, variables() { return { fullPath: this.fullPath, }; }, update({ project }) { - return project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? []; + this.projects = project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? []; + this.groups = project?.ciJobTokenScope?.groupsAllowlist?.nodes ?? []; }, error() { createAlert({ message: this.$options.i18n.projectsFetchError }); @@ -101,14 +103,15 @@ export default { data() { return { inboundJobTokenScopeEnabled: null, - targetProjectPath: '', + targetPath: '', projects: [], + groups: [], isAddFormVisible: false, }; }, computed: { - isProjectPathEmpty() { - return this.targetProjectPath === ''; + isTargetPathEmpty() { + return this.targetPath === ''; }, ciJobTokenHelpPage() { return helpPagePath('ci/jobs/ci_job_token#allow-access-to-your-project-with-a-job-token'); @@ -139,17 +142,17 @@ export default { createAlert({ message: error.message }); } }, - async addProject() { + async addGroupOrProject() { try { const { data: { - ciJobTokenScopeAddProject: { errors }, + ciJobTokenScopeAddGroupOrProject: { errors }, }, } = await this.$apollo.mutate({ - mutation: inboundAddProjectCIJobTokenScopeMutation, + mutation: inboundAddGroupOrProjectCIJobTokenScope, variables: { projectPath: this.fullPath, - targetProjectPath: this.targetProjectPath, + targetPath: this.targetPath, }, }); @@ -159,23 +162,38 @@ export default { } catch (error) { createAlert({ message: error.message }); } finally { - this.clearTargetProjectPath(); - this.getProjects(); + this.clearTargetPath(); + this.getGroupsAndProjects(); } }, - async removeProject(removeTargetPath) { + async removeItem(item) { try { - const { - data: { - ciJobTokenScopeRemoveProject: { errors }, - }, - } = await this.$apollo.mutate({ - mutation: inboundRemoveProjectCIJobTokenScopeMutation, - variables: { - projectPath: this.fullPath, - targetProjectPath: removeTargetPath, - }, - }); + let errors; + + // eslint-disable-next-line no-underscore-dangle + if (item.__typename === TYPENAME_GROUP) { + const { + data: { ciJobTokenScopeRemoveGroup }, + } = await this.$apollo.mutate({ + mutation: inboundRemoveGroupCIJobTokenScopeMutation, + variables: { + projectPath: this.fullPath, + targetGroupPath: item.fullPath, + }, + }); + errors = ciJobTokenScopeRemoveGroup.errors; + } else { + const { + data: { ciJobTokenScopeRemoveProject }, + } = await this.$apollo.mutate({ + mutation: inboundRemoveProjectCIJobTokenScopeMutation, + variables: { + projectPath: this.fullPath, + targetProjectPath: item.fullPath, + }, + }); + errors = ciJobTokenScopeRemoveProject.errors; + } if (errors.length) { throw new Error(errors[0]); @@ -183,15 +201,15 @@ export default { } catch (error) { createAlert({ message: error.message }); } finally { - this.getProjects(); + this.getGroupsAndProjects(); } }, - clearTargetProjectPath() { - this.targetProjectPath = ''; + clearTargetPath() { + this.targetPath = ''; this.isAddFormVisible = false; }, - getProjects() { - this.$apollo.queries.projects.refetch(); + getGroupsAndProjects() { + this.$apollo.queries.groupsAndProjectsWithAccess.refetch(); }, showAddForm() { this.isAddFormVisible = true; @@ -206,6 +224,7 @@ export default { - - diff --git a/app/assets/javascripts/token_access/components/token_access_table.vue b/app/assets/javascripts/token_access/components/token_access_table.vue new file mode 100644 index 00000000000000..700c6b9ef39581 --- /dev/null +++ b/app/assets/javascripts/token_access/components/token_access_table.vue @@ -0,0 +1,67 @@ + + diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue deleted file mode 100644 index 4245b39dec1b08..00000000000000 --- a/app/assets/javascripts/token_access/components/token_projects_table.vue +++ /dev/null @@ -1,64 +0,0 @@ - - diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql new file mode 100644 index 00000000000000..94b34a6c1eb9fe --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql @@ -0,0 +1,5 @@ +mutation inboundAddGroupOrProjectCIJobTokenScope($projectPath: ID!, $targetPath: ID!) { + ciJobTokenScopeAddGroupOrProject(input: { projectPath: $projectPath, targetPath: $targetPath }) { + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql deleted file mode 100644 index f030a892af2ca2..00000000000000 --- a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation inboundAddProjectCIJobTokenScope($projectPath: ID!, $targetProjectPath: ID!) { - ciJobTokenScopeAddProject( - input: { projectPath: $projectPath, targetProjectPath: $targetProjectPath, direction: INBOUND } - ) { - errors - } -} diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql new file mode 100644 index 00000000000000..5681c5967adf8e --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql @@ -0,0 +1,7 @@ +mutation inboundRemoveGroupCIJobTokenScope($projectPath: ID!, $targetGroupPath: ID!) { + ciJobTokenScopeRemoveGroup( + input: { projectPath: $projectPath, targetGroupPath: $targetGroupPath } + ) { + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql similarity index 50% rename from app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql rename to app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql index c51bdcbf7d2ece..641642ffc7677b 100644 --- a/app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql +++ b/app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql @@ -1,15 +1,18 @@ -query inboundGetProjectsWithCIJobTokenScope($fullPath: ID!) { +query inboundGetGroupsAndProjectsWithCIJobTokenScope($fullPath: ID!) { project(fullPath: $fullPath) { id ciJobTokenScope { + groupsAllowlist { + nodes { + id + name + fullPath + } + } inboundAllowlist { nodes { id name - namespace { - id - fullPath - } fullPath } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 67402b1f8ab545..22a1672df44bc4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3101,6 +3101,9 @@ msgstr "" msgid "Add existing issue" msgstr "" +msgid "Add group or project" +msgstr "" + msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface" msgstr "" @@ -9716,7 +9719,7 @@ msgstr "" msgid "CI/CD limits" msgstr "" -msgid "CI/CD|No projects have been added to the scope" +msgid "CI/CD|No %{itemType}s have been added to the scope" msgstr "" msgid "CICDAnalytics|%{percent}%{percentSymbol}" @@ -9772,7 +9775,7 @@ msgstr "" msgid "CICD|Add an existing project to the scope" msgstr "" -msgid "CICD|Allow CI job tokens from the following projects to access this project" +msgid "CICD|Allow access to this project from authorized groups or projects by adding them to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}." msgstr "" msgid "CICD|Auto DevOps" @@ -9805,7 +9808,7 @@ msgstr "" msgid "CICD|Enable feature to limit job token access to the following projects." msgstr "" -msgid "CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token." +msgid "CICD|Groups and projects with access" msgstr "" msgid "CICD|Jobs" @@ -9823,13 +9826,13 @@ msgstr "" msgid "CICD|Maintainer" msgstr "" -msgid "CICD|Pipelines and jobs cannot be cancelled" +msgid "CICD|No access is currently allowed to this project. Enable feature to authorize access from groups or projects in the allowlist below." msgstr "" -msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}." +msgid "CICD|Pipelines and jobs cannot be cancelled" msgstr "" -msgid "CICD|Prevent access to this project from other project CI/CD job tokens, unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}." +msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}." msgstr "" msgid "CICD|The %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}from%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting is deprecated and will be removed in the 18.0 milestone. Use the %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}to%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting and allowlist instead. %{linkStart}How do I do this?%{linkEnd}" @@ -37079,6 +37082,9 @@ msgstr "" msgid "Paste epic link" msgstr "" +msgid "Paste group path (i.e. gitlab-org) or project path (i.e. gitlab-org/gitlab)" +msgstr "" + msgid "Paste issue link" msgstr "" @@ -40046,9 +40052,6 @@ msgstr "" msgid "Project was not found or you do not have permission to add this project to Security Dashboards." msgstr "" -msgid "Project with access" -msgstr "" - msgid "Project: %{name}" msgstr "" diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js index d82d65e35495d5..a7b0a9820b502f 100644 --- a/spec/frontend/token_access/inbound_token_access_spec.js +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -6,21 +6,24 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue'; -import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql'; +import inboundAddGroupOrProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.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 inboundGetProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_projects_with_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 { inboundJobTokenScopeEnabledResponse, inboundJobTokenScopeDisabledResponse, - inboundProjectsWithScopeResponse, - inboundAddProjectSuccessResponse, + inboundGroupsAndProjectsWithScopeResponse, + inboundAddGroupOrProjectSuccessResponse, + inboundRemoveGroupSuccess, inboundRemoveProjectSuccess, inboundUpdateScopeSuccessResponse, } from './mock_data'; const projectPath = 'root/my-repo'; +const testGroupPath = 'gitlab-org'; const testProjectPath = 'root/test'; const message = 'An error occurred'; const error = new Error(message); @@ -38,12 +41,13 @@ describe('TokenAccess component', () => { const inboundJobTokenScopeDisabledResponseHandler = jest .fn() .mockResolvedValue(inboundJobTokenScopeDisabledResponse); - const inboundProjectsWithScopeResponseHandler = jest + const inboundGroupsAndProjectsWithScopeResponseHandler = jest .fn() - .mockResolvedValue(inboundProjectsWithScopeResponse); - const inboundAddProjectSuccessResponseHandler = jest + .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse); + const inboundAddGroupOrProjectSuccessResponseHandler = jest .fn() - .mockResolvedValue(inboundAddProjectSuccessResponse); + .mockResolvedValue(inboundAddGroupOrProjectSuccessResponse); + const inboundRemoveGroupSuccessHandler = jest.fn().mockResolvedValue(inboundRemoveGroupSuccess); const inboundRemoveProjectSuccessHandler = jest .fn() .mockResolvedValue(inboundRemoveProjectSuccess); @@ -57,7 +61,8 @@ describe('TokenAccess component', () => { const findAddProjectBtn = () => wrapper.findByTestId('add-project-btn'); const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' }); const findProjectInput = () => wrapper.findComponent(GlFormInput); - const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' }); + const findRemoveProjectBtnAt = (i) => + wrapper.findAllByRole('button', { name: 'Remove access' }).at(i); const findToggleFormBtn = () => wrapper.findByTestId('toggle-form-btn'); const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert); @@ -78,7 +83,10 @@ describe('TokenAccess component', () => { it('shows loading state while waiting on query to resolve', async () => { createComponent([ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ]); expect(findLoadingIcon().exists()).toBe(true); @@ -89,25 +97,30 @@ describe('TokenAccess component', () => { }); }); - describe('fetching projects and scope', () => { - it('fetches projects and scope correctly', () => { + describe('fetching groups and projects and scope', () => { + it('fetches groups and projects and scope correctly', () => { const expectedVariables = { fullPath: 'root/my-repo', }; createComponent([ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ]); expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables); - expect(inboundProjectsWithScopeResponseHandler).toHaveBeenCalledWith(expectedVariables); + expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledWith( + expectedVariables, + ); }); - it('handles fetch projects error correctly', async () => { + it('handles fetch groups and projects error correctly', async () => { createComponent([ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, failureHandler], + [inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, failureHandler], ]); await waitForPromises(); @@ -120,7 +133,10 @@ describe('TokenAccess component', () => { it('handles fetch scope error correctly', async () => { createComponent([ [inboundGetCIJobTokenScopeQuery, failureHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ]); await waitForPromises(); @@ -135,7 +151,10 @@ describe('TokenAccess component', () => { it('the toggle is on and the alert is hidden', async () => { createComponent([ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ]); await waitForPromises(); @@ -147,7 +166,10 @@ describe('TokenAccess component', () => { it('the toggle is off and the alert is visible', async () => { createComponent([ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ]); await waitForPromises(); @@ -206,13 +228,23 @@ describe('TokenAccess component', () => { }); }); - describe('add project', () => { - it('calls add project mutation', async () => { + describe.each` + type | testPath + ${'group'} | ${testGroupPath} + ${'project'} | ${testProjectPath} + `('add $type', ({ testPath }) => { + it(`calls add group or project mutation`, async () => { createComponent( [ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], - [inboundAddProjectCIJobTokenScopeMutation, inboundAddProjectSuccessResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + [ + inboundAddGroupOrProjectCIJobTokenScopeMutation, + inboundAddGroupOrProjectSuccessResponseHandler, + ], ], mountExtended, ); @@ -220,21 +252,24 @@ describe('TokenAccess component', () => { await waitForPromises(); await findToggleFormBtn().trigger('click'); - await findProjectInput().vm.$emit('input', testProjectPath); + await findProjectInput().vm.$emit('input', testPath); findAddProjectBtn().trigger('click'); - expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({ + expect(inboundAddGroupOrProjectSuccessResponseHandler).toHaveBeenCalledWith({ projectPath, - targetProjectPath: testProjectPath, + targetPath: testPath, }); }); - it('add project handles error correctly', async () => { + it('add group or project handles error correctly', async () => { createComponent( [ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], - [inboundAddProjectCIJobTokenScopeMutation, failureHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + [inboundAddGroupOrProjectCIJobTokenScopeMutation, failureHandler], ], mountExtended, ); @@ -242,7 +277,7 @@ describe('TokenAccess component', () => { await waitForPromises(); await findToggleFormBtn().trigger('click'); - await findProjectInput().vm.$emit('input', testProjectPath); + await findProjectInput().vm.$emit('input', testPath); findAddProjectBtn().trigger('click'); await waitForPromises(); @@ -254,7 +289,10 @@ describe('TokenAccess component', () => { createComponent( [ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], ], mountExtended, ); @@ -265,7 +303,7 @@ describe('TokenAccess component', () => { expect(findProjectInput().exists()).toBe(true); - await findProjectInput().vm.$emit('input', testProjectPath); + await findProjectInput().vm.$emit('input', testPath); await findCancelBtn().trigger('click'); @@ -277,40 +315,50 @@ describe('TokenAccess component', () => { }); }); - describe('remove project', () => { - it('calls remove project mutation', async () => { + describe.each` + type | index | mutation | handler | target + ${'group'} | ${0} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler} | ${'targetGroupPath'} + ${'project'} | ${1} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler} | ${'targetProjectPath'} + `('remove $type', ({ type, index, mutation, handler, target }) => { + it(`calls remove ${type} mutation`, async () => { createComponent( [ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], - [inboundRemoveProjectCIJobTokenScopeMutation, inboundRemoveProjectSuccessHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + [mutation, handler], ], mountExtended, ); await waitForPromises(); - findRemoveProjectBtn().trigger('click'); + findRemoveProjectBtnAt(index).trigger('click'); - expect(inboundRemoveProjectSuccessHandler).toHaveBeenCalledWith({ + expect(handler).toHaveBeenCalledWith({ projectPath, - targetProjectPath: 'root/ci-project', + [target]: expect.any(String), }); }); - it('remove project handles error correctly', async () => { + it(`remove ${type} handles error correctly`, async () => { createComponent( [ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], - [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], - [inboundRemoveProjectCIJobTokenScopeMutation, failureHandler], + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + [mutation, failureHandler], ], mountExtended, ); await waitForPromises(); - findRemoveProjectBtn().trigger('click'); + findRemoveProjectBtnAt(index).trigger('click'); await waitForPromises(); diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js index a4b5532108abd0..a3521f49eb7d6d 100644 --- a/spec/frontend/token_access/mock_data.js +++ b/spec/frontend/token_access/mock_data.js @@ -81,9 +81,30 @@ export const updateScopeSuccess = { }, }; +export const mockGroups = [ + { + id: 1, + name: 'some-group', + fullPath: 'some-group', + __typename: 'Group', + }, + { + id: 2, + name: 'another-group', + fullPath: 'another-group', + __typename: 'Group', + }, + { + id: 3, + name: 'a-sub-group', + fullPath: 'another-group/a-sub-group', + __typename: 'Group', + }, +]; + export const mockProjects = [ { - id: '1', + id: 1, name: 'merge-train-stuff', namespace: { id: '1235', @@ -94,7 +115,7 @@ export const mockProjects = [ __typename: 'Project', }, { - id: '2', + id: 2, name: 'ci-project', namespace: { id: '1236', @@ -108,12 +129,8 @@ export const mockProjects = [ export const mockFields = [ { - key: 'project', - label: 'Project with access', - }, - { - key: 'namespace', - label: 'Namespace', + key: 'fullPath', + label: '', }, { key: 'actions', @@ -147,7 +164,7 @@ export const inboundJobTokenScopeDisabledResponse = { }, }; -export const inboundProjectsWithScopeResponse = { +export const inboundGroupsAndProjectsWithScopeResponse = { data: { project: { __typename: 'Project', @@ -166,12 +183,23 @@ export const inboundProjectsWithScopeResponse = { }, ], }, + groupsAllowlist: { + __typename: 'GroupConnection', + nodes: [ + { + __typename: 'Group', + fullPath: 'root/ci-group', + id: 'gid://gitlab/Group/45', + name: 'ci-group', + }, + ], + }, }, }, }, }; -export const inboundAddProjectSuccessResponse = { +export const inboundAddGroupOrProjectSuccessResponse = { data: { ciJobTokenScopeAddProject: { errors: [], @@ -180,6 +208,15 @@ export const inboundAddProjectSuccessResponse = { }, }; +export const inboundRemoveGroupSuccess = { + data: { + ciJobTokenScopeRemoveProject: { + errors: [], + __typename: 'CiJobTokenScopeRemoveGroupPayload', + }, + }, +}; + export const inboundRemoveProjectSuccess = { data: { ciJobTokenScopeRemoveProject: { diff --git a/spec/frontend/token_access/token_access_table_spec.js b/spec/frontend/token_access/token_access_table_spec.js new file mode 100644 index 00000000000000..df5c19028b30e8 --- /dev/null +++ b/spec/frontend/token_access/token_access_table_spec.js @@ -0,0 +1,56 @@ +import { GlButton, GlTableLite } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import TokenAccessTable from '~/token_access/components/token_access_table.vue'; +import { mockGroups, mockProjects, mockFields } from './mock_data'; + +describe('Token access table', () => { + let wrapper; + + const createComponent = (props) => { + wrapper = mountExtended(TokenAccessTable, { + provide: { + fullPath: 'root/ci-project', + }, + propsData: { + ...props, + }, + }); + }; + + const findTable = () => wrapper.findComponent(GlTableLite); + const findDeleteButton = () => wrapper.findComponent(GlButton); + const findAllTableRows = (type) => wrapper.findAllByTestId(`${type}s-token-table-row`); + const findName = (type) => wrapper.findByTestId(`token-access-${type}-name`); + + describe.each` + type | isGroup | items + ${'group'} | ${true} | ${mockGroups} + ${'project'} | ${false} | ${mockProjects} + `('when provided with $type', ({ type, isGroup, items }) => { + beforeEach(() => { + createComponent({ + isGroup, + items, + tableFields: mockFields, + }); + }); + + it('displays a table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('displays the correct amount of table rows', () => { + expect(findAllTableRows(type)).toHaveLength(items.length); + }); + + it('delete button emits event with correct item to delete', async () => { + await findDeleteButton().trigger('click'); + + expect(wrapper.emitted('removeItem')).toEqual([[items[0]]]); + }); + + it('displays fullpath', () => { + expect(findName(type).text()).toBe(items[0].fullPath); + }); + }); +}); diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js deleted file mode 100644 index 7a78befa0d7031..00000000000000 --- a/spec/frontend/token_access/token_projects_table_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { GlTable, GlButton } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import TokenProjectsTable from '~/token_access/components/token_projects_table.vue'; -import { mockProjects, mockFields } from './mock_data'; - -describe('Token projects table', () => { - let wrapper; - - const defaultProps = { - tableFields: mockFields, - projects: mockProjects, - }; - - const createComponent = (props) => { - wrapper = mountExtended(TokenProjectsTable, { - provide: { - fullPath: 'root/ci-project', - }, - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - const findTable = () => wrapper.findComponent(GlTable); - const findDeleteProjectBtn = () => wrapper.findComponent(GlButton); - const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton); - const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row'); - const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name'); - - it('displays a table', () => { - createComponent(); - - expect(findTable().exists()).toBe(true); - }); - - it('displays the correct amount of table rows', () => { - createComponent(); - - expect(findAllTableRows()).toHaveLength(mockProjects.length); - }); - - it('delete project button emits event with correct project to delete', async () => { - createComponent(); - - await findDeleteProjectBtn().trigger('click'); - - expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]); - }); - - it('does not show the remove icon if the project is locked', () => { - createComponent(); - - // currently two mock projects with one being a locked project - expect(findAllDeleteProjectBtn()).toHaveLength(1); - }); - - it('displays project fullpath', () => { - createComponent(); - - expect(findProjectNameCell().text()).toBe('root/merge-train-stuff'); - }); -}); -- GitLab