Implement Run & Delete scan actions

Adds the ability to run or delete a saved DAST scans from the DAST
profiles library.
parent a617e2f1
<script> <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 ProfilesList from './dast_profiles_list.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue'; import ScanTypeBadge from './dast_scan_type_badge.vue';
export default { export default {
components: { components: {
GlButton,
ProfilesList, ProfilesList,
ScanTypeBadge, 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> </script>
<template> <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 --> <!-- 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" />
</template> </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> </profiles-list>
</template> </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 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 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 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'; import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
...@@ -19,10 +19,10 @@ export const getProfileSettings = ({ createNewProfilePaths, isDastSavedScansEnab ...@@ -19,10 +19,10 @@ export const getProfileSettings = ({ createNewProfilePaths, isDastSavedScansEnab
graphQL: { graphQL: {
query: dastProfilesQuery, query: dastProfilesQuery,
deletion: { deletion: {
mutation: dastSavedScansDelete, mutation: dastProfileDelete,
optimisticResponse: dastProfilesDeleteResponse({ optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'savedScanDelete', mutationName: 'dastProfileDelete',
payloadTypeName: 'DastSavedScanDeletePayload', payloadTypeName: 'DastProfileDeletePayload',
}), }),
}, },
}, },
......
...@@ -2,8 +2,15 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -2,8 +2,15 @@ import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { savedScans } from '../mocks/mock_data'; import { savedScans } from '../mocks/mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
describe('EE - DastSavedScansList', () => { describe('EE - DastSavedScansList', () => {
let wrapper; let wrapper;
...@@ -24,7 +31,8 @@ describe('EE - DastSavedScansList', () => { ...@@ -24,7 +31,8 @@ describe('EE - DastSavedScansList', () => {
}; };
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => { const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn( wrapper = extendedWrapper(
mountFn(
Component, Component,
merge( merge(
{ {
...@@ -32,6 +40,7 @@ describe('EE - DastSavedScansList', () => { ...@@ -32,6 +40,7 @@ describe('EE - DastSavedScansList', () => {
}, },
options, options,
), ),
),
); );
}; };
const createFullComponent = wrapperFactory(mount); const createFullComponent = wrapperFactory(mount);
...@@ -67,4 +76,65 @@ describe('EE - DastSavedScansList', () => { ...@@ -67,4 +76,65 @@ describe('EE - DastSavedScansList', () => {
expect(inputHandler).toHaveBeenCalled(); 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 "" ...@@ -9221,6 +9221,9 @@ msgstr ""
msgid "DastProfiles|Request headers" msgid "DastProfiles|Request headers"
msgstr "" msgstr ""
msgid "DastProfiles|Run scan"
msgstr ""
msgid "DastProfiles|Run the AJAX spider, in addition to the traditional spider, to crawl the target site." msgid "DastProfiles|Run the AJAX spider, in addition to the traditional spider, to crawl the target site."
msgstr "" 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