Commit 6c475235 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'leipert-decouple-security-reports-modal' into 'master'

Decouple Security Reports Modal from Store

See merge request gitlab-org/gitlab-ee!7834
parents b7f88d4b be06e700
<script>
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
import { s__ } from '~/locale';
import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
export default {
export default {
components: {
Modal,
LoadingButton,
ExpandButton,
Icon,
},
props: {
modal: {
type: Object,
required: true,
},
vulnerabilityFeedbackHelpPath: {
type: String,
required: false,
default: '',
},
canCreateIssuePermission: {
type: Boolean,
required: false,
default: false,
},
canCreateFeedbackPermission: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState([
'modal',
'vulnerabilityFeedbackHelpPath',
'canCreateIssuePermission',
'canCreateFeedbackPermission',
]),
revertTitle() {
return this.modal.vulnerability.isDismissed
? s__('ciReport|Revert dismissal')
......@@ -27,6 +41,7 @@
},
hasDismissedBy() {
return (
this.modal.vulnerability &&
this.modal.vulnerability.dismissalFeedback &&
this.modal.vulnerability.dismissalFeedback.pipeline &&
this.modal.vulnerability.dismissalFeedback.author
......@@ -43,12 +58,11 @@
},
},
methods: {
...mapActions(['dismissIssue', 'revertDismissIssue', 'createNewIssue']),
handleDismissClick() {
if (this.modal.vulnerability.isDismissed) {
this.revertDismissIssue();
this.$emit('revertDismissIssue');
} else {
this.dismissIssue();
this.$emit('dismissIssue');
}
},
isLastValue(index, values) {
......@@ -67,7 +81,7 @@
return key === 'links' && this.hasValue(field);
},
},
};
};
</script>
<template>
<modal
......@@ -196,6 +210,7 @@
>#{{ modal.vulnerability.dismissalFeedback.pipeline.id }}</a>.
</template>
<a
v-if="vulnerabilityFeedbackHelpPath"
:href="vulnerabilityFeedbackHelpPath"
class="js-link-vulnerabilityFeedbackHelpPath"
>
......@@ -245,7 +260,7 @@
:disabled="modal.isCreatingNewIssue"
:label="__('Create issue')"
container-class="js-create-issue-btn btn btn-success btn-inverted"
@click="createNewIssue"
@click="$emit('createNewIssue')"
/>
</template>
</div>
......
......@@ -120,7 +120,16 @@ export default {
},
componentNames,
computed: {
...mapState(['sast', 'sastContainer', 'dast', 'dependencyScanning', 'summaryCounts']),
...mapState([
'sast',
'sastContainer',
'dast',
'dependencyScanning',
'summaryCounts',
'modal',
'canCreateIssuePermission',
'canCreateFeedbackPermission',
]),
...mapGetters([
'groupedSastText',
'groupedSummaryText',
......@@ -207,6 +216,9 @@ export default {
'setPipelineId',
'setCanCreateIssuePermission',
'setCanCreateFeedbackPermission',
'dismissIssue',
'revertDismissIssue',
'createNewIssue',
]),
},
};
......@@ -311,7 +323,15 @@ export default {
/>
</template>
<issue-modal />
<issue-modal
:modal="modal"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
:can-create-issue-permission="canCreateIssuePermission"
:can-create-feedback-permission="canCreateFeedbackPermission"
@createNewIssue="createNewIssue"
@dismissIssue="dismissIssue"
@revertDismissIssue="revertDismissIssue"
/>
</div>
</report-section>
</template>
......@@ -92,7 +92,15 @@ export default {
},
componentNames,
computed: {
...mapState(['sast', 'dependencyScanning', 'sastContainer', 'dast']),
...mapState([
'sast',
'dependencyScanning',
'sastContainer',
'dast',
'modal',
'canCreateIssuePermission',
'canCreateFeedbackPermission',
]),
sastText() {
return this.summaryTextBuilder(messages.SAST, this.sast.newIssues.length);
......@@ -188,6 +196,9 @@ export default {
'setPipelineId',
'setCanCreateIssuePermission',
'setCanCreateFeedbackPermission',
'dismissIssue',
'revertDismissIssue',
'createNewIssue',
]),
summaryTextBuilder(reportType, issuesCount = 0) {
if (issuesCount === 0) {
......@@ -265,6 +276,14 @@ export default {
class="js-dast-widget split-report-section"
/>
<issue-modal />
<issue-modal
:modal="modal"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
:can-create-issue-permission="canCreateIssuePermission"
:can-create-feedback-permission="canCreateFeedbackPermission"
@createNewIssue="createNewIssue"
@dismissIssue="dismissIssue"
@revertDismissIssue="revertDismissIssue"
/>
</div>
</template>
......@@ -36,7 +36,6 @@ export const RECEIVE_DEPENDENCY_SCANNING_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_ER
// Dismiss security issue
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
export const CLEAR_ISSUE_MODAL_DATA = 'CLEAR_ISSUE_MODAL_DATA';
export const REQUEST_DISMISS_ISSUE = 'REQUEST_DISMISS_ISSUE';
export const RECEIVE_DISMISS_ISSUE_SUCCESS = 'RECEIVE_DISMISS_ISSUE_SUCCESS';
export const RECEIVE_DISMISS_ISSUE_ERROR = 'RECEIVE_DISMISS_ISSUE_ERROR';
......
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/modal.vue';
import state from 'ee/vue_shared/security_reports/store/state';
import createStore from 'ee/vue_shared/security_reports/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import createState from 'ee/vue_shared/security_reports/store/state';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Security Reports modal', () => {
const Component = Vue.extend(component);
let vm;
const store = createStore();
afterEach(() => {
vm.$destroy();
vm.$store.replaceState(state());
});
beforeEach(() => {
store.dispatch('setVulnerabilityFeedbackPath', 'path');
store.dispatch('setVulnerabilityFeedbackHelpPath', 'feedbacksHelpPath');
store.dispatch('setPipelineId', 123);
});
describe('with permissions', () => {
beforeEach(() => {
store.dispatch('setCanCreateIssuePermission', true);
store.dispatch('setCanCreateFeedbackPermission', true);
});
describe('with dismissed issue', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
const props = {
modal: createState().modal,
canCreateFeedbackPermission: true,
};
props.modal.vulnerability.isDismissed = true;
props.modal.vulnerability.dismissalFeedback = {
author: { username: 'jsmith' },
pipeline: { id: '123' },
};
vm = mountComponent(Component, props);
});
it('renders dismissal author and associated pipeline', () => {
......@@ -75,36 +37,23 @@ describe('Security Reports modal', () => {
);
});
it('calls revertDismissed when revert dismissal button is clicked', () => {
spyOn(vm, 'revertDismissIssue');
it('emits revertDismissIssue when revert dismissal button is clicked', () => {
spyOn(vm, '$emit');
const button = vm.$el.querySelector('.js-dismiss-btn');
button.click();
expect(vm.revertDismissIssue).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('revertDismissIssue');
});
});
describe('with not dismissed isssue', () => {
describe('with not dismissed issue', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
const props = {
modal: createState().modal,
canCreateFeedbackPermission: true,
};
vm = mountComponent(Component, props);
});
it('renders button to dismiss issue', () => {
......@@ -113,48 +62,69 @@ describe('Security Reports modal', () => {
);
});
it('calls dismissIssue when dismiss issue button is clicked', () => {
spyOn(vm, 'dismissIssue');
it('does not render create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).toBe(null);
});
it('renders create issue button and footer', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
it('emits dismissIssue when dismiss issue button is clicked', () => {
spyOn(vm, '$emit');
const button = vm.$el.querySelector('.js-dismiss-btn');
button.click();
expect(vm.dismissIssue).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('dismissIssue');
});
});
describe('with instances', () => {
describe('with create issue', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
title: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
pluginid: '123',
instances: [
{
uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc',
method: 'GET',
evidence:
"<form class='navbar-form' action='/search' accept-charset='UTF-8' method='get'>",
},
{
uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md',
method: 'GET',
evidence:
"<form class='navbar-form' action='/search' accept-charset='UTF-8' method='get'>",
},
],
description: ' No Anti-CSRF tokens were found in a HTML submission form. ',
solution: '',
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
const props = {
modal: createState().modal,
canCreateIssuePermission: true,
};
vm = mountComponent(Component, props);
});
it('does not render dismiss button', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).toBe(null);
});
it('renders create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
it('emits createIssue when create issue button is clicked', () => {
spyOn(vm, '$emit');
const button = vm.$el.querySelector('.js-create-issue-btn');
button.click();
expect(vm.$emit).toHaveBeenCalledWith('createNewIssue');
});
});
describe('with instances', () => {
beforeEach(() => {
const props = {
modal: createState().modal,
};
props.modal.data.instances.value = [
{ uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc' },
{ uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md' },
];
vm = mountComponent(Component, props);
});
it('renders instances list', () => {
......@@ -169,30 +139,18 @@ describe('Security Reports modal', () => {
});
});
describe('data & create issue button', () => {
describe('data', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
cve: 'CVE-2014-9999',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
location: {
file: 'Gemfile.lock',
},
links: [{
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
}],
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
const props = {
modal: createState().modal,
vulnerabilityFeedbackHelpPath: 'feedbacksHelpPath',
};
props.modal.title = 'Arbitrary file existence disclosure in Action Pack';
props.modal.data.solution.value =
'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8';
props.modal.data.file.value = 'Gemfile.lock';
props.modal.data.file.url = 'path/Gemfile.lock';
vm = mountComponent(Component, props);
});
it('renders keys in `data`', () => {
......@@ -203,51 +161,25 @@ describe('Security Reports modal', () => {
});
it('renders link fields with link', () => {
expect(vm.$el.querySelector('.js-link-file').getAttribute('href')).toEqual('path/Gemfile.lock');
expect(vm.$el.querySelector('.js-link-file').getAttribute('href')).toEqual(
'path/Gemfile.lock',
);
});
it('renders help link', () => {
expect(vm.$el.querySelector('.js-link-vulnerabilityFeedbackHelpPath').getAttribute('href')).toEqual('feedbacksHelpPath');
expect(
vm.$el.querySelector('.js-link-vulnerabilityFeedbackHelpPath').getAttribute('href'),
).toEqual('feedbacksHelpPath');
});
});
});
describe('without permissions', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
const props = {
modal: createState().modal,
};
vm = mountComponent(Component, props);
});
it('does not render action buttons', () => {
......@@ -260,130 +192,13 @@ describe('Security Reports modal', () => {
});
});
describe('with permission to create issue', () => {
beforeEach(() => {
store.dispatch('setCanCreateIssuePermission', true);
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
});
it('does not render dismiss button', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).toBe(null);
});
it('renders create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
});
describe('with permission to dismiss issue', () => {
beforeEach(() => {
store.dispatch('setCanCreateFeedbackPermission', true);
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
});
it('does not render create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).toBe(null);
});
it('renders create issue button and footer', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
});
describe('with a resolved issue', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
},
status: 'success',
});
vm = mountComponentWithStore(Component, {
store,
});
const props = {
modal: createState().modal,
};
props.modal.isResolved = true;
vm = mountComponent(Component, props);
});
it('does not display the footer', () => {
......
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