Commit 5b21719b authored by Dave Pisek's avatar Dave Pisek

Refactor: pass error scan from parent

parent 61418b8a
...@@ -71,6 +71,20 @@ export default { ...@@ -71,6 +71,20 @@ export default {
primaryButtonText: s__('SecurityReports|Learn more about setting up your dashboard'), primaryButtonText: s__('SecurityReports|Learn more about setting up your dashboard'),
}; };
}, },
scansWithErrors() {
const getScans = (reportSummary) => reportSummary?.scans || [];
const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.securityReportSummary
? Object.values(this.securityReportSummary)
// generate flat array of all scans
.flatMap(getScans)
.filter(hasErrors)
: [];
},
hasScansWithErrors() {
return this.scansWithErrors.length > 0;
},
}, },
created() { created() {
this.setSourceBranch(this.pipeline.sourceBranch); this.setSourceBranch(this.pipeline.sourceBranch);
...@@ -87,7 +101,7 @@ export default { ...@@ -87,7 +101,7 @@ export default {
<template> <template>
<div> <div>
<div v-if="securityReportSummary" class="gl-my-5"> <div v-if="securityReportSummary" class="gl-my-5">
<scan-errors-alert :security-report-summary="securityReportSummary" class="gl-mb-5" /> <scan-errors-alert v-if="hasScansWithErrors" :scans="scansWithErrors" class="gl-mb-5" />
<security-reports-summary :summary="securityReportSummary" /> <security-reports-summary :summary="securityReportSummary" />
</div> </div>
<security-dashboard <security-dashboard
......
<script> <script>
import { GlAccordion, GlAccordionItem, GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; import { GlAccordion, GlAccordionItem, GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { s__ } from '~/locale';
export default { export default {
components: { components: {
...@@ -12,34 +12,17 @@ export default { ...@@ -12,34 +12,17 @@ export default {
}, },
inject: ['securityReportHelpPageLink'], inject: ['securityReportHelpPageLink'],
props: { props: {
securityReportSummary: { scans: {
type: Object, type: Array,
required: false, required: true,
default: () => ({}),
}, },
}, },
computed: { computed: {
scansWithErrors() { scansWithTitles() {
const getScans = (reportSummary) => reportSummary?.scans || []; return this.scans.map((scan) => ({
const hasErrors = (scan) => Boolean(scan.errors?.length);
const addTitle = (scan) => ({
...scan, ...scan,
title: sprintf(s__('SecurityReports|%{errorName} (%{errorCount})'), { title: `${scan.name} (${scan.errors.length})`,
errorName: scan.name, }));
errorCount: scan.errors.length,
}),
});
return this.securityReportSummary
? Object.values(this.securityReportSummary)
// generate flat array of all scans
.flatMap(getScans)
.filter(hasErrors)
.map(addTitle)
: [];
},
hasScansWithErrors() {
return this.scansWithErrors.length > 0;
}, },
}, },
i18n: { i18n: {
...@@ -52,7 +35,7 @@ export default { ...@@ -52,7 +35,7 @@ export default {
</script> </script>
<template> <template>
<gl-alert v-if="hasScansWithErrors" variant="danger" :dismissible="false"> <gl-alert variant="danger" :dismissible="false">
<strong role="heading"> <strong role="heading">
{{ $options.i18n.title }} {{ $options.i18n.title }}
</strong> </strong>
...@@ -72,7 +55,7 @@ export default { ...@@ -72,7 +55,7 @@ export default {
</p> </p>
<gl-accordion :header-level="3"> <gl-accordion :header-level="3">
<gl-accordion-item <gl-accordion-item
v-for="{ name, errors, title } in scansWithErrors" v-for="{ name, errors, title } in scansWithTitles"
:key="name" :key="name"
:title="title" :title="title"
> >
......
...@@ -150,32 +150,66 @@ describe('Pipeline Security Dashboard component', () => { ...@@ -150,32 +150,66 @@ describe('Pipeline Security Dashboard component', () => {
}); });
}); });
describe('scan errors alert', () => { describe('scans error alert', () => {
const securityReportSummary = { describe('with errors', () => {
dast: { const securityReportSummary = {
scans: [ scanner_1: {
{ // this scan contains errors
name: 'dast', scans: [
errors: [], { errors: ['scanner 1 - error 1', 'scanner 1 - error 2'], name: 'foo' },
{ errors: ['scanner 1 - error 3', 'scanner 1 - error 4'], name: 'bar' },
],
},
scanner_2: null,
scanner_3: {
// this scan contains errors
scans: [{ errors: ['scanner 3 - error 1', 'scanner 3 - error 2'], name: 'baz' }],
},
scanner_4: {
scans: [{ errors: [], name: 'quz' }],
},
};
const scansWithErrors = [
...securityReportSummary.scanner_1.scans,
...securityReportSummary.scanner_3.scans,
];
beforeEach(() => {
factory({
data: {
securityReportSummary,
}, },
], });
}, });
};
beforeEach(() => { it('shows an alert with information about each scan with errors', () => {
factory({ expect(findScanErrorsAlert().props('scans')).toEqual(scansWithErrors);
data: {
securityReportSummary,
},
}); });
}); });
it('includes the alert', () => { describe('without errors', () => {
expect(findScanErrorsAlert().exists()).toBe(true); const securityReportSummary = {
}); dast: {
scans: [
{
name: 'dast',
errors: [],
},
],
},
};
it('passes the security report summary to the alert', () => { beforeEach(() => {
expect(findScanErrorsAlert().props('securityReportSummary')).toBe(securityReportSummary); factory({
data: {
securityReportSummary,
},
});
});
it('does not show the alert', () => {
expect(findScanErrorsAlert().exists()).toBe(false);
});
}); });
}); });
......
...@@ -4,36 +4,22 @@ import PipelineScanErrorsAlert from 'ee/security_dashboard/components/pipeline/s ...@@ -4,36 +4,22 @@ import PipelineScanErrorsAlert from 'ee/security_dashboard/components/pipeline/s
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const TEST_SECURITY_REPORT_SUMMARY = {
scanner_1: {
// this scan contains errors
scans: [
{ errors: ['scanner 1 - error 1', 'scanner 1 - error 2'], name: 'foo' },
{ errors: ['scanner 1 - error 3', 'scanner 1 - error 4'], name: 'bar' },
],
},
scanner_2: null,
scanner_3: {
// this scan contains errors
scans: [{ errors: ['scanner 3 - error 1', 'scanner 3 - error 2'], name: 'baz' }],
},
scanner_4: {
scans: [{ errors: [], name: 'quz' }],
},
};
const TEST_HELP_PAGE_LINK = 'http://help.com'; const TEST_HELP_PAGE_LINK = 'http://help.com';
const TEST_SCANS_WITH_ERRORS = [ const TEST_SCANS_WITH_ERRORS = [
...TEST_SECURITY_REPORT_SUMMARY.scanner_1.scans, { errors: ['scanner 1 - error 1', 'scanner 1 - error 2'], name: 'foo' },
...TEST_SECURITY_REPORT_SUMMARY.scanner_3.scans, { errors: ['scanner 1 - error 3', 'scanner 1 - error 4'], name: 'bar' },
{ errors: ['scanner 3 - error 1', 'scanner 3 - error 2'], name: 'baz' },
]; ];
describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () => { describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () => {
let wrapper; let wrapper;
const createWrapper = (options) => const createWrapper = () =>
extendedWrapper( extendedWrapper(
shallowMount(PipelineScanErrorsAlert, { shallowMount(PipelineScanErrorsAlert, {
...options, propsData: {
scans: TEST_SCANS_WITH_ERRORS,
},
provide: { provide: {
securityReportHelpPageLink: TEST_HELP_PAGE_LINK, securityReportHelpPageLink: TEST_HELP_PAGE_LINK,
}, },
...@@ -55,70 +41,54 @@ describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () = ...@@ -55,70 +41,54 @@ describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () =
wrapper.destroy(); wrapper.destroy();
}); });
describe('without scanner errors', () => { beforeEach(() => {
beforeEach(() => { wrapper = createWrapper();
wrapper = createWrapper({ propsData: { securityReportSummary: {} } }); });
});
it('does not show error alert', () => { it('shows a non-dismissible error alert', () => {
expect(findAlert().exists()).toBe(false); expect(findAlert().props()).toMatchObject({
variant: 'danger',
dismissible: false,
}); });
}); });
describe('with scanner errors', () => { it('shows the correct title for the error alert', () => {
beforeEach(() => { expect(findAlert().text()).toContain('Error parsing security reports');
wrapper = createWrapper({ });
propsData: { securityReportSummary: TEST_SECURITY_REPORT_SUMMARY },
});
});
it('shows a non-dismissible error alert', () => { it('shows the correct description for the error-alert', () => {
expect(findAlert().exists()).toBe(true); expect(trimText(findAlert().text())).toContain(
expect(findAlert().props()).toMatchObject({ 'The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant JSON schema',
variant: 'danger', );
dismissible: false, });
});
});
it('shows the correct title for the error alert', () => { it('links to the security-report help page', () => {
expect(findAlert().text()).toContain('Error parsing security reports'); expect(findHelpPageLink().attributes('href')).toBe(TEST_HELP_PAGE_LINK);
}); });
it('shows the correct description for the error-alert', () => { describe('errors details', () => {
expect(trimText(findAlert().text())).toContain( it('shows an accordion containing a list of scans with errors', () => {
'The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant JSON schema', expect(findAccordion().exists()).toBe(true);
); expect(findAllAccordionItems()).toHaveLength(TEST_SCANS_WITH_ERRORS.length);
}); });
it('links to the security-report help page', () => { it('shows a list containing details about each error', () => {
expect(findHelpPageLink().exists()).toBe(true); expect(findErrorList().exists()).toBe(true);
expect(findHelpPageLink().attributes('href')).toBe(TEST_HELP_PAGE_LINK);
}); });
describe('errors details', () => { describe.each(TEST_SCANS_WITH_ERRORS)('scan errors', (scan) => {
it('shows an accordion containing a list of scans with errors', () => { const currentScanTitle = `${scan.name} (${scan.errors.length})`;
expect(findAccordion().exists()).toBe(true); const findAllAccordionItemsForCurrentScan = () =>
expect(findAllAccordionItems()).toHaveLength(TEST_SCANS_WITH_ERRORS.length); findAccordionItemsWithTitle(currentScanTitle);
}); const findAccordionItemForCurrentScan = () => findAllAccordionItemsForCurrentScan().at(0);
it('shows a list containing details about each error', () => { it(`contains an accordion item with the correct title for scan "${scan.name}"`, () => {
expect(findErrorList().exists()).toBe(true); expect(findAllAccordionItemsForCurrentScan()).toHaveLength(1);
}); });
describe.each(TEST_SCANS_WITH_ERRORS)('scan errors', (scan) => { it(`contains a detailed list of errors for scan "${scan.name}}"`, () => {
const currentScanTitle = `${scan.name} (${scan.errors.length})`; expect(findAccordionItemForCurrentScan().find('ul').exists()).toBe(true);
const findAllAccordionItemsForCurrentScan = () => expect(findAccordionItemForCurrentScan().findAll('li')).toHaveLength(scan.errors.length);
findAccordionItemsWithTitle(currentScanTitle);
const findAccordionItemForCurrentScan = () => findAllAccordionItemsForCurrentScan().at(0);
it(`contains an accordion item with the correct title for scan "${scan.name}"`, () => {
expect(findAllAccordionItemsForCurrentScan()).toHaveLength(1);
});
it(`contains a detailed list of errors for scan "${scan.name}}"`, () => {
expect(findAccordionItemForCurrentScan().find('ul').exists()).toBe(true);
expect(findAccordionItemForCurrentScan().findAll('li')).toHaveLength(scan.errors.length);
});
}); });
}); });
}); });
......
...@@ -29078,9 +29078,6 @@ msgstr "" ...@@ -29078,9 +29078,6 @@ msgstr ""
msgid "SecurityOrchestration|Security policy project" msgid "SecurityOrchestration|Security policy project"
msgstr "" msgstr ""
msgid "SecurityReports|%{errorName} (%{errorCount})"
msgstr ""
msgid "SecurityReports|%{firstProject} and %{secondProject}" msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr "" 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