From d91cacb23cb3b7d04eac320dafefa010450d0bca Mon Sep 17 00:00:00 2001
From: Samantha Ming <sming@gitlab.com>
Date: Thu, 25 Nov 2021 15:15:33 +0100
Subject: [PATCH] Create configuration security training basic ui

Add security training ui to security configuration.
This is within the vulnerability management tab.
---
 .../security_configuration/components/app.vue | 28 ++++++++-
 .../components/training_provider_list.vue     | 36 +++++++++++
 locale/gitlab.pot                             | 15 +++++
 .../components/app_spec.js                    | 11 +++-
 .../components/training_provider_list_spec.js | 60 +++++++++++++++++++
 5 files changed, 148 insertions(+), 2 deletions(-)
 create mode 100644 app/assets/javascripts/security_configuration/components/training_provider_list.vue
 create mode 100644 spec/frontend/security_configuration/components/training_provider_list_spec.js

diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index cd2add6407f2..62c30c941ebb 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -8,6 +8,7 @@ import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
 import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
 import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
 import FeatureCard from './feature_card.vue';
+import TrainingProviderList from './training_provider_list.vue';
 import SectionLayout from './section_layout.vue';
 import UpgradeBanner from './upgrade_banner.vue';
 
@@ -28,8 +29,28 @@ export const i18n = {
   securityTraining: s__('SecurityConfiguration|Security training'),
 };
 
+// This will be removed and replaced with GraphQL query:
+// https://gitlab.com/gitlab-org/gitlab/-/issues/346480
+export const TRAINING_PROVIDERS = [
+  {
+    id: 101,
+    name: __('Kontra'),
+    description: __('Interactive developer security education.'),
+    url: 'https://application.security/',
+    isEnabled: false,
+  },
+  {
+    id: 102,
+    name: __('SecureCodeWarrior'),
+    description: __('Security training with guide and learning pathways.'),
+    url: 'https://www.securecodewarrior.com/',
+    isEnabled: true,
+  },
+];
+
 export default {
   i18n,
+  TRAINING_PROVIDERS,
   components: {
     AutoDevOpsAlert,
     AutoDevOpsEnabledAlert,
@@ -43,6 +64,7 @@ export default {
     SectionLayout,
     UpgradeBanner,
     UserCalloutDismisser,
+    TrainingProviderList,
   },
   mixins: [glFeatureFlagsMixin()],
   inject: ['projectPath'],
@@ -240,7 +262,11 @@ export default {
         data-testid="vulnerability-management-tab"
         :title="$options.i18n.vulnerabilityManagement"
       >
-        <section-layout :heading="$options.i18n.securityTraining" />
+        <section-layout :heading="$options.i18n.securityTraining">
+          <template #features>
+            <training-provider-list :providers="$options.TRAINING_PROVIDERS" />
+          </template>
+        </section-layout>
       </gl-tab>
     </gl-tabs>
   </article>
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
new file mode 100644
index 000000000000..160540f69895
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlCard, GlToggle, GlLink } from '@gitlab/ui';
+
+export default {
+  components: {
+    GlCard,
+    GlToggle,
+    GlLink,
+  },
+  props: {
+    providers: {
+      type: Array,
+      required: true,
+    },
+  },
+};
+</script>
+
+<template>
+  <ul class="gl-list-style-none gl-m-0 gl-p-0">
+    <li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6">
+      <gl-card>
+        <div class="gl-display-flex">
+          <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
+          <div class="gl-ml-5">
+            <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
+            <p>
+              {{ description }}
+              <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
+            </p>
+          </div>
+        </div>
+      </gl-card>
+    </li>
+  </ul>
+</template>
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9b5b254b522b..fa4169cc75c6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18846,6 +18846,9 @@ msgstr ""
 msgid "Integrations|can't exceed %{recipients_limit}"
 msgstr ""
 
+msgid "Interactive developer security education."
+msgstr ""
+
 msgid "Interactive mode"
 msgstr ""
 
@@ -20199,6 +20202,9 @@ msgstr ""
 msgid "Ki"
 msgstr ""
 
+msgid "Kontra"
+msgstr ""
+
 msgid "Kroki"
 msgstr ""
 
@@ -30674,6 +30680,9 @@ msgstr ""
 msgid "Secure token that identifies an external storage request."
 msgstr ""
 
+msgid "SecureCodeWarrior"
+msgstr ""
+
 msgid "Security"
 msgstr ""
 
@@ -30698,6 +30707,9 @@ msgstr ""
 msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
 msgstr ""
 
+msgid "Security training with guide and learning pathways."
+msgstr ""
+
 msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
 msgstr ""
 
@@ -36609,6 +36621,9 @@ msgstr ""
 msgid "Track your GitLab projects with GitLab for Slack."
 msgstr ""
 
+msgid "Training mode"
+msgstr ""
+
 msgid "Transfer"
 msgstr ""
 
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 4e0ea6a97171..759844b941a4 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -5,7 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
 import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
 import stubChildren from 'helpers/stub_children';
 import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
+import SecurityConfigurationApp, {
+  i18n,
+  TRAINING_PROVIDERS,
+} from '~/security_configuration/components/app.vue';
 import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
 import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
 import {
@@ -20,6 +23,7 @@ import {
   AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
 } from '~/security_configuration/components/constants';
 import FeatureCard from '~/security_configuration/components/feature_card.vue';
+import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
 
 import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
 import {
@@ -78,6 +82,7 @@ describe('App component', () => {
   const findTabs = () => wrapper.findAllComponents(GlTab);
   const findByTestId = (id) => wrapper.findByTestId(id);
   const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
+  const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
   const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
   const findLink = ({ href, text, container = wrapper }) => {
     const selector = `a[href="${href}"]`;
@@ -180,6 +185,10 @@ describe('App component', () => {
       expect(findComplianceViewHistoryLink().exists()).toBe(false);
       expect(findSecurityViewHistoryLink().exists()).toBe(false);
     });
+
+    it('renders training provider list with correct props', () => {
+      expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS);
+    });
   });
 
   describe('Manage via MR Error Alert', () => {
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
new file mode 100644
index 000000000000..1169a977d442
--- /dev/null
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -0,0 +1,60 @@
+import { GlLink, GlToggle, GlCard } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue';
+
+const DEFAULT_PROPS = {
+  providers: TRAINING_PROVIDERS,
+};
+
+describe('TrainingProviderList component', () => {
+  let wrapper;
+
+  const createComponent = (props = {}) => {
+    wrapper = shallowMount(TrainingProviderList, {
+      propsData: {
+        ...DEFAULT_PROPS,
+        ...props,
+      },
+    });
+  };
+
+  const findCards = () => wrapper.findAllComponents(GlCard);
+  const findLinks = () => wrapper.findAllComponents(GlLink);
+  const findToggles = () => wrapper.findAllComponents(GlToggle);
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  describe('basic structure', () => {
+    beforeEach(() => {
+      createComponent();
+    });
+
+    it('renders correct amount of cards', () => {
+      expect(findCards()).toHaveLength(DEFAULT_PROPS.providers.length);
+    });
+
+    DEFAULT_PROPS.providers.forEach(({ name, description, url, isEnabled }, index) => {
+      it(`shows the name for card ${index}`, () => {
+        expect(findCards().at(index).text()).toContain(name);
+      });
+
+      it(`shows the description for card ${index}`, () => {
+        expect(findCards().at(index).text()).toContain(description);
+      });
+
+      it(`shows the learn more link for card ${index}`, () => {
+        expect(findLinks().at(index).attributes()).toEqual({
+          target: '_blank',
+          href: url,
+        });
+      });
+
+      it(`shows the toggle with the correct value for card ${index}`, () => {
+        expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+      });
+    });
+  });
+});
-- 
GitLab