Commit 1a4718d8 authored by Fatih Acet's avatar Fatih Acet

Merge branch '10364-dismissal-reason-any-comment' into 'master'

Allows any user to comment on a dismissed vulnerability (Dismissal reason: Step 3)

Closes #10364

See merge request gitlab-org/gitlab-ee!12067
parents f12c208e 00be2e6c
......@@ -63,6 +63,10 @@
margin-left: $grid-size;
}
.btn-group .btn + .btn {
margin-left: -1px;
}
@include media-breakpoint-down(xs) {
flex-direction: column;
......@@ -72,6 +76,11 @@
margin-left: 0;
margin-top: $grid-size;
}
.btn-group .btn + .btn {
margin-left: -1px;
margin-top: 0;
}
}
}
......
......@@ -96,6 +96,7 @@ export default {
},
methods: {
...mapActions('vulnerabilities', [
'addDismissalComment',
'closeDismissalCommentBox',
'createIssue',
'createMergeRequest',
......@@ -136,6 +137,7 @@ export default {
:can-create-issue="canCreateIssue"
:can-create-merge-request="canCreateMergeRequest"
:can-dismiss-vulnerability="canDismissVulnerability"
@addDismissalComment="addDismissalComment({ vulnerability, comment: $event })"
@closeDismissalCommentBox="closeDismissalCommentBox()"
@createMergeRequest="createMergeRequest({ vulnerability })"
@createNewIssue="createIssue({ vulnerability })"
......
......@@ -6,6 +6,15 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import createFlash from '~/flash';
/**
* A lot of this file has duplicate actions in
* ee/app/assets/javascripts/vue_shared/security_reports/store/actions.js
* This is being addressed in the following issues:
*
* https://gitlab.com/gitlab-org/gitlab-ee/issues/8146
* https://gitlab.com/gitlab-org/gitlab-ee/issues/8519
*/
const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide');
export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => {
......@@ -182,6 +191,41 @@ export const receiveDismissVulnerabilityError = ({ commit }, { flashError }) =>
}
};
export const addDismissalComment = ({ dispatch }, { vulnerability, comment }) => {
dispatch('requestAddDismissalComment');
const { dismissal_feedback } = vulnerability;
const url = `${vulnerability.create_vulnerability_feedback_dismissal_path}/${dismissal_feedback.id}`;
axios
.patch(url, {
project_id: dismissal_feedback.project_id,
id: dismissal_feedback.id,
comment,
})
.then(({ data }) => {
const { id } = vulnerability;
dispatch('closeDismissalCommentBox');
dispatch('receiveAddDismissalCommentSuccess', { id, data });
})
.catch(() => {
dispatch('receiveAddDismissalCommentError');
});
};
export const requestAddDismissalComment = ({ commit }) => {
commit(types.REQUEST_ADD_DISMISSAL_COMMENT);
};
export const receiveAddDismissalCommentSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS, payload);
hideModal();
};
export const receiveAddDismissalCommentError = ({ commit }) => {
commit(types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR);
};
export const undoDismiss = ({ dispatch }, { vulnerability, flashError }) => {
const { destroy_vulnerability_feedback_dismissal_path } = vulnerability.dismissal_feedback;
......
......@@ -25,6 +25,10 @@ export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
export const RECEIVE_DISMISS_VULNERABILITY_SUCCESS = 'RECEIVE_DISMISS_VULNERABILITY_SUCCESS';
export const RECEIVE_DISMISS_VULNERABILITY_ERROR = 'RECEIVE_DISMISS_VULNERABILITY_ERROR';
export const REQUEST_ADD_DISMISSAL_COMMENT = 'REQUEST_ADD_DISMISSAL_COMMENT';
export const RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS = 'RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS';
export const RECEIVE_ADD_DISMISSAL_COMMENT_ERROR = 'RECEIVE_ADD_DISMISSAL_COMMENT_ERROR';
export const REQUEST_REVERT_DISMISSAL = 'REQUEST_REVERT_DISMISSAL';
export const RECEIVE_REVERT_DISMISSAL_SUCCESS = 'RECEIVE_REVERT_DISMISSAL_SUCCESS';
export const RECEIVE_REVERT_DISMISSAL_ERROR = 'RECEIVE_REVERT_DISMISSAL_ERROR';
......
......@@ -179,6 +179,26 @@ export default {
s__('Security Reports|There was an error dismissing the vulnerability.'),
);
},
[types.REQUEST_ADD_DISMISSAL_COMMENT](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id);
if (!vulnerability) {
return;
}
vulnerability.dismissal_feedback = payload.data;
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', true);
},
[types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR](state) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal, 'error', s__('Security Reports|There was an error adding the comment.'));
},
[types.REQUEST_REVERT_DISMISSAL](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
......
......@@ -50,7 +50,7 @@ export default {
:loading="isDismissing"
:disabled="isDismissing"
:label="buttonText"
container-class="js-dismiss-btn btn btn-close m-0"
container-class="js-dismiss-btn btn btn-close"
@click="handleDismissClick"
/>
<gl-button
......@@ -58,7 +58,8 @@ export default {
v-gl-tooltip.hover
v-gl-tooltip.focus
:title="s__('vulnerability|Add comment & dismiss')"
class="js-dismiss-with-comment btn-close m-0"
variant="close"
class="js-dismiss-with-comment "
@click="$emit('openDismissalCommentBox')"
>
<icon name="comment" />
......
......@@ -3,7 +3,6 @@
// It's dangerous to go alone! take this
// https://zaengle.com/blog/using-v-model-on-nested-vue-components
import { s__ } from '~/locale';
import { GlFormTextarea } from '@gitlab/ui';
export default {
......@@ -12,6 +11,11 @@ export default {
GlFormTextarea,
},
props: {
placeholder: {
type: String,
required: false,
default: '',
},
value: {
type: String,
required: false,
......@@ -23,9 +27,6 @@ export default {
default: '',
},
},
data: () => ({
placeholder: s__('vulnerability|Add a comment or reason for dismissal'),
}),
computed: {
localComment: {
get() {
......@@ -59,10 +60,10 @@ export default {
<template>
<div>
<hr />
<gl-form-textarea
ref="dismissalComment"
v-model="localComment"
rows="3"
:state="textAreaState"
:placeholder="placeholder"
@keydown.native="handleKeyPress"
......
<script>
// Nested `v-model`s in custom components are weird.
// It's dangerous to go alone! take this
// https://zaengle.com/blog/using-v-model-on-nested-vue-components
import { GlFormTextarea } from '@gitlab/ui';
import { s__ } from '~/locale';
import DismissalCommentBox from 'ee/vue_shared/security_reports/components/dismissal_comment_box.vue';
const PLACEHOLDER = s__('vulnerability|Add a comment or reason for dismissal');
export default {
name: 'DismissalCommentBoxToggle',
components: {
DismissalCommentBox,
GlFormTextarea,
},
props: {
value: {
type: String,
required: false,
default: '',
},
errorMessage: {
type: String,
required: false,
default: '',
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
PLACEHOLDER,
computed: {
localComment: {
get() {
return this.value;
},
set(localComment) {
this.$emit('input', localComment);
},
},
},
};
</script>
<template>
<div>
<hr class="my-3" />
<dismissal-comment-box
v-if="isActive"
v-model="localComment"
:error-message="errorMessage"
:placeholder="$options.PLACEHOLDER"
@submit="$emit('submit')"
@clearError="$emit('clearError')"
/>
<gl-form-textarea
v-else
:placeholder="$options.PLACEHOLDER"
class="bg-gray-light js-comment-placeholder"
@focus.native="$emit('openDismissalCommentBox')"
/>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import Stats from 'ee/stats';
import { s__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
......@@ -14,22 +16,52 @@ export default {
required: false,
default: false,
},
isDismissed: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
submitLabel() {
return this.isDismissed
? s__('vulnerability|Add comment')
: s__('vulnerability|Add comment & dismiss');
},
},
methods: {
addCommentAndDismiss() {
Stats.trackEvent(document.body.dataset.page, 'click_add_comment_and_dismiss');
this.$emit('addCommentAndDismiss');
},
addDismissalComment() {
Stats.trackEvent(document.body.dataset.page, 'click_add_comment');
this.$emit('addDismissalComment');
},
handleSubmit() {
if (this.isDismissed) {
this.addDismissalComment();
} else {
this.addCommentAndDismiss();
}
},
},
};
</script>
<template>
<div>
<gl-button @click="$emit('cancel')">
<gl-button class="js-cancel" @click="$emit('cancel')">
{{ __('Cancel') }}
</gl-button>
<loading-button
:loading="isDismissingVulnerability"
:disabled="isDismissingVulnerability"
:label="s__('vulnerability|Add comment & dismiss')"
:label="submitLabel"
class="js-loading-button"
container-class="btn btn-close"
@click="$emit('dismissVulnerability')"
@click="handleSubmit"
/>
</div>
</template>
......@@ -4,7 +4,7 @@ import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
import DismissalNote from 'ee/vue_shared/security_reports/components/dismissal_note.vue';
import DismissalCommentBox from 'ee/vue_shared/security_reports/components/dismissal_comment_box.vue';
import DismissalCommentBoxToggle from 'ee/vue_shared/security_reports/components/dismissal_comment_box_toggle.vue';
import DismissalCommentModalFooter from 'ee/vue_shared/security_reports/components/dismissal_comment_modal_footer.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
......@@ -16,7 +16,7 @@ import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vuln
export default {
components: {
DismissalNote,
DismissalCommentBox,
DismissalCommentBoxToggle,
DismissalCommentModalFooter,
EventItem,
ExpandButton,
......@@ -88,9 +88,9 @@ export default {
this.vulnerability && this.vulnerability.remediations && this.vulnerability.remediations[0]
);
},
/**
* The slot for the footer should be rendered if any of the conditions is true.
*/
renderSolutionCard() {
return this.solution || this.remediation;
},
shouldRenderFooterSection() {
return !this.modal.isResolved && (this.canCreateIssue || this.canDismissVulnerability);
},
......@@ -155,13 +155,30 @@ export default {
},
},
methods: {
dismissVulnerabilityWithComment() {
handleDismissalCommentSubmission() {
if (this.dismissalFeedback) {
this.addDismissalComment();
} else {
this.addCommentAndDismiss();
}
},
addCommentAndDismiss() {
if (this.localDismissalComment.length) {
this.$emit('dismissVulnerability', this.localDismissalComment);
} else {
this.dismissalCommentErrorMessage = __('Please add a comment in the text area above');
this.addDismissalError();
}
},
addDismissalComment() {
if (this.localDismissalComment.length) {
this.$emit('addDismissalComment', this.localDismissalComment);
} else {
this.addDismissalError();
}
},
addDismissalError() {
this.dismissalCommentErrorMessage = __('Please add a comment in the text area above');
},
clearDismissalError() {
this.dismissalCommentErrorMessage = '';
},
......@@ -203,11 +220,13 @@ export default {
<div v-if="dismissalFeedback || modal.isCommentingOnDismissal" class="card my-4">
<div class="card-body">
<dismissal-note :feedback="dismissalFeedbackObject" :project="project" />
<dismissal-comment-box
v-if="modal.isCommentingOnDismissal"
<dismissal-comment-box-toggle
v-if="!dismissalFeedback || !dismissalFeedback.comment_details"
v-model="localDismissalComment"
:is-active="modal.isCommentingOnDismissal"
:error-message="dismissalCommentErrorMessage"
@submit="dismissVulnerabilityWithComment"
@openDismissalCommentBox="$emit('openDismissalCommentBox')"
@submit="handleDismissalCommentSubmission"
@clearError="clearDismissalError"
/>
</div>
......@@ -218,7 +237,9 @@ export default {
<div slot="footer">
<dismissal-comment-modal-footer
v-if="modal.isCommentingOnDismissal"
@dismissVulnerability="dismissVulnerabilityWithComment"
:is-dismissed="vulnerability.isDismissed"
@addCommentAndDismiss="addCommentAndDismiss"
@addDismissalComment="addDismissalComment"
@cancel="$emit('closeDismissalCommentBox')"
/>
<modal-footer
......
......@@ -91,7 +91,7 @@ export default {
<dismiss-button
v-if="canDismissVulnerability"
:is-dismissing="modal.isDismissingIssue"
:is-dismissing="modal.isDismissingVulnerability"
:is-dismissed="isDismissed"
@dismissVulnerability="$emit('dismissVulnerability')"
@openDismissalCommentBox="$emit('openDismissalCommentBox')"
......
......@@ -257,6 +257,7 @@ export default {
'openDismissalCommentBox',
'closeDismissalCommentBox',
'downloadPatch',
'addDismissalComment',
]),
},
};
......@@ -363,6 +364,7 @@ export default {
@openDismissalCommentBox="openDismissalCommentBox()"
@revertDismissVulnerability="revertDismissVulnerability"
@downloadPatch="downloadPatch"
@addDismissalComment="addDismissalComment({ comment: $event })"
/>
</div>
</report-section>
......
......@@ -237,6 +237,7 @@ export default {
'openDismissalCommentBox',
'closeDismissalCommentBox',
'downloadPatch',
'addDismissalComment',
]),
summaryTextBuilder(reportType, issuesCount = 0) {
if (issuesCount === 0) {
......@@ -327,6 +328,7 @@ export default {
@openDismissalCommentBox="openDismissalCommentBox()"
@revertDismissVulnerability="revertDismissVulnerability"
@downloadPatch="downloadPatch"
@addDismissalComment="addDismissalComment({ comment: $event })"
/>
</div>
</template>
......@@ -5,6 +5,17 @@ import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
import downloadPatchHelper from './utils/download_patch_helper';
/**
* A lot of this file has duplicate actions to
* ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js
* This is being addressed in the following issues:
*
* https://gitlab.com/gitlab-org/gitlab-ee/issues/8146
* https://gitlab.com/gitlab-org/gitlab-ee/issues/8519
*/
const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide');
export const setHeadBlobPath = ({ commit }, blobPath) => commit(types.SET_HEAD_BLOB_PATH, blobPath);
export const setBaseBlobPath = ({ commit }, blobPath) => commit(types.SET_BASE_BLOB_PATH, blobPath);
......@@ -267,7 +278,7 @@ export const dismissVulnerability = ({ state, dispatch }, comment) => {
default:
}
$('#modal-mrwidget-security-issue').modal('hide');
hideModal();
})
.catch(() => {
dispatch(
......@@ -277,6 +288,44 @@ export const dismissVulnerability = ({ state, dispatch }, comment) => {
});
};
export const addDismissalComment = ({ state, dispatch }, { comment }) => {
dispatch('requestAddDismissalComment');
const { vulnerability } = state.modal;
const { dismissalFeedback } = vulnerability;
const url = `${state.createVulnerabilityFeedbackDismissalPath}/${dismissalFeedback.id}`;
axios
.patch(url, {
project_id: dismissalFeedback.project_id,
id: dismissalFeedback.id,
comment,
})
.then(({ data }) => {
dispatch('closeDismissalCommentBox');
dispatch('receiveAddDismissalCommentSuccess', { data });
})
.catch(() => {
dispatch(
'receiveAddDismissalCommentError',
s__('Security Reports|There was an error adding the comment.'),
);
});
};
export const requestAddDismissalComment = ({ commit }) => {
commit(types.REQUEST_ADD_DISMISSAL_COMMENT);
};
export const receiveAddDismissalCommentSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS, payload);
hideModal();
};
export const receiveAddDismissalCommentError = ({ commit }, error) => {
commit(types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR, error);
};
export const revertDismissVulnerability = ({ state, dispatch }) => {
dispatch('requestDismissVulnerability');
......@@ -309,7 +358,7 @@ export const revertDismissVulnerability = ({ state, dispatch }) => {
default:
}
$('#modal-mrwidget-security-issue').modal('hide');
hideModal();
})
.catch(() =>
dispatch(
......
......@@ -46,6 +46,10 @@ export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
export const RECEIVE_DISMISS_VULNERABILITY_SUCCESS = 'RECEIVE_DISMISS_VULNERABILITY_SUCCESS';
export const RECEIVE_DISMISS_VULNERABILITY_ERROR = 'RECEIVE_DISMISS_VULNERABILITY_ERROR';
export const REQUEST_ADD_DISMISSAL_COMMENT = 'REQUEST_ADD_DISMISSAL_COMMENT';
export const RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS = 'RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS';
export const RECEIVE_ADD_DISMISSAL_COMMENT_ERROR = 'RECEIVE_ADD_DISMISSAL_COMMENT_ERROR';
export const REQUEST_CREATE_ISSUE = 'CREATE_DISMISS_VULNERABILITY';
export const RECEIVE_CREATE_ISSUE_SUCCESS = 'CREATE_DISMISS_VULNERABILITY_SUCCESS';
export const RECEIVE_CREATE_ISSUE_ERROR = 'CREATE_DISMISS_VULNERABILITY_ERROR';
......
......@@ -309,13 +309,37 @@ export default {
},
[types.REQUEST_DISMISS_VULNERABILITY](state) {
Vue.set(state.modal, 'isDismissingIssue', true);
Vue.set(state.modal, 'isDismissingVulnerability', true);
// reset error in case previous state was error
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state) {
Vue.set(state.modal, 'isDismissingIssue', false);
Vue.set(state.modal, 'isDismissingVulnerability', false);
},
[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](state, error) {
Vue.set(state.modal, 'error', error);
Vue.set(state.modal, 'isDismissingVulnerability', false);
},
[types.REQUEST_ADD_DISMISSAL_COMMENT](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](state, payload) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', true);
Vue.set(state.modal.vulnerability, 'dismissalFeedback', payload.data);
},
[types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR](state, error) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal, 'error', error);
},
[types.UPDATE_SAST_ISSUE](state, issue) {
......@@ -390,11 +414,6 @@ export default {
}
},
[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](state, error) {
Vue.set(state.modal, 'error', error);
Vue.set(state.modal, 'isDismissingIssue', false);
},
[types.REQUEST_CREATE_ISSUE](state) {
Vue.set(state.modal, 'isCreatingNewIssue', true);
// reset error in case previous state was error
......
......@@ -139,7 +139,7 @@ export default () => ({
},
isCreatingNewIssue: false,
isDismissingIssue: false,
isDismissingVulnerability: false,
error: null,
},
......
---
title: Allows any user to comment on a dismissed vulnerability
merge_request: 12067
author:
type: added
......@@ -31,4 +31,10 @@ describe('DismissalCommentBox', () => {
wrapper.setProps({ errorMessage });
expect(wrapper.find('.js-error').text()).toBe(errorMessage);
});
it('should render the placeholder', () => {
const placeholder = 'Please type into the box';
wrapper.setProps({ placeholder });
expect(wrapper.find(GlFormTextarea).attributes('placeholder')).toBe(placeholder);
});
});
import { mount } from '@vue/test-utils';
import DismissalCommentBox from 'ee/vue_shared/security_reports/components/dismissal_comment_box.vue';
import component from 'ee/vue_shared/security_reports/components/dismissal_comment_box_toggle.vue';
describe('DismissalCommentBox', () => {
let wrapper;
describe('when the box is inactive', () => {
beforeEach(() => {
wrapper = mount(component);
});
it('should render the placeholder text box', () => {
expect(wrapper.find('.js-comment-placeholder').exists()).toBeTruthy();
});
it('should not render the dismissal comment box', () => {
expect(wrapper.find(DismissalCommentBox).exists()).toBeFalsy();
});
});
describe('when the box is active', () => {
beforeEach(() => {
wrapper = mount(component, {
propsData: {
isActive: true,
},
});
});
it('should render the dismissal comment box', () => {
expect(wrapper.find(DismissalCommentBox).exists()).toBeTruthy();
});
it('should not render the placeholder text box', () => {
expect(wrapper.find('.js-comment-placeholder').exists()).toBeFalsy();
});
});
});
import { mount } from '@vue/test-utils';
import Stats from 'ee/stats';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import component from 'ee/vue_shared/security_reports/components/dismissal_comment_modal_footer.vue';
jest.mock('ee/stats');
describe('DismissalCommentModalFooter', () => {
let wrapper;
describe('with an non-dismissed vulnerability', () => {
beforeEach(() => {
wrapper = mount(component, { sync: false });
});
it('should render the "Add comment and dismiss" button', () => {
expect(wrapper.find(LoadingButton).text()).toBe('Add comment & dismiss');
});
it('should emit the "addCommentAndDismiss" event when clicked', () => {
wrapper.find(LoadingButton).trigger('click');
expect(wrapper.emitted().addCommentAndDismiss).toBeTruthy();
expect(Stats.trackEvent).toHaveBeenCalledWith(
document.body.dataset.page,
'click_add_comment_and_dismiss',
);
});
it('should emit the cancel event when the cancel button is clicked', () => {
wrapper.find('.js-cancel').trigger('click');
expect(wrapper.emitted().cancel).toBeTruthy();
});
});
describe('with an already dismissed vulnerability', () => {
beforeEach(() => {
const propsData = {
isDismissed: true,
};
wrapper = mount(component, { propsData });
});
it('should render the "Add comment and dismiss" button', () => {
expect(wrapper.find(LoadingButton).text()).toBe('Add comment');
});
it('should emit the "addCommentAndDismiss" event when clicked', () => {
wrapper.find(LoadingButton).trigger('click');
expect(wrapper.emitted().addDismissalComment).toBeTruthy();
expect(Stats.trackEvent).toHaveBeenCalledWith(
document.body.dataset.page,
'click_add_comment',
);
});
});
});
......@@ -7,10 +7,6 @@ describe('Security Reports modal', () => {
const Component = Vue.extend(component);
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('with permissions', () => {
describe('with dismissed issue', () => {
beforeEach(() => {
......@@ -31,6 +27,10 @@ describe('Security Reports modal', () => {
expect(wrapper.text().trim()).toContain('@jsmith');
expect(wrapper.text().trim()).toContain('#123');
});
it('renders the dismissal comment placeholder', () => {
expect(wrapper.find('.js-comment-placeholder')).not.toBeNull();
});
});
describe('with not dismissed issue', () => {
......@@ -225,4 +225,73 @@ describe('Security Reports modal', () => {
expect(wrapper.contains('hr')).toBe(false);
});
});
describe('add dismissal comment', () => {
const comment = "Pirates don't eat the tourists";
let propsData;
beforeEach(() => {
propsData = {
modal: createState().modal,
};
propsData.modal.isCommentingOnDismissal = true;
});
beforeAll(() => {
// https://github.com/vuejs/vue-test-utils/issues/532#issuecomment-398449786
Vue.config.silent = true;
});
afterAll(() => {
Vue.config.silent = false;
});
describe('with a non-dismissed vulnerability', () => {
beforeEach(() => {
wrapper = mount(Component, { propsData });
});
it('creates an error when an empty comment is submitted', () => {
const { vm } = wrapper;
vm.handleDismissalCommentSubmission();
expect(vm.dismissalCommentErrorMessage).toBe('Please add a comment in the text area above');
});
it('submits the comment and dismisses the vulnerability if text has been entered', () => {
const { vm } = wrapper;
vm.addCommentAndDismiss = jasmine.createSpy();
vm.localDismissalComment = comment;
vm.handleDismissalCommentSubmission();
expect(vm.addCommentAndDismiss).toHaveBeenCalled();
expect(vm.dismissalCommentErrorMessage).toBe('');
});
});
describe('with a dismissed vulnerability', () => {
beforeEach(() => {
propsData.modal.vulnerability.dismissal_feedback = { author: {} };
wrapper = mount(Component, { propsData });
});
it('creates an error when an empty comment is submitted', () => {
const { vm } = wrapper;
vm.handleDismissalCommentSubmission();
expect(vm.dismissalCommentErrorMessage).toBe('Please add a comment in the text area above');
});
it('submits the comment if text is entered and the vulnerability is already dismissed', () => {
const { vm } = wrapper;
vm.addDismissalComment = jasmine.createSpy();
vm.localDismissalComment = comment;
vm.handleDismissalCommentSubmission();
expect(vm.addDismissalComment).toHaveBeenCalled();
expect(vm.dismissalCommentErrorMessage).toBe('');
});
});
});
});
......@@ -435,7 +435,7 @@ describe('security reports mutations', () => {
expect(stateCopy.modal.vulnerability.hasIssue).toEqual(false);
expect(stateCopy.modal.isCreatingNewIssue).toEqual(false);
expect(stateCopy.modal.isDismissingIssue).toEqual(false);
expect(stateCopy.modal.isDismissingVulnerability).toEqual(false);
expect(stateCopy.modal.title).toEqual(null);
expect(stateCopy.modal.learnMoreUrl).toEqual(null);
......@@ -505,31 +505,94 @@ describe('security reports mutations', () => {
});
describe('REQUEST_DISMISS_VULNERABILITY', () => {
it('sets isDismissingIssue prop to true and resets error', () => {
it('sets isDismissingVulnerability prop to true and resets error', () => {
mutations[types.REQUEST_DISMISS_VULNERABILITY](stateCopy);
expect(stateCopy.modal.isDismissingIssue).toEqual(true);
expect(stateCopy.modal.isDismissingVulnerability).toEqual(true);
expect(stateCopy.modal.error).toBeNull();
});
});
describe('RECEIVE_DISMISS_VULNERABILITY_SUCCESS', () => {
it('sets isDismissingIssue prop to false', () => {
it('sets isDismissingVulnerability prop to false', () => {
mutations[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](stateCopy);
expect(stateCopy.modal.isDismissingIssue).toEqual(false);
expect(stateCopy.modal.isDismissingVulnerability).toEqual(false);
});
});
describe('RECEIVE_DISMISS_VULNERABILITY_ERROR', () => {
it('sets isDismissingIssue prop to false and sets error', () => {
it('sets isDismissingVulnerability prop to false and sets error', () => {
mutations[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](stateCopy, 'error');
expect(stateCopy.modal.isDismissingIssue).toEqual(false);
expect(stateCopy.modal.isDismissingVulnerability).toEqual(false);
expect(stateCopy.modal.error).toEqual('error');
});
});
describe(types.REQUEST_ADD_DISMISSAL_COMMENT, () => {
beforeEach(() => {
mutations[types.REQUEST_ADD_DISMISSAL_COMMENT](stateCopy);
});
it('should set isDismissingVulnerability to true', () => {
expect(stateCopy.isDismissingVulnerability).toBe(true);
});
it('should set isDismissingVulnerability in the modal data to true', () => {
expect(stateCopy.modal.isDismissingVulnerability).toBe(true);
});
it('should nullify the error state on the modal', () => {
expect(stateCopy.modal.error).toBeNull();
});
});
describe(types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS, () => {
let payload;
let vulnerability;
let data;
beforeEach(() => {
vulnerability = { id: 1 };
data = { name: 'dismissal feedback' };
payload = { id: vulnerability.id, data };
mutations[types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](stateCopy, payload);
});
it('should set isDismissingVulnerability to false', () => {
expect(stateCopy.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability on the modal to false', () => {
expect(stateCopy.modal.isDismissingVulnerability).toBe(false);
});
it('shoulfd set isDissmissed on the modal vulnerability to be true', () => {
expect(stateCopy.modal.vulnerability.isDismissed).toBe(true);
});
});
describe(types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR, () => {
const error = 'There was an error adding the comment.';
beforeEach(() => {
mutations[types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR](stateCopy, error);
});
it('should set isDismissingVulnerability to false', () => {
expect(stateCopy.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability in the modal data to false', () => {
expect(stateCopy.modal.isDismissingVulnerability).toBe(false);
});
it('should set the error state on the modal', () => {
expect(stateCopy.modal.error).toEqual(error);
});
});
describe('REQUEST_CREATE_ISSUE', () => {
it('sets isCreatingNewIssue prop to true and resets error', () => {
mutations[types.REQUEST_CREATE_ISSUE](stateCopy);
......
......@@ -680,7 +680,7 @@ describe('vulnerability dismissal', () => {
[],
[
{ type: 'requestDismissVulnerability' },
{ type: 'receiveDismissVulnerabilityError', payload: { flashError: false } },
{ type: 'receiveDismissVulnerabilityError', payload: { flashError } },
],
done,
);
......@@ -740,6 +740,115 @@ describe('vulnerability dismissal', () => {
});
});
describe('add vulnerability dismissal comment', () => {
describe('addDismissalComment', () => {
const vulnerability = mockDataVulnerabilities[2];
const data = { vulnerability };
const url = `${vulnerability.create_vulnerability_feedback_dismissal_path}/${vulnerability.dismissal_feedback.id}`;
const comment = 'Well, we’re back in the car again.';
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onPatch(url).replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
const checkPassedData = () => {
const { project_id, id } = vulnerability.dismissal_feedback;
const expected = { project_id, id, comment };
expect(mock.history.patch[0].data).toBe(JSON.stringify(expected));
done();
};
testAction(
actions.addDismissalComment,
{ vulnerability, comment },
{},
[],
[
{ type: 'requestAddDismissalComment' },
{ type: 'closeDismissalCommentBox' },
{ type: 'receiveAddDismissalCommentSuccess', payload: { data, id: vulnerability.id } },
],
checkPassedData,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onPatch(url).replyOnce(404);
});
it('should dispatch the request and error actions', done => {
testAction(
actions.addDismissalComment,
{ vulnerability, comment },
{},
[],
[{ type: 'requestAddDismissalComment' }, { type: 'receiveAddDismissalCommentError' }],
done,
);
});
});
describe('receiveAddDismissalCommentSuccess', () => {
it('should commit the success mutation', done => {
const state = initialState;
testAction(
actions.receiveAddDismissalCommentSuccess,
{ data },
state,
[{ type: types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS, payload: { data } }],
[],
done,
);
});
});
describe('receiveAddDismissalCommentError', () => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveAddDismissalCommentError,
{},
state,
[{ type: types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR }],
[],
done,
);
});
});
describe('requestAddDismissalComment', () => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestAddDismissalComment,
{},
state,
[{ type: types.REQUEST_ADD_DISMISSAL_COMMENT }],
[],
done,
);
});
});
});
});
describe('revert vulnerability dismissal', () => {
describe('undoDismiss', () => {
const vulnerability = mockDataVulnerabilities[2];
......@@ -789,7 +898,7 @@ describe('revert vulnerability dismissal', () => {
[],
[
{ type: 'requestUndoDismiss' },
{ type: 'receiveUndoDismissError', payload: { flashError: false } },
{ type: 'receiveUndoDismissError', payload: { flashError } },
],
done,
);
......
......@@ -542,6 +542,80 @@ describe('vulnerabilities module mutations', () => {
});
});
describe('REQUEST_DISMISSAL_COMMENT', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.REQUEST_ADD_DISMISSAL_COMMENT](state);
});
it('should set isDismissingVulnerability to true', () => {
expect(state.isDismissingVulnerability).toBe(true);
});
it('should set isDismissingVulnerability in the modal data to true', () => {
expect(state.modal.isDismissingVulnerability).toBe(true);
});
it('should nullify the error state on the modal', () => {
expect(state.modal.error).toBeNull();
});
});
describe('RECEIVE_DISMISSAL_COMMENT_SUCCESS', () => {
let state;
let payload;
let vulnerability;
let data;
beforeEach(() => {
state = createState();
state.vulnerabilities = mockData;
[vulnerability] = mockData;
data = { name: 'dismissal feedback' };
payload = { id: vulnerability.id, data };
mutations[types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](state, payload);
});
it('should set the dismissal feedback on the passed vulnerability', () => {
expect(state.vulnerabilities[0].dismissal_feedback).toEqual(data);
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability on the modal to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('should set isDissmissed on the modal vulnerability to be true', () => {
expect(state.modal.vulnerability.isDismissed).toBe(true);
});
});
describe('RECEIVE_DISMISSAL_COMMENT_ERROR', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR](state);
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability in the modal data to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('should set the error state on the modal', () => {
expect(state.modal.error).toEqual('There was an error adding the comment.');
});
});
describe('REQUEST_REVERT_DISMISSAL', () => {
let state;
......
......@@ -52,6 +52,10 @@ import actions, {
updateDependencyScanningIssue,
updateContainerScanningIssue,
updateDastIssue,
addDismissalComment,
receiveAddDismissalCommentError,
receiveAddDismissalCommentSuccess,
requestAddDismissalComment,
} from 'ee/vue_shared/security_reports/store/actions';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import state from 'ee/vue_shared/security_reports/store/state';
......@@ -1300,6 +1304,108 @@ describe('security reports actions', () => {
});
});
describe('addDismissalComment', () => {
const vulnerability = {
id: 0,
vulnerability_feedback_dismissal_path: 'foo',
dismissalFeedback: { id: 1 },
};
const data = { vulnerability };
const url = `${state.createVulnerabilityFeedbackDismissalPath}/${vulnerability.dismissalFeedback.id}`;
const comment = 'Well, we’re back in the car again.';
describe('on success', () => {
beforeEach(() => {
mock.onPatch(url).replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
testAction(
addDismissalComment,
{ comment },
{ modal: { vulnerability } },
[],
[
{ type: 'requestAddDismissalComment' },
{ type: 'closeDismissalCommentBox' },
{
type: 'receiveAddDismissalCommentSuccess',
payload: { data },
},
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onPatch(url).replyOnce(404);
});
it('should dispatch the request and error actions', done => {
testAction(
addDismissalComment,
{ comment },
{ modal: { vulnerability } },
[],
[
{ type: 'requestAddDismissalComment' },
{
type: 'receiveAddDismissalCommentError',
payload: 'There was an error adding the comment.',
},
],
done,
);
});
});
describe('receiveAddDismissalCommentSuccess', () => {
it('should commit the success mutation', done => {
testAction(
receiveAddDismissalCommentSuccess,
{ data },
state,
[{ type: types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS, payload: { data } }],
[],
done,
);
});
});
describe('receiveAddDismissalCommentError', () => {
it('should commit the error mutation', done => {
testAction(
receiveAddDismissalCommentError,
{},
state,
[
{
type: types.RECEIVE_ADD_DISMISSAL_COMMENT_ERROR,
payload: {},
},
],
[],
done,
);
});
});
describe('requestAddDismissalComment', () => {
it('should commit the request mutation', done => {
testAction(
requestAddDismissalComment,
{},
state,
[{ type: types.REQUEST_ADD_DISMISSAL_COMMENT }],
[],
done,
);
});
});
});
describe('revertDismissVulnerability', () => {
describe('with success', () => {
beforeEach(() => {
......
......@@ -12206,6 +12206,9 @@ msgstr ""
msgid "Security Reports|Oops, something doesn't seem right."
msgstr ""
msgid "Security Reports|There was an error adding the comment."
msgstr ""
msgid "Security Reports|There was an error creating the issue."
msgstr ""
......@@ -17561,6 +17564,9 @@ msgstr ""
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
msgid "vulnerability|Add comment"
msgstr ""
msgid "vulnerability|Add comment & dismiss"
msgstr ""
......
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