Improve error handling

Use existing mechanism to display run scan errors without blocking other
potential erros from being presented to the user (eg profile deletion
errors).
parent f2e9fbf3
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { ERROR_RUN_SCAN, ERROR_MESSAGES } from 'ee/on_demand_scans/settings'; import { ERROR_RUN_SCAN, ERROR_MESSAGES } from 'ee/on_demand_scans/settings';
import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql'; import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql';
import ProfilesList from './dast_profiles_list.vue'; import ProfilesList from './dast_profiles_list.vue';
...@@ -18,13 +18,43 @@ export default { ...@@ -18,13 +18,43 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
errorMessage: {
type: String,
required: false,
default: '',
},
errorDetails: {
type: Array,
required: false,
default: () => [],
},
}, },
data: () => ({ data: () => ({
isRunningScan: null, isRunningScan: null,
hasRunScanError: false,
runScanErrors: [],
}), }),
computed: {
error() {
if (this.hasRunScanError) {
return {
errorMessage: ERROR_MESSAGES[ERROR_RUN_SCAN],
errorDetails: this.runScanErrors,
};
}
const { errorMessage, errorDetails } = this;
return { errorMessage, errorDetails };
},
},
watch: {
errorMessage() {
this.hasRunScanError = false;
},
},
methods: { methods: {
async runScan({ id }) { async runScan({ id }) {
this.isRunningScan = id; this.isRunningScan = id;
this.hasRunScanError = false;
try { try {
const { const {
data: { data: {
...@@ -41,24 +71,34 @@ export default { ...@@ -41,24 +71,34 @@ export default {
}); });
if (errors.length) { if (errors.length) {
this.handleError(); this.handleRunScanError({ errors });
} else { } else {
redirectTo(pipelineUrl); redirectTo(pipelineUrl);
} }
} catch (error) { } catch (error) {
this.handleError(error); this.handleRunScanError(error);
} }
}, },
handleError(error) { handleRunScanError({ exception = null, errors = [] } = {}) {
this.isRunningScan = null; this.isRunningScan = null;
createFlash({ message: ERROR_MESSAGES[ERROR_RUN_SCAN], error, captureError: true }); this.hasRunScanError = true;
this.runScanErrors = errors;
if (exception !== null) {
Sentry.captureException(exception);
}
}, },
}, },
}; };
</script> </script>
<template> <template>
<profiles-list :full-path="fullPath" v-bind="$attrs" v-on="$listeners"> <profiles-list
:full-path="fullPath"
:error-message="error.errorMessage"
:error-details="error.errorDetails"
v-bind="$attrs"
v-on="$listeners"
>
<!-- eslint-disable-next-line vue/valid-v-slot --> <!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastScannerProfile.scanType)="{ value }"> <template #cell(dastScannerProfile.scanType)="{ value }">
<scan-type-badge :scan-type="value" /> <scan-type-badge :scan-type="value" />
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { ERROR_RUN_SCAN, ERROR_MESSAGES } from 'ee/on_demand_scans/settings';
import Component from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue'; import Component from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue';
import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue'; import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -124,31 +125,48 @@ describe('EE - DastSavedScansList', () => { ...@@ -124,31 +125,48 @@ describe('EE - DastSavedScansList', () => {
expect(createFlash).not.toHaveBeenCalled(); expect(createFlash).not.toHaveBeenCalled();
}); });
it('create a flash error on failure', async () => { it('passes the error message down to the list on failure but does not block errors passed by the parent', async () => {
const initialErrorMessage = 'Initial error message';
const finalErrorMessage = 'Final error message';
createFullComponent({ createFullComponent({
propsData: { profiles: savedScans }, propsData: {
profiles: savedScans,
errorMessage: initialErrorMessage,
},
mocks: { mocks: {
$apollo: { $apollo: {
mutate: jest.fn().mockRejectedValue(), mutate: jest.fn().mockRejectedValue(),
}, },
}, },
}); });
const profilesList = findProfileList();
expect(profilesList.props('errorMessage')).toBe(initialErrorMessage);
wrapper.findByTestId('dast-scan-run-button').trigger('click'); wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalled(); expect(profilesList.props('errorMessage')).toBe(ERROR_MESSAGES[ERROR_RUN_SCAN]);
expect(redirectTo).not.toHaveBeenCalled(); expect(redirectTo).not.toHaveBeenCalled();
await wrapper.setProps({ errorMessage: finalErrorMessage });
expect(profilesList.props('errorMessage')).toBe(finalErrorMessage);
}); });
it('create a flash error if the API responds with errors-as-data', async () => { it('passes the error message and details down to the list if the API responds with errors-as-data', async () => {
const errors = ['error-as-data'];
createFullComponent({ createFullComponent({
propsData: { profiles: savedScans }, propsData: { profiles: savedScans },
mocks: { mocks: {
$apollo: { $apollo: {
mutate: jest.fn().mockResolvedValue({ mutate: jest.fn().mockResolvedValue({
dastProfileRun: { data: {
pipelineUrl: null, dastProfileRun: {
errors: ['error-as-data'], pipelineUrl: null,
errors,
},
}, },
}), }),
}, },
...@@ -157,7 +175,8 @@ describe('EE - DastSavedScansList', () => { ...@@ -157,7 +175,8 @@ describe('EE - DastSavedScansList', () => {
wrapper.findByTestId('dast-scan-run-button').trigger('click'); wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalled(); expect(findProfileList().props('errorMessage')).toBe(ERROR_MESSAGES[ERROR_RUN_SCAN]);
expect(findProfileList().props('errorDetails')).toBe(errors);
expect(redirectTo).not.toHaveBeenCalled(); expect(redirectTo).not.toHaveBeenCalled();
}); });
}); });
......
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