Commit ddd9651c authored by Alexander Turinske's avatar Alexander Turinske

Add download patch functionality for vulnerability

- add ability to download the patch in the split button
- add tests
parent 42fce179
---
title: Add ability to download patch from vulnerability page
merge_request: 32000
author:
type: added
...@@ -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,21 @@ export default { ...@@ -69,12 +70,21 @@ 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 !== 'resolved' && !this.vulnerability.hasMr && this.hasRemediation
);
},
hasIssue() { hasIssue() {
return Boolean(this.finding.issue_feedback?.issue_iid); return Boolean(this.finding.issue_feedback?.issue_iid);
}, },
...@@ -200,6 +210,9 @@ export default { ...@@ -200,6 +210,9 @@ export default {
); );
}); });
}, },
downloadPatch() {
download({ fileData: this.finding.remediations[0].diff, fileName: `remediation.patch` });
},
}, },
}; };
</script> </script>
...@@ -248,6 +261,7 @@ export default { ...@@ -248,6 +261,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"
......
...@@ -41,6 +41,11 @@ export const HEADER_ACTION_BUTTONS = { ...@@ -41,6 +41,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 = {
......
...@@ -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,29 @@ describe('Vulnerability Header', () => { ...@@ -282,6 +286,29 @@ describe('Vulnerability Header', () => {
}); });
}); });
}); });
describe('can download 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();
expect(glDeprecatedButton.exists()).toBe(true);
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