Коммит c814b392 создал по автору David Pisek's avatar David Pisek Зафиксировано автором Savas Vedova
Просмотр файлов

Add dismissal to graphql sec-finding modal

This commit adds a button to the modal that allows the user to dismiss
the given finding.
владелец c34302ee
......@@ -6,8 +6,12 @@ import IssueNote from 'ee/vue_shared/security_reports/components/issue_note_grap
import VulnerabilityDetailsGraphql from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dismiss_finding.mutation.graphql';
import revertFindingToDetectedMutation from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql';
import { updateFindingState } from 'ee/security_dashboard/graphql/cache_updates';
export const STATE_DISMISSED = 'DISMISSED';
export const STATE_DETECTED = 'DETECTED';
export default {
components: {
GlAlert,
......@@ -37,7 +41,7 @@ export default {
loadingFinding: 0,
hasFindingFetchError: false,
errorMessage: '',
isDismissingFinding: false,
isUpdatingFindingState: false,
};
},
apollo: {
......@@ -78,41 +82,70 @@ export default {
isLoading() {
return Boolean(this.loadingFinding);
},
isFindingDismissed() {
return this.finding?.state === STATE_DISMISSED;
},
toggledFindingStateMutationConfig() {
const { i18n } = this.$options;
const toggledState = this.isFindingDismissed ? STATE_DETECTED : STATE_DISMISSED;
const configs = {
[STATE_DISMISSED]: {
mutationName: 'securityFindingDismiss',
mutation: dismissFindingMutation,
errorMessage: i18n.dismissErrorMessage,
},
[STATE_DETECTED]: {
mutationName: 'securityFindingRevertToDetected',
mutation: revertFindingToDetectedMutation,
errorMessage: i18n.revertDismissErrorMessage,
},
};
return { ...configs[toggledState], toggledState };
},
},
methods: {
closeModal() {
this.$refs.modal.hide();
},
async dismissFinding() {
this.isDismissingFinding = true;
async toggleFindingState() {
this.isUpdatingFindingState = true;
const {
mutation,
errorMessage,
mutationName,
toggledState,
} = this.toggledFindingStateMutationConfig;
try {
await this.$apollo.mutate({
mutation: dismissFindingMutation,
mutation,
variables: {
uuid: this.findingUuid,
},
update: (store, response) => {
const { errors } = response.data.securityFindingDismiss;
const { errors } = response.data[mutationName];
if (errors.length > 0) {
throw new Error(errors[0]);
}
updateFindingState({
state: 'DISMISSED',
state: toggledState,
store,
query: securityReportFindingQuery,
variables: this.queryVariables,
});
this.$emit('dismissed-finding');
this.$emit('state-update', toggledState);
this.closeModal();
},
});
} catch {
this.errorMessage = this.$options.i18n.dismissErrorMessage;
this.errorMessage = errorMessage;
} finally {
this.isDismissingFinding = false;
this.isUpdatingFindingState = false;
}
},
},
......@@ -124,8 +157,10 @@ export default {
dismissErrorMessage: s__(
'SecurityReports|There was an error dismissing the finding. Please try again.',
),
revertDismissErrorMessage: s__('SecurityReports|There was an error reverting the dismissal.'),
cancel: __('Cancel'),
dismissFinding: __('Dismiss'),
dismissFinding: s__('SecurityReports|Dismiss vulnerability'),
revertDismissFinding: s__('SecurityReports|Undo dismiss'),
},
};
</script>
......@@ -176,10 +211,12 @@ export default {
</gl-button>
<gl-button
data-testid="dismiss-button"
:loading="isDismissingFinding"
@click="dismissFinding"
:loading="isUpdatingFindingState"
@click="toggleFindingState"
>
{{ $options.i18n.dismissFinding }}
{{
isFindingDismissed ? $options.i18n.revertDismissFinding : $options.i18n.dismissFinding
}}
</gl-button>
</div>
</template>
......
mutation revertFindingToDetected($uuid: String!) {
securityFindingRevertToDetected(input: { uuid: $uuid }) {
errors
}
}
......@@ -504,13 +504,18 @@ export const pipelineSecurityReportFinding = {
details: Object.values(vulnerabilityDetails),
};
export const getPipelineSecurityReportFindingResponse = (withoutFindingData = false) => ({
export const getPipelineSecurityReportFindingResponse = ({
overrides = {},
withoutFindingData = false,
} = {}) => ({
data: {
project: {
id: '1',
pipeline: {
id: '1',
securityReportFinding: withoutFindingData ? null : pipelineSecurityReportFinding,
securityReportFinding: withoutFindingData
? null
: { ...pipelineSecurityReportFinding, ...overrides },
},
},
},
......
......@@ -5,11 +5,15 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import VulnerabilityFindingModal from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import VulnerabilityFindingModal, {
STATE_DETECTED,
STATE_DISMISSED,
} from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_graphql.vue';
import VulnerabilityDetailsGraphql from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dismiss_finding.mutation.graphql';
import securityFindingRevertToDetected from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql';
import {
getPipelineSecurityReportFindingResponse,
pipelineSecurityReportFinding,
......@@ -21,43 +25,44 @@ const TEST_FINDING = pipelineSecurityReportFinding;
const TEST_PIPELINE_IID = 1;
const TEST_PROJECT_FULL_PATH = 'path/to/my/project';
describe('Vulnerability finding modal', () => {
describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue', () => {
let wrapper;
const createMockApolloProvider = ({
withFindingFetchError,
withDismissalMutationError,
withoutFindingData,
}) => {
const response = getPipelineSecurityReportFindingResponse(withoutFindingData);
const createMockApolloProvider = ({ handlers = {} } = {}) => {
const requestHandlers = [
[
securityReportFindingQuery,
withFindingFetchError
? jest.fn().mockRejectedValue()
: jest.fn().mockResolvedValue(response),
handlers.securityReportFindingQuery ||
jest.fn().mockResolvedValue(getPipelineSecurityReportFindingResponse()),
],
[
dismissFindingMutation,
jest.fn().mockResolvedValue({
data: {
securityFindingDismiss: {
errors: [...(withDismissalMutationError ? ['Error happened'] : [])],
handlers.dismissMutation ||
jest.fn().mockResolvedValue({
data: {
securityFindingDismiss: {
errors: [],
},
},
},
}),
}),
],
[
securityFindingRevertToDetected,
handlers.revertToDetectedMutation ||
jest.fn().mockResolvedValue({
data: {
securityFindingRevertToDetected: {
errors: [],
},
},
}),
],
];
return createMockApollo(requestHandlers);
};
const createWrapper = ({
withFindingFetchError = false,
withDismissalMutationError = false,
withoutFindingData = false,
} = {}) =>
const createWrapper = ({ responseHandlers } = {}) =>
shallowMountExtended(VulnerabilityFindingModal, {
propsData: {
findingUuid: TEST_FINDING.uuid,
......@@ -70,9 +75,7 @@ describe('Vulnerability finding modal', () => {
}),
},
apolloProvider: createMockApolloProvider({
withFindingFetchError,
withDismissalMutationError,
withoutFindingData,
handlers: responseHandlers,
}),
});
......@@ -88,7 +91,7 @@ describe('Vulnerability finding modal', () => {
];
const findDismissButton = () => withinFooter().findByTestId('dismiss-button');
const dismissFinding = () => findDismissButton().vm.$emit('click');
const toggleFindingState = () => findDismissButton().vm.$emit('click');
const expectModalToBeHiddenAfter = async ({ action }) => {
const modalHideSpy = jest.spyOn(findModal().vm, 'hide');
......@@ -198,56 +201,91 @@ describe('Vulnerability finding modal', () => {
});
});
describe.each([{ withFindingFetchError: true }, { withoutFindingData: true }])(
'when an error happens - %s',
(errorContext) => {
beforeEach(async () => {
wrapper = createWrapper(errorContext);
await waitForFindingToBeLoaded();
describe.each`
description | handlers
${'error response'} | ${jest.fn().mockRejectedValue()}
${'empty data'} | ${jest.fn().mockResolvedValue(getPipelineSecurityReportFindingResponse({ withoutFindingData: true }))}
`('with $description', ({ handlers }) => {
beforeEach(async () => {
wrapper = createWrapper({
responseHandlers: { securityReportFindingQuery: handlers },
});
await waitForFindingToBeLoaded();
});
it(`shows an error message within the modal's heading`, () => {
expect(wrapper.findByRole('heading').text()).toBe('Error');
});
it(`shows an error message within the modal's heading`, () => {
expect(wrapper.findByRole('heading').text()).toBe('Error');
});
it('shows an error alert with the correct error message', () => {
expect(findErrorAlert().text()).toBe(
'There was an error fetching the finding. Please try again.',
);
});
},
);
it('shows an error alert with the correct error message', () => {
expect(findErrorAlert().text()).toBe(
'There was an error fetching the finding. Please try again.',
);
});
});
describe('dismissal', () => {
describe('success', () => {
beforeEach(async () => {
wrapper = createWrapper();
await waitForFindingToBeLoaded();
});
describe('state toggle', () => {
it.each`
initialState | expectedNewState
${STATE_DISMISSED} | ${STATE_DETECTED}
${STATE_DETECTED} | ${STATE_DISMISSED}
`(
"updates to '$expectedNewState' when initial state is '$initialState'",
async ({ initialState, expectedNewState }) => {
const response = getPipelineSecurityReportFindingResponse({
overrides: {
state: initialState,
},
});
wrapper = createWrapper({
responseHandlers: {
securityReportFindingQuery: jest.fn().mockResolvedValue(response),
},
});
await waitForFindingToBeLoaded();
it('sets the finding to be dismissed', async () => {
expect(findVulnerabilityDetails().props()).toMatchObject({
state: 'CONFIRMED',
});
expect(findVulnerabilityDetails().props()).toMatchObject({
state: initialState,
});
dismissFinding();
await waitForFindingToBeDismissed();
toggleFindingState();
await waitForFindingToBeDismissed();
expect(findVulnerabilityDetails().props()).toMatchObject({
state: 'DISMISSED',
expect(findVulnerabilityDetails().props()).toMatchObject({
state: expectedNewState,
});
},
);
});
describe('success', () => {
it.each([
{ initialState: STATE_DISMISSED, expectedPayload: STATE_DETECTED },
{ initialState: STATE_DETECTED, expectedPayload: STATE_DISMISSED },
])('emits "state-updated" with %s', async ({ initialState, expectedPayload }) => {
const response = getPipelineSecurityReportFindingResponse({
overrides: {
state: initialState,
},
});
});
wrapper = createWrapper({
responseHandlers: {
securityReportFindingQuery: jest.fn().mockResolvedValue(response),
},
});
await waitForFindingToBeLoaded();
it('emits "dismissed-finding"', async () => {
expect(wrapper.emitted('dismissed-finding')).toBeUndefined();
expect(wrapper.emitted('state-update')).toBeUndefined();
dismissFinding();
toggleFindingState();
await waitForFindingToBeDismissed();
expect(wrapper.emitted('dismissed-finding')).toHaveLength(1);
expect(wrapper.emitted('state-update')).toEqual([[expectedPayload]]);
});
it('hides the modal', () => {
wrapper = createWrapper();
expectModalToBeHiddenAfter({
action: async () => {
findDismissButton().vm.$emit('click');
......@@ -260,21 +298,46 @@ describe('Vulnerability finding modal', () => {
describe('error', () => {
beforeEach(async () => {
wrapper = createWrapper({
withDismissalMutationError: true,
responseHandlers: {
dismissMutation: jest.fn().mockRejectedValue(),
},
});
await waitForFindingToBeLoaded();
});
it('shows an message within the error alert when the response contains errors', async () => {
expect(findErrorAlert().exists()).toBe(false);
it.each([
{
initialState: STATE_DETECTED,
expectedErrorMessage: 'There was an error dismissing the finding. Please try again.',
},
{
initialState: STATE_DISMISSED,
expectedErrorMessage: 'There was an error reverting the dismissal.',
},
])(
'shows an error alert when the state mutation fails: %s',
async ({ initialState, expectedErrorMessage }) => {
const response = getPipelineSecurityReportFindingResponse({
overrides: {
state: initialState,
},
});
wrapper = createWrapper({
responseHandlers: {
securityReportFindingQuery: jest.fn().mockResolvedValue(response),
dismissMutation: jest.fn().mockRejectedValue(),
revertToDetectedMutation: jest.fn().mockRejectedValue(),
},
});
await waitForFindingToBeLoaded();
expect(findErrorAlert().exists()).toBe(false);
dismissFinding();
await waitForFindingToBeDismissed();
toggleFindingState();
await waitForFindingToBeDismissed();
expect(findErrorAlert().text()).toBe(
'There was an error dismissing the finding. Please try again.',
);
});
expect(findErrorAlert().text()).toBe(expectedErrorMessage);
},
);
});
});
});
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать