Commit fca02890 authored by Mark Florian's avatar Mark Florian

Merge branch '254711-fuzzing-download-mr-widget' into 'master'

Implement fuzzing artifcact download

See merge request gitlab-org/gitlab!44263
parents 64b58410 493c8945
...@@ -38,13 +38,12 @@ export default { ...@@ -38,13 +38,12 @@ export default {
<template> <template>
<div> <div>
<strong>{{ s__('SecurityReports|Download Report') }}</strong> <slot name="label"></slot>
<gl-dropdown <gl-dropdown
v-if="hasDropdown" v-if="hasDropdown"
class="d-block mt-1" class="d-block mt-1"
:text="$options.i18n.FUZZING_ARTIFACTS" :text="$options.i18n.FUZZING_ARTIFACTS"
category="secondary" category="secondary"
variant="info"
size="small" size="small"
> >
<gl-deprecated-dropdown-item <gl-deprecated-dropdown-item
...@@ -58,7 +57,6 @@ export default { ...@@ -58,7 +57,6 @@ export default {
v-else v-else
class="d-block mt-1" class="d-block mt-1"
category="secondary" category="secondary"
variant="info"
size="small" size="small"
:href="artifactDownloadUrl(jobs[0])" :href="artifactDownloadUrl(jobs[0])"
> >
......
...@@ -167,7 +167,11 @@ export default { ...@@ -167,7 +167,11 @@ export default {
<vulnerability-count-list v-if="shouldShowCountList" /> <vulnerability-count-list v-if="shouldShowCountList" />
<filters> <filters>
<template v-if="hasFuzzingArtifacts" #buttons> <template v-if="hasFuzzingArtifacts" #buttons>
<fuzzing-artifacts-download :jobs="fuzzingJobsWithArtifact" :project-id="projectId" /> <fuzzing-artifacts-download :jobs="fuzzingJobsWithArtifact" :project-id="projectId">
<template #label>
<strong>{{ s__('SecurityReports|Download Report') }}</strong>
</template>
</fuzzing-artifacts-download>
</template> </template>
</filters> </filters>
</template> </template>
......
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setPipelineJobsPath = ({ commit }, path) => commit(types.SET_PIPELINE_JOBS_PATH, path); export const setPipelineJobsPath = ({ commit }, path) => commit(types.SET_PIPELINE_JOBS_PATH, path);
export const setProjectId = ({ commit }, id) => commit(types.SET_PROJECT_ID, id); export const setProjectId = ({ commit }, id) => commit(types.SET_PROJECT_ID, id);
export const setPipelineId = ({ commit }, id) => commit(types.SET_PIPELINE_ID, id);
export const fetchPipelineJobs = ({ commit, state }) => { export const fetchPipelineJobs = ({ commit, state }) => {
if (!state.pipelineJobsPath) { if (!state.pipelineJobsPath && !(state.projectId && state.pipelineId)) {
return commit(types.RECEIVE_PIPELINE_JOBS_ERROR); return commit(types.RECEIVE_PIPELINE_JOBS_ERROR);
} }
commit(types.REQUEST_PIPELINE_JOBS); commit(types.REQUEST_PIPELINE_JOBS);
return axios({ let requestPromise;
// Support existing usages that rely on server provided path,
// otherwise generate client side
if (state.pipelineJobsPath) {
requestPromise = axios({
method: 'GET', method: 'GET',
url: state.pipelineJobsPath, url: state.pipelineJobsPath,
}) });
} else {
requestPromise = Api.pipelineJobs(state.projectId, state.pipelineId);
}
return requestPromise
.then(response => { .then(response => {
const { data } = response; const { data } = response;
commit(types.RECEIVE_PIPELINE_JOBS_SUCCESS, data); commit(types.RECEIVE_PIPELINE_JOBS_SUCCESS, data);
......
export const SET_PIPELINE_JOBS_PATH = 'SET_PIPELINE_JOBS_PATH'; export const SET_PIPELINE_JOBS_PATH = 'SET_PIPELINE_JOBS_PATH';
export const SET_PROJECT_ID = 'SET_PROJECT_ID '; export const SET_PROJECT_ID = 'SET_PROJECT_ID ';
export const SET_PIPELINE_ID = 'SET_PIPELINE_ID ';
export const REQUEST_PIPELINE_JOBS = 'REQUEST_PIPELINE_JOBS'; export const REQUEST_PIPELINE_JOBS = 'REQUEST_PIPELINE_JOBS';
export const RECEIVE_PIPELINE_JOBS_SUCCESS = 'RECEIVE_PIPELINE_JOBS_SUCESS'; export const RECEIVE_PIPELINE_JOBS_SUCCESS = 'RECEIVE_PIPELINE_JOBS_SUCESS';
......
...@@ -7,6 +7,9 @@ export default { ...@@ -7,6 +7,9 @@ export default {
[types.SET_PROJECT_ID](state, payload) { [types.SET_PROJECT_ID](state, payload) {
state.projectId = payload; state.projectId = payload;
}, },
[types.SET_PIPELINE_ID](state, payload) {
state.pipelineId = payload;
},
[types.REQUEST_PIPELINE_JOBS](state) { [types.REQUEST_PIPELINE_JOBS](state) {
state.isLoading = true; state.isLoading = true;
}, },
......
export default () => ({ export default () => ({
projectId: undefined, projectId: undefined,
pipelineId: undefined,
pipelineJobsPath: '', pipelineJobsPath: '',
isLoading: false, isLoading: false,
pipelineJobs: [], pipelineJobs: [],
......
...@@ -326,6 +326,7 @@ export default { ...@@ -326,6 +326,7 @@ export default {
:pipeline-path="mr.pipeline.path" :pipeline-path="mr.pipeline.path"
:pipeline-id="mr.securityReportsPipelineId" :pipeline-id="mr.securityReportsPipelineId"
:pipeline-iid="mr.securityReportsPipelineIid" :pipeline-iid="mr.securityReportsPipelineIid"
:project-id="mr.targetProjectId"
:project-full-path="mr.sourceProjectFullPath" :project-full-path="mr.sourceProjectFullPath"
:diverged-commits-count="mr.divergedCommitsCount" :diverged-commits-count="mr.divergedCommitsCount"
:mr-state="mr.state" :mr-state="mr.state"
......
...@@ -4,6 +4,7 @@ import { once } from 'lodash'; ...@@ -4,6 +4,7 @@ import { once } from 'lodash';
import { componentNames } from 'ee/reports/components/issue_body'; import { componentNames } from 'ee/reports/components/issue_body';
import { GlButton, GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants'; import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import FuzzingArtifactsDownload from 'ee/security_dashboard/components/fuzzing_artifacts_download.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue'; import SummaryRow from '~/reports/components/summary_row.vue';
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
GlLink, GlLink,
DastModal, DastModal,
GlButton, GlButton,
FuzzingArtifactsDownload,
}, },
directives: { directives: {
'gl-modal': GlModalDirective, 'gl-modal': GlModalDirective,
...@@ -171,6 +173,11 @@ export default { ...@@ -171,6 +173,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
projectId: {
type: Number,
required: false,
default: null,
},
projectFullPath: { projectFullPath: {
type: String, type: String,
required: true, required: true,
...@@ -210,6 +217,7 @@ export default { ...@@ -210,6 +217,7 @@ export default {
'canDismissVulnerability', 'canDismissVulnerability',
]), ]),
...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']), ...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
securityTab() { securityTab() {
return `${this.pipelinePath}/security`; return `${this.pipelinePath}/security`;
}, },
...@@ -274,7 +282,9 @@ export default { ...@@ -274,7 +282,9 @@ export default {
this.createVulnerabilityFeedbackMergeRequestPath, this.createVulnerabilityFeedbackMergeRequestPath,
); );
this.setCreateVulnerabilityFeedbackDismissalPath(this.createVulnerabilityFeedbackDismissalPath); this.setCreateVulnerabilityFeedbackDismissalPath(this.createVulnerabilityFeedbackDismissalPath);
this.setProjectId(this.projectId);
this.setPipelineId(this.pipelineId); this.setPipelineId(this.pipelineId);
this.setPipelineJobsId(this.pipelineId);
const sastDiffEndpoint = gl?.mrWidgetData?.sast_comparison_path; const sastDiffEndpoint = gl?.mrWidgetData?.sast_comparison_path;
...@@ -315,6 +325,7 @@ export default { ...@@ -315,6 +325,7 @@ export default {
if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) { if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint); this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint);
this.fetchCoverageFuzzingDiff(); this.fetchCoverageFuzzingDiff();
this.fetchPipelineJobs();
} }
}, },
methods: { methods: {
...@@ -356,6 +367,10 @@ export default { ...@@ -356,6 +367,10 @@ export default {
setSastDiffEndpoint: 'setDiffEndpoint', setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastDiff: 'fetchDiff', fetchSastDiff: 'fetchDiff',
}), }),
...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']),
...mapActions('pipelineJobs', {
setPipelineJobsId: 'setPipelineId',
}),
}, },
summarySlots: ['success', 'error', 'loading'], summarySlots: ['success', 'error', 'loading'],
}; };
...@@ -555,6 +570,11 @@ export default { ...@@ -555,6 +570,11 @@ export default {
<template #summary> <template #summary>
<security-summary :message="groupedCoverageFuzzingText" /> <security-summary :message="groupedCoverageFuzzingText" />
</template> </template>
<fuzzing-artifacts-download
v-if="hasFuzzingArtifacts"
:jobs="fuzzingJobsWithArtifact"
:project-id="projectId"
/>
</summary-row> </summary-row>
<grouped-issues-list <grouped-issues-list
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import pipelineJobs from 'ee/security_dashboard/store/modules/pipeline_jobs';
import configureMediator from './mediator'; import configureMediator from './mediator';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
...@@ -14,6 +15,7 @@ export default () => ...@@ -14,6 +15,7 @@ export default () =>
new Vuex.Store({ new Vuex.Store({
modules: { modules: {
sast, sast,
pipelineJobs,
}, },
actions, actions,
getters, getters,
......
---
title: Update fuzzing download button styles
merge_request: 44263
author:
type: changed
...@@ -54,6 +54,26 @@ describe('pipeling jobs actions', () => { ...@@ -54,6 +54,26 @@ describe('pipeling jobs actions', () => {
}); });
}); });
describe('setPipelineId', () => {
const pipelineId = 123;
it('should commit the SET_PIPELINE_ID mutation', done => {
testAction(
actions.setPipelineId,
pipelineId,
state,
[
{
type: types.SET_PIPELINE_ID,
payload: pipelineId,
},
],
[],
done,
);
});
});
describe('fetchPipelineJobs', () => { describe('fetchPipelineJobs', () => {
let mock; let mock;
const jobs = [{}, {}]; const jobs = [{}, {}];
...@@ -67,7 +87,7 @@ describe('pipeling jobs actions', () => { ...@@ -67,7 +87,7 @@ describe('pipeling jobs actions', () => {
mock.restore(); mock.restore();
}); });
describe('on success', () => { describe('on success with pipeline path', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(state.pipelineJobsPath).replyOnce(200, jobs); mock.onGet(state.pipelineJobsPath).replyOnce(200, jobs);
}); });
...@@ -90,6 +110,33 @@ describe('pipeling jobs actions', () => { ...@@ -90,6 +110,33 @@ describe('pipeling jobs actions', () => {
}); });
}); });
describe('on success with pipeline id and project id', () => {
beforeEach(() => {
mock.onGet('/api/undefined/projects/123/pipelines/321/jobs').replyOnce(200, jobs);
});
it('should commit the request and success mutations', done => {
state.pipelineJobsPath = '';
state.projectId = 123;
state.pipelineId = 321;
testAction(
actions.fetchPipelineJobs,
{},
state,
[
{ type: types.REQUEST_PIPELINE_JOBS },
{
type: types.RECEIVE_PIPELINE_JOBS_SUCCESS,
payload: jobs,
},
],
[],
done,
);
});
});
describe('without pipelineJobsPath set', () => { describe('without pipelineJobsPath set', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(state.pipelineJobsPath).replyOnce(200, jobs); mock.onGet(state.pipelineJobsPath).replyOnce(200, jobs);
...@@ -113,6 +160,31 @@ describe('pipeling jobs actions', () => { ...@@ -113,6 +160,31 @@ describe('pipeling jobs actions', () => {
}); });
}); });
describe('without projectId or pipelineId set', () => {
beforeEach(() => {
mock.onGet(state.pipelineJobsPath).replyOnce(200, jobs);
});
it('should commit RECEIVE_PIPELINE_JOBS_ERROR mutation', done => {
state.pipelineJobsPath = '';
state.projectId = undefined;
state.pipelineId = undefined;
testAction(
actions.fetchPipelineJobs,
{},
state,
[
{
type: types.RECEIVE_PIPELINE_JOBS_ERROR,
},
],
[],
done,
);
});
});
describe('with server error', () => { describe('with server error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(state.pipelineJobsPath).replyOnce(404); mock.onGet(state.pipelineJobsPath).replyOnce(404);
......
...@@ -26,6 +26,15 @@ describe('pipeline jobs module mutations', () => { ...@@ -26,6 +26,15 @@ describe('pipeline jobs module mutations', () => {
}); });
}); });
describe('SET_PIPELINE_ID', () => {
const pipelineId = 123;
it(`should set the pipelineId to ${pipelineId}`, () => {
mutations[types.SET_PIPELINE_ID](state, pipelineId);
expect(state.pipelineId).toBe(pipelineId);
});
});
describe('REQUEST_PIPELINE_JOBS', () => { describe('REQUEST_PIPELINE_JOBS', () => {
it('should set the isLoading to true', () => { it('should set the isLoading to true', () => {
mutations[types.REQUEST_PIPELINE_JOBS](state); mutations[types.REQUEST_PIPELINE_JOBS](state);
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue'; import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import state from 'ee/vue_shared/security_reports/store/state'; import appStore from 'ee/vue_shared/security_reports/store';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types'; 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 * as sastTypes from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { waitForMutation } from 'helpers/vue_test_utils_helper'; import { waitForMutation } from 'helpers/vue_test_utils_helper';
...@@ -29,6 +28,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json'; ...@@ -29,6 +28,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json';
const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json'; const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json';
const DAST_DIFF_ENDPOINT = 'dast.json'; const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json'; const SAST_DIFF_ENDPOINT = 'sast.json';
const PIPELINE_JOBS_ENDPOINT = 'jobs.json';
const SECRET_SCANNING_DIFF_ENDPOINT = 'secret_detection.json'; const SECRET_SCANNING_DIFF_ENDPOINT = 'secret_detection.json';
const COVERAGE_FUZZING_DIFF_ENDPOINT = 'coverage_fuzzing.json'; const COVERAGE_FUZZING_DIFF_ENDPOINT = 'coverage_fuzzing.json';
...@@ -51,6 +51,7 @@ describe('Grouped security reports app', () => { ...@@ -51,6 +51,7 @@ describe('Grouped security reports app', () => {
vulnerabilityFeedbackHelpPath: 'path', vulnerabilityFeedbackHelpPath: 'path',
coverageFuzzingHelpPath: 'path', coverageFuzzingHelpPath: 'path',
pipelineId: 123, pipelineId: 123,
projectId: 321,
projectFullPath: 'path', projectFullPath: 'path',
}; };
...@@ -78,6 +79,7 @@ describe('Grouped security reports app', () => { ...@@ -78,6 +79,7 @@ describe('Grouped security reports app', () => {
}, },
}, },
}, },
store: appStore(),
}); });
}; };
...@@ -87,10 +89,6 @@ describe('Grouped security reports app', () => { ...@@ -87,10 +89,6 @@ describe('Grouped security reports app', () => {
}); });
afterEach(() => { afterEach(() => {
wrapper.vm.$store.replaceState({
...state(),
sast: sastState(),
});
wrapper.vm.$destroy(); wrapper.vm.$destroy();
mock.restore(); mock.restore();
}); });
...@@ -169,6 +167,7 @@ describe('Grouped security reports app', () => { ...@@ -169,6 +167,7 @@ describe('Grouped security reports app', () => {
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(PIPELINE_JOBS_ENDPOINT).reply(200, {});
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, {}); mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {}); mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {}); mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {});
...@@ -606,7 +605,7 @@ describe('Grouped security reports app', () => { ...@@ -606,7 +605,7 @@ describe('Grouped security reports app', () => {
}); });
}); });
it(`${shouldShowFuzzing ? 'renders' : 'does not render'}`, () => { it(`${shouldShowFuzzing ? 'renders' : 'does not render'} security row`, () => {
expect(wrapper.find('[data-qa-selector="coverage_fuzzing_report"]').exists()).toBe( expect(wrapper.find('[data-qa-selector="coverage_fuzzing_report"]').exists()).toBe(
shouldShowFuzzing, shouldShowFuzzing,
); );
......
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