Commit 3c6da329 authored by Fernando's avatar Fernando

Implement fuzzing artifcact download

* Add utility function
* Pass in stubbed url
* Render component

Update unit tests

* Re-factor how store is initialized
* Mock out ajax request

Set default value for when apiPath is undefined

* Fixes failing unit tests

Add unit tests for api helper

* Cover different test cases for api util function

Run prettier to fix pipeline

* Fix formatting
parent d415bf72
......@@ -777,6 +777,45 @@ const Api = {
return { data, headers };
});
},
/**
* Generates a dynamic route URL based off a server provided stubbed URL
*
* Example:
*
* User provides string from
*
* expose_path(api_v4_projects_pipelines_jobs_fuzing_artifacts_path(id: project.id, job_ref: ':job_ref'))
*
* User calls:
*
* getApiPath(this.fuzzingArtifactsPath, {
* keys: { ':job_ref': job.ref },
* params: {job: job.name}
* }
*
* Output is:
*
* https://gitlab.com/api/v4/projects/19413496/jobs/artifacts/refs/merge-requests/5/head/download?job=my_fuzz_target
*
* @param {String} path The stubbed url
* @param {Object} keys The stubbed values to replace
* @param {Object} params The url query params
* @returns {String} The dynamically generated URL
*/
getApiPath(path = '', { keys, params }) {
if (path) {
let outputPath = path;
Object.entries(keys).forEach(([key, value]) => {
outputPath = outputPath.replace(key, value);
});
return params
? `${outputPath}?${Object.keys(params)
.map(key => `${key}=${params[key]}`)
.join('&')}`
: outputPath;
}
return path;
},
};
export default Api;
......@@ -20,6 +20,11 @@ export default {
type: Number,
required: true,
},
hasLabel: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
hasDropdown() {
......@@ -38,13 +43,12 @@ export default {
<template>
<div>
<strong>{{ s__('SecurityReports|Download Report') }}</strong>
<strong v-if="hasLabel">{{ s__('SecurityReports|Download Report') }}</strong>
<gl-dropdown
v-if="hasDropdown"
class="d-block mt-1"
:text="$options.i18n.FUZZING_ARTIFACTS"
category="secondary"
variant="info"
size="small"
>
<gl-deprecated-dropdown-item
......@@ -58,7 +62,6 @@ export default {
v-else
class="d-block mt-1"
category="secondary"
variant="info"
size="small"
:href="artifactDownloadUrl(jobs[0])"
>
......
......@@ -4,6 +4,7 @@ import { once } from 'lodash';
import { componentNames } from 'ee/reports/components/issue_body';
import { GlButton, GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui';
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 ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue';
......@@ -17,6 +18,7 @@ import { mrStates } from '~/mr_popover/constants';
import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue';
import Api from '~/api';
export default {
store: createStore(),
......@@ -30,6 +32,7 @@ export default {
GlLink,
DastModal,
GlButton,
FuzzingArtifactsDownload,
},
directives: {
'gl-modal': GlModalDirective,
......@@ -191,6 +194,7 @@ export default {
'isDismissingVulnerability',
'isCreatingMergeRequest',
]),
...mapState('pipelineJobs', ['projectId']),
...mapGetters([
'groupedSummaryText',
'summaryStatus',
......@@ -210,6 +214,7 @@ export default {
'canDismissVulnerability',
]),
...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
securityTab() {
return `${this.pipelinePath}/security`;
},
......@@ -311,10 +316,16 @@ export default {
}
const coverageFuzzingDiffEndpoint = gl?.mrWidgetData?.coverage_fuzzing_comparison_path;
const pipelineJobsPath = Api.getApiPath(gl?.mrWidgetData?.pipeline_jobs_path, {
keys: { ':pipeline_id': gl?.mrWidgetData?.pipeline_id },
});
if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint);
this.fetchCoverageFuzzingDiff();
this.setPipelineJobsPath(pipelineJobsPath);
this.setProjectId(gl?.mrWidgetData?.project_id);
this.fetchPipelineJobs();
}
},
methods: {
......@@ -356,6 +367,7 @@ export default {
setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastDiff: 'fetchDiff',
}),
...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']),
},
summarySlots: ['success', 'error', 'loading'],
};
......@@ -555,6 +567,12 @@ export default {
<template #summary>
<security-summary :message="groupedCoverageFuzzingText" />
</template>
<fuzzing-artifacts-download
v-if="hasFuzzingArtifacts"
:jobs="fuzzingJobsWithArtifact"
:project-id="projectId"
:has-label="false"
/>
</summary-row>
<grouped-issues-list
......
import Vue from 'vue';
import Vuex from 'vuex';
import pipelineJobs from 'ee/security_dashboard/store/modules/pipeline_jobs';
import configureMediator from './mediator';
import * as actions from './actions';
import * as getters from './getters';
......@@ -14,6 +15,7 @@ export default () =>
new Vuex.Store({
modules: {
sast,
pipelineJobs,
},
actions,
getters,
......
......@@ -20,3 +20,5 @@
window.gl.mrWidgetData.sast_comparison_path = '#{sast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:sast)}'
window.gl.mrWidgetData.dast_comparison_path = '#{dast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.secret_scanning_comparison_path = '#{secret_detection_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.pipeline_jobs_path = '#{expose_path(api_v4_projects_pipelines_jobs_path(id: @project.id, pipeline_id: ":pipeline_id"))}'
window.gl.mrWidgetData.project_id = #{@project.id}
......@@ -870,6 +870,7 @@ describe('Api', () => {
});
});
<<<<<<< HEAD
describe('Billable members list', () => {
let expectedUrl;
let namespaceId;
......@@ -886,6 +887,20 @@ describe('Api', () => {
return Api.fetchBillableGroupMembersList(namespaceId).then(({ data }) => {
expect(data).toEqual([]);
});
=======
describe('getApiPath', () => {
describe.each`
path | keys | params | expected
${undefined} | ${{ ':user_id': 4 }} | ${{ project_id: 3 }} | ${''}
${'/foo/bar'} | ${{ ':user_id': 4 }} | ${{ project_id: 3 }} | ${'/foo/bar?project_id=3'}
${'/:user_id/bar'} | ${{ ':user_id': 4 }} | ${{ project_id: 3 }} | ${'/4/bar?project_id=3'}
${'/:wrong_id/bar'} | ${{ ':user_id': 4 }} | ${{ project_id: 3 }} | ${'/:wrong_id/bar?project_id=3'}
${'/:first_id/bar/:second_id'} | ${{ ':first_id': 1, ':second_id': 2 }} | ${{ project_id: 3 }} | ${'/1/bar/2?project_id=3'}
${'/:first_id/bar/:second_id'} | ${{ ':first_id': 1, ':second_id': 2 }} | ${{ project_id: 3, user_id: 7 }} | ${'/1/bar/2?project_id=3&user_id=7'}
`('When path: $path, keys: $keys, params: $params', ({ path, keys, params, expected }) => {
it(`returns ${expected}`, () => {
expect(Api.getApiPath(path, { keys, params })).toBe(expected);
>>>>>>> 55722995fad... Add unit tests for api helper
});
});
});
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
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 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 { mount } from '@vue/test-utils';
import { waitForMutation } from 'helpers/vue_test_utils_helper';
......@@ -29,6 +28,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json';
const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json';
const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json';
const PIPELINE_JOBS_ENDPOINT = 'jobs.json';
const SECRET_SCANNING_DIFF_ENDPOINT = 'secret_detection.json';
const COVERAGE_FUZZING_DIFF_ENDPOINT = 'coverage_fuzzing.json';
......@@ -78,6 +78,7 @@ describe('Grouped security reports app', () => {
},
},
},
store: appStore(),
});
};
......@@ -87,10 +88,6 @@ describe('Grouped security reports app', () => {
});
afterEach(() => {
wrapper.vm.$store.replaceState({
...state(),
sast: sastState(),
});
wrapper.vm.$destroy();
mock.restore();
});
......@@ -116,6 +113,7 @@ describe('Grouped security reports app', () => {
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.coverage_fuzzing_comparison_path = COVERAGE_FUZZING_DIFF_ENDPOINT;
gl.mrWidgetData.pipeline_jobs_path = PIPELINE_JOBS_ENDPOINT;
});
describe('with error', () => {
......@@ -169,6 +167,7 @@ describe('Grouped security reports app', () => {
describe('while loading', () => {
beforeEach(() => {
mock.onGet(PIPELINE_JOBS_ENDPOINT).reply(200, {});
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {});
......@@ -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(
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