Commit 86d3dcae authored by Simon Knox's avatar Simon Knox

Merge branch '210327-fix-pipeline-security-tab-filters' into 'master'

Fix pipeline security tab filters not showing

See merge request gitlab-org/gitlab!47294
parents f3b3cc3f 0432a5f6
---
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]),
filters: {
scope: 'dismissed',
},
{
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,
});
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,
[
return testAction(actions.setFilter, payload, state, [
{
type: types.SET_FILTER,
payload,
},
],
[],
done,
);
]);
});
});
describe('setFilterOptions', () => {
it('should commit the SET_FILTER_OPTIONS mutuation', done => {
describe('toggleHideDismissed', () => {
it('should commit the TOGGLE_HIDE_DISMISSED mutation', () => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: ALL }] };
testAction(
actions.setFilterOptions,
payload,
state,
[
return testAction(actions.toggleHideDismissed, undefined, 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,
);
});
});
});
describe('setToggleValue', () => {
it('should commit the SET_TOGGLE_VALUE mutation', done => {
const state = createState();
const payload = { key: 'foo', value: 'bar' };
testAction(
actions.setToggleValue,
payload,
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload,
type: types.TOGGLE_HIDE_DISMISSED,
},
],
[],
done,
);
]);
});
});
});
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']));
});
});
});
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 });
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],
expect(state.filters[severityFilter.id]).toEqual(expected);
});
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]);
it('sets multiple filters correctly with the right casing', () => {
const filter1 = { oneWord: ['ABC', 'DEF'] };
const filter2 = { twoWords: ['123', '456'] };
const filter3 = { threeTotalWords: ['Abc123', 'dEF456'] };
state.filters.forEach(filter => {
expect(filter.selection).toEqual(expected);
});
});
mutations[SET_FILTER](state, filter1);
mutations[SET_FILTER](state, filter2);
mutations[SET_FILTER](state, filter3);
it('should set options to `all` if payload contains an empty array', () => {
mutations[types.SET_ALL_FILTERS](state, {
[severityFilter.id]: [],
expect(state.filters).toMatchObject({
one_word: ['abc', 'def'],
two_words: ['123', '456'],
three_total_words: ['abc123', 'def456'],
});
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],
}),
);
});
});
......@@ -24036,9 +24036,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