From 6e440f272f26f630bae030058c23c4e8fecdf366 Mon Sep 17 00:00:00 2001 From: Samantha Ming <sming@gitlab.com> Date: Fri, 17 Dec 2021 18:24:50 +0100 Subject: [PATCH] Connect secuity configuration to a local mutation Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/347414 --- .../components/training_provider_list.vue | 42 +++++++++++++- ...curity_training_providers.mutation.graphql | 8 +++ .../security_configuration/index.js | 30 +--------- .../security_configuration/resolver.js | 56 +++++++++++++++++++ .../components/training_provider_list_spec.js | 43 +++++++++++++- .../security_configuration/mock_data.js | 8 ++- 6 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql create mode 100644 app/assets/javascripts/security_configuration/resolver.js diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index 509377a63e8d..c574412096a8 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -1,6 +1,7 @@ <script> import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui'; import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql'; +import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql'; export default { components: { @@ -9,6 +10,7 @@ export default { GlLink, GlSkeletonLoader, }, + inject: ['projectPath'], apollo: { securityTrainingProviders: { query: securityTrainingProvidersQuery, @@ -16,6 +18,7 @@ export default { }, data() { return { + toggleLoading: false, securityTrainingProviders: [], }; }, @@ -24,6 +27,37 @@ export default { return this.$apollo.queries.securityTrainingProviders.loading; }, }, + methods: { + toggleProvider(selectedProviderId) { + const toggledProviders = this.securityTrainingProviders.map((provider) => ({ + ...provider, + ...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }), + })); + + this.storeEnabledProviders(toggledProviders); + }, + storeEnabledProviders(toggledProviders) { + const enabledProviderIds = toggledProviders + .filter(({ isEnabled }) => isEnabled) + .map(({ id }) => id); + + this.toggleLoading = true; + + return this.$apollo + .mutate({ + mutation: configureSecurityTrainingProvidersMutation, + variables: { + input: { + enabledProviders: enabledProviderIds, + fullPath: this.projectPath, + }, + }, + }) + .then(() => { + this.toggleLoading = false; + }); + }, + }, }; </script> @@ -46,7 +80,13 @@ export default { > <gl-card> <div class="gl-display-flex"> - <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" /> + <gl-toggle + :value="isEnabled" + :label="__('Training mode')" + label-position="hidden" + :is-loading="toggleLoading" + @change="toggleProvider(id)" + /> <div class="gl-ml-5"> <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3> <p> diff --git a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql new file mode 100644 index 000000000000..df8c7c9e30ad --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql @@ -0,0 +1,8 @@ +mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) { + configureSecurityTrainingProviders(input: $input) @client { + securityTrainingProviders { + id + isEnabled + } + } +} diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index c86ff1a58f2b..24c0585e077a 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -2,38 +2,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; -import { __ } from '~/locale'; import SecurityConfigurationApp from './components/app.vue'; import { securityFeatures, complianceFeatures } from './components/constants'; import { augmentFeatures } from './utils'; - -// Note: this is behind a feature flag and only a placeholder -// until the actual GraphQL fields have been added -// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480 -export const tempResolvers = { - Query: { - securityTrainingProviders() { - return [ - { - __typename: 'SecurityTrainingProvider', - id: 101, - name: __('Kontra'), - description: __('Interactive developer security education.'), - url: 'https://application.security/', - isEnabled: false, - }, - { - __typename: 'SecurityTrainingProvider', - id: 102, - name: __('SecureCodeWarrior'), - description: __('Security training with guide and learning pathways.'), - url: 'https://www.securecodewarrior.com/', - isEnabled: true, - }, - ]; - }, - }, -}; +import tempResolvers from './resolver'; export const initSecurityConfiguration = (el) => { if (!el) { diff --git a/app/assets/javascripts/security_configuration/resolver.js b/app/assets/javascripts/security_configuration/resolver.js new file mode 100644 index 000000000000..93175d4a3d1e --- /dev/null +++ b/app/assets/javascripts/security_configuration/resolver.js @@ -0,0 +1,56 @@ +import produce from 'immer'; +import { __ } from '~/locale'; +import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql'; + +// Note: this is behind a feature flag and only a placeholder +// until the actual GraphQL fields have been added +// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480 +export default { + Query: { + securityTrainingProviders() { + return [ + { + __typename: 'SecurityTrainingProvider', + id: 101, + name: __('Kontra'), + description: __('Interactive developer security education.'), + url: 'https://application.security/', + isEnabled: false, + }, + { + __typename: 'SecurityTrainingProvider', + id: 102, + name: __('SecureCodeWarrior'), + description: __('Security training with guide and learning pathways.'), + url: 'https://www.securecodewarrior.com/', + isEnabled: true, + }, + ]; + }, + }, + + Mutation: { + configureSecurityTrainingProviders: ( + _, + { input: { enabledProviders, primaryProvider } }, + { cache }, + ) => { + const sourceData = cache.readQuery({ + query: securityTrainingProvidersQuery, + }); + + const data = produce(sourceData.securityTrainingProviders, (draftData) => { + /* eslint-disable no-param-reassign */ + draftData.forEach((provider) => { + provider.isPrimary = provider.id === primaryProvider; + provider.isEnabled = + provider.id === primaryProvider || enabledProviders.includes(provider.id); + }); + }); + return { + __typename: 'configureSecurityTrainingProvidersPayload', + securityTrainingProviders: data, + }; + }, + }, +}; diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 60cc36a634c8..183deec0f101 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -4,8 +4,14 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; +import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql'; import waitForPromises from 'helpers/wait_for_promises'; -import { securityTrainingProviders, mockResolvers } from '../mock_data'; +import { + securityTrainingProviders, + mockResolvers, + testProjectPath, + textProviderIds, +} from '../mock_data'; Vue.use(VueApollo); @@ -18,6 +24,9 @@ describe('TrainingProviderList component', () => { mockApollo = createMockApollo([], mockResolvers); wrapper = shallowMount(TrainingProviderList, { + provide: { + projectPath: testProjectPath, + }, apolloProvider: mockApollo, }); }; @@ -85,4 +94,36 @@ describe('TrainingProviderList component', () => { }); }); }); + + describe('success mutation', () => { + const firstToggle = () => findToggles().at(0); + + beforeEach(async () => { + jest.spyOn(mockApollo.defaultClient, 'mutate'); + + await waitForQueryToBeLoaded(); + + firstToggle().vm.$emit('change'); + }); + + it('calls mutation when toggle is changed', () => { + expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + mutation: configureSecurityTrainingProvidersMutation, + variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } }, + }), + ); + }); + + it.each` + loading | wait | desc + ${true} | ${false} | ${'enables loading of GlToggle when mutation is called'} + ${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'} + `('$desc', async ({ loading, wait }) => { + if (wait) { + await waitForPromises(); + } + expect(firstToggle().props('isLoading')).toBe(loading); + }); + }); }); diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index cdb859c38006..b01dd1fc07b7 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -1,13 +1,17 @@ +export const testProjectPath = 'foo/bar'; + +export const textProviderIds = [101, 102]; + export const securityTrainingProviders = [ { - id: 101, + id: textProviderIds[0], name: 'Kontra', description: 'Interactive developer security education.', url: 'https://application.security/', isEnabled: false, }, { - id: 102, + id: textProviderIds[1], name: 'SecureCodeWarrior', description: 'Security training with guide and learning pathways.', url: 'https://www.securecodewarrior.com/', -- GitLab