Commit f6697c96 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '216651-create-download-patch-from-vulnerability' into 'master'

Add download patch functionality for vulnerability

See merge request gitlab-org/gitlab!32000
parents 00e41c41 d7f51165
......@@ -2,6 +2,7 @@
import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils';
import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { s__ } from '~/locale';
......@@ -69,12 +70,23 @@ export default {
buttons.push(HEADER_ACTION_BUTTONS.mergeRequestCreation);
}
if (this.canDownloadPatch) {
buttons.push(HEADER_ACTION_BUTTONS.patchDownload);
}
if (!this.hasIssue) {
buttons.push(HEADER_ACTION_BUTTONS.issueCreation);
}
return buttons;
},
canDownloadPatch() {
return (
this.vulnerability.state !== VULNERABILITY_STATE_OBJECTS.resolved.state &&
!this.vulnerability.hasMr &&
this.hasRemediation
);
},
hasIssue() {
return Boolean(this.finding.issue_feedback?.issue_iid);
},
......@@ -95,7 +107,8 @@ export default {
},
showResolutionAlert() {
return (
this.vulnerability.resolved_on_default_branch && this.vulnerability.state !== 'resolved'
this.vulnerability.resolved_on_default_branch &&
this.vulnerability.state !== VULNERABILITY_STATE_OBJECTS.resolved.state
);
},
},
......@@ -200,6 +213,9 @@ export default {
);
});
},
downloadPatch() {
download({ fileData: this.finding.remediations[0].diff, fileName: `remediation.patch` });
},
},
};
</script>
......@@ -248,6 +264,7 @@ export default {
class="js-split-button"
@createMergeRequest="createMergeRequest"
@createIssue="createIssue"
@downloadPatch="downloadPatch"
/>
<gl-deprecated-button
v-else-if="actionButtons.length > 0"
......
......@@ -3,18 +3,21 @@ import { s__ } from '~/locale';
export const VULNERABILITY_STATE_OBJECTS = {
dismissed: {
action: 'dismiss',
state: 'dismissed',
statusBoxStyle: 'upcoming',
displayName: s__('VulnerabilityManagement|Dismiss'),
description: s__('VulnerabilityManagement|Will not fix or a false-positive'),
},
confirmed: {
action: 'confirm',
state: 'confirmed',
statusBoxStyle: 'closed',
displayName: s__('VulnerabilityManagement|Confirm'),
description: s__('VulnerabilityManagement|A true-positive and will fix'),
},
resolved: {
action: 'resolve',
state: 'resolved',
statusBoxStyle: 'open',
displayName: s__('VulnerabilityManagement|Resolved'),
description: s__('VulnerabilityManagement|Verified as fixed or mitigated'),
......@@ -41,6 +44,11 @@ export const HEADER_ACTION_BUTTONS = {
tagline: s__('ciReport|Automatically apply the patch in a new branch'),
action: 'createMergeRequest',
},
patchDownload: {
name: s__('ciReport|Download patch to resolve'),
tagline: s__('ciReport|Download the patch to apply it manually'),
action: 'downloadPatch',
},
};
export const FEEDBACK_TYPES = {
......
---
title: Add ability to download patch from vulnerability page
merge_request: 32000
author:
type: added
......@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import download from '~/lib/utils/downloader';
import * as urlUtility from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import Header from 'ee/vulnerabilities/components/header.vue';
......@@ -18,6 +19,7 @@ import { FEEDBACK_TYPES, VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/
const vulnerabilityStateEntries = Object.entries(VULNERABILITY_STATE_OBJECTS);
const mockAxios = new MockAdapter(axios);
jest.mock('~/flash');
jest.mock('~/lib/utils/downloader');
describe('Vulnerability Header', () => {
let wrapper;
......@@ -75,10 +77,11 @@ describe('Vulnerability Header', () => {
const findResolutionAlert = () => wrapper.find(ResolutionAlert);
const findStatusDescription = () => wrapper.find(StatusDescription);
const createWrapper = ({ vulnerability = {}, finding = getFinding({}) }) => {
const createWrapper = ({ vulnerability = {}, finding = getFinding({}), props = {} }) => {
wrapper = shallowMount(Header, {
propsData: {
...dataset,
...props,
initialVulnerability: { ...defaultVulnerability, ...vulnerability },
finding,
},
......@@ -163,9 +166,10 @@ describe('Vulnerability Header', () => {
});
expect(findSplitButton().exists()).toBe(true);
const buttons = findSplitButton().props('buttons');
expect(buttons).toHaveLength(2);
expect(buttons).toHaveLength(3);
expect(buttons[0].name).toBe('Resolve with merge request');
expect(buttons[1].name).toBe('Create issue');
expect(buttons[1].name).toBe('Download patch to resolve');
expect(buttons[2].name).toBe('Create issue');
});
it('does not render the split button if there is only one action', () => {
......@@ -282,6 +286,28 @@ describe('Vulnerability Header', () => {
});
});
});
describe('can download patch', () => {
beforeEach(() => {
createWrapper({
finding: getFinding({ shouldShowMergeRequestButton: true }),
props: { createMrUrl: '' },
});
});
it('only renders the download patch button', () => {
expect(findGlDeprecatedButton().exists()).toBe(true);
expect(findGlDeprecatedButton().text()).toBe('Download patch to resolve');
});
it('emits downloadPatch when download patch button is clicked', () => {
const glDeprecatedButton = findGlDeprecatedButton();
glDeprecatedButton.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(download).toHaveBeenCalledWith({ fileData: diff, fileName: `remediation.patch` });
});
});
});
});
describe('state badge', () => {
......
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