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 = { ...@@ -777,6 +777,45 @@ const Api = {
return { data, headers }; 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; export default Api;
...@@ -20,6 +20,11 @@ export default { ...@@ -20,6 +20,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
hasLabel: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
hasDropdown() { hasDropdown() {
...@@ -38,13 +43,12 @@ export default { ...@@ -38,13 +43,12 @@ export default {
<template> <template>
<div> <div>
<strong>{{ s__('SecurityReports|Download Report') }}</strong> <strong v-if="hasLabel">{{ s__('SecurityReports|Download Report') }}</strong>
<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 +62,6 @@ export default { ...@@ -58,7 +62,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])"
> >
......
...@@ -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';
...@@ -17,6 +18,7 @@ import { mrStates } from '~/mr_popover/constants'; ...@@ -17,6 +18,7 @@ import { mrStates } from '~/mr_popover/constants';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql'; import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue'; import SecuritySummary from './components/security_summary.vue';
import Api from '~/api';
export default { export default {
store: createStore(), store: createStore(),
...@@ -30,6 +32,7 @@ export default { ...@@ -30,6 +32,7 @@ export default {
GlLink, GlLink,
DastModal, DastModal,
GlButton, GlButton,
FuzzingArtifactsDownload,
}, },
directives: { directives: {
'gl-modal': GlModalDirective, 'gl-modal': GlModalDirective,
...@@ -191,6 +194,7 @@ export default { ...@@ -191,6 +194,7 @@ export default {
'isDismissingVulnerability', 'isDismissingVulnerability',
'isCreatingMergeRequest', 'isCreatingMergeRequest',
]), ]),
...mapState('pipelineJobs', ['projectId']),
...mapGetters([ ...mapGetters([
'groupedSummaryText', 'groupedSummaryText',
'summaryStatus', 'summaryStatus',
...@@ -210,6 +214,7 @@ export default { ...@@ -210,6 +214,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`;
}, },
...@@ -311,10 +316,16 @@ export default { ...@@ -311,10 +316,16 @@ export default {
} }
const coverageFuzzingDiffEndpoint = gl?.mrWidgetData?.coverage_fuzzing_comparison_path; 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) { if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint); this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint);
this.fetchCoverageFuzzingDiff(); this.fetchCoverageFuzzingDiff();
this.setPipelineJobsPath(pipelineJobsPath);
this.setProjectId(gl?.mrWidgetData?.project_id);
this.fetchPipelineJobs();
} }
}, },
methods: { methods: {
...@@ -356,6 +367,7 @@ export default { ...@@ -356,6 +367,7 @@ export default {
setSastDiffEndpoint: 'setDiffEndpoint', setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastDiff: 'fetchDiff', fetchSastDiff: 'fetchDiff',
}), }),
...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']),
}, },
summarySlots: ['success', 'error', 'loading'], summarySlots: ['success', 'error', 'loading'],
}; };
...@@ -555,6 +567,12 @@ export default { ...@@ -555,6 +567,12 @@ 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"
:has-label="false"
/>
</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,
......
...@@ -20,3 +20,5 @@ ...@@ -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.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.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.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', () => { ...@@ -870,6 +870,7 @@ describe('Api', () => {
}); });
}); });
<<<<<<< HEAD
describe('Billable members list', () => { describe('Billable members list', () => {
let expectedUrl; let expectedUrl;
let namespaceId; let namespaceId;
...@@ -886,6 +887,20 @@ describe('Api', () => { ...@@ -886,6 +887,20 @@ describe('Api', () => {
return Api.fetchBillableGroupMembersList(namespaceId).then(({ data }) => { return Api.fetchBillableGroupMembersList(namespaceId).then(({ data }) => {
expect(data).toEqual([]); 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 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';
...@@ -78,6 +78,7 @@ describe('Grouped security reports app', () => { ...@@ -78,6 +78,7 @@ describe('Grouped security reports app', () => {
}, },
}, },
}, },
store: appStore(),
}); });
}; };
...@@ -87,10 +88,6 @@ describe('Grouped security reports app', () => { ...@@ -87,10 +88,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();
}); });
...@@ -116,6 +113,7 @@ describe('Grouped security reports app', () => { ...@@ -116,6 +113,7 @@ describe('Grouped security reports app', () => {
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT; gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT; gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.coverage_fuzzing_comparison_path = COVERAGE_FUZZING_DIFF_ENDPOINT; gl.mrWidgetData.coverage_fuzzing_comparison_path = COVERAGE_FUZZING_DIFF_ENDPOINT;
gl.mrWidgetData.pipeline_jobs_path = PIPELINE_JOBS_ENDPOINT;
}); });
describe('with error', () => { describe('with error', () => {
...@@ -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