Commit f5f3ca18 authored by Jonathan Schafer's avatar Jonathan Schafer Committed by Mayra Cabrera

Use issue creation for vulnerabilities

This MR changes the call that is used by the front end
to allow the user to edit an issue before creation from
a vulnerability.
parent 473ee64b
...@@ -47,9 +47,9 @@ and allows you to comment on a change. ...@@ -47,9 +47,9 @@ and allows you to comment on a change.
You can create an issue for a vulnerability by selecting the **Create issue** button. You can create an issue for a vulnerability by selecting the **Create issue** button.
This creates a [confidential issue](../../project/issues/confidential_issues.md) in the This allows the user to create a [confidential issue](../../project/issues/confidential_issues.md)
project the vulnerability came from and pre-populates it with useful information from in the project the vulnerability came from. Fields are pre-populated with pertinent information
the vulnerability report. After the issue is created, GitLab redirects you to the from the vulnerability report. After the issue is created, GitLab redirects you to the
issue page so you can edit, assign, or comment on the issue. issue page so you can edit, assign, or comment on the issue.
## Link issues to the vulnerability ## Link issues to the vulnerability
......
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { GlButton, GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import RelatedIssuesStore from '~/related_issues/stores/related_issues_store'; import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue'; import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants'; import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
...@@ -15,9 +15,6 @@ export default { ...@@ -15,9 +15,6 @@ export default {
components: { components: {
RelatedIssuesBlock, RelatedIssuesBlock,
GlButton, GlButton,
GlAlert,
GlSprintf,
GlLink,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -60,7 +57,7 @@ export default { ...@@ -60,7 +57,7 @@ export default {
return Boolean(this.state.relatedIssues.find(i => i.lockIssueRemoval)); return Boolean(this.state.relatedIssues.find(i => i.lockIssueRemoval));
}, },
canCreateIssue() { canCreateIssue() {
return !this.isIssueAlreadyCreated && !this.isFetching && Boolean(this.createIssueUrl); return !this.isIssueAlreadyCreated && !this.isFetching && Boolean(this.newIssueUrl);
}, },
}, },
inject: { inject: {
...@@ -70,7 +67,7 @@ export default { ...@@ -70,7 +67,7 @@ export default {
projectFingerprint: { projectFingerprint: {
default: '', default: '',
}, },
createIssueUrl: { newIssueUrl: {
default: '', default: '',
}, },
reportType: { reportType: {
...@@ -89,17 +86,7 @@ export default { ...@@ -89,17 +86,7 @@ export default {
methods: { methods: {
createIssue() { createIssue() {
this.isProcessingAction = true; this.isProcessingAction = true;
this.errorCreatingIssue = false; redirectTo(this.newIssueUrl, { params: { vulnerability_id: this.vulnerabilityId } });
return axios
.post(this.createIssueUrl)
.then(({ data: { web_url } }) => {
redirectTo(web_url);
})
.catch(() => {
this.isProcessingAction = false;
this.errorCreatingIssue = true;
});
}, },
toggleFormVisibility() { toggleFormVisibility() {
this.isFormVisible = !this.isFormVisible; this.isFormVisible = !this.isFormVisible;
...@@ -218,28 +205,6 @@ export default { ...@@ -218,28 +205,6 @@ export default {
<template> <template>
<div> <div>
<gl-alert
v-if="errorCreatingIssue"
variant="danger"
class="gl-mt-5"
@dismiss="errorCreatingIssue = false"
>
<p class="gl-font-weight-bold gl-mb-2">{{ $options.i18n.createIssueErrorTitle }}</p>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.createIssueErrorBody">
<template #tracking="{ content }">
<gl-link class="gl-display-inline-block" :href="issueTrackingHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #permissions="{ content }">
<gl-link class="gl-display-inline-block" :href="permissionsHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</gl-alert>
<related-issues-block <related-issues-block
:help-path="helpPath" :help-path="helpPath"
:is-fetching="isFetching" :is-fetching="isFetching"
......
...@@ -15,7 +15,7 @@ export default el => { ...@@ -15,7 +15,7 @@ export default el => {
provide: { provide: {
reportType: vulnerability.reportType, reportType: vulnerability.reportType,
createIssueUrl: vulnerability.createIssueUrl, newIssueUrl: vulnerability.newIssueUrl,
projectFingerprint: vulnerability.projectFingerprint, projectFingerprint: vulnerability.projectFingerprint,
vulnerabilityId: vulnerability.id, vulnerabilityId: vulnerability.id,
issueTrackingHelpPath: vulnerability.issueTrackingHelpPath, issueTrackingHelpPath: vulnerability.issueTrackingHelpPath,
......
...@@ -10,7 +10,7 @@ module VulnerabilitiesHelper ...@@ -10,7 +10,7 @@ module VulnerabilitiesHelper
result = { result = {
timestamp: Time.now.to_i, timestamp: Time.now.to_i,
create_issue_url: create_issue_url_for(vulnerability), new_issue_url: new_issue_url_for(vulnerability),
create_jira_issue_url: create_jira_issue_url_for(vulnerability), create_jira_issue_url: create_jira_issue_url_for(vulnerability),
related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]), related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]),
has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid), has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid),
...@@ -27,10 +27,10 @@ module VulnerabilitiesHelper ...@@ -27,10 +27,10 @@ module VulnerabilitiesHelper
result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability)) result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability))
end end
def create_issue_url_for(vulnerability) def new_issue_url_for(vulnerability)
return unless vulnerability.project.issues_enabled? return unless vulnerability.project.issues_enabled?
create_issue_project_security_vulnerability_path(vulnerability.project, vulnerability) new_project_issue_path(vulnerability.project, { vulnerability_id: vulnerability.id })
end end
def create_jira_issue_url_for(vulnerability) def create_jira_issue_url_for(vulnerability)
......
---
title: Creating an issue from a vulnerability takes user to the new issue page
merge_request: 48926
author:
type: changed
...@@ -30,7 +30,7 @@ describe('Vulnerability Header', () => { ...@@ -30,7 +30,7 @@ describe('Vulnerability Header', () => {
reportType: 'sast', reportType: 'sast',
state: 'detected', state: 'detected',
createMrUrl: '/create_mr_url', createMrUrl: '/create_mr_url',
createIssueUrl: '/create_issue_url', newIssueUrl: '/new_issue_url',
projectFingerprint: 'abc123', projectFingerprint: 'abc123',
pipeline: { pipeline: {
id: 2, id: 2,
......
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue'; import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
...@@ -25,7 +23,7 @@ describe('Vulnerability related issues component', () => { ...@@ -25,7 +23,7 @@ describe('Vulnerability related issues component', () => {
}; };
const vulnerabilityId = 5131; const vulnerabilityId = 5131;
const createIssueUrl = '/create/issue'; const newIssueUrl = '/new/issue';
const projectFingerprint = 'project-fingerprint'; const projectFingerprint = 'project-fingerprint';
const issueTrackingHelpPath = '/help/issue/tracking'; const issueTrackingHelpPath = '/help/issue/tracking';
const permissionsHelpPath = '/help/permissions'; const permissionsHelpPath = '/help/permissions';
...@@ -40,7 +38,7 @@ describe('Vulnerability related issues component', () => { ...@@ -40,7 +38,7 @@ describe('Vulnerability related issues component', () => {
provide: { provide: {
vulnerabilityId, vulnerabilityId,
projectFingerprint, projectFingerprint,
createIssueUrl, newIssueUrl,
reportType, reportType,
issueTrackingHelpPath, issueTrackingHelpPath,
permissionsHelpPath, permissionsHelpPath,
...@@ -59,7 +57,6 @@ describe('Vulnerability related issues component', () => { ...@@ -59,7 +57,6 @@ describe('Vulnerability related issues component', () => {
const blockProp = prop => relatedIssuesBlock().props(prop); const blockProp = prop => relatedIssuesBlock().props(prop);
const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data); const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data);
const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' }); const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' });
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -283,14 +280,10 @@ describe('Vulnerability related issues component', () => { ...@@ -283,14 +280,10 @@ describe('Vulnerability related issues component', () => {
}); });
describe('when linked issue is not yet created', () => { describe('when linked issue is not yet created', () => {
const failCreateIssueAction = async () => { let redirectToSpy;
mockAxios.onPost(createIssueUrl).reply(500);
expect(findAlert().exists()).toBe(false);
findCreateIssueButton().vm.$emit('click');
await waitForPromises();
};
beforeEach(async () => { beforeEach(async () => {
redirectToSpy = jest.spyOn(urlUtility, 'redirectTo').mockImplementation(() => {});
mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]); mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]);
createWrapper({ stubs: { RelatedIssuesBlock } }); createWrapper({ stubs: { RelatedIssuesBlock } });
await axios.waitForAll(); await axios.waitForAll();
...@@ -300,34 +293,11 @@ describe('Vulnerability related issues component', () => { ...@@ -300,34 +293,11 @@ describe('Vulnerability related issues component', () => {
expect(findCreateIssueButton().exists()).toBe(true); expect(findCreateIssueButton().exists()).toBe(true);
}); });
it('calls create issue endpoint on click and redirects to new issue', async () => { it('calls new issue endpoint on click', () => {
const issueUrl = `/group/project/-/security/vulnerabilities/${vulnerabilityId}/create_issue`;
const spy = jest.spyOn(urlUtility, 'redirectTo');
mockAxios.onPost(propsData.createIssueUrl).reply(200, {
web_url: issueUrl,
});
findCreateIssueButton().vm.$emit('click'); findCreateIssueButton().vm.$emit('click');
expect(redirectToSpy).toHaveBeenCalledWith(newIssueUrl, {
await waitForPromises(); params: { vulnerability_id: vulnerabilityId },
const [postRequest] = mockAxios.history.post;
expect(mockAxios.history.post).toHaveLength(1);
expect(postRequest.url).toBe(createIssueUrl);
expect(spy).toHaveBeenCalledWith(issueUrl);
});
it('shows an error message when issue creation fails', async () => {
await failCreateIssueAction();
expect(mockAxios.history.post).toHaveLength(1);
expect(findAlert().exists()).toBe(true);
}); });
it('dismisses the error message', async () => {
await failCreateIssueAction();
findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
}); });
}); });
...@@ -335,7 +305,7 @@ describe('Vulnerability related issues component', () => { ...@@ -335,7 +305,7 @@ describe('Vulnerability related issues component', () => {
it('hides the "Create Issue" button', () => { it('hides the "Create Issue" button', () => {
createWrapper({ createWrapper({
provide: { provide: {
createIssueUrl: undefined, newIssueUrl: undefined,
}, },
}); });
......
...@@ -16,7 +16,7 @@ describe('Vulnerability', () => { ...@@ -16,7 +16,7 @@ describe('Vulnerability', () => {
report_type: 'sast', report_type: 'sast',
state: 'detected', state: 'detected',
create_mr_url: '/create_mr_url', create_mr_url: '/create_mr_url',
create_issue_url: '/create_issue_url', new_issue_url: '/new_issue_url',
project_fingerprint: 'abc123', project_fingerprint: 'abc123',
pipeline: { pipeline: {
id: 2, id: 2,
......
...@@ -58,7 +58,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -58,7 +58,7 @@ RSpec.describe VulnerabilitiesHelper do
it 'has expected vulnerability properties' do it 'has expected vulnerability properties' do
expect(subject).to include( expect(subject).to include(
timestamp: Time.now.to_i, timestamp: Time.now.to_i,
create_issue_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/create_issue", new_issue_url: "/#{project.full_path}/-/issues/new?vulnerability_id=#{vulnerability.id}",
create_jira_issue_url: nil, create_jira_issue_url: nil,
related_jira_issues_path: "/#{project.full_path}/-/integrations/jira/issues?vulnerability_ids%5B%5D=#{vulnerability.id}", related_jira_issues_path: "/#{project.full_path}/-/integrations/jira/issues?vulnerability_ids%5B%5D=#{vulnerability.id}",
has_mr: anything, has_mr: anything,
...@@ -76,8 +76,8 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -76,8 +76,8 @@ RSpec.describe VulnerabilitiesHelper do
allow(project).to receive(:issues_enabled?).and_return(false) allow(project).to receive(:issues_enabled?).and_return(false)
end end
it 'has `create_issue_url` set as nil' do it 'has `new_issue_url` set as nil' do
expect(subject).to include(create_issue_url: nil) expect(subject).to include(new_issue_url: nil)
end end
end end
end end
......
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