Commit 3d355055 authored by Mark Florian's avatar Mark Florian Committed by Jose Ivan Vargas

Colorize security summaries in merge requests

This adds bold and severity-specific colour styling to the numbers of
critical and high severity vulnerabilities in the security-related merge
request widgets.

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/221084.
parent f35d6e43
......@@ -21,7 +21,8 @@ export default {
props: {
summary: {
type: String,
required: true,
required: false,
default: '',
},
statusIcon: {
type: String,
......@@ -58,8 +59,8 @@ export default {
class="report-block-list-issue-description-text"
data-testid="test-summary-row-description"
>
{{ summary
}}<span v-if="popoverOptions" class="text-nowrap"
<slot name="summary">{{ summary }}</slot
><span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span>
</div>
......
......@@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following:
GitLab compares the found vulnerabilities between the source and target branches, and shows the
information directly in the merge request.
![Container Scanning Widget](img/container_scanning_v13_1.png)
![Container Scanning Widget](img/container_scanning_v13_2.png)
<!-- NOTE: The container scanning tool references the following heading in the code, so if you
make a change to this heading, make sure to update the documentation URLs used in the
......
......@@ -36,7 +36,7 @@ NOTE: **Note:**
This comparison logic uses only the latest pipeline executed for the target branch's base commit.
Running the pipeline on any other commit has no effect on the merge request.
![DAST Widget](img/dast_all_v13_1.png)
![DAST Widget](img/dast_v13_2.png)
By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected.
......
......@@ -27,7 +27,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information on the
merge request.
![Dependency Scanning Widget](img/dependency_scanning_v13_1.png)
![Dependency Scanning Widget](img/dependency_scanning_v13_2.png)
The results are sorted by the severity of the vulnerability:
......
......@@ -28,7 +28,7 @@ You can take advantage of SAST by doing one of the following:
GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches, and shows the information right on the merge request.
![SAST Widget](img/sast_v13_1.png)
![SAST Widget](img/sast_v13_2.png)
The results are sorted by the priority of the vulnerability:
......
......@@ -25,7 +25,7 @@ GitLab displays identified secrets as part of the SAST reports visibly in a few
- Pipelines' **Security** tab
- Report in the merge request widget
![Secret Detection in merge request widget](img/secret-detection-merge-request-ui.png)
![Secret Detection in merge request widget](img/secret_detection_v13_2.png)
## Use cases
......
import { s__ } from '~/locale';
export const SEVERITY_CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-300',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export const SEVERITY_TOOLTIP_TITLE_MAP = {
unknown: s__(
`SecurityReports|The rating "unknown" indicates that the underlying scanner doesn’t contain or provide a severity rating.`,
),
};
<script>
import { GlSprintf } from '@gitlab/ui';
import { SEVERITY_CLASS_NAME_MAP } from './constants';
const makeSeveritySlot = (createElement, severity) => ({ content }) =>
createElement('strong', { class: SEVERITY_CLASS_NAME_MAP[severity] }, content);
export default {
functional: true,
props: {
message: {
type: String,
required: true,
},
},
render(createElement, context) {
const { message } = context.props;
return createElement(GlSprintf, {
props: { message },
scopedSlots: {
critical: makeSeveritySlot(createElement, 'critical'),
high: makeSeveritySlot(createElement, 'high'),
},
});
},
};
</script>
<script>
import { s__ } from '~/locale';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
export const CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-300',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export const TOOLTIP_TITLE_MAP = {
unknown: s__(
`SecurityReports|The rating "unknown" indicates that the underlying scanner doesn’t contain or provide a severity rating.`,
),
};
import { SEVERITY_CLASS_NAME_MAP, SEVERITY_TOOLTIP_TITLE_MAP } from './constants';
export default {
name: 'SeverityBadge',
......@@ -34,13 +19,13 @@ export default {
},
computed: {
hasSeverityBadge() {
return Object.keys(CLASS_NAME_MAP).includes(this.severityKey);
return Object.keys(SEVERITY_CLASS_NAME_MAP).includes(this.severityKey);
},
severityKey() {
return this.severity.toLowerCase();
},
className() {
return CLASS_NAME_MAP[this.severityKey];
return SEVERITY_CLASS_NAME_MAP[this.severityKey];
},
iconName() {
return `severity-${this.severityKey}`;
......@@ -49,7 +34,7 @@ export default {
return SEVERITY_LEVELS[this.severityKey] || this.severity;
},
tooltipTitle() {
return TOOLTIP_TITLE_MAP[this.severityKey];
return SEVERITY_TOOLTIP_TITLE_MAP[this.severityKey];
},
},
};
......
......@@ -17,6 +17,7 @@ import { mrStates } from '~/mr_popover/constants';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue';
export default {
store: createStore(),
......@@ -24,6 +25,7 @@ export default {
GroupedIssuesList,
ReportSection,
SummaryRow,
SecuritySummary,
IssueModal,
Icon,
GlSprintf,
......@@ -326,20 +328,22 @@ export default {
fetchSastDiff: 'fetchDiff',
}),
},
summarySlots: ['success', 'error', 'loading'],
};
</script>
<template>
<report-section
:status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="true"
:should-emit-toggle-event="true"
class="mr-widget-border-top grouped-security-reports mr-report"
data-qa-selector="vulnerability_report_grouped"
@toggleEvent="handleToggleEvent"
>
<template v-for="slot in $options.summarySlots" #[slot]>
<security-summary :key="slot" :message="groupedSummaryText" />
</template>
<template v-if="pipelinePath" #actionButtons>
<div>
<a :href="securityTab" target="_blank" class="btn btn-default btn-sm float-right gl-mr-3">
......@@ -388,12 +392,15 @@ export default {
<div class="mr-widget-grouped-section report-block">
<template v-if="hasSastReports">
<summary-row
:summary="groupedSastText"
:status-icon="sastStatusIcon"
:popover-options="sastPopover"
class="js-sast-widget"
data-qa-selector="sast_scan_report"
/>
>
<template #summary>
<security-summary :message="groupedSastText" />
</template>
</summary-row>
<grouped-issues-list
v-if="sast.newIssues.length || sast.resolvedIssues.length"
......@@ -407,12 +414,15 @@ export default {
<template v-if="hasDependencyScanningReports">
<summary-row
:summary="groupedDependencyText"
:status-icon="dependencyScanningStatusIcon"
:popover-options="dependencyScanningPopover"
class="js-dependency-scanning-widget"
data-qa-selector="dependency_scan_report"
/>
>
<template #summary>
<security-summary :message="groupedDependencyText" />
</template>
</summary-row>
<grouped-issues-list
v-if="dependencyScanning.newIssues.length || dependencyScanning.resolvedIssues.length"
......@@ -426,12 +436,15 @@ export default {
<template v-if="hasContainerScanningReports">
<summary-row
:summary="groupedContainerScanningText"
:status-icon="containerScanningStatusIcon"
:popover-options="containerScanningPopover"
class="js-container-scanning"
data-qa-selector="container_scan_report"
/>
>
<template #summary>
<security-summary :message="groupedContainerScanningText" />
</template>
</summary-row>
<grouped-issues-list
v-if="containerScanning.newIssues.length || containerScanning.resolvedIssues.length"
......@@ -445,12 +458,15 @@ export default {
<template v-if="hasDastReports">
<summary-row
:summary="groupedDastText"
:status-icon="dastStatusIcon"
:popover-options="dastPopover"
class="js-dast-widget"
data-qa-selector="dast_scan_report"
>
<template #summary>
<security-summary :message="groupedDastText" />
</template>
<template v-if="dastScans.length">
<div class="text-nowrap">
{{ n__('%d URL scanned', '%d URLs scanned', dastScans[0].scanned_resources_count) }}
......@@ -478,12 +494,15 @@ export default {
<template v-if="hasSecretScanningReports">
<summary-row
:summary="groupedSecretScanningText"
:status-icon="secretScanningStatusIcon"
:popover-options="secretScanningPopover"
class="js-secret-scanning"
data-qa-selector="secret_scan_report"
/>
>
<template #summary>
<security-summary :message="groupedSecretScanningText" />
</template>
</summary-row>
<grouped-issues-list
v-if="secretScanning.newIssues.length || secretScanning.resolvedIssues.length"
......
......@@ -46,6 +46,10 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
/**
* Takes an object of options and returns an externalized string representing
* the critical, high, and other severity vulnerabilities for a given report.
*
* The resulting string _may_ still contain sprintf-style placeholders. These
* are left in place so they can be replaced with markup, via the
* SecuritySummary component.
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
* @returns {string}
*/
......@@ -84,16 +88,16 @@ export const groupedTextBuilder = ({
switch (options) {
case HAS_CRITICAL:
message = n__(
'%{reportType} %{status} detected %{critical} critical severity vulnerability.',
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities.',
'%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability.',
'%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities.',
critical,
);
break;
case HAS_HIGH:
message = n__(
'%{reportType} %{status} detected %{high} high severity vulnerability.',
'%{reportType} %{status} detected %{high} high severity vulnerabilities.',
'%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability.',
'%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities.',
high,
);
break;
......@@ -108,25 +112,25 @@ export const groupedTextBuilder = ({
case HAS_CRITICAL + HAS_HIGH:
message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities.',
'%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities.',
);
break;
case HAS_CRITICAL + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}.',
'%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}.',
);
break;
case HAS_HIGH + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}.',
'%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}.',
);
break;
case HAS_CRITICAL + HAS_HIGH + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}.',
'%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}.',
);
break;
......
---
title: Colorize security summaries in merge requests
merge_request: 36035
author:
type: changed
......@@ -139,7 +139,7 @@ describe('ee merge request widget options', () => {
`${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('SAST detected 1 vulnerability.');
).toEqual('SAST detected 1 new critical severity vulnerability.');
done();
});
});
......@@ -229,7 +229,9 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Dependency scanning detected 2 vulnerabilities.');
).toEqual(
'Dependency scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done();
});
});
......@@ -845,7 +847,9 @@ describe('ee merge request widget options', () => {
`${CONTAINER_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Container scanning detected 2 vulnerabilities.');
).toEqual(
'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done();
});
});
......@@ -915,7 +919,7 @@ describe('ee merge request widget options', () => {
findSecurityWidget()
.querySelector(`${DAST_SELECTOR} .report-block-list-issue-description`)
.textContent.trim(),
).toEqual('DAST detected 1 vulnerability.');
).toEqual('DAST detected 1 new critical severity vulnerability.');
done();
});
});
......@@ -989,7 +993,9 @@ describe('ee merge request widget options', () => {
`${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Secret scanning detected 2 vulnerabilities.');
).toEqual(
'Secret scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done();
});
});
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Severity Summary given the message "" interpolates correctly 1`] = `<div />`;
exports[`Severity Summary given the message "%{criticalStart}1 critical%{criticalEnd} and %{highStart}2 high%{highEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-800"
>
1 critical
</strong>
and
<strong
class="text-danger-600"
>
2 high
</strong>
</div>
`;
exports[`Severity Summary given the message "%{criticalStart}1 critical%{criticalEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-800"
>
1 critical
</strong>
</div>
`;
exports[`Severity Summary given the message "%{highStart}1 high%{highEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-600"
>
1 high
</strong>
</div>
`;
exports[`Severity Summary given the message "foo" interpolates correctly 1`] = `
<div>
foo
</div>
`;
import { mount } from '@vue/test-utils';
import SecuritySummary from 'ee/vue_shared/security_reports/components/security_summary.vue';
describe('Severity Summary', () => {
let wrapper;
const createWrapper = message => {
wrapper = mount({
components: {
SecuritySummary,
},
data() {
return {
message,
};
},
template: `<div><security-summary :message="message" /></div>`,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each([
'',
'foo',
'%{criticalStart}1 critical%{criticalEnd}',
'%{highStart}1 high%{highEnd}',
'%{criticalStart}1 critical%{criticalEnd} and %{highStart}2 high%{highEnd}',
])('given the message %p', message => {
beforeEach(() => {
createWrapper(message);
});
it('interpolates correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
import { shallowMount } from '@vue/test-utils';
import SeverityBadge, {
CLASS_NAME_MAP,
TOOLTIP_TITLE_MAP,
} from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import {
SEVERITY_CLASS_NAME_MAP,
SEVERITY_TOOLTIP_TITLE_MAP,
} from 'ee/vue_shared/security_reports/components/constants';
import { GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
......@@ -33,7 +34,7 @@ describe('Severity Badge', () => {
createWrapper({ severity });
});
const className = CLASS_NAME_MAP[severity];
const className = SEVERITY_CLASS_NAME_MAP[severity];
it(`renders the component with ${severity} badge`, () => {
expect(wrapper.find(`.${className}`).exists()).toBe(true);
......@@ -49,7 +50,7 @@ describe('Severity Badge', () => {
});
it('renders tooltip', () => {
expect(findTooltip()).toBe(TOOLTIP_TITLE_MAP[severity]);
expect(findTooltip()).toBe(SEVERITY_TOOLTIP_TITLE_MAP[severity]);
});
});
......
......@@ -139,7 +139,7 @@ key2: value2"
>
<severity-badge-stub
class="d-inline"
severity="unknown"
severity="critical"
/>
</vulnerability-detail-stub>
......
......@@ -183,6 +183,55 @@ describe('Grouped security reports app', () => {
});
});
describe('with empty reports', () => {
beforeEach(() => {
const emptyResponse = { ...dastDiffSuccessMock, fixed: [], added: [] };
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
createWrapper(allReportProps);
return Promise.all([
waitForMutation(wrapper.vm.$store, `sast/${sastTypes.RECEIVE_DIFF_SUCCESS}`),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_CONTAINER_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS),
]);
});
it('renders reports', () => {
// It's not loading
expect(wrapper.vm.$el.querySelector('.gl-spinner')).toBeNull();
// Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected no new vulnerabilities.',
);
// Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'SAST detected no new vulnerabilities.',
);
// Renders DSS result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Dependency scanning detected no new vulnerabilities.',
);
// Renders container scanning result
expect(wrapper.vm.$el.textContent).toContain(
'Container scanning detected no new vulnerabilities.',
);
// Renders DAST result
expect(wrapper.vm.$el.textContent).toContain('DAST detected no new vulnerabilities.');
});
});
describe('with successful responses', () => {
beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
......@@ -208,7 +257,7 @@ describe('Grouped security reports app', () => {
// Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 8 vulnerabilities.',
'Security scanning detected 5 new critical and 3 new high severity vulnerabilities.',
);
// Renders the expand button
......@@ -217,20 +266,24 @@ describe('Grouped security reports app', () => {
);
// Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain('SAST detected 1 vulnerability');
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'SAST detected 1 new critical severity vulnerability',
);
// Renders DSS result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Dependency scanning detected 2 vulnerabilities.',
'Dependency scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
// Renders container scanning result
expect(wrapper.vm.$el.textContent).toContain(
'Container scanning detected 2 vulnerabilities.',
'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
// Renders DAST result
expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability.');
expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new critical severity vulnerability.',
);
});
it('opens modal with more information', () => {
......@@ -311,7 +364,9 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain('Container scanning detected 2 vulnerabilities.');
expect(wrapper.text()).toContain(
'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
});
});
......@@ -340,7 +395,7 @@ describe('Grouped security reports app', () => {
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain(
'Dependency scanning detected 2 vulnerabilities.',
'Dependency scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
});
});
......@@ -378,7 +433,9 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability');
expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new critical severity vulnerability',
);
});
it('shows the scanned URLs count and opens a modal', async () => {
......@@ -449,7 +506,9 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain('Secret scanning detected 2 vulnerabilities.');
expect(wrapper.text()).toContain(
'Secret scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
});
});
......@@ -486,7 +545,9 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain('SAST detected 1 vulnerability.');
expect(wrapper.vm.$el.textContent).toContain(
'SAST detected 1 new critical severity vulnerability.',
);
});
});
......
......@@ -307,7 +307,7 @@ export const mockFindings = [
id: null,
report_type: 'dependency_scanning',
name: 'Cross-site Scripting in serialize-javascript',
severity: 'unknown',
severity: 'critical',
scanner: {
external_id: 'gemnasium',
name: 'Gemnasium',
......@@ -360,7 +360,7 @@ export const mockFindings = [
id: null,
report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery',
severity: 'medium',
severity: 'high',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
......
......@@ -54,11 +54,11 @@ describe('Security reports getters', () => {
it.each`
vulnerabilities | message
${[]} | ${`${name} detected no new vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(CRITICAL)]} | ${`${name} detected 2 critical severity vulnerabilities.`}
${[generateVuln(HIGH), generateVuln(HIGH)]} | ${`${name} detected 2 high severity vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(CRITICAL)]} | ${`${name} detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities.`}
${[generateVuln(HIGH), generateVuln(HIGH)]} | ${`${name} detected %{highStart}2 new high%{highEnd} severity vulnerabilities.`}
${[generateVuln(LOW), generateVuln(MEDIUM)]} | ${`${name} detected 2 vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(HIGH)]} | ${`${name} detected 1 critical and 1 high severity vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(LOW)]} | ${`${name} detected 1 critical severity vulnerabilities out of 2.`}
${[generateVuln(CRITICAL), generateVuln(HIGH)]} | ${`${name} detected %{criticalStart}1 new critical%{criticalEnd} and %{highStart}1 new high%{highEnd} severity vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(LOW)]} | ${`${name} detected %{criticalStart}1 new critical%{criticalEnd} severity vulnerabilities out of 2.`}
`('should build the message as "$message"', ({ vulnerabilities, message }) => {
state[scanner].newIssues = vulnerabilities;
expect(getter(state)).toEqual(message);
......@@ -133,7 +133,7 @@ describe('Security reports getters', () => {
},
}),
).toEqual(
'Security scanning (is loading) detected 2 critical and 4 high severity vulnerabilities.',
'Security scanning (is loading) detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities.',
);
});
......
......@@ -128,21 +128,21 @@ describe('security reports utils', () => {
it.each`
vulnerabilities | message
${undefined} | ${' detected no new vulnerabilities.'}
${{ critical }} | ${' detected 2 critical severity vulnerabilities.'}
${{ high }} | ${' detected 4 high severity vulnerabilities.'}
${{ critical }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities.'}
${{ high }} | ${' detected %{highStart}4 new high%{highEnd} severity vulnerabilities.'}
${{ other }} | ${' detected 7 vulnerabilities.'}
${{ critical, high }} | ${' detected 2 critical and 4 high severity vulnerabilities.'}
${{ critical, other }} | ${' detected 2 critical severity vulnerabilities out of 9.'}
${{ high, other }} | ${' detected 4 high severity vulnerabilities out of 11.'}
${{ critical, high, other }} | ${' detected 2 critical and 4 high severity vulnerabilities out of 13.'}
${{ critical, high }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities.'}
${{ critical, other }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities out of 9.'}
${{ high, other }} | ${' detected %{highStart}4 new high%{highEnd} severity vulnerabilities out of 11.'}
${{ critical, high, other }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities out of 13.'}
`('should build the message as "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
});
it.each`
vulnerabilities | message
${{ critical: 1 }} | ${' detected 1 critical severity vulnerability.'}
${{ high: 1 }} | ${' detected 1 high severity vulnerability.'}
${{ critical: 1 }} | ${' detected %{criticalStart}1 new critical%{criticalEnd} severity vulnerability.'}
${{ high: 1 }} | ${' detected %{highStart}1 new high%{highEnd} severity vulnerability.'}
${{ other: 1 }} | ${' detected 1 vulnerability.'}
`('should handle single vulnerabilities for "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
......
......@@ -571,25 +571,25 @@ msgstr[1] ""
msgid "%{remaining_approvals} left"
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{critical} critical severity vulnerabilities."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{high} high severity vulnerabilities."
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
......
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import component from '~/reports/components/summary_row.vue';
import { mount } from '@vue/test-utils';
import SummaryRow from '~/reports/components/summary_row.vue';
describe('Summary row', () => {
const Component = Vue.extend(component);
let vm;
let wrapper;
const props = {
summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
......@@ -15,23 +13,42 @@ describe('Summary row', () => {
statusIcon: 'warning',
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
const createComponent = ({ propsData = {}, slots = {} } = {}) => {
wrapper = mount(SummaryRow, {
propsData: {
...props,
...propsData,
},
slots,
});
};
const findSummary = () => wrapper.find('.report-block-list-issue-description-text');
afterEach(() => {
vm.$destroy();
wrapper.destroy();
wrapper = null;
});
it('renders provided summary', () => {
expect(
vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
).toEqual(props.summary);
createComponent();
expect(findSummary().text()).toEqual(props.summary);
});
it('renders provided icon', () => {
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
createComponent();
expect(wrapper.find('.report-block-list-icon span').classes()).toContain(
'js-ci-status-icon-warning',
);
});
describe('summary slot', () => {
it('replaces the summary prop', () => {
const summarySlotContent = 'Summary slot content';
createComponent({ slots: { summary: summarySlotContent } });
expect(wrapper.text()).not.toContain(props.summary);
expect(findSummary().text()).toEqual(summarySlotContent);
});
});
});
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