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