Commit ffc54040 authored by Fatih Acet's avatar Fatih Acet

Merge branch '11975-move-SAST-to-the-frontend' into 'master'

Splits out SAST reports to a new module

See merge request gitlab-org/gitlab-ee!14299
parents 0b1bcb60 4f7a24a7
......@@ -155,17 +155,16 @@ export default {
'canCreateFeedbackPermission',
]),
...mapGetters([
'groupedSastText',
'groupedSummaryText',
'summaryStatus',
'groupedSastContainerText',
'groupedDastText',
'groupedDependencyText',
'sastStatusIcon',
'sastContainerStatusIcon',
'dastStatusIcon',
'dependencyScanningStatusIcon',
]),
...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']),
securityTab() {
return `${this.pipelinePath}/security`;
},
......@@ -230,15 +229,12 @@ export default {
'setHeadBlobPath',
'setBaseBlobPath',
'setSourceBranch',
'setSastHeadPath',
'setSastBasePath',
'setSastContainerHeadPath',
'setSastContainerBasePath',
'setDastHeadPath',
'setDastBasePath',
'setDependencyScanningHeadPath',
'setDependencyScanningBasePath',
'fetchSastReports',
'fetchSastContainerReports',
'fetchDastReports',
'fetchDependencyScanningReports',
......@@ -259,6 +255,11 @@ export default {
'downloadPatch',
'addDismissalComment',
]),
...mapActions('sast', {
setSastHeadPath: 'setHeadPath',
setSastBasePath: 'setBasePath',
fetchSastReports: 'fetchReports',
}),
},
};
</script>
......
......@@ -214,11 +214,9 @@ export default {
...mapActions([
'setHeadBlobPath',
'setSourceBranch',
'setSastHeadPath',
'setDependencyScanningHeadPath',
'setSastContainerHeadPath',
'setDastHeadPath',
'fetchSastReports',
'fetchDependencyScanningReports',
'fetchSastContainerReports',
'fetchDastReports',
......@@ -239,6 +237,10 @@ export default {
'downloadPatch',
'addDismissalComment',
]),
...mapActions('sast', {
setSastHeadPath: 'setHeadPath',
fetchSastReports: 'fetchReports',
}),
summaryTextBuilder(reportType, issuesCount = 0) {
if (issuesCount === 0) {
return sprintf(s__('ciReport|%{reportType} detected no vulnerabilities'), {
......
......@@ -45,49 +45,6 @@ export const setCanCreateIssuePermission = ({ commit }, permission) =>
export const setCanCreateFeedbackPermission = ({ commit }, permission) =>
commit(types.SET_CAN_CREATE_FEEDBACK_PERMISSION, permission);
/**
* SAST
*/
export const setSastHeadPath = ({ commit }, path) => commit(types.SET_SAST_HEAD_PATH, path);
export const setSastBasePath = ({ commit }, path) => commit(types.SET_SAST_BASE_PATH, path);
export const requestSastReports = ({ commit }) => commit(types.REQUEST_SAST_REPORTS);
export const receiveSastReports = ({ commit }, response) =>
commit(types.RECEIVE_SAST_REPORTS, response);
export const receiveSastError = ({ commit }, error) =>
commit(types.RECEIVE_SAST_REPORTS_ERROR, error);
export const fetchSastReports = ({ state, dispatch }) => {
const { base, head } = state.sast.paths;
dispatch('requestSastReports');
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
axios.get(state.vulnerabilityFeedbackPath, {
params: {
category: 'sast',
},
}),
])
.then(values => {
dispatch('receiveSastReports', {
head: values && values[0] ? values[0].data : null,
base: values && values[1] ? values[1].data : null,
enrichData: values && values[2] ? values[2].data : [],
});
})
.catch(() => {
dispatch('receiveSastError');
});
};
export const updateSastIssue = ({ commit }, issue) => commit(types.UPDATE_SAST_ISSUE, issue);
/**
* SAST CONTAINER
*/
......@@ -233,8 +190,8 @@ export const openModal = ({ dispatch }, payload) => {
export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
export const requestDismissVulnerability = ({ commit }) =>
commit(types.REQUEST_DISMISS_VULNERABILITY);
export const receiveDismissVulnerability = ({ commit }) =>
commit(types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS);
export const receiveDismissVulnerability = ({ commit }, payload) =>
commit(types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS, payload);
export const receiveDismissVulnerabilityError = ({ commit }, error) =>
commit(types.RECEIVE_DISMISS_VULNERABILITY_ERROR, error);
......@@ -253,30 +210,14 @@ export const dismissVulnerability = ({ state, dispatch }, comment) => {
},
})
.then(({ data }) => {
dispatch('closeDismissalCommentBox');
dispatch('receiveDismissVulnerability');
// Update the issue with the created dismissal feedback applied
const updatedIssue = {
...state.modal.vulnerability,
isDismissed: true,
dismissalFeedback: data,
};
switch (updatedIssue.category) {
case 'sast':
dispatch('updateSastIssue', updatedIssue);
break;
case 'dependency_scanning':
dispatch('updateDependencyScanningIssue', updatedIssue);
break;
case 'container_scanning':
dispatch('updateContainerScanningIssue', updatedIssue);
break;
case 'dast':
dispatch('updateDastIssue', updatedIssue);
break;
default:
}
dispatch('closeDismissalCommentBox');
dispatch('receiveDismissVulnerability', updatedIssue);
hideModal();
})
......@@ -334,29 +275,13 @@ export const revertDismissVulnerability = ({ state, dispatch }) => {
state.modal.vulnerability.dismissalFeedback.destroy_vulnerability_feedback_dismissal_path,
)
.then(() => {
dispatch('receiveDismissVulnerability');
// Update the issue with the reverted dismissal feedback applied
const updatedIssue = {
...state.modal.vulnerability,
isDismissed: false,
dismissalFeedback: null,
};
switch (updatedIssue.category) {
case 'sast':
dispatch('updateSastIssue', updatedIssue);
break;
case 'dependency_scanning':
dispatch('updateDependencyScanningIssue', updatedIssue);
break;
case 'container_scanning':
dispatch('updateContainerScanningIssue', updatedIssue);
break;
case 'dast':
dispatch('updateDastIssue', updatedIssue);
break;
default:
}
dispatch('receiveDismissVulnerability', updatedIssue);
hideModal();
})
......
import { s__, sprintf } from '~/locale';
import { countIssues, groupedTextBuilder, statusIcon } from './utils';
import { countIssues, groupedTextBuilder, statusIcon, groupedReportText } from './utils';
import { LOADING, ERROR, SUCCESS } from './constants';
import messages from './messages';
const groupedReportText = (report, reportType, errorMessage, loadingMessage) => {
const { paths } = report;
if (report.hasError) {
return errorMessage;
}
if (report.isLoading) {
return loadingMessage;
}
return groupedTextBuilder({
...countIssues(report),
reportType,
paths,
});
};
export const groupedSastText = ({ sast }) =>
groupedReportText(sast, messages.SAST, messages.SAST_HAS_ERROR, messages.SAST_IS_LOADING);
export const groupedSastContainerText = ({ sastContainer }) =>
groupedReportText(
sastContainer,
......@@ -102,9 +81,6 @@ export const summaryStatus = (state, getters) => {
return SUCCESS;
};
export const sastStatusIcon = ({ sast }) =>
statusIcon(sast.isLoading, sast.hasError, sast.newIssues.length);
export const sastContainerStatusIcon = ({ sastContainer }) =>
statusIcon(sastContainer.isLoading, sastContainer.hasError, sastContainer.newIssues.length);
......
import Vue from 'vue';
import Vuex from 'vuex';
import configureMediator from './mediator';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
import sast from './modules/sast';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
modules: {
sast,
},
actions,
getters,
mutations,
state: state(),
plugins: [configureMediator],
});
import * as types from './mutation_types';
const updateIssueActionsMap = {
sast: 'sast/updateVulnerability',
dependency_scanning: 'updateDependencyScanningIssue',
container_scanning: 'updateContainerScanningIssue',
dast: 'updateDastIssue',
};
export default function configureMediator(store) {
store.subscribe(({ type, payload }) => {
switch (type) {
case types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS:
if (updateIssueActionsMap[payload.category]) {
store.dispatch(updateIssueActionsMap[payload.category], payload);
}
break;
default:
}
});
}
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
export const setHeadPath = ({ commit }, path) => commit(types.SET_HEAD_PATH, path);
export const setBasePath = ({ commit }, path) => commit(types.SET_BASE_PATH, path);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
export const receiveReports = ({ commit }, response) => commit(types.RECEIVE_REPORTS, response);
export const receiveError = ({ commit }, error) => commit(types.RECEIVE_REPORTS_ERROR, error);
export const fetchReports = ({ state, rootState, dispatch }) => {
const { base, head } = state.paths;
const { blobPath, vulnerabilityFeedbackPath } = rootState;
dispatch('requestReports');
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
axios.get(vulnerabilityFeedbackPath, {
params: {
category: 'sast',
},
}),
])
.then(values => {
dispatch('receiveReports', {
reports: {
head: values && values[0] ? values[0].data : null,
base: values && values[1] ? values[1].data : null,
enrichData: values && values[2] ? values[2].data : [],
},
blobPath,
});
})
.catch(() => {
dispatch('receiveError');
});
};
export const updateVulnerability = ({ commit }, vulnerability) =>
commit(types.UPDATE_VULNERABILITY, vulnerability);
export default () => {};
import messages from '../../messages';
export const { SAST, SAST_HAS_ERROR, SAST_IS_LOADING } = messages;
import { statusIcon, groupedReportText } from '../../utils';
import { SAST, SAST_HAS_ERROR, SAST_IS_LOADING } from './constants';
export const groupedSastText = state =>
groupedReportText(state, SAST, SAST_HAS_ERROR, SAST_IS_LOADING);
export const sastStatusIcon = ({ isLoading, hasError, newIssues }) =>
statusIcon(isLoading, hasError, newIssues.length);
export default () => {};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
actions,
};
export const SET_HEAD_PATH = 'SET_HEAD_PATH';
export const SET_BASE_PATH = 'SET_BASE_PATH';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS = 'RECEIVE_REPORTS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const UPDATE_VULNERABILITY = 'UPDATE_VULNERABILITY';
import Vue from 'vue';
import * as types from './mutation_types';
import { parseSastIssues, findIssueIndex } from '../../utils';
import filterByKey from '../../utils/filter_by_key';
export default {
[types.SET_HEAD_PATH](state, path) {
Vue.set(state.paths, 'head', path);
},
[types.SET_BASE_PATH](state, path) {
Vue.set(state.paths, 'base', path);
},
[types.REQUEST_REPORTS](state) {
state.isLoading = true;
},
/**
* Compares sast results and returns the formatted report
*
* Sast has 3 types of issues: newIssues, resolvedIssues and allIssues.
*
* When we have both base and head:
* - newIssues = head - base
* - resolvedIssues = base - head
* - allIssues = head - newIssues - resolvedIssues
*
* When we only have head
* - newIssues = head
* - resolvedIssues = 0
* - allIssues = 0
*/
[types.RECEIVE_REPORTS](state, payload) {
const { reports, blobPath } = payload;
if (reports.base && reports.head) {
const filterKey = 'cve';
const parsedHead = parseSastIssues(reports.head, reports.enrichData, blobPath.head);
const parsedBase = parseSastIssues(reports.base, reports.enrichData, blobPath.base);
const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
state.newIssues = newIssues;
state.resolvedIssues = resolvedIssues;
state.allIssues = allIssues;
state.isLoading = false;
} else if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, reports.enrichData, blobPath.head);
state.newIssues = newIssues;
state.isLoading = false;
}
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.UPDATE_VULNERABILITY](state, issue) {
const newIssuesIndex = findIssueIndex(state.newIssues, issue);
if (newIssuesIndex !== -1) {
state.newIssues.splice(newIssuesIndex, 1, issue);
return;
}
const resolvedIssuesIndex = findIssueIndex(state.resolvedIssues, issue);
if (resolvedIssuesIndex !== -1) {
state.resolvedIssues.splice(resolvedIssuesIndex, 1, issue);
return;
}
const allIssuesIndex = findIssueIndex(state.allIssues, issue);
if (allIssuesIndex !== -1) {
state.allIssues.splice(allIssuesIndex, 1, issue);
}
},
};
export default () => ({
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
});
......@@ -13,13 +13,6 @@ export const SET_PIPELINE_ID = 'SET_PIPELINE_ID';
export const SET_CAN_CREATE_ISSUE_PERMISSION = 'SET_CAN_CREATE_ISSUE_PERMISSION';
export const SET_CAN_CREATE_FEEDBACK_PERMISSION = 'SET_CAN_CREATE_FEEDBACK_PERMISSION';
// SAST
export const SET_SAST_HEAD_PATH = 'SET_SAST_HEAD_PATH';
export const SET_SAST_BASE_PATH = 'SET_SAST_BASE_PATH';
export const REQUEST_SAST_REPORTS = 'REQUEST_SAST_REPORTS';
export const RECEIVE_SAST_REPORTS = 'RECEIVE_SAST_REPORTS';
export const RECEIVE_SAST_REPORTS_ERROR = 'RECEIVE_SAST_REPORTS_ERROR';
// SAST CONTAINER
export const SET_SAST_CONTAINER_HEAD_PATH = 'SET_SAST_CONTAINER_HEAD_PATH';
export const SET_SAST_CONTAINER_BASE_PATH = 'SET_SAST_CONTAINER_BASE_PATH';
......@@ -58,7 +51,6 @@ export const REQUEST_CREATE_MERGE_REQUEST = 'REQUEST_CREATE_MERGE_REQUEST';
export const RECEIVE_CREATE_MERGE_REQUEST_SUCCESS = 'RECEIVE_CREATE_MERGE_REQUEST_SUCCESS';
export const RECEIVE_CREATE_MERGE_REQUEST_ERROR = 'RECEIVE_CREATE_MERGE_REQUEST_ERROR';
export const UPDATE_SAST_ISSUE = 'UPDATE_SAST_ISSUE';
export const UPDATE_DEPENDENCY_SCANNING_ISSUE = 'UPDATE_DEPENDENCY_SCANNING_ISSUE';
export const UPDATE_CONTAINER_SCANNING_ISSUE = 'UPDATE_CONTAINER_SCANNING_ISSUE';
export const UPDATE_DAST_ISSUE = 'UPDATE_DAST_ISSUE';
......
import Vue from 'vue';
import * as types from './mutation_types';
import {
parseSastIssues,
parseDependencyScanningIssues,
getDastSite,
parseDastIssues,
......@@ -57,61 +56,6 @@ export default {
state.canCreateFeedbackPermission = permission;
},
// SAST
[types.SET_SAST_HEAD_PATH](state, path) {
Vue.set(state.sast.paths, 'head', path);
},
[types.SET_SAST_BASE_PATH](state, path) {
Vue.set(state.sast.paths, 'base', path);
},
[types.REQUEST_SAST_REPORTS](state) {
Vue.set(state.sast, 'isLoading', true);
},
/**
* Compares sast results and returns the formatted report
*
* Sast has 3 types of issues: newIssues, resolvedIssues and allIssues.
*
* When we have both base and head:
* - newIssues = head - base
* - resolvedIssues = base - head
* - allIssues = head - newIssues - resolvedIssues
*
* When we only have head
* - newIssues = head
* - resolvedIssues = 0
* - allIssues = 0
*/
[types.RECEIVE_SAST_REPORTS](state, reports) {
if (reports.base && reports.head) {
const filterKey = 'cve';
const parsedHead = parseSastIssues(reports.head, reports.enrichData, state.blobPath.head);
const parsedBase = parseSastIssues(reports.base, reports.enrichData, state.blobPath.base);
const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Vue.set(state.sast, 'newIssues', newIssues);
Vue.set(state.sast, 'resolvedIssues', resolvedIssues);
Vue.set(state.sast, 'allIssues', allIssues);
Vue.set(state.sast, 'isLoading', false);
} else if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, reports.enrichData, state.blobPath.head);
Vue.set(state.sast, 'newIssues', newIssues);
Vue.set(state.sast, 'isLoading', false);
}
},
[types.RECEIVE_SAST_REPORTS_ERROR](state) {
Vue.set(state.sast, 'isLoading', false);
Vue.set(state.sast, 'hasError', true);
},
// SAST CONTAINER
[types.SET_SAST_CONTAINER_HEAD_PATH](state, path) {
Vue.set(state.sastContainer.paths, 'head', path);
......@@ -342,27 +286,6 @@ export default {
Vue.set(state.modal, 'error', error);
},
[types.UPDATE_SAST_ISSUE](state, issue) {
// Find issue in the correct list and update it
const newIssuesIndex = findIssueIndex(state.sast.newIssues, issue);
if (newIssuesIndex !== -1) {
state.sast.newIssues.splice(newIssuesIndex, 1, issue);
return;
}
const resolvedIssuesIndex = findIssueIndex(state.sast.resolvedIssues, issue);
if (resolvedIssuesIndex !== -1) {
state.sast.resolvedIssues.splice(resolvedIssuesIndex, 1, issue);
return;
}
const allIssuesIndex = findIssueIndex(state.sast.allIssues, issue);
if (allIssuesIndex !== -1) {
state.sast.allIssues.splice(allIssuesIndex, 1, issue);
}
},
[types.UPDATE_DEPENDENCY_SCANNING_ISSUE](state, issue) {
// Find issue in the correct list and update it
......
......@@ -16,19 +16,6 @@ export default () => ({
canCreateIssuePermission: false,
canCreateFeedbackPermission: false,
sast: {
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
sastContainer: {
paths: {
head: null,
......
......@@ -387,3 +387,30 @@ export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [
fixed: resolvedIssues.length,
};
};
/**
* Generates a report message based on some of the report parameters and supplied messages.
*
* @param {Object} report The report to generate the text for
* @param {String} reportType The report type. e.g. SAST
* @param {String} errorMessage The message to show if there's an error in the report
* @param {String} loadingMessage The message to show if the report is still loading
* @returns {String}
*/
export const groupedReportText = (report, reportType, errorMessage, loadingMessage) => {
const { paths } = report;
if (report.hasError) {
return errorMessage;
}
if (report.isLoading) {
return loadingMessage;
}
return groupedTextBuilder({
...countIssues(report),
reportType,
paths,
});
};
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import configureMediator from 'ee/vue_shared/security_reports/store/mediator';
const mockedStore = {
dispatch: jest.fn(),
};
mockedStore.subscribe = callback => {
mockedStore.commit = callback;
};
describe('security reports mediator', () => {
beforeEach(() => {
configureMediator(mockedStore);
});
describe(types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS, () => {
const type = types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS;
it.each`
action | category
${'sast/updateVulnerability'} | ${'sast'}
${'updateDastIssue'} | ${'dast'}
${'updateDependencyScanningIssue'} | ${'dependency_scanning'}
${'updateContainerScanningIssue'} | ${'container_scanning'}
`(`should trigger $action on when a $category is updated`, data => {
const { action, category } = data;
const payload = { category };
mockedStore.commit({ type, payload });
expect(mockedStore.dispatch).toHaveBeenCalledWith(action, payload);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import testAction from 'helpers/vuex_action_helper';
import createState from 'ee/vue_shared/security_reports/store/modules/sast/state';
import * as types from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types';
import * as actions from 'ee/vue_shared/security_reports/store/modules/sast/actions';
const headPath = 'head-path.json';
const basePath = 'base-path.json';
const blobPath = 'blob-path.json';
const reports = {
base: 'base',
head: 'head',
enrichData: 'enrichData',
};
const error = 'Something went wrong';
const issue = {};
const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
const rootState = { vulnerabilityFeedbackPath, blobPath };
let state;
describe('sast report actions', () => {
beforeEach(() => {
state = createState();
});
describe('setHeadPath', () => {
it(`should commit ${types.SET_HEAD_PATH} with the correct path`, done => {
testAction(
actions.setHeadPath,
headPath,
state,
[
{
type: types.SET_HEAD_PATH,
payload: headPath,
},
],
[],
done,
);
});
});
describe('setBasePath', () => {
it(`should commit ${types.SET_BASE_PATH} with the correct path`, done => {
testAction(
actions.setBasePath,
basePath,
state,
[
{
type: types.SET_BASE_PATH,
payload: basePath,
},
],
[],
done,
);
});
});
describe('requestReports', () => {
it(`should commit ${types.REQUEST_REPORTS}`, done => {
testAction(actions.requestReports, {}, state, [{ type: types.REQUEST_REPORTS }], [], done);
});
});
describe('receiveReports', () => {
it(`should commit ${types.RECEIVE_REPORTS} with the correct response`, done => {
testAction(
actions.receiveReports,
reports,
state,
[
{
type: types.RECEIVE_REPORTS,
payload: reports,
},
],
[],
done,
);
});
});
describe('receiveError', () => {
it(`should commit ${types.RECEIVE_REPORTS_ERROR} with the correct response`, done => {
testAction(
actions.receiveError,
error,
state,
[
{
type: types.RECEIVE_REPORTS_ERROR,
payload: error,
},
],
[],
done,
);
});
});
describe('fetchReports', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.paths.head = headPath;
state.paths.base = basePath;
});
afterEach(() => {
mock.restore();
});
describe('when everything goes according to plan', () => {
beforeEach(() => {
mock
.onGet(headPath)
.replyOnce(200, reports.head)
.onGet(basePath)
.replyOnce(200, reports.base)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(200, reports.enrichData);
});
it('should dispatch the `receiveReports` action', done => {
testAction(
actions.fetchReports,
{},
{ ...rootState, ...state },
[],
[
{ type: 'requestReports' },
{
type: 'receiveReports',
payload: {
blobPath,
reports,
},
},
],
done,
);
});
});
describe('when the vulnerability feedback endpoint fails', () => {
beforeEach(() => {
mock
.onGet(headPath)
.replyOnce(200, reports.head)
.onGet(basePath)
.replyOnce(200, reports.base)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(404);
});
it('should dispatch the `receiveError` action', done => {
testAction(
actions.fetchReports,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestReports' }, { type: 'receiveError' }],
done,
);
});
});
});
describe('updateVulnerability', () => {
it(`should commit ${types.UPDATE_VULNERABILITY} with the correct response`, done => {
testAction(
actions.updateVulnerability,
issue,
state,
[
{
type: types.UPDATE_VULNERABILITY,
payload: issue,
},
],
[],
done,
);
});
});
});
import {
SAST_HAS_ERROR,
SAST_IS_LOADING,
} from 'ee/vue_shared/security_reports/store/modules/sast/constants';
import * as getters from 'ee/vue_shared/security_reports/store/modules/sast/getters';
const createReport = (config = {}) => ({
paths: [],
newIssues: [],
...config,
});
describe('groupedSastText', () => {
it("should return the error message if there's an error", () => {
const sast = createReport({ hasError: true });
const result = getters.groupedSastText(sast);
expect(result).toBe(SAST_HAS_ERROR);
});
it("should return the loading message if it's still loading", () => {
const sast = createReport({ isLoading: true });
const result = getters.groupedSastText(sast);
expect(result).toBe(SAST_IS_LOADING);
});
it('should call groupedTextBuilder if everything is fine', () => {
const sast = createReport();
const result = getters.groupedSastText(sast);
expect(result).toBe('SAST detected no vulnerabilities for the source branch only');
});
});
describe('sastStatusIcon', () => {
it("should return `loading` when we're still loading", () => {
const sast = createReport({ isLoading: true });
const result = getters.sastStatusIcon(sast);
expect(result).toBe('loading');
});
it("should return `warning` when there's an issue", () => {
const sast = createReport({ hasError: true });
const result = getters.sastStatusIcon(sast);
expect(result).toBe('warning');
});
it('should return `success` when nothing is wrong', () => {
const sast = createReport();
const result = getters.sastStatusIcon(sast);
expect(result).toBe('success');
});
});
import * as types from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types';
import createState from 'ee/vue_shared/security_reports/store/modules/sast/state';
import mutations from 'ee/vue_shared/security_reports/store/modules/sast/mutations';
const createIssue = ({ ...config }) => ({ changed: false, ...config });
describe('sast module mutations', () => {
const path = 'path';
let state;
beforeEach(() => {
state = createState();
});
describe(types.SET_HEAD_PATH, () => {
it('should set the SAST head path', () => {
mutations[types.SET_HEAD_PATH](state, path);
expect(state.paths.head).toBe(path);
});
});
describe(types.SET_BASE_PATH, () => {
it('should set the SAST base path', () => {
mutations[types.SET_BASE_PATH](state, path);
expect(state.paths.base).toBe(path);
});
});
describe(types.REQUEST_REPORTS, () => {
it('should set the `isLoading` status to `true`', () => {
mutations[types.REQUEST_REPORTS](state);
expect(state.isLoading).toBe(true);
});
});
describe(types.RECEIVE_REPORTS_ERROR, () => {
beforeEach(() => {
state.isLoading = true;
mutations[types.RECEIVE_REPORTS_ERROR](state);
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should set the `hasError` status to `true`', () => {
expect(state.hasError).toBe(true);
});
});
describe(types.UPDATE_VULNERABILITY, () => {
let newIssue;
let resolvedIssue;
let allIssue;
beforeEach(() => {
newIssue = createIssue({ project_fingerprint: 'new' });
resolvedIssue = createIssue({ project_fingerprint: 'resolved' });
allIssue = createIssue({ project_fingerprint: 'all' });
state.newIssues.push(newIssue);
state.resolvedIssues.push(resolvedIssue);
state.allIssues.push(allIssue);
});
describe('with a `new` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...newIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.newIssues[0].changed).toBe(true);
});
});
describe('with a `resolved` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...resolvedIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.resolvedIssues[0].changed).toBe(true);
});
});
describe('with an `all` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...allIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.allIssues[0].changed).toBe(true);
});
});
describe('with an invalid issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](
state,
createIssue({ project_fingerprint: 'invalid', changed: true }),
);
});
it('should ignore the issue', () => {
expect(state.newIssues[0].changed).toBe(false);
expect(state.resolvedIssues[0].changed).toBe(false);
expect(state.allIssues[0].changed).toBe(false);
});
});
});
describe(types.RECEIVE_REPORTS, () => {
const head = [
createIssue({ cve: 'CVE-1' }),
createIssue({ cve: 'CVE-4' }),
createIssue({ cve: 'CVE-5' }),
createIssue({ cve: 'CVE-6' }),
];
const base = [
createIssue({ cve: 'CVE-1' }),
createIssue({ cve: 'CVE-2' }),
createIssue({ cve: 'CVE-3' }),
];
const enrichData = [];
const blobPath = 'blobPath';
beforeEach(() => {
state.isLoading = true;
});
describe('with only the head report', () => {
beforeEach(() => {
const reports = { head, enrichData };
mutations[types.RECEIVE_REPORTS](state, { reports, blobPath });
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should have the relevant `new` issues', () => {
expect(state.newIssues.length).toBe(4);
});
it('should not have any `resolved` issues', () => {
expect(state.resolvedIssues.length).toBe(0);
});
it('should not have any `all` issues', () => {
expect(state.allIssues.length).toBe(0);
});
});
describe('with the base and head reports', () => {
beforeEach(() => {
const reports = { head, base, enrichData };
mutations[types.RECEIVE_REPORTS](state, { reports, blobPath });
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should have the relevant `new` issues', () => {
expect(state.newIssues.length).toBe(3);
});
it('should have the relevant `resolved` issues', () => {
expect(state.resolvedIssues.length).toBe(2);
});
it('should have the relevant `all` issues', () => {
expect(state.allIssues.length).toBe(1);
});
});
});
});
......@@ -2,11 +2,6 @@ import state from 'ee/vue_shared/security_reports/store/state';
import mutations from 'ee/vue_shared/security_reports/store/mutations';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import {
sastIssues,
sastIssuesBase,
parsedSastIssuesHead,
parsedSastBaseStore,
parsedSastIssuesStore,
dependencyScanningIssuesOld,
dependencyScanningIssuesBase,
parsedDependencyScanningIssuesHead,
......@@ -91,69 +86,6 @@ describe('security reports mutations', () => {
});
});
describe('SET_SAST_HEAD_PATH', () => {
it('should set sast head path', () => {
mutations[types.SET_SAST_HEAD_PATH](stateCopy, 'sast_head_path');
expect(stateCopy.sast.paths.head).toEqual('sast_head_path');
});
});
describe('SET_SAST_BASE_PATH', () => {
it('sets sast base path', () => {
mutations[types.SET_SAST_BASE_PATH](stateCopy, 'sast_base_path');
expect(stateCopy.sast.paths.base).toEqual('sast_base_path');
});
});
describe('REQUEST_SAST_REPORTS', () => {
it('should set sast loading flag to true', () => {
mutations[types.REQUEST_SAST_REPORTS](stateCopy);
expect(stateCopy.sast.isLoading).toEqual(true);
});
});
describe('RECEIVE_SAST_REPORTS', () => {
describe('with head and base', () => {
it('should set new, fixed and all issues', () => {
mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'path');
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_SAST_REPORTS](stateCopy, {
head: sastIssues,
base: sastIssuesBase,
});
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.newIssues).toEqual(parsedSastIssuesHead);
expect(stateCopy.sast.resolvedIssues).toEqual(parsedSastBaseStore);
});
});
describe('with head', () => {
it('should set new issues', () => {
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_SAST_REPORTS](stateCopy, {
head: sastIssues,
});
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.newIssues).toEqual(parsedSastIssuesStore);
});
});
});
describe('RECEIVE_SAST_REPORTS_ERROR', () => {
it('should set loading flag to false and error flag to true for sast', () => {
mutations[types.RECEIVE_SAST_REPORTS_ERROR](stateCopy);
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.hasError).toEqual(true);
});
});
describe('SET_SAST_CONTAINER_HEAD_PATH', () => {
it('should set sast container head path', () => {
mutations[types.SET_SAST_CONTAINER_HEAD_PATH](stateCopy, 'head_path');
......@@ -646,50 +578,6 @@ describe('security reports mutations', () => {
});
});
describe('UPDATE_SAST_ISSUE', () => {
it('updates issue in the new issues list', () => {
stateCopy.sast.newIssues = parsedSastIssuesHead;
stateCopy.sast.resolvedIssues = [];
stateCopy.sast.allIssues = [];
const updatedIssue = {
...parsedSastIssuesHead[0],
foo: 'bar',
};
mutations[types.UPDATE_SAST_ISSUE](stateCopy, updatedIssue);
expect(stateCopy.sast.newIssues[0]).toEqual(updatedIssue);
});
it('updates issue in the resolved issues list', () => {
stateCopy.sast.newIssues = [];
stateCopy.sast.resolvedIssues = parsedSastIssuesHead;
stateCopy.sast.allIssues = [];
const updatedIssue = {
...parsedSastIssuesHead[0],
foo: 'bar',
};
mutations[types.UPDATE_SAST_ISSUE](stateCopy, updatedIssue);
expect(stateCopy.sast.resolvedIssues[0]).toEqual(updatedIssue);
});
it('updates issue in the all issues list', () => {
stateCopy.sast.newIssues = [];
stateCopy.sast.resolvedIssues = [];
stateCopy.sast.allIssues = parsedSastIssuesHead;
const updatedIssue = {
...parsedSastIssuesHead[0],
foo: 'bar',
};
mutations[types.UPDATE_SAST_ISSUE](stateCopy, updatedIssue);
expect(stateCopy.sast.allIssues[0]).toEqual(updatedIssue);
});
});
describe('UPDATE_DEPENDENCY_SCANNING_ISSUE', () => {
it('updates issue in the new issues list', () => {
stateCopy.dependencyScanning.newIssues = parsedDependencyScanningIssuesHead;
......
......@@ -10,6 +10,7 @@ import {
groupedTextBuilder,
statusIcon,
countIssues,
groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import {
......@@ -593,4 +594,32 @@ describe('security reports utils', () => {
});
});
});
describe('groupedReportText', () => {
const reportType = 'dummyReport';
const errorMessage = 'Something went wrong';
const loadingMessage = 'The report is still loading';
const baseReport = { paths: [] };
it("should return the error message when there's an error", () => {
const report = { ...baseReport, hasError: true };
const result = groupedReportText(report, reportType, errorMessage, loadingMessage);
expect(result).toBe(errorMessage);
});
it("should return the loading message when it's loading", () => {
const report = { ...baseReport, isLoading: true };
const result = groupedReportText(report, reportType, errorMessage, loadingMessage);
expect(result).toBe(loadingMessage);
});
it("should call groupedTextBuilder if it isn't loading and doesn't have an error", () => {
const report = { ...baseReport };
const result = groupedReportText(report, reportType, errorMessage, loadingMessage);
expect(result).toBe(`${reportType} detected no vulnerabilities for the source branch only`);
});
});
});
......@@ -3,7 +3,11 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import state from 'ee/vue_shared/security_reports/store/state';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import sastState from 'ee/vue_shared/security_reports/store/modules/sast/state';
import * as sastTypes from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { waitForMutation } from 'spec/helpers/vue_test_utils_helper';
import { trimText } from 'spec/helpers/text_helper';
import {
sastIssues,
......@@ -24,13 +28,16 @@ describe('Grouped security reports app', () => {
});
afterEach(() => {
vm.$store.replaceState(state());
vm.$store.replaceState({
...state(),
sast: sastState(),
});
vm.$destroy();
mock.restore();
});
describe('with error', () => {
beforeEach(() => {
beforeEach(done => {
mock.onGet('sast_head.json').reply(500);
mock.onGet('sast_base.json').reply(500);
mock.onGet('dast_head.json').reply(500);
......@@ -63,26 +70,32 @@ describe('Grouped security reports app', () => {
canCreateMergeRequest: true,
canDismissVulnerability: true,
});
Promise.all([
waitForMutation(vm.$store, `sast/${sastTypes.RECEIVE_REPORTS_ERROR}`),
waitForMutation(vm.$store, types.RECEIVE_SAST_CONTAINER_ERROR),
waitForMutation(vm.$store, types.RECEIVE_DAST_ERROR),
waitForMutation(vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_ERROR),
])
.then(done)
.catch();
});
it('renders loading state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning failed loading any results',
);
it('renders error state', () => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning failed loading any results',
);
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(trimText(vm.$el.textContent)).toContain('SAST: Loading resulted in an error');
expect(trimText(vm.$el.textContent)).toContain(
'Dependency scanning: Loading resulted in an error',
);
expect(trimText(vm.$el.textContent)).toContain('SAST: Loading resulted in an error');
expect(trimText(vm.$el.textContent)).toContain(
'Dependency scanning: Loading resulted in an error',
);
expect(vm.$el.textContent).toContain('Container scanning: Loading resulted in an error');
expect(vm.$el.textContent).toContain('DAST: Loading resulted in an error');
done();
}, 0);
expect(vm.$el.textContent).toContain('Container scanning: Loading resulted in an error');
expect(vm.$el.textContent).toContain('DAST: Loading resulted in an error');
});
});
......@@ -122,7 +135,7 @@ describe('Grouped security reports app', () => {
});
});
it('renders loading summary text + spinner', done => {
it('renders loading summary text + spinner', () => {
expect(vm.$el.querySelector('.spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning is loading',
......@@ -134,15 +147,11 @@ describe('Grouped security reports app', () => {
expect(vm.$el.textContent).toContain('Dependency scanning is loading');
expect(vm.$el.textContent).toContain('Container scanning is loading');
expect(vm.$el.textContent).toContain('DAST is loading');
setTimeout(() => {
done();
}, 0);
});
});
describe('with all reports', () => {
beforeEach(() => {
beforeEach(done => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('sast_base.json').reply(200, sastIssuesBase);
mock.onGet('dast_head.json').reply(200, dast);
......@@ -175,40 +184,46 @@ describe('Grouped security reports app', () => {
canCreateMergeRequest: true,
canDismissVulnerability: true,
});
Promise.all([
waitForMutation(vm.$store, `sast/${sastTypes.RECEIVE_REPORTS}`),
waitForMutation(vm.$store, types.RECEIVE_DAST_REPORTS),
waitForMutation(vm.$store, types.RECEIVE_SAST_CONTAINER_REPORTS),
waitForMutation(vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_REPORTS),
])
.then(done)
.catch();
});
it('renders reports', done => {
setTimeout(() => {
// It's not loading
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
it('renders reports', () => {
// It's not loading
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
// Renders the summary text
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 6 new, and 3 fixed vulnerabilities',
);
// Renders the summary text
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 6 new, and 3 fixed vulnerabilities',
);
// Renders the expand button
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
// Renders the expand button
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
// Renders Sast result
expect(trimText(vm.$el.textContent)).toContain(
'SAST detected 2 new, and 1 fixed vulnerabilities',
);
// Renders Sast result
expect(trimText(vm.$el.textContent)).toContain(
'SAST detected 2 new, and 1 fixed vulnerabilities',
);
// Renders DSS result
expect(trimText(vm.$el.textContent)).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities',
);
// Renders DSS result
expect(trimText(vm.$el.textContent)).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities',
);
// Renders container scanning result
expect(vm.$el.textContent).toContain(
'Container scanning detected 1 new, and 1 fixed vulnerabilities',
);
// Renders container scanning result
expect(vm.$el.textContent).toContain(
'Container scanning detected 1 new, and 1 fixed vulnerabilities',
);
// Renders DAST result
expect(vm.$el.textContent).toContain('DAST detected 1 new vulnerability');
done();
}, 0);
// Renders DAST result
expect(vm.$el.textContent).toContain('DAST detected 1 new vulnerability');
});
it('opens modal with more information', done => {
......
/* eslint-disable import/prefer-default-export */
const vNodeContainsText = (vnode, text) =>
(vnode.text && vnode.text.includes(text)) ||
(vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length);
......@@ -19,3 +17,19 @@ export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =
Boolean(
shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
);
/**
* Returns a promise that waits for a mutation to be fired before resolving
* NOTE: There's no reject action here so it will hang if it waits for a mutation that won't happen.
* @param {Object} store - The Vue store that contains the mutations
* @param {String} expectedMutationType - The Mutation to wait for
*/
export const waitForMutation = (store, expectedMutationType) =>
new Promise(resolve => {
const unsubscribe = store.subscribe(mutation => {
if (mutation.type === expectedMutationType) {
unsubscribe();
resolve();
}
});
});
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