Commit 91e139d8 authored by Savas Vedova's avatar Savas Vedova

Merge branch '346066-sec-training-det-page' into 'master'

Add training section (basic UI) to vulnerability details page

See merge request gitlab-org/gitlab!77317
parents 48d4d05d 7caaccdd
......@@ -6,8 +6,10 @@ import convertReportType from 'ee/vue_shared/security_reports/store/utils/conver
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { s__, __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DetailItem from './detail_item.vue';
import VulnerabilityDetailSection from './vulnerability_detail_section.vue';
import VulnerabilityTraining from './vulnerability_training.vue';
export default {
name: 'VulnerabilityDetails',
......@@ -18,10 +20,12 @@ export default {
DetailItem,
GlSprintf,
VulnerabilityDetailSection,
VulnerabilityTraining,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
vulnerability: {
type: Object,
......@@ -148,6 +152,9 @@ export default {
hasResponses() {
return Boolean(this.hasResponse || this.hasRecordedResponse);
},
hasTraining() {
return this.glFeatures.secureVulnerabilityTraining && this.vulnerability.identifiers?.length;
},
},
methods: {
getHeadersAsCodeBlockLines(headers) {
......@@ -373,5 +380,7 @@ export default {
</li>
</ul>
</template>
<vulnerability-training v-if="hasTraining" :identifiers="vulnerability.identifiers" />
</div>
</template>
<script>
import { s__ } from '~/locale';
import { SUPPORTED_REFERENCE_SCHEMA } from '../constants';
export const i18n = {
trainingTitle: s__('Vulnerability|Training'),
trainingDescription: s__(
'Vulnerability|Learn more about this vulnerability and the best way to resolve it.',
),
trainingUnavailable: s__('Vulnerability|Training not available for this vulnerability.'),
};
export default {
i18n,
props: {
identifiers: {
type: Array,
required: true,
},
},
computed: {
isSupportedReferenceSchema() {
return this.referenceSchemas.some(
(referenceSchema) => referenceSchema?.toLowerCase() === SUPPORTED_REFERENCE_SCHEMA.cwe,
);
},
referenceSchemas() {
return this.identifiers.map((identifier) => identifier?.externalType);
},
},
};
</script>
<template>
<div>
<h3>{{ $options.i18n.trainingTitle }}</h3>
<p class="gl-text-gray-600!" data-testid="description">
{{ $options.i18n.trainingDescription }}
</p>
<p v-if="!isSupportedReferenceSchema" data-testid="unavailable-message">
{{ $options.i18n.trainingUnavailable }}
</p>
</div>
</template>
......@@ -85,3 +85,7 @@ export const SUPPORTING_MESSAGE_TYPES = {
// eslint-disable-next-line @gitlab/require-i18n-strings
RECORDED: 'Recorded',
};
export const SUPPORTED_REFERENCE_SCHEMA = {
cwe: 'cwe',
};
......@@ -10,6 +10,7 @@ module Projects
before_action do
push_frontend_feature_flag(:create_vulnerability_jira_issue_via_graphql, @project, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, @project, default_enabled: :yaml)
end
before_action :vulnerability, except: [:index, :new]
......
......@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
describe('Vulnerability Details', () => {
let wrapper;
......@@ -16,16 +17,24 @@ describe('Vulnerability Details', () => {
descriptionHtml: 'vulnerability description <code>sample</code>',
};
const createWrapper = (vulnerabilityOverrides) => {
const createWrapper = (vulnerabilityOverrides, { secureVulnerabilityTraining = true } = {}) => {
const propsData = {
vulnerability: { ...vulnerability, ...vulnerabilityOverrides },
};
wrapper = mount(VulnerabilityDetails, { propsData });
wrapper = mount(VulnerabilityDetails, {
propsData,
provide: {
glFeatures: {
secureVulnerabilityTraining,
},
},
});
};
const getById = (id) => wrapper.find(`[data-testid="${id}"]`);
const getAllById = (id) => wrapper.findAll(`[data-testid="${id}"]`);
const getText = (id) => getById(id).text();
const findVulnerabilityTraining = () => wrapper.findComponent(VulnerabilityTraining);
afterEach(() => {
wrapper.destroy();
......@@ -397,4 +406,26 @@ describe('Vulnerability Details', () => {
expect(getSectionData('recorded-response')).toEqual(expectedData);
});
});
describe('vulnerability training', () => {
it('renders the component', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }];
createWrapper({ identifiers });
expect(wrapper.findComponent(VulnerabilityTraining).props()).toMatchObject({
identifiers,
});
});
it('does not render the component when there are no identifiers', () => {
createWrapper();
expect(findVulnerabilityTraining().exists()).toBe(false);
});
});
describe('when secureVulnerabilityTraining feature flag is disabled', () => {
it('does not render the VulnerabilityTraining component', () => {
createWrapper(undefined, { secureVulnerabilityTraining: false });
expect(findVulnerabilityTraining().exists()).toBe(false);
});
});
});
import VulnerabilityTraining, {
i18n,
} from 'ee/vulnerabilities/components/vulnerability_training.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { SUPPORTED_REFERENCE_SCHEMA } from 'ee/vulnerabilities/constants';
const defaultProps = {
identifiers: [{ externalType: SUPPORTED_REFERENCE_SCHEMA.cwe }, { externalType: 'cve' }],
};
describe('VulnerabilityTraining component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(VulnerabilityTraining, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findTitle = () => wrapper.findByRole('heading', i18n.trainingTitle);
const findDescription = () => wrapper.findByTestId('description');
const findUnavailableMessage = () => wrapper.findByTestId('unavailable-message');
describe('basic structure', () => {
beforeEach(() => {
createComponent();
});
it('displays the title', () => {
expect(findTitle().text()).toBe(i18n.trainingTitle);
});
it('displays the description', () => {
expect(findDescription().text()).toBe(i18n.trainingDescription);
});
});
describe('training availability message', () => {
it('displays the message', () => {
createComponent({ identifiers: [{ externalType: 'not supported identifier' }] });
expect(findUnavailableMessage().text()).toBe(i18n.trainingUnavailable);
});
it.each`
identifiers | exists
${[{ externalType: 'cve' }]} | ${true}
${[{ externalType: SUPPORTED_REFERENCE_SCHEMA.cwe.toUpperCase() }]} | ${false}
${[{ externalType: SUPPORTED_REFERENCE_SCHEMA.cwe.toLowerCase() }]} | ${false}
`('sets it to "$exists" for "$identifiers"', ({ identifiers, exists }) => {
createComponent({ identifiers });
expect(findUnavailableMessage().exists()).toBe(exists);
});
});
});
......@@ -39578,6 +39578,9 @@ msgstr ""
msgid "Vulnerability|Information related how the vulnerability was discovered and its impact to the system."
msgstr ""
msgid "Vulnerability|Learn more about this vulnerability and the best way to resolve it."
msgstr ""
msgid "Vulnerability|Links"
msgstr ""
......@@ -39626,6 +39629,12 @@ msgstr ""
msgid "Vulnerability|Tool"
msgstr ""
msgid "Vulnerability|Training"
msgstr ""
msgid "Vulnerability|Training not available for this vulnerability."
msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment