diff options
Diffstat (limited to 'spec/frontend/token_access')
-rw-r--r-- | spec/frontend/token_access/inbound_token_access_spec.js | 311 | ||||
-rw-r--r-- | spec/frontend/token_access/mock_data.js | 122 | ||||
-rw-r--r-- | spec/frontend/token_access/opt_in_jwt_spec.js | 144 | ||||
-rw-r--r-- | spec/frontend/token_access/outbound_token_access_spec.js (renamed from spec/frontend/token_access/token_access_spec.js) | 8 | ||||
-rw-r--r-- | spec/frontend/token_access/token_access_app_spec.js | 47 | ||||
-rw-r--r-- | spec/frontend/token_access/token_projects_table_spec.js | 7 |
6 files changed, 628 insertions, 11 deletions
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js new file mode 100644 index 00000000000..fcd1a33fa68 --- /dev/null +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -0,0 +1,311 @@ +import { GlAlert, GlFormInput, GlToggle, GlLoadingIcon } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +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 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 { + inboundJobTokenScopeEnabledResponse, + inboundJobTokenScopeDisabledResponse, + inboundProjectsWithScopeResponse, + inboundAddProjectSuccessResponse, + inboundRemoveProjectSuccess, + inboundUpdateScopeSuccessResponse, +} from './mock_data'; + +const projectPath = 'root/my-repo'; +const message = 'An error occurred'; +const error = new Error(message); + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +describe('TokenAccess component', () => { + let wrapper; + + const inboundJobTokenScopeEnabledResponseHandler = jest + .fn() + .mockResolvedValue(inboundJobTokenScopeEnabledResponse); + const inboundJobTokenScopeDisabledResponseHandler = jest + .fn() + .mockResolvedValue(inboundJobTokenScopeDisabledResponse); + const inboundProjectsWithScopeResponseHandler = jest + .fn() + .mockResolvedValue(inboundProjectsWithScopeResponse); + const inboundAddProjectSuccessResponseHandler = jest + .fn() + .mockResolvedValue(inboundAddProjectSuccessResponse); + const inboundRemoveProjectSuccessHandler = jest + .fn() + .mockResolvedValue(inboundRemoveProjectSuccess); + const inboundUpdateScopeSuccessResponseHandler = jest + .fn() + .mockResolvedValue(inboundUpdateScopeSuccessResponse); + const failureHandler = jest.fn().mockRejectedValue(error); + + const findToggle = () => wrapper.findComponent(GlToggle); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' }); + const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' }); + const findProjectInput = () => wrapper.findComponent(GlFormInput); + const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' }); + const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert); + + const createMockApolloProvider = (requestHandlers) => { + return createMockApollo(requestHandlers); + }; + + const createComponent = (requestHandlers, mountFn = shallowMountExtended) => { + wrapper = mountFn(InboundTokenAccess, { + provide: { + fullPath: projectPath, + }, + apolloProvider: createMockApolloProvider(requestHandlers), + data() { + return { + targetProjectPath: 'root/test', + }; + }, + }); + }; + + describe('loading state', () => { + it('shows loading state while waiting on query to resolve', async () => { + createComponent([ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ]); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + + describe('fetching projects and scope', () => { + it('fetches projects and scope correctly', () => { + const expectedVariables = { + fullPath: 'root/my-repo', + }; + + createComponent([ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ]); + + expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables); + expect(inboundProjectsWithScopeResponseHandler).toHaveBeenCalledWith(expectedVariables); + }); + + it('handles fetch projects error correctly', async () => { + createComponent([ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, failureHandler], + ]); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'There was a problem fetching the projects', + }); + }); + + it('handles fetch scope error correctly', async () => { + createComponent([ + [inboundGetCIJobTokenScopeQuery, failureHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ]); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'There was a problem fetching the job token scope value', + }); + }); + }); + + describe('toggle', () => { + it('the toggle is on and the alert is hidden', async () => { + createComponent([ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(true); + expect(findTokenDisabledAlert().exists()).toBe(false); + }); + + it('the toggle is off and the alert is visible', async () => { + createComponent([ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(findTokenDisabledAlert().exists()).toBe(true); + }); + + describe('update ci job token scope', () => { + it('calls inboundUpdateCIJobTokenScopeMutation mutation', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(true); + + findToggle().vm.$emit('change', false); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(inboundUpdateScopeSuccessResponseHandler).toHaveBeenCalledWith({ + input: { + fullPath: 'root/my-repo', + inboundJobTokenScopeEnabled: false, + }, + }); + }); + + it('handles update scope error correctly', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler], + [inboundUpdateCIJobTokenScopeMutation, failureHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + + findToggle().vm.$emit('change', true); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(createAlert).toHaveBeenCalledWith({ message }); + }); + }); + }); + + describe('add project', () => { + it('calls add project mutation', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [inboundAddProjectCIJobTokenScopeMutation, inboundAddProjectSuccessResponseHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findAddProjectBtn().trigger('click'); + + expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({ + projectPath, + targetProjectPath: 'root/test', + }); + }); + + it('add project handles error correctly', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [inboundAddProjectCIJobTokenScopeMutation, failureHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findAddProjectBtn().trigger('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message }); + }); + + it('clicking cancel clears target path', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + expect(findProjectInput().element.value).toBe('root/test'); + + await findCancelBtn().trigger('click'); + + expect(findProjectInput().element.value).toBe(''); + }); + }); + + describe('remove project', () => { + it('calls remove project mutation', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [inboundRemoveProjectCIJobTokenScopeMutation, inboundRemoveProjectSuccessHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findRemoveProjectBtn().trigger('click'); + + expect(inboundRemoveProjectSuccessHandler).toHaveBeenCalledWith({ + projectPath, + targetProjectPath: 'root/ci-project', + }); + }); + + it('remove project handles error correctly', async () => { + createComponent( + [ + [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler], + [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler], + [inboundRemoveProjectCIJobTokenScopeMutation, failureHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findRemoveProjectBtn().trigger('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message }); + }); + }); +}); diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js index 0c8ba266201..ab04735b985 100644 --- a/spec/frontend/token_access/mock_data.js +++ b/spec/frontend/token_access/mock_data.js @@ -105,3 +105,125 @@ export const mockProjects = [ __typename: 'Project', }, ]; + +export const mockFields = [ + { + key: 'project', + label: 'Project with access', + }, + { + key: 'namespace', + label: 'Namespace', + }, + { + key: 'actions', + label: '', + }, +]; + +export const optInJwtQueryResponse = (optInJwt) => ({ + data: { + project: { + id: '1', + ciCdSettings: { + optInJwt, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}); + +export const optInJwtMutationResponse = (optInJwt) => ({ + data: { + ciCdSettingsUpdate: { + ciCdSettings: { + optInJwt, + __typename: 'ProjectCiCdSetting', + }, + errors: [], + __typename: 'CiCdSettingsUpdatePayload', + }, + }, +}); + +export const inboundJobTokenScopeEnabledResponse = { + data: { + project: { + id: '1', + ciCdSettings: { + inboundJobTokenScopeEnabled: true, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}; + +export const inboundJobTokenScopeDisabledResponse = { + data: { + project: { + id: '1', + ciCdSettings: { + inboundJobTokenScopeEnabled: false, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}; + +export const inboundProjectsWithScopeResponse = { + data: { + project: { + __typename: 'Project', + id: '1', + ciJobTokenScope: { + __typename: 'CiJobTokenScopeType', + inboundAllowlist: { + __typename: 'ProjectConnection', + nodes: [ + { + __typename: 'Project', + fullPath: 'root/ci-project', + id: 'gid://gitlab/Project/23', + name: 'ci-project', + namespace: { id: 'gid://gitlab/Namespaces::UserNamespace/1', fullPath: 'root' }, + }, + ], + }, + }, + }, + }, +}; + +export const inboundAddProjectSuccessResponse = { + data: { + ciJobTokenScopeAddProject: { + errors: [], + __typename: 'CiJobTokenScopeAddProjectPayload', + }, + }, +}; + +export const inboundRemoveProjectSuccess = { + data: { + ciJobTokenScopeRemoveProject: { + errors: [], + __typename: 'CiJobTokenScopeRemoveProjectPayload', + }, + }, +}; + +export const inboundUpdateScopeSuccessResponse = { + data: { + ciCdSettingsUpdate: { + ciCdSettings: { + inboundJobTokenScopeEnabled: false, + __typename: 'ProjectCiCdSetting', + }, + errors: [], + __typename: 'CiCdSettingsUpdatePayload', + }, + }, +}; diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js new file mode 100644 index 00000000000..3a68f247aa6 --- /dev/null +++ b/spec/frontend/token_access/opt_in_jwt_spec.js @@ -0,0 +1,144 @@ +import { GlLink, GlLoadingIcon, GlToggle, GlSprintf } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants'; +import OptInJwt from '~/token_access/components/opt_in_jwt.vue'; +import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql'; +import updateOptInJwtMutation from '~/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql'; +import { optInJwtMutationResponse, optInJwtQueryResponse } from './mock_data'; + +const errorMessage = 'An error occurred'; +const error = new Error(errorMessage); + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +describe('OptInJwt component', () => { + let wrapper; + + const failureHandler = jest.fn().mockRejectedValue(error); + const enabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(true)); + const disabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(false)); + const updateOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtMutationResponse(true)); + + const findHelpLink = () => wrapper.findComponent(GlLink); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findToggle = () => wrapper.findComponent(GlToggle); + const findOptInJwtExpandedSection = () => wrapper.findByTestId('opt-in-jwt-expanded-section'); + + const createMockApolloProvider = (requestHandlers) => { + return createMockApollo(requestHandlers); + }; + + const createComponent = (requestHandlers, mountFn = shallowMountExtended, options = {}) => { + wrapper = mountFn(OptInJwt, { + provide: { + fullPath: 'root/my-repo', + }, + apolloProvider: createMockApolloProvider(requestHandlers), + data() { + return { + targetProjectPath: 'root/test', + }; + }, + ...options, + }); + }; + + const createShallowComponent = (requestHandlers, options = {}) => + createComponent(requestHandlers, shallowMountExtended, options); + const createFullComponent = (requestHandlers, options = {}) => + createComponent(requestHandlers, mountExtended, options); + + describe('loading state', () => { + it('shows loading state and hides toggle while waiting on query to resolve', async () => { + createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findToggle().exists()).toBe(false); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + expect(findToggle().exists()).toBe(true); + }); + }); + + describe('template', () => { + it('renders help link', async () => { + createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]], { + stubs: { + GlToggle, + GlSprintf, + GlLink, + }, + }); + await waitForPromises(); + + expect(findHelpLink().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe(OPT_IN_JWT_HELP_LINK); + }); + }); + + describe('toggle JWT token access', () => { + it('code instruction is visible when toggle is enabled', async () => { + createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(true); + expect(findOptInJwtExpandedSection().exists()).toBe(true); + }); + + it('code instruction is hidden when toggle is disabled', async () => { + createShallowComponent([[getOptInJwtSettingQuery, disabledOptInJwtHandler]]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(findOptInJwtExpandedSection().exists()).toBe(false); + }); + + describe('update JWT token access', () => { + it('calls updateOptInJwtMutation with correct arguments', async () => { + createFullComponent([ + [getOptInJwtSettingQuery, disabledOptInJwtHandler], + [updateOptInJwtMutation, updateOptInJwtHandler], + ]); + + await waitForPromises(); + + findToggle().vm.$emit('change', true); + + expect(updateOptInJwtHandler).toHaveBeenCalledWith({ + input: { + fullPath: 'root/my-repo', + optInJwt: true, + }, + }); + }); + + it('handles update error', async () => { + createFullComponent([ + [getOptInJwtSettingQuery, enabledOptInJwtHandler], + [updateOptInJwtMutation, failureHandler], + ]); + + await waitForPromises(); + + findToggle().vm.$emit('change', false); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while update the setting. Please try again.', + }); + }); + }); + }); +}); diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js index 6fe94e28548..893a021197f 100644 --- a/spec/frontend/token_access/token_access_spec.js +++ b/spec/frontend/token_access/outbound_token_access_spec.js @@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; -import TokenAccess from '~/token_access/components/token_access.vue'; +import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue'; import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql'; import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql'; @@ -50,7 +50,7 @@ describe('TokenAccess component', () => { }; const createComponent = (requestHandlers, mountFn = shallowMountExtended) => { - wrapper = mountFn(TokenAccess, { + wrapper = mountFn(OutboundTokenAccess, { provide: { fullPath: projectPath, }, @@ -63,10 +63,6 @@ describe('TokenAccess component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('shows loading state while waiting on query to resolve', async () => { createComponent([ diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js new file mode 100644 index 00000000000..7f269ee5fda --- /dev/null +++ b/spec/frontend/token_access/token_access_app_spec.js @@ -0,0 +1,47 @@ +import { shallowMount } from '@vue/test-utils'; +import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue'; +import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue'; +import OptInJwt from '~/token_access/components/opt_in_jwt.vue'; +import TokenAccessApp from '~/token_access/components/token_access_app.vue'; + +describe('TokenAccessApp component', () => { + let wrapper; + + const findOutboundTokenAccess = () => wrapper.findComponent(OutboundTokenAccess); + const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess); + const findOptInJwt = () => wrapper.findComponent(OptInJwt); + + const createComponent = (flagState = false) => { + wrapper = shallowMount(TokenAccessApp, { + provide: { + glFeatures: { ciInboundJobTokenScope: flagState }, + }, + }); + }; + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the opt in jwt component', () => { + expect(findOptInJwt().exists()).toBe(true); + }); + + it('renders the outbound token access component', () => { + expect(findOutboundTokenAccess().exists()).toBe(true); + }); + + it('does not render the inbound token access component', () => { + expect(findInboundTokenAccess().exists()).toBe(false); + }); + }); + + describe('with feature flag enabled', () => { + it('renders the inbound token access component', () => { + createComponent(true); + + expect(findInboundTokenAccess().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js index 0fa1a2453f7..b51d8b3ccea 100644 --- a/spec/frontend/token_access/token_projects_table_spec.js +++ b/spec/frontend/token_access/token_projects_table_spec.js @@ -1,7 +1,7 @@ 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 } from './mock_data'; +import { mockProjects, mockFields } from './mock_data'; describe('Token projects table', () => { let wrapper; @@ -12,6 +12,7 @@ describe('Token projects table', () => { fullPath: 'root/ci-project', }, propsData: { + tableFields: mockFields, projects: mockProjects, }, }); @@ -28,10 +29,6 @@ describe('Token projects table', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a table', () => { expect(findTable().exists()).toBe(true); }); |