Commit e2309055 authored by Scott Hampton's avatar Scott Hampton

Merge branch '295247-dast-saved-scans-run-delete' into 'master'

Implement Run & Delete scan actions

See merge request gitlab-org/gitlab!53533
parents 4ab1e2c3 b3b5892a
...@@ -176,7 +176,7 @@ export default { ...@@ -176,7 +176,7 @@ export default {
mutation: deletion.mutation, mutation: deletion.mutation,
variables: { variables: {
input: { input: {
fullPath: projectFullPath, ...(profileType !== 'dastProfiles' ? { fullPath: projectFullPath } : {}),
id: profileId, id: profileId,
}, },
}, },
......
<script> <script>
import { GlButton } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { redirectTo } from '~/lib/utils/url_utility';
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,
},
errorMessage: {
type: String,
required: false,
default: '',
},
errorDetails: {
type: Array,
required: false,
default: () => [],
},
},
data: () => ({
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: {
async runScan({ id }) {
this.isRunningScan = id;
this.hasRunScanError = false;
try {
const {
data: {
dastProfileRun: { pipelineUrl, errors },
},
} = await this.$apollo.mutate({
mutation: dastProfileRunMutation,
variables: {
input: {
fullPath: this.fullPath,
id,
},
},
});
if (errors.length) {
this.handleRunScanError({ errors });
} else {
redirectTo(pipelineUrl);
}
} catch (error) {
this.handleRunScanError(error);
}
},
handleRunScanError({ exception = null, errors = [] } = {}) {
this.isRunningScan = null;
this.hasRunScanError = true;
this.runScanErrors = errors;
if (exception !== null) {
Sentry.captureException(exception);
}
},
},
}; };
</script> </script>
<template> <template>
<profiles-list 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" />
</template> </template>
<template #actions="{ profile }">
<gl-button
size="small"
data-testid="dast-scan-run-button"
:loading="isRunningScan === profile.id"
:disabled="Boolean(isRunningScan)"
@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',
}), }),
}, },
}, },
......
...@@ -171,6 +171,31 @@ describe('EE - DastProfiles', () => { ...@@ -171,6 +171,31 @@ describe('EE - DastProfiles', () => {
}); });
}); });
it.each`
profileType | key | givenData | expectedValue | exposedAsProp
${'dastProfiles'} | ${'errorMessage'} | ${{ errorMessage: 'foo' }} | ${'foo'} | ${true}
${'dastProfiles'} | ${'errorDetails'} | ${{ errorDetails: ['foo'] }} | ${['foo']} | ${true}
${'dastProfiles'} | ${'has-more-profiles-to-load'} | ${{ pageInfo: { hasNextPage: true } }} | ${'true'} | ${false}
${'siteProfiles'} | ${'error-message'} | ${{ errorMessage: 'foo' }} | ${'foo'} | ${false}
${'siteProfiles'} | ${'error-details'} | ${{ errorDetails: ['foo'] }} | ${'foo'} | ${false}
${'siteProfiles'} | ${'has-more-profiles-to-load'} | ${{ pageInfo: { hasNextPage: true } }} | ${'true'} | ${false}
${'scannerProfiles'} | ${'error-message'} | ${{ errorMessage: 'foo' }} | ${'foo'} | ${false}
${'scannerProfiles'} | ${'error-details'} | ${{ errorDetails: ['foo'] }} | ${'foo'} | ${false}
${'scannerProfiles'} | ${'has-more-profiles-to-load'} | ${{ pageInfo: { hasNextPage: true } }} | ${'true'} | ${false}
`(
'passes down $key properly for $profileType',
async ({ profileType, key, givenData, expectedValue, exposedAsProp }) => {
const propGetter = exposedAsProp ? 'props' : 'attributes';
createComponent();
wrapper.setData({
profileTypes: { [profileType]: givenData },
});
await wrapper.vm.$nextTick();
expect(getProfilesComponent(profileType)[propGetter](key)).toEqual(expectedValue);
},
);
describe.each` describe.each`
description | profileType description | profileType
${'Saved Scans List'} | ${'dastProfiles'} ${'Saved Scans List'} | ${'dastProfiles'}
...@@ -187,19 +212,6 @@ describe('EE - DastProfiles', () => { ...@@ -187,19 +212,6 @@ describe('EE - DastProfiles', () => {
expect(getProfilesComponent(profileType).attributes('is-loading')).toBe('true'); expect(getProfilesComponent(profileType).attributes('is-loading')).toBe('true');
}); });
it.each`
givenData | propName | expectedPropValue
${{ profileTypes: { [profileType]: { errorMessage: 'foo' } } }} | ${'error-message'} | ${'foo'}
${{ profileTypes: { [profileType]: { errorDetails: ['foo'] } } }} | ${'error-details'} | ${'foo'}
${{ profileTypes: { [profileType]: { pageInfo: { hasNextPage: true } } } }} | ${'has-more-profiles-to-load'} | ${'true'}
`('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => {
wrapper.setData(givenData);
await wrapper.vm.$nextTick();
expect(getProfilesComponent(profileType).attributes(propName)).toEqual(expectedPropValue);
});
it('fetches more results when "@load-more-profiles" is emitted', () => { it('fetches more results when "@load-more-profiles" is emitted', () => {
const { const {
$apollo: { $apollo: {
......
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 { 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 +32,8 @@ describe('EE - DastSavedScansList', () => { ...@@ -24,7 +32,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 +41,7 @@ describe('EE - DastSavedScansList', () => { ...@@ -32,6 +41,7 @@ describe('EE - DastSavedScansList', () => {
}, },
options, options,
), ),
),
); );
}; };
const createFullComponent = wrapperFactory(mount); const createFullComponent = wrapperFactory(mount);
...@@ -67,4 +77,107 @@ describe('EE - DastSavedScansList', () => { ...@@ -67,4 +77,107 @@ describe('EE - DastSavedScansList', () => {
expect(inputHandler).toHaveBeenCalled(); expect(inputHandler).toHaveBeenCalled();
}); });
describe('run scan', () => {
const pipelineUrl = '/pipeline/url';
const successHandler = jest.fn().mockResolvedValue({
data: {
dastProfileRun: {
pipelineUrl,
errors: [],
},
},
});
it('puts the clicked button in the loading state and disabled other buttons', async () => {
createFullComponent({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: successHandler,
},
},
});
const buttons = wrapper.findAll('[data-testid="dast-scan-run-button"]');
expect(buttons.at(0).props('loading')).toBe(false);
expect(buttons.at(1).props('disabled')).toBe(false);
await buttons.at(0).trigger('click');
expect(buttons.at(0).props('loading')).toBe(true);
expect(buttons.at(1).props('disabled')).toBe(true);
});
it('redirects to the running pipeline page on success', async () => {
createFullComponent({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: successHandler,
},
},
});
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledWith(pipelineUrl);
expect(createFlash).not.toHaveBeenCalled();
});
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({
propsData: {
profiles: savedScans,
errorMessage: initialErrorMessage,
},
mocks: {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
},
});
const profilesList = findProfileList();
expect(profilesList.props('errorMessage')).toBe(initialErrorMessage);
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(profilesList.props('errorMessage')).toBe(ERROR_MESSAGES[ERROR_RUN_SCAN]);
expect(redirectTo).not.toHaveBeenCalled();
await wrapper.setProps({ errorMessage: finalErrorMessage });
expect(profilesList.props('errorMessage')).toBe(finalErrorMessage);
});
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({
propsData: { profiles: savedScans },
mocks: {
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
dastProfileRun: {
pipelineUrl: null,
errors,
},
},
}),
},
},
});
wrapper.findByTestId('dast-scan-run-button').trigger('click');
await waitForPromises();
expect(findProfileList().props('errorMessage')).toBe(ERROR_MESSAGES[ERROR_RUN_SCAN]);
expect(findProfileList().props('errorDetails')).toBe(errors);
expect(redirectTo).not.toHaveBeenCalled();
});
});
}); });
...@@ -64,10 +64,17 @@ export const scannerProfiles = [ ...@@ -64,10 +64,17 @@ export const scannerProfiles = [
export const savedScans = [ export const savedScans = [
{ {
id: 'gid://gitlab/DastScan/1', id: 'gid://gitlab/DastProfile/1',
name: 'Scan 1', name: 'Scan 1',
dastSiteProfile: siteProfiles[0], dastSiteProfile: siteProfiles[0],
dastScannerProfile: scannerProfiles[0], dastScannerProfile: scannerProfiles[0],
editPath: '/1/edit', editPath: '/1/edit',
}, },
{
id: 'gid://gitlab/DastProfile/2',
name: 'Scan 2',
dastSiteProfile: siteProfiles[1],
dastScannerProfile: scannerProfiles[1],
editPath: '/2/edit',
},
]; ];
...@@ -9224,6 +9224,9 @@ msgstr "" ...@@ -9224,6 +9224,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