Implement Run & Delete scan actions

Adds the ability to run or delete a saved DAST scans from the DAST
profiles library.
parent a617e2f1
<script>
import { GlButton } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { ERROR_RUN_SCAN, ERROR_MESSAGES } from 'ee/on_demand_scans/settings';
import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql';
import ProfilesList from './dast_profiles_list.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue';
export default {
components: {
GlButton,
ProfilesList,
ScanTypeBadge,
},
props: {
fullPath: {
type: String,
required: true,
},
},
methods: {
async runScan({ id }) {
try {
const {
dastProfileRun: { pipelineUrl, errors },
} = await this.$apollo.mutate({
mutation: dastProfileRunMutation,
variables: {
input: {
fullPath: this.fullPath,
id,
},
},
});
if (errors.length) {
this.handleError();
} else {
redirectTo(pipelineUrl);
}
} catch (error) {
this.handleError(error);
}
},
handleError(error) {
createFlash({ message: ERROR_MESSAGES[ERROR_RUN_SCAN], error, captureError: true });
},
},
};
</script>
<template>
<profiles-list v-bind="$attrs" v-on="$listeners">
<profiles-list :full-path="fullPath" v-bind="$attrs" v-on="$listeners">
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastScannerProfile.scanType)="{ value }">
<scan-type-badge :scan-type="value" />
</template>
<template #actions="{ profile }">
<gl-button size="small" data-testid="dast-scan-run-button" @click="runScan(profile)">{{
s__('DastProfiles|Run scan')
}}</gl-button>
</template>
</profiles-list>
</template>
mutation dastProfileDelete($input: DastProfileDeleteInput!) {
dastProfileDelete(input: $input) {
errors
}
}
mutation dastProfileRun($input: DastProfileRunInput!) {
dastProfileRun(input: $input) {
pipelineUrl
errors
}
}
mutation dastSavedScansDelete($input: DastSavedScansDeleteInput!) {
savedScansDelete(input: $input) @client {
errors
}
}
import dastProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_profiles.query.graphql';
import dastSavedScansDelete from 'ee/security_configuration/dast_profiles/graphql/dast_saved_scans_delete.mutation.graphql';
import dastProfileDelete from 'ee/security_configuration/dast_profiles/graphql/dast_profile_delete.mutation.graphql';
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles_delete.mutation.graphql';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
......@@ -19,10 +19,10 @@ export const getProfileSettings = ({ createNewProfilePaths, isDastSavedScansEnab
graphQL: {
query: dastProfilesQuery,
deletion: {
mutation: dastSavedScansDelete,
mutation: dastProfileDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'savedScanDelete',
payloadTypeName: 'DastSavedScanDeletePayload',
mutationName: 'dastProfileDelete',
payloadTypeName: 'DastProfileDeletePayload',
}),
},
},
......
......@@ -2,8 +2,15 @@ import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
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 waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { savedScans } from '../mocks/mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
describe('EE - DastSavedScansList', () => {
let wrapper;
......@@ -24,13 +31,15 @@ describe('EE - DastSavedScansList', () => {
};
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(
Component,
merge(
{
propsData: defaultProps,
},
options,
wrapper = extendedWrapper(
mountFn(
Component,
merge(
{
propsData: defaultProps,
},
options,
),
),
);
};
......@@ -67,4 +76,65 @@ describe('EE - DastSavedScansList', () => {
expect(inputHandler).toHaveBeenCalled();
});
describe('run scan', () => {
it('redirects to the running pipeline page on success', async () => {
const pipelineUrl = '/pipeline/url';
createFullComponent({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: jest.fn().mockResolvedValue({
dastProfileRun: {
pipelineUrl,
errors: [],
},
}),
},
},
});
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledWith(pipelineUrl);
expect(createFlash).not.toHaveBeenCalled();
});
it('create a flash error on failure', async () => {
createFullComponent({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
},
});
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(redirectTo).not.toHaveBeenCalled();
});
it('create a flash error if the API responds with errors-as-data', async () => {
createFullComponent({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: jest.fn().mockResolvedValue({
dastProfileRun: {
pipelineUrl: null,
errors: ['error-as-data'],
},
}),
},
},
});
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(redirectTo).not.toHaveBeenCalled();
});
});
});
......@@ -9221,6 +9221,9 @@ msgstr ""
msgid "DastProfiles|Request headers"
msgstr ""
msgid "DastProfiles|Run scan"
msgstr ""
msgid "DastProfiles|Run the AJAX spider, in addition to the traditional spider, to crawl the target site."
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