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 02605dfb4fb3af6a382b3595f0fb81ed67e42c21..31575cd36b71842bc6ccc65d8c978d5ac56db7af 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 0000000000000000000000000000000000000000..700c6b9ef3958131e70b36bd74b17fea167d04c5 --- /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 4245b39dec1b08031ff7ef19a65039e8be6a7590..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..94b34a6c1eb9feb42c1b0a1f4fa0739d49594a3f --- /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 f030a892af2ca2b27cbbaaaeb444e534ad11e817..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..5681c5967adf8e2f24fa7dadde58dc9efc3b3c93 --- /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 c51bdcbf7d2ece38c34804704a018375e91f541e..641642ffc7677b2f1b3c5f17810a48c9a1610b2d 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 67402b1f8ab545e79123f89575d541a1e501f5c0..22a1672df44bc4cdeae562b6e3f65a7a0a82f119 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 d82d65e35495d5affb9019a6a7ea614534648782..a7b0a9820b502fa97333639f8166528dc8fb5f52 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 a4b5532108abd0369e861805914ac6d69b62ad5d..a3521f49eb7d6dd3d49737d6e35507415d1f4119 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 0000000000000000000000000000000000000000..df5c19028b30e8abb250973406910bacf2010de8 --- /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 7a78befa0d7031c1e6f51116a9163d3fd4e5f784..0000000000000000000000000000000000000000 --- 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'); - }); -});