Commit 3ef54b6f authored by Mark Florian's avatar Mark Florian Committed by Savas Vedova

Move scanner warnings in Vulnerability Report

parent eb16b8b2
<script> <script>
import { GlAlert, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import produce from 'immer'; import produce from 'immer';
import { difference } from 'lodash';
import { Portal } from 'portal-vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import securityScannersQuery from '../graphql/queries/project_security_scanners.query.graphql'; import securityScannersQuery from '../graphql/queries/project_security_scanners.query.graphql';
import vulnerabilitiesQuery from '../graphql/queries/project_vulnerabilities.query.graphql'; import vulnerabilitiesQuery from '../graphql/queries/project_vulnerabilities.query.graphql';
import { preparePageInfo } from '../helpers'; import { preparePageInfo } from '../helpers';
import { VULNERABILITIES_PER_PAGE } from '../store/constants'; import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import SecurityScannerAlert from './security_scanner_alert.vue';
import VulnerabilityList from './vulnerability_list.vue'; import VulnerabilityList from './vulnerability_list.vue';
export default { export default {
...@@ -14,9 +19,15 @@ export default { ...@@ -14,9 +19,15 @@ export default {
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
GlIntersectionObserver, GlIntersectionObserver,
LocalStorageSync,
Portal,
SecurityScannerAlert,
VulnerabilityList, VulnerabilityList,
}, },
inject: { inject: {
vulnerabilityReportAlertsPortal: {
default: '',
},
projectFullPath: { projectFullPath: {
default: '', default: '',
}, },
...@@ -35,6 +46,7 @@ export default { ...@@ -35,6 +46,7 @@ export default {
return { return {
pageInfo: {}, pageInfo: {},
vulnerabilities: [], vulnerabilities: [],
scannerAlertDismissed: false,
securityScanners: {}, securityScanners: {},
errorLoadingVulnerabilities: false, errorLoadingVulnerabilities: false,
sortBy: 'severity', sortBy: 'severity',
...@@ -97,6 +109,21 @@ export default { ...@@ -97,6 +109,21 @@ export default {
sort() { sort() {
return `${this.sortBy}_${this.sortDirection}`; return `${this.sortBy}_${this.sortDirection}`;
}, },
notEnabledSecurityScanners() {
const { available = [], enabled = [] } = this.securityScanners;
return difference(available, enabled);
},
noPipelineRunSecurityScanners() {
const { enabled = [], pipelineRun = [] } = this.securityScanners;
return difference(enabled, pipelineRun);
},
shouldShowScannersAlert() {
return (
!this.scannerAlertDismissed &&
(this.notEnabledSecurityScanners.length > 0 ||
this.noPipelineRunSecurityScanners.length > 0)
);
},
}, },
watch: { watch: {
filters() { filters() {
...@@ -128,7 +155,11 @@ export default { ...@@ -128,7 +155,11 @@ export default {
this.sortDirection = sortDesc ? 'desc' : 'asc'; this.sortDirection = sortDesc ? 'desc' : 'asc';
this.sortBy = sortBy; this.sortBy = sortBy;
}, },
setScannerAlertDismissed(value) {
this.scannerAlertDismissed = parseBoolean(value);
},
}, },
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY: 'vulnerability_list_scanner_alert_dismissed',
i18n: { i18n: {
API_FUZZING: __('API Fuzzing'), API_FUZZING: __('API Fuzzing'),
CONTAINER_SCANNING: __('Container Scanning'), CONTAINER_SCANNING: __('Container Scanning'),
...@@ -148,21 +179,36 @@ export default { ...@@ -148,21 +179,36 @@ export default {
) )
}} }}
</gl-alert> </gl-alert>
<vulnerability-list
v-else <template v-else>
:is-loading="isLoadingFirstVulnerabilities" <local-storage-sync
:filters="filters" :value="String(scannerAlertDismissed)"
:vulnerabilities="vulnerabilities" :storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY"
:security-scanners="securityScanners" @input="setScannerAlertDismissed"
@sort-changed="handleSortChange" />
/>
<gl-intersection-observer <portal v-if="shouldShowScannersAlert" :to="vulnerabilityReportAlertsPortal">
v-if="pageInfo.hasNextPage" <security-scanner-alert
class="text-center" :not-enabled-scanners="notEnabledSecurityScanners"
@appear="fetchNextPage" :no-pipeline-run-scanners="noPipelineRunSecurityScanners"
> @dismiss="setScannerAlertDismissed('true')"
<gl-loading-icon v-if="isLoadingVulnerabilities" size="md" /> />
<span v-else>&nbsp;</span> </portal>
</gl-intersection-observer>
<vulnerability-list
:is-loading="isLoadingFirstVulnerabilities"
:filters="filters"
:vulnerabilities="vulnerabilities"
@sort-changed="handleSortChange"
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="text-center"
@appear="fetchNextPage"
>
<gl-loading-icon v-if="isLoadingVulnerabilities" size="md" />
<span v-else>&nbsp;</span>
</gl-intersection-observer>
</template>
</div> </div>
</template> </template>
...@@ -8,7 +8,6 @@ import { ...@@ -8,7 +8,6 @@ import {
GlTooltipDirective, GlTooltipDirective,
GlTable, GlTable,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { difference } from 'lodash';
import AutoFixHelpText from 'ee/security_dashboard/components/auto_fix_help_text.vue'; import AutoFixHelpText from 'ee/security_dashboard/components/auto_fix_help_text.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue'; import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue'; import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
...@@ -21,15 +20,10 @@ import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; ...@@ -21,15 +20,10 @@ import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants'; import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import IssuesBadge from './issues_badge.vue'; import IssuesBadge from './issues_badge.vue';
import SecurityScannerAlert from './security_scanner_alert.vue';
import SelectionSummary from './selection_summary.vue'; import SelectionSummary from './selection_summary.vue';
export const SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY =
'vulnerability_list_scanner_alert_dismissed';
export default { export default {
components: { components: {
GlFormCheckbox, GlFormCheckbox,
...@@ -39,10 +33,8 @@ export default { ...@@ -39,10 +33,8 @@ export default {
GlTable, GlTable,
GlTruncate, GlTruncate,
IssuesBadge, IssuesBadge,
LocalStorageSync,
AutoFixHelpText, AutoFixHelpText,
RemediatedBadge, RemediatedBadge,
SecurityScannerAlert,
SelectionSummary, SelectionSummary,
SeverityBadge, SeverityBadge,
VulnerabilityCommentIcon, VulnerabilityCommentIcon,
...@@ -67,11 +59,6 @@ export default { ...@@ -67,11 +59,6 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
securityScanners: {
type: Object,
required: false,
default: () => ({}),
},
shouldShowSelection: { shouldShowSelection: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -95,7 +82,6 @@ export default { ...@@ -95,7 +82,6 @@ export default {
data() { data() {
return { return {
selectedVulnerabilities: {}, selectedVulnerabilities: {},
scannerAlertDismissed: 'false',
sortBy: 'severity', sortBy: 'severity',
sortDesc: true, sortDesc: true,
}; };
...@@ -117,21 +103,6 @@ export default { ...@@ -117,21 +103,6 @@ export default {
(v) => v.scanner?.vendor !== 'GitLab' && v.scanner?.vendor !== '', (v) => v.scanner?.vendor !== 'GitLab' && v.scanner?.vendor !== '',
); );
}, },
notEnabledSecurityScanners() {
const { available = [], enabled = [] } = this.securityScanners;
return difference(available, enabled);
},
noPipelineRunSecurityScanners() {
const { enabled = [], pipelineRun = [] } = this.securityScanners;
return difference(enabled, pipelineRun);
},
shouldShowScannersAlert() {
return (
this.scannerAlertDismissed !== 'true' &&
(this.notEnabledSecurityScanners.length > 0 ||
this.noPipelineRunSecurityScanners.length > 0)
);
},
hasSelectedAllVulnerabilities() { hasSelectedAllVulnerabilities() {
if (!this.filteredVulnerabilities.length) { if (!this.filteredVulnerabilities.length) {
return false; return false;
...@@ -261,9 +232,6 @@ export default { ...@@ -261,9 +232,6 @@ export default {
primaryIdentifier(identifiers) { primaryIdentifier(identifiers) {
return getPrimaryIdentifier(identifiers, 'externalType'); return getPrimaryIdentifier(identifiers, 'externalType');
}, },
setScannerAlertDismissed(value) {
this.scannerAlertDismissed = value;
},
isSelected(vulnerability = {}) { isSelected(vulnerability = {}) {
return Boolean(this.selectedVulnerabilities[vulnerability.id]); return Boolean(this.selectedVulnerabilities[vulnerability.id]);
}, },
...@@ -331,17 +299,11 @@ export default { ...@@ -331,17 +299,11 @@ export default {
}, },
}, },
VULNERABILITIES_PER_PAGE, VULNERABILITIES_PER_PAGE,
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
}; };
</script> </script>
<template> <template>
<div class="vulnerability-list"> <div class="vulnerability-list">
<local-storage-sync
:value="scannerAlertDismissed"
:storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY"
@input="setScannerAlertDismissed"
/>
<selection-summary <selection-summary
v-if="shouldShowSelectionSummary" v-if="shouldShowSelectionSummary"
:selected-vulnerabilities="Object.values(selectedVulnerabilities)" :selected-vulnerabilities="Object.values(selectedVulnerabilities)"
...@@ -376,16 +338,6 @@ export default { ...@@ -376,16 +338,6 @@ export default {
/> />
</template> </template>
<template v-if="shouldShowScannersAlert" #top-row>
<td :colspan="fields.length" class="gl-px-0!">
<security-scanner-alert
:not-enabled-scanners="notEnabledSecurityScanners"
:no-pipeline-run-scanners="noPipelineRunSecurityScanners"
@dismiss="setScannerAlertDismissed('true')"
/>
</td>
</template>
<template #cell(checkbox)="{ item }"> <template #cell(checkbox)="{ item }">
<gl-form-checkbox <gl-form-checkbox
class="gl-display-inline-block! gl-m-0 gl-pointer-events-none" class="gl-display-inline-block! gl-m-0 gl-pointer-events-none"
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { PortalTarget } from 'portal-vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
...@@ -33,11 +34,17 @@ export default { ...@@ -33,11 +34,17 @@ export default {
DashboardNotConfiguredGroup, DashboardNotConfiguredGroup,
DashboardNotConfiguredInstance, DashboardNotConfiguredInstance,
DashboardNotConfiguredProject, DashboardNotConfiguredProject,
PortalTarget,
ProjectPipelineStatus, ProjectPipelineStatus,
GlLoadingIcon, GlLoadingIcon,
VulnerabilitiesCountList, VulnerabilitiesCountList,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
provide() {
return {
vulnerabilityReportAlertsPortal: this.$options.vulnerabilityReportAlertsPortal,
};
},
inject: { inject: {
dashboardType: {}, dashboardType: {},
groupFullPath: { default: undefined }, groupFullPath: { default: undefined },
...@@ -106,6 +113,7 @@ export default { ...@@ -106,6 +113,7 @@ export default {
this.shouldShowAutoFixUserCallout = false; this.shouldShowAutoFixUserCallout = false;
}, },
}, },
vulnerabilityReportAlertsPortal: 'vulnerability-report-alerts-portal',
autoFixUserCalloutCookieName: 'auto_fix_user_callout_dismissed', autoFixUserCalloutCookieName: 'auto_fix_user_callout_dismissed',
i18n: { i18n: {
title: s__('SecurityReports|Vulnerability Report'), title: s__('SecurityReports|Vulnerability Report'),
...@@ -123,6 +131,7 @@ export default { ...@@ -123,6 +131,7 @@ export default {
<dashboard-not-configured-project v-else-if="isProject" /> <dashboard-not-configured-project v-else-if="isProject" />
</template> </template>
<template v-else> <template v-else>
<portal-target :name="$options.vulnerabilityReportAlertsPortal" multiple />
<auto-fix-user-callout <auto-fix-user-callout
v-if="shouldShowAutoFixUserCallout" v-if="shouldShowAutoFixUserCallout"
:help-page-path="autoFixDocumentation" :help-page-path="autoFixDocumentation"
......
---
title: Show scanner warning at the top of the Vulnerability Report
merge_request: 60716
author:
type: changed
...@@ -114,7 +114,6 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => { ...@@ -114,7 +114,6 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({ expect(findVulnerabilities().props()).toEqual({
filters: {}, filters: {},
isLoading: false, isLoading: false,
securityScanners: {},
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
vulnerabilities, vulnerabilities,
......
...@@ -95,7 +95,6 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => { ...@@ -95,7 +95,6 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({ expect(findVulnerabilities().props()).toEqual({
filters: {}, filters: {},
isLoading: false, isLoading: false,
securityScanners: {},
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
vulnerabilities, vulnerabilities,
......
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { Portal } from 'portal-vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue'; import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue';
import SecurityScannerAlert from 'ee/security_dashboard/components/security_scanner_alert.vue';
import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue'; import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue';
import securityScannersQuery from 'ee/security_dashboard/graphql/queries/project_security_scanners.query.graphql';
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql'; import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { generateVulnerabilities } from './mock_data'; import { generateVulnerabilities } from './mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
describe('Vulnerabilities app component', () => { describe('Vulnerabilities app component', () => {
useLocalStorageSpy();
let wrapper; let wrapper;
const apolloMock = { const apolloMock = {
queries: { vulnerabilities: { loading: true } }, queries: { vulnerabilities: { loading: true } },
...@@ -35,8 +43,21 @@ describe('Vulnerabilities app component', () => { ...@@ -35,8 +43,21 @@ describe('Vulnerabilities app component', () => {
}); });
}; };
const securityScannersHandler = async ({
available = [],
enabled = [],
pipelineRun = [],
} = {}) => ({
data: {
project: {
securityScanners: { available, enabled, pipelineRun },
},
},
});
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver); const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const findSecurityScannerAlert = (root = wrapper) => root.findComponent(SecurityScannerAlert);
const findVulnerabilityList = () => wrapper.find(VulnerabilityList); const findVulnerabilityList = () => wrapper.find(VulnerabilityList);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
...@@ -45,10 +66,6 @@ describe('Vulnerabilities app component', () => { ...@@ -45,10 +66,6 @@ describe('Vulnerabilities app component', () => {
expect(findLoadingIcon().exists()).toBe(nextPage); expect(findLoadingIcon().exists()).toBe(nextPage);
}; };
beforeEach(() => {
createWrapper();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -165,30 +182,8 @@ describe('Vulnerabilities app component', () => { ...@@ -165,30 +182,8 @@ describe('Vulnerabilities app component', () => {
}); });
}); });
describe('security scanners', () => {
const notEnabledScannersHelpPath = '#not-enabled';
const noPipelineRunScannersHelpPath = '#no-pipeline';
beforeEach(() => {
createWrapper({
props: { notEnabledScannersHelpPath, noPipelineRunScannersHelpPath },
});
});
it('should pass the security scanners to the vulnerability list', () => {
const securityScanners = {
enabled: ['SAST', 'DAST', 'API_FUZZING', 'COVERAGE_FUZZING'],
pipelineRun: ['SAST', 'DAST', 'API_FUZZING', 'COVERAGE_FUZZING'],
};
wrapper.setData({ securityScanners });
expect(findVulnerabilityList().props().securityScanners).toEqual(securityScanners);
});
});
describe('filters prop', () => { describe('filters prop', () => {
const mockQuery = jest.fn().mockResolvedValue({ const vulnerabilitiesHandler = jest.fn().mockResolvedValue({
data: { data: {
project: { project: {
vulnerabilities: { vulnerabilities: {
...@@ -199,25 +194,125 @@ describe('Vulnerabilities app component', () => { ...@@ -199,25 +194,125 @@ describe('Vulnerabilities app component', () => {
}, },
}); });
const createWrapperWithApollo = ({ query, filters }) => { const createWrapperWithApollo = ({ filters }) => {
wrapper = shallowMount(ProjectVulnerabilitiesApp, { wrapper = shallowMount(ProjectVulnerabilitiesApp, {
localVue, localVue,
apolloProvider: createMockApollo([[vulnerabilitiesQuery, query]]), apolloProvider: createMockApollo([
[vulnerabilitiesQuery, vulnerabilitiesHandler],
[securityScannersQuery, securityScannersHandler],
]),
propsData: { filters }, propsData: { filters },
provide: { groupFullPath: 'path' }, provide: { groupFullPath: 'path' },
}); });
}; };
it('does not run the query when filters is null', () => { it('does not run the query when filters is null', () => {
createWrapperWithApollo({ query: mockQuery, filters: null }); createWrapperWithApollo({ filters: null });
expect(mockQuery).not.toHaveBeenCalled(); expect(vulnerabilitiesHandler).not.toHaveBeenCalled();
}); });
it('runs query when filters is an object', () => { it('runs query when filters is an object', () => {
createWrapperWithApollo({ query: mockQuery, filters: {} }); createWrapperWithApollo({ filters: {} });
expect(vulnerabilitiesHandler).toHaveBeenCalled();
});
});
describe('security scanner alerts', () => {
const vulnerabilityReportAlertsPortal = 'test-alerts-portal';
const createWrapperForScannerAlerts = async ({ securityScanners }) => {
wrapper = shallowMount(ProjectVulnerabilitiesApp, {
localVue,
apolloProvider: createMockApollo([
[securityScannersQuery, () => securityScannersHandler(securityScanners)],
]),
provide: {
vulnerabilityReportAlertsPortal,
projectFullPath: 'path',
},
stubs: {
LocalStorageSync,
},
});
await waitForPromises();
};
describe.each`
available | enabled | pipelineRun | expectAlertShown
${['DAST']} | ${[]} | ${[]} | ${true}
${['DAST']} | ${['DAST']} | ${[]} | ${true}
${['DAST']} | ${[]} | ${['DAST']} | ${true}
${['DAST']} | ${['DAST']} | ${['DAST']} | ${false}
${[]} | ${[]} | ${[]} | ${false}
`('visibility', ({ available, enabled, pipelineRun, expectAlertShown }) => {
beforeEach(() => {});
it(`should${expectAlertShown ? '' : ' not'} show the alert`, async () => {
await createWrapperForScannerAlerts({
securityScanners: { available, enabled, pipelineRun },
});
expect(findSecurityScannerAlert().exists()).toBe(expectAlertShown);
});
if (expectAlertShown) {
it('should portal the alert to the provided vulnerabilityReportAlertsPortal', async () => {
await createWrapperForScannerAlerts({
securityScanners: { available, enabled, pipelineRun },
});
const portal = wrapper.findComponent(Portal);
expect(portal.props('to')).toBe(vulnerabilityReportAlertsPortal);
expect(findSecurityScannerAlert(portal).exists()).toBe(true);
});
}
it('should never show the alert once it has been dismissed', async () => {
window.localStorage.setItem(
ProjectVulnerabilitiesApp.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
'true',
);
await createWrapperForScannerAlerts({
securityScanners: { available, enabled, pipelineRun },
});
expect(findSecurityScannerAlert().exists()).toBe(false);
});
});
describe('dismissal', () => {
beforeEach(() => {
return createWrapperForScannerAlerts({
securityScanners: { available: ['DAST'], enabled: [], pipelineRun: [] },
});
});
it('should hide the alert when it is dismissed', async () => {
const scannerAlert = findSecurityScannerAlert();
expect(scannerAlert.exists()).toBe(true);
scannerAlert.vm.$emit('dismiss');
expect(mockQuery).toHaveBeenCalled(); await wrapper.vm.$nextTick();
expect(scannerAlert.exists()).toBe(false);
});
it('should remember the dismissal state', async () => {
findSecurityScannerAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(window.localStorage.setItem.mock.calls).toContainEqual([
ProjectVulnerabilitiesApp.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
'true',
]);
});
}); });
}); });
}); });
...@@ -4,21 +4,15 @@ import { capitalize } from 'lodash'; ...@@ -4,21 +4,15 @@ import { capitalize } from 'lodash';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue'; import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue'; import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import IssuesBadge from 'ee/security_dashboard/components/issues_badge.vue'; import IssuesBadge from 'ee/security_dashboard/components/issues_badge.vue';
import SecurityScannerAlert from 'ee/security_dashboard/components/security_scanner_alert.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue'; import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue'; import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import VulnerabilityList, { import VulnerabilityList from 'ee/security_dashboard/components/vulnerability_list.vue';
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
} from 'ee/security_dashboard/components/vulnerability_list.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'; import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
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';
import { generateVulnerabilities, vulnerabilities } from './mock_data'; import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => { describe('Vulnerability list component', () => {
useLocalStorageSpy();
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners, provide = {} } = {}) => { const createWrapper = ({ props = {}, listeners, provide = {} } = {}) => {
...@@ -56,8 +50,6 @@ describe('Vulnerability list component', () => { ...@@ -56,8 +50,6 @@ describe('Vulnerability list component', () => {
const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]'); const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]');
const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index); const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index);
const findRemediatedBadge = () => wrapper.findComponent(RemediatedBadge); const findRemediatedBadge = () => wrapper.findComponent(RemediatedBadge);
const findSecurityScannerAlert = () => wrapper.findComponent(SecurityScannerAlert);
const findDismissalButton = () => findSecurityScannerAlert().find('button[aria-label="Dismiss"]');
const findSelectionSummary = () => wrapper.findComponent(SelectionSummary); const findSelectionSummary = () => wrapper.findComponent(SelectionSummary);
const findRowVulnerabilityCommentIcon = (row) => const findRowVulnerabilityCommentIcon = (row) =>
findRow(row).findComponent(VulnerabilityCommentIcon); findRow(row).findComponent(VulnerabilityCommentIcon);
...@@ -510,66 +502,6 @@ describe('Vulnerability list component', () => { ...@@ -510,66 +502,6 @@ describe('Vulnerability list component', () => {
}); });
}); });
describe('security scanner alerts', () => {
describe.each`
available | enabled | pipelineRun | expectAlertShown
${['DAST']} | ${[]} | ${[]} | ${true}
${['DAST']} | ${['DAST']} | ${[]} | ${true}
${['DAST']} | ${[]} | ${['DAST']} | ${true}
${['DAST']} | ${['DAST']} | ${['DAST']} | ${false}
${[]} | ${[]} | ${[]} | ${false}
`('visibility', ({ available, enabled, pipelineRun, expectAlertShown }) => {
it(`should${expectAlertShown ? '' : ' not'} show the alert`, () => {
wrapper = createWrapper({
props: { securityScanners: { available, enabled, pipelineRun } },
});
expect(findSecurityScannerAlert().exists()).toBe(expectAlertShown);
});
it('should never show the alert once it has been dismissed', async () => {
window.localStorage.setItem(SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY, 'true');
wrapper = createWrapper({
props: { securityScanners: { available, enabled, pipelineRun } },
});
await wrapper.vm.$nextTick();
expect(findSecurityScannerAlert().exists()).toBe(false);
});
});
describe('dismissal', () => {
beforeEach(() => {
wrapper = createWrapper({
props: { securityScanners: { available: ['DAST'], enabled: [] } },
});
});
it('should hide the alert when it is dismissed', async () => {
expect(findSecurityScannerAlert().exists()).toBe(true);
findDismissalButton().trigger('click');
await wrapper.vm.$nextTick();
expect(findSecurityScannerAlert().exists()).toBe(false);
});
it('should remember the dismissal state', async () => {
findDismissalButton().trigger('click');
await wrapper.vm.$nextTick();
expect(window.localStorage.setItem.mock.calls).toContainEqual([
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
'true',
]);
});
});
});
describe('when has a sort-changed listener defined', () => { describe('when has a sort-changed listener defined', () => {
let spy; let spy;
......
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { PortalTarget } from 'portal-vue';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue'; import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue';
...@@ -26,6 +27,7 @@ import { mockVulnerableProjectsInstance, mockVulnerableProjectsGroup } from '../ ...@@ -26,6 +27,7 @@ import { mockVulnerableProjectsInstance, mockVulnerableProjectsGroup } from '../
describe('Vulnerability Report', () => { describe('Vulnerability Report', () => {
let wrapper; let wrapper;
const findAlertsPortalTarget = () => wrapper.findComponent(PortalTarget);
const findSurveyRequestBanner = () => wrapper.findComponent(SurveyRequestBanner); const findSurveyRequestBanner = () => wrapper.findComponent(SurveyRequestBanner);
const findInstanceVulnerabilities = () => wrapper.findComponent(InstanceVulnerabilities); const findInstanceVulnerabilities = () => wrapper.findComponent(InstanceVulnerabilities);
const findGroupVulnerabilities = () => wrapper.findComponent(GroupVulnerabilities); const findGroupVulnerabilities = () => wrapper.findComponent(GroupVulnerabilities);
...@@ -78,6 +80,12 @@ describe('Vulnerability Report', () => { ...@@ -78,6 +80,12 @@ describe('Vulnerability Report', () => {
}); });
}); });
it('renders the alerts portal target', () => {
const portalTarget = findAlertsPortalTarget();
expect(portalTarget.exists()).toBe(true);
expect(portalTarget.props('name')).toBe(VulnerabilityReport.vulnerabilityReportAlertsPortal);
});
it('should show the header', () => { it('should show the header', () => {
expect(findHeader().exists()).toBe(true); expect(findHeader().exists()).toBe(true);
}); });
...@@ -172,6 +180,7 @@ describe('Vulnerability Report', () => { ...@@ -172,6 +180,7 @@ describe('Vulnerability Report', () => {
}); });
it('only renders the empty state', () => { it('only renders the empty state', () => {
expect(findAlertsPortalTarget().exists()).toBe(false);
expect(findGroupEmptyState().exists()).toBe(true); expect(findGroupEmptyState().exists()).toBe(true);
expect(findInstanceEmptyState().exists()).toBe(false); expect(findInstanceEmptyState().exists()).toBe(false);
expect(findProjectEmptyState().exists()).toBe(false); expect(findProjectEmptyState().exists()).toBe(false);
......
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