Commit 60cd9c28 authored by samdbeckham's avatar samdbeckham

Adds filtering to the GSD Frontend

Where GSD is the group security dashboard

- Removes the tabs from the GSD
- Adds a filter component to the GSD
- Adds the filter module to the GSD
- hooks up the filters to the UI
- Fixes up some styling in the GSD filters
- Adds the filters to the vuln list endpoint
- Adds the popover back in as its own component
- Made some changes after my own initial review
- Adds filtering to the counts and history endpoints too
- Removes filtering logic for counts from the FE
parent 4aa14d46
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import Filters from './filters.vue';
import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityChart from './vulnerability_chart.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue';
import Icon from '~/vue_shared/components/icon.vue';
import popover from '~/vue_shared/directives/popover';
export default {
name: 'SecurityDashboardApp',
directives: {
popover,
},
components: {
Icon,
Filters,
IssueModal,
SecurityDashboardTable,
Tab,
Tabs,
VulnerabilityChart,
VulnerabilityCountList,
},
......@@ -52,33 +42,7 @@ export default {
},
},
computed: {
...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']),
...mapState('vulnerabilities', ['modal']),
sastCount() {
return this.vulnerabilitiesCountByReportType('sast');
},
popoverOptions() {
return {
trigger: 'click',
placement: 'right',
title: s__('Security Reports|At this time, the security dashboard only supports SAST.'),
content: `
<a
title="${s__('Security Reports|Security dashboard documentation')}"
href="${this.dashboardDocumentation}"
target="_blank"
rel="noopener
noreferrer"
>
<span class="vertical-align-middle">${s__(
'Security Reports|Security dashboard documentation',
)}</span>
${spriteIcon('external-link', 's16 vertical-align-middle')}
</a>
`,
html: true,
};
},
},
created() {
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
......@@ -88,33 +52,28 @@ export default {
},
methods: {
...mapActions('vulnerabilities', [
'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesHistoryEndpoint',
'setVulnerabilitiesEndpoint',
'fetchVulnerabilitiesCount',
'createIssue',
'dismissVulnerability',
'fetchVulnerabilities',
'fetchVulnerabilitiesCount',
'fetchVulnerabilitiesHistory',
'revertDismissal',
'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesEndpoint',
'setVulnerabilitiesHistoryEndpoint',
]),
filterChange() {
this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
},
},
};
</script>
<template>
<div>
<tabs stop-propagation>
<tab active>
<template slot="title">
<span>{{ __('SAST') }}</span>
<span v-if="sastCount" class="badge badge-pill"> {{ sastCount }} </span>
<span
v-popover="popoverOptions"
class="text-muted prepend-left-4"
:aria-label="__('help')"
>
<icon name="question" class="vertical-align-middle" />
</span>
</template>
<filters :dashboard-documentation="dashboardDocumentation" @change="filterChange" />
<vulnerability-count-list />
<h4 class="my-4">{{ __('Vulnerability Chart') }}</h4>
<vulnerability-chart />
......@@ -123,8 +82,6 @@ export default {
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
/>
</tab>
</tabs>
<issue-modal
:modal="modal"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
......
<script>
import { mapGetters, mapMutations } from 'vuex';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Help from './help.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
Help,
Icon,
},
props: {
filterId: {
type: String,
required: true,
},
dashboardDocumentation: {
type: String,
required: true,
},
},
computed: {
...mapGetters('filters', ['getFilter', 'getSelectedOptions']),
filter() {
return this.getFilter(this.filterId);
},
selectedOptionText() {
const selectedOption = this.getSelectedOptions(this.filterId)[0];
return (selectedOption && selectedOption.name) || '-';
},
},
methods: {
...mapMutations('filters', ['SET_FILTER']),
clickFilter(option) {
const { filterId } = this;
const optionId = option.id;
this.SET_FILTER({ filterId, optionId });
this.$emit('change');
},
},
};
</script>
<template>
<div class="dashboard-filter">
<strong class="js-name">{{ filter.name }}</strong>
<help v-if="filterId === 'type'" :dashboard-documentation="dashboardDocumentation" />
<gl-dropdown :text="selectedOptionText" class="d-block mt-1">
<gl-dropdown-item
v-for="option in filter.options"
:key="option.id"
@click="clickFilter(option);"
>
<icon
v-if="option.selected"
class="vertical-align-middle js-check"
name="mobile-issue-close"
/>
<span class="vertical-align-middle" :class="{ 'prepend-left-20': !option.selected }">{{
option.name
}}</span>
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
<style>
.dashboard-filter .dropdown-toggle {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
<script>
import { mapState } from 'vuex';
import GlFilter from './filter.vue';
export default {
components: {
GlFilter,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
},
computed: {
...mapState('filters', ['filters']),
},
};
</script>
<template>
<div class="dashboard-filters border-bottom bg-light">
<div class="row mx-0 p-2">
<gl-filter
v-for="filter in filters"
:key="filter.id"
class="col-sm p-2 js-filter"
:filter-id="filter.id"
:dashboard-documentation="dashboardDocumentation"
@change="$emit('change');"
/>
<div class="col-sm p-2 d-md-block d-none"></div>
<div class="col-sm p-2 d-md-block d-none"></div>
<div class="col-sm p-2 d-md-block d-none"></div>
</div>
</div>
</template>
<script>
import { GlPopover } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlPopover,
Icon,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
},
};
</script>
<template>
<span class="vertical-align-middle text-muted js-help">
<icon id="reports-info" name="question" :aria-label="__('help')" />
<gl-popover
target="reports-info"
placement="right"
triggers="click"
:title="s__('Security Reports|At this time, the security dashboard only supports SAST.')"
>
<a
v-if="dashboardDocumentation"
target="_blank"
rel="noopener noreferrer"
:title="s__('Security Reports|Security dashboard documentation')"
:href="dashboardDocumentation"
>
<span class="vertical-align-middle">{{
s__('Security Reports|Security dashboard documentation')
}}</span>
<icon name="external-link" :size="16" class="vertical-align-middle" />
</a>
</gl-popover>
</span>
</template>
......@@ -47,6 +47,9 @@ export default {
},
methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilities', 'openModal']),
fetchPage(page) {
this.fetchVulnerabilities({ page });
},
},
};
</script>
......@@ -93,7 +96,7 @@ export default {
<pagination
v-if="showPagination"
:change="fetchVulnerabilities"
:change="fetchPage"
:page-info="pageInfo"
class="justify-content-center prepend-top-default"
/>
......
......@@ -11,15 +11,11 @@ export default {
VulnerabilityCount,
},
computed: {
...mapGetters('vulnerabilities', [
'vulnerabilitiesCountBySeverity',
'dashboardCountError',
'dashboardError',
]),
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount']),
...mapGetters('vulnerabilities', ['dashboardCountError', 'dashboardError']),
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount', 'vulnerabilitiesCount']),
counts() {
return SEVERITIES.map(severity => {
const count = this.vulnerabilitiesCountBySeverity(severity);
const count = this.vulnerabilitiesCount[severity] || 0;
return { severity, count };
});
},
......
import Vue from 'vue';
import Vuex from 'vuex';
import vulnerabilities from './modules/vulnerabilities/index';
import filters from './modules/filters/index';
Vue.use(Vuex);
......@@ -8,5 +9,6 @@ export default () =>
new Vuex.Store({
modules: {
vulnerabilities,
filters,
},
});
export const getFilter = state => filterId => state.filters.find(filter => filter.id === filterId);
export const getSelectedOptions = (state, getters) => filterId =>
getters.getFilter(filterId).options.filter(option => option.selected);
export const activeFilters = (state, getters) =>
state.filters
.map(filter => filter.id)
.reduce(
(obj, filterId) => ({
...obj,
[filterId]: getters
.getSelectedOptions(filterId)
.map(option => option.id)
.filter(option => option !== 'all'),
}),
{},
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged
export default () => {};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
export default {
namespaced: true,
state,
mutations,
getters,
};
export const SET_FILTER = 'SET_FILTER';
// This is here because es-lint requires a default export when there are less than two named exports
export default SET_FILTER;
import * as types from './mutation_types';
export default {
[types.SET_FILTER](state, payload) {
const { filterId, optionId } = payload;
const activeFilter = state.filters.find(filter => filter.id === filterId);
activeFilter.options.find(option => option.selected).selected = false;
activeFilter.options.find(option => option.id === optionId).selected = true;
},
};
export default () => ({
filters: [
{
name: 'Severity',
id: 'severity',
options: [
{
name: 'All',
id: 'all',
selected: true,
},
{
name: 'Critical',
id: 7,
selected: false,
},
{
name: 'High',
id: 6,
selected: false,
},
{
name: 'Medium',
id: 5,
selected: false,
},
{
name: 'Low',
id: 4,
selected: false,
},
{
name: 'Unknown',
id: 2,
selected: false,
},
{
name: 'Experimental',
id: 3,
selected: false,
},
{
name: 'Ignore',
id: 1,
selected: false,
},
{
name: 'Undefined',
id: 0,
selected: false,
},
],
},
{
name: 'Report type',
id: 'type',
options: [
{
name: 'SAST',
id: 'sast',
selected: true,
},
],
},
],
});
......@@ -13,12 +13,13 @@ export const setVulnerabilitiesCountEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_COUNT_ENDPOINT, endpoint);
};
export const fetchVulnerabilitiesCount = ({ state, dispatch }) => {
export const fetchVulnerabilitiesCount = ({ state, dispatch }, params = {}) => {
dispatch('requestVulnerabilitiesCount');
axios({
method: 'GET',
url: state.vulnerabilitiesCountEndpoint,
params,
})
.then(response => {
const { data } = response;
......@@ -41,15 +42,13 @@ export const receiveVulnerabilitiesCountError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR);
};
export const fetchVulnerabilities = ({ state, dispatch }, pageNumber) => {
export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => {
dispatch('requestVulnerabilities');
const page = pageNumber || (state.pageInfo && state.pageInfo.page) || 1;
axios({
method: 'GET',
url: state.vulnerabilitiesEndpoint,
params: { page },
params,
})
.then(response => {
const { headers, data } = response;
......@@ -208,12 +207,13 @@ export const setVulnerabilitiesHistoryEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_HISTORY_ENDPOINT, endpoint);
};
export const fetchVulnerabilitiesHistory = ({ state, dispatch }) => {
export const fetchVulnerabilitiesHistory = ({ state, dispatch }, params = {}) => {
dispatch('requestVulnerabilitiesHistory');
axios({
method: 'GET',
url: state.vulnerabilitiesHistoryEndpoint,
params,
})
.then(response => {
const { data } = response;
......
import { sum } from '~/lib/utils/number_utils';
export const vulnerabilitiesCountBySeverity = state => severity =>
Object.values(state.vulnerabilitiesCount)
.map(count => count[severity])
.reduce(sum, 0);
export const vulnerabilitiesCountByReportType = state => type => {
const counts = state.vulnerabilitiesCount[type];
return counts ? Object.values(counts).reduce(sum, 0) : 0;
};
export const dashboardError = state =>
state.errorLoadingVulnerabilities && state.errorLoadingVulnerabilitiesCount;
export const dashboardListError = state =>
......
import Vue from 'vue';
import component from 'ee/security_dashboard/components/filter.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Filter component', () => {
let vm;
let props;
const store = createStore();
const Component = Vue.extend(component);
describe('severity', () => {
beforeEach(() => {
props = { filterId: 'severity', dashboardDocumentation: '' };
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should display all 9 severity options', () => {
expect(vm.$el.querySelectorAll('.dropdown-item').length).toEqual(9);
});
it('should display a check next to only the selected item', () => {
expect(vm.$el.querySelectorAll('.dropdown-item .js-check').length).toEqual(1);
});
it('should display "Severity" as the option name', () => {
expect(vm.$el.querySelector('.js-name').textContent).toContain('Severity');
});
it('should not display the help popover', () => {
expect(vm.$el.querySelector('.js-help')).toBeNull();
});
});
describe('type', () => {
beforeEach(() => {
props = { filterId: 'type', dashboardDocumentation: '' };
vm = mountComponentWithStore(Component, { store, props });
});
it('should display the help popover', () => {
expect(vm.$el.querySelector('.js-help')).not.toBeNull();
});
});
});
import Vue from 'vue';
import component from 'ee/security_dashboard/components/filters.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Filter component', () => {
let vm;
const props = { dashboardDocumentation: '' };
const store = createStore();
const Component = Vue.extend(component);
describe('severity', () => {
beforeEach(() => {
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should display both filters', () => {
expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(2);
});
});
});
......@@ -3,19 +3,15 @@ import component from 'ee/security_dashboard/components/vulnerability_count_list
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import mockData from '../store/vulnerabilities/data/mock_data_vulnerabilities_count.json';
describe('Vulnerability Count List', () => {
const Component = Vue.extend(component);
const store = createStore();
const counts = {
sast: {
critical: 22,
},
};
let vm;
beforeEach(() => {
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: counts });
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: mockData });
vm = mountComponentWithStore(Component, { store });
});
......@@ -25,7 +21,7 @@ describe('Vulnerability Count List', () => {
});
it('should fetch the counts for each severity', () => {
expect(vm.counts[0]).toEqual({ severity: 'critical', count: 22 });
expect(vm.counts[0]).toEqual({ severity: 'critical', count: mockData.critical });
});
it('should render a counter for each severity', () => {
......
import vulnerabilitiesState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import filterState from 'ee/security_dashboard/store/modules/filters/state';
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
const newState = {
vulnerabilities: vulnerabilitiesState(),
filters: filterState(),
};
store.replaceState(newState);
};
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as getters from 'ee/security_dashboard/store/modules/filters/getters';
describe('filters module getters', () => {
const mockedGetters = {};
mockedGetters.getFilter = filterId => getters.getFilter(createState())(filterId);
mockedGetters.getSelectedOptions = filterId =>
getters.getSelectedOptions(createState(), mockedGetters)(filterId);
describe('getFilter', () => {
it('should return the type filter information', () => {
const state = createState();
const typeFilter = getters.getFilter(state)('type');
expect(typeFilter.name).toEqual('Report type');
});
});
describe('getSelectedOptions', () => {
it('should return "SAST" as the selcted option', () => {
const state = createState();
const selectedOptions = getters.getSelectedOptions(state, mockedGetters)('type');
expect(selectedOptions).toHaveLength(1);
expect(selectedOptions[0].name).toEqual('SAST');
});
});
describe('activeFilters', () => {
it('should return no severity filters', () => {
const state = createState();
const activeFilters = getters.activeFilters(state, mockedGetters);
expect(activeFilters.severity).toHaveLength(0);
});
it('should return the SAST type filter', () => {
const state = createState();
const activeFilters = getters.activeFilters(state, mockedGetters);
expect(activeFilters.type).toHaveLength(1);
expect(activeFilters.type[0]).toEqual('sast');
});
});
});
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import mutations from 'ee/security_dashboard/store/modules/filters/mutations';
describe('filters module mutations', () => {
describe('SET_FILTER', () => {
let state;
let typeFilter;
let sastOption;
let allOption;
beforeEach(() => {
state = createState();
[typeFilter] = state.filters;
[allOption, sastOption] = typeFilter.options;
const filterId = typeFilter.id;
const optionId = sastOption.id;
mutations[types.SET_FILTER](state, { filterId, optionId });
});
it('should make SAST the selected option', () => {
expect(sastOption.selected).toEqual(true);
});
it('should remove ALL as the selected option', () => {
expect(allOption.selected).toEqual(false);
});
});
});
......@@ -13,6 +13,8 @@ import mockDataVulnerabilitiesHistory from './data/mock_data_vulnerabilities_his
describe('vulnerabiliites count actions', () => {
const data = mockDataVulnerabilitiesCount;
const params = { filters: { type: ['sast'] } };
const filteredData = mockDataVulnerabilitiesCount.sast;
describe('setVulnerabilitiesCountEndpoint', () => {
it('should commit the correct mutuation', done => {
......@@ -50,7 +52,11 @@ describe('vulnerabiliites count actions', () => {
describe('on success', () => {
beforeEach(() => {
mock.onGet(state.vulnerabilitiesCountEndpoint).replyOnce(200, data);
mock
.onGet(state.vulnerabilitiesCountEndpoint, { params })
.replyOnce(200, filteredData)
.onGet(state.vulnerabilitiesCountEndpoint)
.replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
......@@ -69,6 +75,23 @@ describe('vulnerabiliites count actions', () => {
done,
);
});
it('should send the passed filters to the endpoint', done => {
testAction(
actions.fetchVulnerabilitiesCount,
params,
state,
[],
[
{ type: 'requestVulnerabilitiesCount' },
{
type: 'receiveVulnerabilitiesCountSuccess',
payload: { data: filteredData },
},
],
done,
);
});
});
describe('on error', () => {
......@@ -137,6 +160,8 @@ describe('vulnerabiliites count actions', () => {
describe('vulnerabilities actions', () => {
const data = mockDataVulnerabilities;
const params = { filters: { severity: ['critical'] } };
const filteredData = mockDataVulnerabilities.filter(vuln => vuln.severity === 'critical');
const pageInfo = {
page: 1,
nextPage: 2,
......@@ -169,7 +194,11 @@ describe('vulnerabilities actions', () => {
describe('on success', () => {
beforeEach(() => {
mock.onGet(state.vulnerabilitiesEndpoint).replyOnce(200, data, headers);
mock
.onGet(state.vulnerabilitiesEndpoint, { params })
.replyOnce(200, filteredData, headers)
.onGet(state.vulnerabilitiesEndpoint)
.replyOnce(200, data, headers);
});
it('should dispatch the request and success actions', done => {
......@@ -188,6 +217,23 @@ describe('vulnerabilities actions', () => {
done,
);
});
it('should pass through the filters', done => {
testAction(
actions.fetchVulnerabilities,
params,
state,
[],
[
{ type: 'requestVulnerabilities' },
{
type: 'receiveVulnerabilitiesSuccess',
payload: { data: filteredData, headers },
},
],
done,
);
});
});
describe('on error', () => {
......@@ -636,8 +682,10 @@ describe('revert vulnerability dismissal', () => {
});
});
describe('vulnerabiliites timeline actions', () => {
describe('vulnerabiliites history actions', () => {
const data = mockDataVulnerabilitiesHistory;
const params = { filters: { severity: ['critical'] } };
const filteredData = mockDataVulnerabilitiesHistory.critical;
describe('setVulnerabilitiesHistoryEndpoint', () => {
it('should commit the correct mutuation', done => {
......@@ -675,7 +723,11 @@ describe('vulnerabiliites timeline actions', () => {
describe('on success', () => {
beforeEach(() => {
mock.onGet(state.vulnerabilitiesHistoryEndpoint).replyOnce(200, data);
mock
.onGet(state.vulnerabilitiesHistoryEndpoint, { params })
.replyOnce(200, filteredData)
.onGet(state.vulnerabilitiesHistoryEndpoint)
.replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
......@@ -694,6 +746,23 @@ describe('vulnerabiliites timeline actions', () => {
done,
);
});
it('return the filtered results', done => {
testAction(
actions.fetchVulnerabilitiesHistory,
params,
state,
[],
[
{ type: 'requestVulnerabilitiesHistory' },
{
type: 'receiveVulnerabilitiesHistorySuccess',
payload: { data: filteredData },
},
],
done,
);
});
});
describe('on error', () => {
......
{
"sast": {
"critical": 2,
"high": 4,
"low": 7,
"medium": 8,
"unknown": 9
},
"container_scanning": {
"critical": 3,
"high": 3,
"low": 2,
"medium": 9,
"unknown": 7
},
"dependency_scanning": {
"critical": 2,
"high": 3,
"low": 9,
"medium": 4,
"unknown": 7
},
"dast": {
"critical": 2,
"high": 3,
"low": 9,
"medium": 4,
"unknown": 7
}
"unknown": 0
}
\ No newline at end of file
import State from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as getters from 'ee/security_dashboard/store/modules/vulnerabilities/getters';
describe('vulnerabilities module getters', () => {
const initialState = State();
describe('vulnerabilitiesCountBySeverity', () => {
const sast = { critical: 10 };
const dast = { critical: 66 };
const expectedValue = sast.critical + dast.critical;
const vulnerabilitiesCount = { sast, dast };
const state = { vulnerabilitiesCount };
it('should add up all the counts with `high` severity', () => {
const result = getters.vulnerabilitiesCountBySeverity(state)('critical');
expect(result).toBe(expectedValue);
});
it('should return 0 if no counts match the severity name', () => {
const result = getters.vulnerabilitiesCountBySeverity(state)('medium');
expect(result).toBe(0);
});
it('should return 0 if there are no counts at all', () => {
const result = getters.vulnerabilitiesCountBySeverity(initialState)('critical');
expect(result).toBe(0);
});
});
describe('vulnerabilitiesCountByReportType', () => {
const sast = { critical: 10, medium: 22 };
const dast = { critical: 66 };
const expectedValue = sast.critical + sast.medium;
const vulnerabilitiesCount = { sast, dast };
const state = { vulnerabilitiesCount };
it('should add up all the counts in the sast report', () => {
const result = getters.vulnerabilitiesCountByReportType(state)('sast');
expect(result).toBe(expectedValue);
});
it('should return 0 if there are no reports for a severity type', () => {
const result = getters.vulnerabilitiesCountByReportType(initialState)('sast');
expect(result).toBe(0);
});
});
describe('dashboardError', () => {
it('should return true when both error states exist', () => {
const errorLoadingVulnerabilities = true;
......
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