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