Commit 0432a5f6 authored by Daniel Tian's avatar Daniel Tian

Fix pipeline security tab filters

Fix the dropdown filters not showing on the security tab of the
pipeline details page
parent 1aa7f24b
---
title: Fix pipeline security tab filters not showing
merge_request: 47294
author:
type: fixed
<script>
import { mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import { severityFilter, scannerFilter } from 'ee/security_dashboard/helpers';
import { GlToggle } from '@gitlab/ui';
import StandardFilter from './filters/standard_filter.vue';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
import { DISMISSAL_STATES } from '../store/modules/filters/constants';
export default {
components: {
StandardFilter,
GlToggleVuex,
GlToggle,
},
data: () => ({
filterConfigs: [severityFilter, scannerFilter],
}),
computed: {
...mapGetters('filters', ['visibleFilters']),
...mapState('filters', ['filters']),
hideDismissed() {
return this.filters.scope === DISMISSAL_STATES.DISMISSED;
},
},
methods: {
...mapActions('filters', ['setFilter']),
...mapActions('filters', ['setFilter', 'toggleHideDismissed']),
},
};
</script>
......@@ -21,21 +29,20 @@ export default {
<div class="dashboard-filters border-bottom bg-gray-light">
<div class="row mx-0 p-2">
<standard-filter
v-for="filter in visibleFilters"
v-for="filter in filterConfigs"
:key="filter.id"
class="col-sm-6 col-md-4 col-lg-2 p-2 js-filter"
:filter="filter"
@setFilter="setFilter"
@filter-changed="setFilter"
/>
<div class="gl-display-flex ml-lg-auto p-2">
<slot name="buttons"></slot>
<div class="pl-md-6">
<strong>{{ s__('SecurityReports|Hide dismissed') }}</strong>
<gl-toggle-vuex
<gl-toggle
class="d-block mt-1 js-toggle"
store-module="filters"
state-property="hideDismissed"
set-action="setToggleValue"
:value="hideDismissed"
@change="toggleHideDismissed"
/>
</div>
</div>
......
......@@ -46,7 +46,7 @@ export default {
);
},
routeQueryIds() {
const ids = this.$route.query[this.filter.id] || [];
const ids = this.$route?.query[this.filter.id] || [];
return Array.isArray(ids) ? ids : [ids];
},
routeQueryOptions() {
......@@ -83,9 +83,9 @@ export default {
this.updateRouteQuery();
},
updateRouteQuery() {
const query = { query: { ...this.$route.query, ...this.queryObject } };
const query = { query: { ...this.$route?.query, ...this.queryObject } };
// To avoid a console error, don't update the querystring if it's the same as the current one.
if (!isEqual(this.routeQueryIds, this.queryObject[this.filter.id])) {
if (this.$router && !isEqual(this.routeQueryIds, this.queryObject[this.filter.id])) {
this.$router.push(query);
}
},
......
......@@ -23,7 +23,7 @@ export default {
'pageInfo',
'vulnerabilities',
]),
...mapGetters('filters', ['activeFilters']),
...mapState('filters', ['filters']),
...mapGetters('vulnerabilities', [
'dashboardListError',
'hasSelectedAllVulnerabilities',
......@@ -49,7 +49,7 @@ export default {
'selectAllVulnerabilities',
]),
fetchPage(page) {
this.fetchVulnerabilities({ ...this.activeFilters, page });
this.fetchVulnerabilities({ ...this.filters, page });
},
handleSelectAll() {
return this.hasSelectedAllVulnerabilities
......
<script>
import { isUndefined } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import Filters from './filters.vue';
......@@ -29,12 +28,6 @@ export default {
required: false,
default: '',
},
lockToProject: {
type: Object,
required: false,
default: null,
validator: project => !isUndefined(project.id),
},
pipelineId: {
type: Number,
required: false,
......@@ -56,7 +49,7 @@ export default {
'isCreatingMergeRequest',
]),
...mapState('pipelineJobs', ['projectId']),
...mapGetters('filters', ['activeFilters']),
...mapState('filters', ['filters']),
...mapGetters('vulnerabilities', ['loadingVulnerabilitiesFailedWithRecognizedErrorCode']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
canCreateIssue() {
......@@ -74,9 +67,6 @@ export default {
vulnerability() {
return this.modal.vulnerability;
},
isLockedToProject() {
return this.lockToProject !== null;
},
shouldShowAside() {
return this.shouldShowChart;
},
......@@ -85,18 +75,11 @@ export default {
},
},
created() {
if (this.isLockedToProject) {
this.lockFilter({
filterId: 'project_id',
optionId: this.lockToProject.id,
});
}
this.setPipelineId(this.pipelineId);
this.setHideDismissedToggleInitialState();
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchVulnerabilities({ ...this.filters, page: this.pageInfo.page });
this.fetchVulnerabilitiesHistory(this.filters);
this.fetchPipelineJobs();
},
methods: {
......
import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
import { ALL } from './constants';
import { hasValidSelection } from './utils';
export const setFilter = ({ commit }, { filterId, optionId, lazy = false }) => {
commit(types.SET_FILTER, { filterId, optionId, lazy });
Tracking.event(document.body.dataset.page, 'set_filter', {
label: filterId,
value: optionId,
});
};
export const setFilterOptions = ({ commit, state }, { filterId, options, lazy = false }) => {
commit(types.SET_FILTER_OPTIONS, { filterId, options });
const { selection } = state.filters.find(({ id }) => id === filterId);
if (!hasValidSelection({ selection, options })) {
commit(types.SET_FILTER, { filterId, optionId: ALL, lazy });
}
};
export const setAllFilters = ({ commit }, payload) => {
commit(types.SET_ALL_FILTERS, payload);
export const setFilter = ({ commit }, filter) => {
commit(types.SET_FILTER, filter);
};
export const lockFilter = ({ commit }, payload) => {
commit(types.SET_FILTER, payload);
commit(types.HIDE_FILTER, payload);
};
export const setHideDismissedToggleInitialState = ({ commit }) => {
const [urlParam] = getParameterValues('scope');
const showDismissed = urlParam === 'all';
commit(types.SET_TOGGLE_VALUE, { key: 'hideDismissed', value: !showDismissed });
};
export const setToggleValue = ({ commit }, { key, value }) => {
commit(types.SET_TOGGLE_VALUE, { key, value });
Tracking.event(document.body.dataset.page, 'set_toggle', {
label: key,
value,
});
export const toggleHideDismissed = ({ commit }) => {
commit(types.TOGGLE_HIDE_DISMISSED);
};
......@@ -5,6 +5,10 @@ export const STATE = {
DETECTED: 'DETECTED',
CONFIRMED: 'CONFIRMED',
};
export const DISMISSAL_STATES = {
DISMISSED: 'dismissed',
ALL: 'all',
};
export const BASE_FILTERS = {
state: {
......
import { isBaseFilterOption } from './utils';
/**
* Loops through all the filters and returns all the active ones
* stripping out base filter options.
* @returns Object
* e.g. { type: ['sast'], severity: ['high', 'medium'] }
*/
export const activeFilters = state => {
const filters = state.filters.reduce((acc, filter) => {
acc[filter.id] = [...Array.from(filter.selection)].filter(id => !isBaseFilterOption(id));
return acc;
}, {});
// hide_dismissed is hardcoded as it currently is an edge-case, more info in the MR:
// https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15333#note_208301144
filters.scope = state.hideDismissed ? 'dismissed' : 'all';
return filters;
};
export const visibleFilters = ({ filters }) => filters.filter(({ hidden }) => !hidden);
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default {
namespaced: true,
actions,
getters,
mutations,
state,
};
export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
export const SET_ALL_FILTERS = 'SET_ALL_FILTERS';
export const HIDE_FILTER = 'HIDE_FILTER';
export const SET_TOGGLE_VALUE = 'SET_TOGGLE_VALUE';
export const TOGGLE_HIDE_DISMISSED = 'TOGGLE_HIDE_DISMISSED';
import { mapValues } from 'lodash';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import { ALL } from './constants';
import { setFilter } from './utils';
import { DISMISSAL_STATES } from './constants';
import Tracking from '~/tracking';
export default {
[types.SET_ALL_FILTERS](state, payload = {}) {
state.filters = state.filters.map(filter => {
// If the payload is empty, we fall back to an empty selection
const selectedOptions = (payload && payload[filter.id]) || [];
[types.SET_FILTER](state, filter) {
// Convert the filter key to snake case and the selected option IDs to lower case. The API
// endpoint needs them to be in this format.
const convertedFilter = mapValues(convertObjectPropsToSnakeCase(filter), array =>
array.map(element => element.toLowerCase()),
);
const selection = Array.isArray(selectedOptions)
? new Set(selectedOptions)
: new Set([selectedOptions]);
state.filters = { ...state.filters, ...convertedFilter };
// This prevents us from selecting nothing at all
if (selection.size === 0) {
selection.add(ALL);
}
return { ...filter, selection };
});
state.hideDismissed = payload.scope !== 'all';
},
[types.SET_FILTER](state, payload) {
state.filters = setFilter(state.filters, payload);
},
[types.SET_FILTER_OPTIONS](state, payload) {
const { filterId, options } = payload;
state.filters.find(filter => filter.id === filterId).options = options;
const [label, value] = Object.values(filter);
Tracking.event(document.body.dataset.page, 'set_filter', { label, value });
},
[types.HIDE_FILTER](state, { filterId }) {
const hiddenFilter = state.filters.find(({ id }) => id === filterId);
if (hiddenFilter) {
hiddenFilter.hidden = true;
}
},
[types.SET_TOGGLE_VALUE](state, { key, value }) {
state[key] = value;
[types.TOGGLE_HIDE_DISMISSED](state) {
const scope =
state.filters.scope === DISMISSAL_STATES.DISMISSED
? DISMISSAL_STATES.ALL
: DISMISSAL_STATES.DISMISSED;
state.filters = { ...state.filters, scope };
Tracking.event(document.body.dataset.page, 'set_toggle', { label: 'scope', value: scope });
},
};
import { s__ } from '~/locale';
import { BASE_FILTERS } from './constants';
import { SEVERITY_LEVELS, REPORT_TYPES } from '../../constants';
const optionsObjectToArray = obj => Object.entries(obj).map(([id, name]) => ({ id, name }));
export default () => ({
filters: [
{
name: s__('SecurityReports|Severity'),
id: 'severity',
options: [BASE_FILTERS.severity, ...optionsObjectToArray(SEVERITY_LEVELS)],
hidden: false,
selection: new Set([BASE_FILTERS.severity.id]),
},
{
name: s__('SecurityReports|Scanner'),
id: 'report_type',
options: [BASE_FILTERS.report_type, ...optionsObjectToArray(REPORT_TYPES)],
hidden: false,
selection: new Set([BASE_FILTERS.report_type.id]),
},
{
name: s__('SecurityReports|Project'),
id: 'project_id',
options: [BASE_FILTERS.project_id],
hidden: false,
selection: new Set([BASE_FILTERS.project_id.id]),
},
],
hideDismissed: true,
filters: {
scope: 'dismissed',
},
});
import { isSubset } from '~/lib/utils/set';
import { ALL } from './constants';
export const isBaseFilterOption = id => id === ALL;
/**
* Returns whether or not the given state filter has a valid selection,
* considering its available options.
* @param {Object} filter The filter from the state to check.
* @returns boolean
*/
export const hasValidSelection = ({ selection, options }) =>
isSubset(selection, new Set(options.map(({ id }) => id)));
/**
* Takes a filter array and a selected payload.
* It then either adds or removes that option from the appropriate selected filter.
* With a few extra exceptions around the `ALL` special case.
* @param {Array} filters the filters to mutate
* @param {Object} payload
* @param {String} payload.optionId the ID of the option that was just selected
* @param {String} payload.filterId the ID of the filter that the selected option belongs to
* @returns {Array} the mutated filters array
*/
export const setFilter = (filters, { optionId, filterId }) =>
filters.map(filter => {
if (filter.id === filterId) {
const { selection } = filter;
if (optionId === ALL) {
selection.clear();
} else if (selection.has(optionId)) {
selection.delete(optionId);
} else {
selection.delete(ALL);
selection.add(optionId);
}
if (selection.size === 0) {
selection.add(ALL);
}
return {
...filter,
selection,
};
}
return filter;
});
import * as filtersMutationTypes from '../modules/filters/mutation_types';
import * as vulnerabilitiesMutationTypes from '../modules/vulnerabilities/mutation_types';
import { SET_FILTER, TOGGLE_HIDE_DISMISSED } from '../modules/filters/mutation_types';
const refreshTypes = [`filters/${SET_FILTER}`, `filters/${TOGGLE_HIDE_DISMISSED}`];
export default store => {
const refreshVulnerabilities = payload => {
......@@ -7,27 +8,9 @@ export default store => {
store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', payload);
};
store.subscribe(({ type, payload = {} }) => {
switch (type) {
// SET_ALL_FILTERS mutations are triggered by navigation events, in such case we
// want to preserve the page number that was set in the sync_with_router plugin
case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`:
refreshVulnerabilities({
...store.getters['filters/activeFilters'],
page: store.state.vulnerabilities.pageInfo.page,
});
break;
// These mutations happen when users interact with the UI,
// in that case we want to reset the page number
case `vulnerabilities/${vulnerabilitiesMutationTypes.RECEIVE_DISMISS_SELECTED_VULNERABILITIES_SUCCESS}`:
case `filters/${filtersMutationTypes.SET_FILTER}`:
case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
if (!payload.lazy) {
refreshVulnerabilities(store.getters['filters/activeFilters']);
}
break;
}
default:
store.subscribe(({ type }) => {
if (refreshTypes.includes(type)) {
refreshVulnerabilities(store.state.filters.filters);
}
});
};
import projectSelectorModule from '../modules/project_selector';
import * as projectSelectorMutationTypes from '../modules/project_selector/mutation_types';
import { BASE_FILTERS } from '../modules/filters/constants';
export default store => {
store.registerModule('projectSelector', projectSelectorModule());
store.subscribe(({ type, payload }) => {
if (type === `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
BASE_FILTERS.project_id,
...payload.map(({ name, id }) => ({
name,
id: id.toString(),
})),
],
lazy: true,
});
}
});
};
import projectsModule from '../modules/projects';
import * as projectsMutationTypes from '../modules/projects/mutation_types';
import { BASE_FILTERS } from '../modules/filters/constants';
export default store => {
store.registerModule('projects', projectsModule);
store.subscribe(({ type, payload }) => {
if (type === `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
BASE_FILTERS.project_id,
...payload.projects.map(({ name, id }) => ({
name,
id: id.toString(),
})),
],
});
}
});
};
......@@ -38,7 +38,7 @@ describe('Filter component', () => {
});
it('should display all filters', () => {
expect(wrapper.findAll('.js-filter')).toHaveLength(3);
expect(wrapper.findAll('.js-filter')).toHaveLength(2);
});
it('should display "Hide dismissed vulnerabilities" toggle', () => {
......
......@@ -88,7 +88,6 @@ describe('Pipeline Security Dashboard component', () => {
const dashboard = wrapper.find(SecurityDashboard);
expect(dashboard.exists()).toBe(true);
expect(dashboard.props()).toMatchObject({
lockToProject: { id: projectId },
pipelineId,
vulnerabilitiesEndpoint,
});
......
......@@ -11,7 +11,6 @@ import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_c
import LoadingError from 'ee/security_dashboard/components/loading_error.vue';
import createStore from 'ee/security_dashboard/store';
import { getParameterValues } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
const pipelineId = 123;
......@@ -25,7 +24,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('Security Dashboard component', () => {
let wrapper;
let mock;
let lockFilterSpy;
let setPipelineIdSpy;
let fetchPipelineJobsSpy;
let store;
......@@ -37,7 +35,6 @@ describe('Security Dashboard component', () => {
SecurityDashboardLayout,
},
methods: {
lockFilter: lockFilterSpy,
setPipelineId: setPipelineIdSpy,
fetchPipelineJobs: fetchPipelineJobsSpy,
},
......@@ -53,7 +50,6 @@ describe('Security Dashboard component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
lockFilterSpy = jest.fn();
setPipelineIdSpy = jest.fn();
fetchPipelineJobsSpy = jest.fn();
store = createStore();
......@@ -83,14 +79,6 @@ describe('Security Dashboard component', () => {
expect(wrapper.find(VulnerabilityChart).exists()).toBe(true);
});
it('does not lock to a project', () => {
expect(wrapper.vm.isLockedToProject).toBe(false);
});
it('does not lock project filters', () => {
expect(lockFilterSpy).not.toHaveBeenCalled();
});
it('sets the pipeline id', () => {
expect(setPipelineIdSpy).toHaveBeenCalledWith(pipelineId);
});
......@@ -155,28 +143,6 @@ describe('Security Dashboard component', () => {
);
});
describe('with project lock', () => {
const project = {
id: 123,
};
beforeEach(() => {
createComponent({
lockToProject: project,
});
});
it('locks to a given project', () => {
expect(wrapper.vm.isLockedToProject).toBe(true);
});
it('locks the filters to a given project', () => {
expect(lockFilterSpy).toHaveBeenCalledWith({
filterId: 'project_id',
optionId: project.id,
});
});
});
describe.each`
endpointProp | Component
${'vulnerabilitiesHistoryEndpoint'} | ${VulnerabilityChart}
......@@ -192,19 +158,6 @@ describe('Security Dashboard component', () => {
});
});
describe('dismissed vulnerabilities', () => {
it.each`
description | getParameterValuesReturnValue | expected
${'hides dismissed vulnerabilities by default'} | ${[]} | ${true}
${'shows dismissed vulnerabilities if scope param is "all"'} | ${['all']} | ${false}
${'hides dismissed vulnerabilities if scope param is "dismissed"'} | ${['dismissed']} | ${true}
`('$description', ({ getParameterValuesReturnValue, expected }) => {
getParameterValues.mockImplementation(() => getParameterValuesReturnValue);
createComponent();
expect(wrapper.vm.$store.state.filters.hideDismissed).toBe(expected);
});
});
describe('on error', () => {
beforeEach(() => {
createComponent();
......
......@@ -2,9 +2,7 @@ import testAction from 'helpers/vuex_action_helper';
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as actions from 'ee/security_dashboard/store/modules/filters/actions';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockReturnValue([]),
......@@ -16,199 +14,28 @@ describe('filters actions', () => {
});
describe('setFilter', () => {
it('should commit the SET_FILTER mutuation', done => {
it('should commit the SET_FILTER mutuation', () => {
const state = createState();
const payload = { filterId: 'report_type', optionId: 'sast' };
const payload = { reportType: ['sast'] };
testAction(
actions.setFilter,
payload,
state,
[
{
type: types.SET_FILTER,
payload: { ...payload, lazy: false },
},
],
[],
done,
);
});
it('should commit the SET_FILTER mutuation passing through lazy = true', done => {
const state = createState();
const payload = { filterId: 'report_type', optionId: 'sast', lazy: true };
testAction(
actions.setFilter,
payload,
state,
[
{
type: types.SET_FILTER,
payload,
},
],
[],
done,
);
});
});
describe('setFilterOptions', () => {
it('should commit the SET_FILTER_OPTIONS mutuation', done => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: ALL }] };
testAction(
actions.setFilterOptions,
payload,
state,
[
{
type: types.SET_FILTER_OPTIONS,
payload,
},
],
[],
done,
);
});
it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid', done => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
testAction(
actions.setFilterOptions,
payload,
state,
[
{
type: types.SET_FILTER_OPTIONS,
payload,
},
{
type: types.SET_FILTER,
payload: expect.objectContaining({
filterId: 'project_id',
optionId: ALL,
}),
},
],
[],
done,
);
});
it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid, passing the lazy flag', done => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
testAction(
actions.setFilterOptions,
{ ...payload, lazy: true },
state,
[
{
type: types.SET_FILTER_OPTIONS,
payload,
},
{
type: types.SET_FILTER,
payload: {
filterId: 'project_id',
optionId: ALL,
lazy: true,
},
},
],
[],
done,
);
});
});
describe('setAllFilters', () => {
it('should commit the SET_ALL_FILTERS mutuation', done => {
const state = createState();
const payload = { project_id: ['12', '15'] };
testAction(
actions.setAllFilters,
payload,
state,
[
{
type: types.SET_ALL_FILTERS,
payload,
},
],
[],
done,
);
});
});
describe('setHideDismissedToggleInitialState', () => {
[
{
description: 'should set hideDismissed to true if scope param is not present',
returnValue: [],
hideDismissedValue: true,
},
{
description: 'should set hideDismissed to false if scope param is "all"',
returnValue: ['all'],
hideDismissedValue: false,
},
{
description: 'should set hideDismissed to true if scope param is "dismissed"',
returnValue: ['dismissed'],
hideDismissedValue: true,
},
].forEach(testCase => {
it(testCase.description, done => {
getParameterValues.mockReturnValue(testCase.returnValue);
const state = createState();
testAction(
actions.setHideDismissedToggleInitialState,
{},
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload: {
key: 'hideDismissed',
value: testCase.hideDismissedValue,
},
},
],
[],
done,
);
});
return testAction(actions.setFilter, payload, state, [
{
type: types.SET_FILTER,
payload,
},
]);
});
});
describe('setToggleValue', () => {
it('should commit the SET_TOGGLE_VALUE mutation', done => {
describe('toggleHideDismissed', () => {
it('should commit the TOGGLE_HIDE_DISMISSED mutation', () => {
const state = createState();
const payload = { key: 'foo', value: 'bar' };
testAction(
actions.setToggleValue,
payload,
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload,
},
],
[],
done,
);
return testAction(actions.toggleHideDismissed, undefined, state, [
{
type: types.TOGGLE_HIDE_DISMISSED,
},
]);
});
});
});
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', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('activeFilters', () => {
it('should return no severity filters', () => {
const activeFilters = getters.activeFilters(state);
expect(activeFilters.severity).toHaveLength(0);
});
it('should return multiple dummy filters"', () => {
const dummyFilter = {
id: 'dummy',
options: [{ id: 'one' }, { id: 'two' }],
selection: new Set(['one', 'two']),
};
state.filters.push(dummyFilter);
const activeFilters = getters.activeFilters(state);
expect(activeFilters.dummy).toHaveLength(2);
});
});
});
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import {
SET_FILTER,
TOGGLE_HIDE_DISMISSED,
} from 'ee/security_dashboard/store/modules/filters/mutation_types';
import mutations from 'ee/security_dashboard/store/modules/filters/mutations';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import { severityFilter } from 'ee/security_dashboard/helpers';
const criticalOption = severityFilter.options.find(x => x.id === 'CRITICAL');
const highOption = severityFilter.options.find(x => x.id === 'HIGH');
describe('filters module mutations', () => {
let state;
let severityFilter;
let criticalOption;
let highOption;
beforeEach(() => {
state = createState();
[severityFilter] = state.filters;
[, criticalOption, highOption] = severityFilter.options;
});
describe('SET_FILTER', () => {
beforeEach(() => {
mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: criticalOption.id,
});
});
it('should make critical the selected option', () => {
expect(state.filters[0].selection).toEqual(new Set(['critical']));
});
it('should set to `all` if no option is selected', () => {
mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: criticalOption.id,
});
expect(state.filters[0].selection).toEqual(new Set([ALL]));
});
describe('on subsequent changes', () => {
it('should add "high" to the selected options', () => {
mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: highOption.id,
});
expect(state.filters[0].selection).toEqual(new Set(['high', 'critical']));
});
});
});
describe('SET_ALL_FILTERS', () => {
it('should set options if they are a single string', () => {
mutations[types.SET_ALL_FILTERS](state, { [severityFilter.id]: criticalOption.id });
const expected = new Set([criticalOption.id]);
expect(state.filters[0].selection).toEqual(expected);
});
it('should set options if they are given as an array', () => {
mutations[types.SET_ALL_FILTERS](state, {
[severityFilter.id]: [criticalOption.id, highOption.id],
});
const expected = new Set([criticalOption.id, highOption.id]);
expect(state.filters[0].selection).toEqual(expected);
});
it('should set options to `all` if no payload is given', () => {
mutations[types.SET_ALL_FILTERS](state);
const expected = new Set([ALL]);
state.filters.forEach(filter => {
expect(filter.selection).toEqual(expected);
it.each`
options | expected
${[]} | ${[]}
${[criticalOption.id]} | ${[criticalOption.id.toLowerCase()]}
${[criticalOption.id, highOption.id]} | ${[criticalOption.id.toLowerCase(), highOption.id.toLowerCase()]}
`('sets the filter to $options', ({ options, expected }) => {
mutations[SET_FILTER](state, { [severityFilter.id]: options });
expect(state.filters[severityFilter.id]).toEqual(expected);
});
it('sets multiple filters correctly with the right casing', () => {
const filter1 = { oneWord: ['ABC', 'DEF'] };
const filter2 = { twoWords: ['123', '456'] };
const filter3 = { threeTotalWords: ['Abc123', 'dEF456'] };
mutations[SET_FILTER](state, filter1);
mutations[SET_FILTER](state, filter2);
mutations[SET_FILTER](state, filter3);
expect(state.filters).toMatchObject({
one_word: ['abc', 'def'],
two_words: ['123', '456'],
three_total_words: ['abc123', 'def456'],
});
});
it('should set options to `all` if payload contains an empty array', () => {
mutations[types.SET_ALL_FILTERS](state, {
[severityFilter.id]: [],
});
const expected = new Set([ALL]);
expect(state.filters[0].selection).toEqual(expected);
});
});
describe('SET_FILTER_OPTIONS', () => {
const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }];
beforeEach(() => {
const filterId = severityFilter.id;
mutations[types.SET_FILTER_OPTIONS](state, { filterId, options });
});
describe('TOGGLE_HIDE_DISMISSED', () => {
it('toggles scope filter', () => {
const toggleAndCheck = expected => {
mutations[TOGGLE_HIDE_DISMISSED](state);
expect(state.filters.scope).toBe(expected);
};
it('should add all the options to the type filter', () => {
expect(severityFilter.options).toEqual(options);
toggleAndCheck('all');
toggleAndCheck('dismissed');
toggleAndCheck('all');
});
});
});
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import { hasValidSelection, setFilter } from 'ee/security_dashboard/store/modules/filters/utils';
describe('filters module utils', () => {
describe('hasValidSelection', () => {
describe.each`
selection | options | expected
${[]} | ${[]} | ${true}
${[]} | ${['foo']} | ${true}
${['foo']} | ${['foo']} | ${true}
${['foo']} | ${['foo', 'bar']} | ${true}
${['bar', 'foo']} | ${['foo', 'bar']} | ${true}
${['foo']} | ${[]} | ${false}
${['foo']} | ${['bar']} | ${false}
${['foo', 'bar']} | ${['foo']} | ${false}
`('given selection $selection and options $options', ({ selection, options, expected }) => {
let filter;
beforeEach(() => {
filter = {
selection,
options: options.map(id => ({ id })),
};
});
it(`return ${expected}`, () => {
expect(hasValidSelection(filter)).toBe(expected);
});
});
});
describe('setFilter', () => {
const filterId = 'foo';
const option1 = 'bar';
const option2 = 'baz';
const initFilters = (initiallySelected = [ALL]) => [
{ id: filterId, selection: new Set(initiallySelected) },
];
let filters;
let filter;
describe('when ALL is initially selected', () => {
beforeEach(() => {
filters = initFilters();
});
describe('when a valid filter is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should select the passed option', () => {
expect(filter.selection.has(option1)).toBe(true);
});
it('should remove the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
describe('when an invalid filter is passed ', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId: 'baz', optionId: option1 });
});
it('should not select the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should not remove the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(true);
});
});
});
describe('when an option is initially selected', () => {
beforeEach(() => {
filters = initFilters([option1]);
});
describe('when the selected option is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should remove the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(true);
});
});
describe('when another option is passed ', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option2 });
});
it('should not remove the initially selected option', () => {
expect(filter.selection.has(option1)).toBe(true);
});
it('should add the passed selected option', () => {
expect(filter.selection.has(option2)).toBe(true);
});
it('should not select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
});
describe('when two options are initially selected', () => {
beforeEach(() => {
filters = initFilters([option1, option2]);
});
describe('when a selected option is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should remove the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should not remove the other option', () => {
expect(filter.selection.has(option2)).toBe(true);
});
it('should not select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
});
});
});
import createStore from 'ee/security_dashboard/store/index';
import * as filtersMutationTypes from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as vulnerabilityMutationTypes from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import {
SET_FILTER,
TOGGLE_HIDE_DISMISSED,
} from 'ee/security_dashboard/store/modules/filters/mutation_types';
function expectRefreshDispatches(store, payload) {
expect(store.dispatch).toHaveBeenCalledTimes(2);
......@@ -20,51 +22,24 @@ describe('mediator', () => {
});
it('triggers fetching vulnerabilities after one filter changes', () => {
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, {});
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${SET_FILTER}`, {});
expectRefreshDispatches(store, activeFilters);
expectRefreshDispatches(store, store.state.filters.filters);
});
it('does not fetch vulnerabilities after one filter changes with lazy = true', () => {
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, { lazy: true });
expect(store.dispatch).not.toHaveBeenCalled();
});
it('triggers fetching vulnerabilities after filters change', () => {
const payload = {
...store.getters['filters/activeFilters'],
page: store.state.vulnerabilities.pageInfo.page,
it('triggers fetching vulnerabilities after multiple filters change', () => {
const filters = {
filter1: ['abc', 'def'],
filter2: ['123', '456'],
};
store.commit(`filters/${SET_FILTER}`, filters);
store.commit(`filters/${filtersMutationTypes.SET_ALL_FILTERS}`, {});
expectRefreshDispatches(store, payload);
});
it('triggers fetching vulnerabilities multiple vulnerabilities have been dismissed', () => {
const activeFilters = store.getters['filters/activeFilters'];
store.commit(
`vulnerabilities/${vulnerabilityMutationTypes.RECEIVE_DISMISS_SELECTED_VULNERABILITIES_SUCCESS}`,
{},
);
expectRefreshDispatches(store, activeFilters);
expectRefreshDispatches(store, expect.objectContaining(filters));
});
it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => {
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, {});
expectRefreshDispatches(store, activeFilters);
});
it('does not fetch vulnerabilities after "Hide dismissed" toggle changes with lazy = true', () => {
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, { lazy: true });
store.commit(`filters/${TOGGLE_HIDE_DISMISSED}`);
expect(store.dispatch).not.toHaveBeenCalled();
expectRefreshDispatches(store, store.state.filters.filters);
});
});
import Vuex from 'vuex';
import createStore from 'ee/security_dashboard/store';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import projectSelectorModule from 'ee/security_dashboard/store/modules/project_selector';
import projectSelectorPlugin from 'ee/security_dashboard/store/plugins/project_selector';
import * as projectSelectorMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
describe('project selector plugin', () => {
let store;
beforeEach(() => {
jest.spyOn(Vuex.Store.prototype, 'registerModule');
store = createStore({ plugins: [projectSelectorPlugin] });
});
it('registers the project selector module on the store', () => {
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith(
'projectSelector',
projectSelectorModule(),
);
});
it('sets project filter options with lazy = true after projects have been received', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
const projects = [{ name: 'foo', id: '1' }];
store.commit(
`projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`,
projects,
);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('filters/setFilterOptions', {
filterId: 'project_id',
options: [BASE_FILTERS.project_id, ...projects],
lazy: true,
});
});
});
import Vuex from 'vuex';
import createStore from 'ee/security_dashboard/store';
import projectsModule from 'ee/security_dashboard/store/modules/projects';
import projectsPlugin from 'ee/security_dashboard/store/plugins/projects';
import * as projectsMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('projects plugin', () => {
let store;
beforeEach(() => {
jest.spyOn(Vuex.Store.prototype, 'registerModule');
store = createStore({ plugins: [projectsPlugin] });
});
it('registers the projects module on the store', () => {
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith('projects', projectsModule);
});
it('sets project filter options after projects have been received', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
const projectOption = { name: 'foo', id: '1' };
store.commit(`projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`, {
projects: [{ ...projectOption, irrelevantProperty: 'foobar' }],
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(
'filters/setFilterOptions',
Object({
filterId: 'project_id',
options: [BASE_FILTERS.project_id, projectOption],
}),
);
});
});
......@@ -23972,9 +23972,6 @@ msgstr ""
msgid "SecurityReports|Scan details"
msgstr ""
msgid "SecurityReports|Scanner"
msgstr ""
msgid "SecurityReports|Security Dashboard"
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