Commit 2aaa1509 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '246780-refactor-on-demand-profiles-selection' into 'master'

Improve reusability in On-demand DAST Scans form

See merge request gitlab-org/gitlab!42013
parents f00e93e9 76aa9c03
...@@ -737,8 +737,8 @@ To run an on-demand DAST scan, you need: ...@@ -737,8 +737,8 @@ To run an on-demand DAST scan, you need:
1. From your project's home page, go to **Security & Compliance > On-demand Scans** in the left sidebar. 1. From your project's home page, go to **Security & Compliance > On-demand Scans** in the left sidebar.
1. Click **Create new DAST scan**. 1. Click **Create new DAST scan**.
1. In **Scanner settings**, select a scanner profile from the dropdown. 1. In **Scanner profile**, select a scanner profile from the dropdown.
1. In **Site profiles**, select a site profile from the dropdown. 1. In **Site profile**, select a site profile from the dropdown.
1. Click **Run scan**. 1. Click **Run scan**.
The on-demand DAST scan runs and the project's dashboard shows the results. The on-demand DAST scan runs and the project's dashboard shows the results.
......
...@@ -5,52 +5,52 @@ import { ...@@ -5,52 +5,52 @@ import {
GlButton, GlButton,
GlCard, GlCard,
GlForm, GlForm,
GlFormGroup,
GlLink, GlLink,
GlDropdown,
GlDropdownItem,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import {
ERROR_RUN_SCAN,
ERROR_FETCH_SCANNER_PROFILES,
ERROR_FETCH_SITE_PROFILES,
ERROR_MESSAGES,
SCANNER_PROFILES_QUERY,
SITE_PROFILES_QUERY,
} from '../settings';
import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql'; import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql';
import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue'; import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue';
import OnDemandScansScannerProfileSelector from './profile_selector/scanner_profile_selector.vue';
import OnDemandScansSiteProfileSelector from './profile_selector/site_profile_selector.vue';
const ERROR_RUN_SCAN = 'ERROR_RUN_SCAN'; const createProfilesApolloOptions = (name, { fetchQuery, fetchError }) => ({
const ERROR_FETCH_SCANNER_PROFILES = 'ERROR_FETCH_SCANNER_PROFILES'; query: fetchQuery,
const ERROR_FETCH_SITE_PROFILES = 'ERROR_FETCH_SITE_PROFILES'; variables() {
return {
const ERROR_MESSAGES = { fullPath: this.projectPath,
[ERROR_RUN_SCAN]: s__('OnDemandScans|Could not run the scan. Please try again.'), };
[ERROR_FETCH_SCANNER_PROFILES]: s__( },
'OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later.', update(data) {
), const edges = data?.project?.[name]?.edges ?? [];
[ERROR_FETCH_SITE_PROFILES]: s__( return edges.map(({ node }) => node);
'OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later.', },
), error(e) {
}; Sentry.captureException(e);
this.showErrors(fetchError);
const initField = value => ({ },
value,
state: null,
feedback: null,
}); });
export default { export default {
components: { components: {
OnDemandScansScannerProfileSelector,
OnDemandScansSiteProfileSelector,
GlAlert, GlAlert,
GlButton, GlButton,
GlCard, GlCard,
GlForm, GlForm,
GlFormGroup,
GlLink, GlLink,
GlDropdown,
GlDropdownItem,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
DismissibleFeedbackAlert, DismissibleFeedbackAlert,
...@@ -60,38 +60,8 @@ export default { ...@@ -60,38 +60,8 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
apollo: { apollo: {
scannerProfiles: { scannerProfiles: createProfilesApolloOptions('scannerProfiles', SCANNER_PROFILES_QUERY),
query: dastScannerProfilesQuery, siteProfiles: createProfilesApolloOptions('siteProfiles', SITE_PROFILES_QUERY),
variables() {
return {
fullPath: this.projectPath,
};
},
update(data) {
const scannerProfilesEdges = data?.project?.scannerProfiles?.edges ?? [];
return scannerProfilesEdges.map(({ node }) => node);
},
error(e) {
Sentry.captureException(e);
this.showErrors(ERROR_FETCH_SCANNER_PROFILES);
},
},
siteProfiles: {
query: dastSiteProfilesQuery,
variables() {
return {
fullPath: this.projectPath,
};
},
update(data) {
const siteProfileEdges = data?.project?.siteProfiles?.edges ?? [];
return siteProfileEdges.map(({ node }) => node);
},
error(e) {
Sentry.captureException(e);
this.showErrors(ERROR_FETCH_SITE_PROFILES);
},
},
}, },
props: { props: {
helpPagePath: { helpPagePath: {
...@@ -126,8 +96,8 @@ export default { ...@@ -126,8 +96,8 @@ export default {
scannerProfiles: [], scannerProfiles: [],
siteProfiles: [], siteProfiles: [],
form: { form: {
dastScannerProfileId: initField(null), [SCANNER_PROFILES_QUERY.field]: null,
dastSiteProfileId: initField(null), [SITE_PROFILES_QUERY.field]: null,
}, },
loading: false, loading: false,
errorType: null, errorType: null,
...@@ -140,60 +110,16 @@ export default { ...@@ -140,60 +110,16 @@ export default {
return ERROR_MESSAGES[this.errorType] || null; return ERROR_MESSAGES[this.errorType] || null;
}, },
isLoadingProfiles() { isLoadingProfiles() {
return ['scanner', 'site'].some( return ['scannerProfiles', 'siteProfiles'].some(name => this.$apollo.queries[name].loading);
profileType => this.$apollo.queries[`${profileType}Profiles`].loading,
);
}, },
failedToLoadProfiles() { failedToLoadProfiles() {
return [ERROR_FETCH_SCANNER_PROFILES, ERROR_FETCH_SITE_PROFILES].includes(this.errorType); return [ERROR_FETCH_SCANNER_PROFILES, ERROR_FETCH_SITE_PROFILES].includes(this.errorType);
}, },
formData() {
return {
fullPath: this.projectPath,
...Object.fromEntries(Object.entries(this.form).map(([key, { value }]) => [key, value])),
};
},
formHasErrors() {
return Object.values(this.form).some(({ state }) => state === false);
},
someFieldEmpty() { someFieldEmpty() {
return Object.values(this.form).some(({ value }) => !value); return Object.values(this.form).some(value => !value);
},
isSubmitDisabled() {
return this.formHasErrors || this.someFieldEmpty;
},
selectedScannerProfile() {
const selectedScannerProfile = this.form.dastScannerProfileId.value;
return selectedScannerProfile === null
? null
: this.scannerProfiles.find(({ id }) => id === selectedScannerProfile);
},
selectedSiteProfile() {
const selectedSiteProfileId = this.form.dastSiteProfileId.value;
return selectedSiteProfileId === null
? null
: this.siteProfiles.find(({ id }) => id === selectedSiteProfileId);
},
scannerProfileText() {
const { selectedScannerProfile } = this;
return selectedScannerProfile
? selectedScannerProfile.profileName
: s__('OnDemandScans|Select one of the existing profiles');
},
siteProfileText() {
const { selectedSiteProfile } = this;
return selectedSiteProfile
? `${selectedSiteProfile.profileName}: ${selectedSiteProfile.targetUrl}`
: s__('OnDemandScans|Select one of the existing profiles');
}, },
}, },
methods: { methods: {
setScannerProfile({ id }) {
this.form.dastScannerProfileId.value = id;
},
setSiteProfile({ id }) {
this.form.dastSiteProfileId.value = id;
},
onSubmit() { onSubmit() {
this.loading = true; this.loading = true;
this.hideErrors(); this.hideErrors();
...@@ -201,7 +127,10 @@ export default { ...@@ -201,7 +127,10 @@ export default {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: dastOnDemandScanCreateMutation, mutation: dastOnDemandScanCreateMutation,
variables: this.formData, variables: {
fullPath: this.projectPath,
...this.form,
},
}) })
.then(({ data: { dastOnDemandScanCreate: { pipelineUrl, errors } } }) => { .then(({ data: { dastOnDemandScanCreate: { pipelineUrl, errors } } }) => {
if (errors?.length) { if (errors?.length) {
...@@ -291,185 +220,22 @@ export default { ...@@ -291,185 +220,22 @@ export default {
</gl-card> </gl-card>
</template> </template>
<template v-else-if="!failedToLoadProfiles"> <template v-else-if="!failedToLoadProfiles">
<gl-card> <on-demand-scans-scanner-profile-selector
<template #header> v-model="form.dastScannerProfileId"
<div class="row"> :profiles="scannerProfiles"
<div class="col-7"> />
<h3 class="gl-font-lg gl-display-inline"> <on-demand-scans-site-profile-selector
{{ s__('OnDemandScans|Scanner settings') }} v-model="form.dastSiteProfileId"
</h3> :profiles="siteProfiles"
</div> />
<div class="col-5 gl-text-right">
<gl-button
:href="scannerProfiles.length ? scannerProfilesLibraryPath : null"
:disabled="!scannerProfiles.length"
variant="success"
category="secondary"
size="small"
data-testid="manage-scanner-profiles-button"
>
{{ s__('OnDemandScans|Manage profiles') }}
</gl-button>
</div>
</div>
</template>
<gl-form-group v-if="scannerProfiles.length">
<template #label>
{{ s__('OnDemandScans|Use existing scanner profile') }}
</template>
<gl-dropdown
v-model="form.dastScannerProfileId.value"
:text="scannerProfileText"
class="mw-460"
data-testid="scanner-profiles-dropdown"
>
<gl-dropdown-item
v-for="scannerProfile in scannerProfiles"
:key="scannerProfile.id"
:is-checked="form.dastScannerProfileId.value === scannerProfile.id"
is-check-item
@click="setScannerProfile(scannerProfile)"
>
{{ scannerProfile.profileName }}
</gl-dropdown-item>
</gl-dropdown>
<template v-if="selectedScannerProfile">
<hr />
<div data-testid="scanner-profile-summary">
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Scan mode') }}:</div>
<div class="col-md-9">
<strong>{{ s__('DastProfiles|Passive') }}</strong>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Spider timeout') }}:</div>
<div class="col-md-9">
<strong>
{{ n__('%d minute', '%d minutes', selectedScannerProfile.spiderTimeout) }}
</strong>
</div>
</div>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Target timeout') }}:</div>
<div class="col-md-9">
<strong>
{{ n__('%d second', '%d seconds', selectedScannerProfile.targetTimeout) }}
</strong>
</div>
</div>
</div>
</div>
</div>
</template>
</gl-form-group>
<template v-else>
<p class="gl-text-gray-700">
{{
s__(
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile.',
)
}}
</p>
<gl-button
:href="newScannerProfilePath"
variant="success"
category="secondary"
data-testid="create-scanner-profile-link"
>
{{ s__('OnDemandScans|Create a new scanner profile') }}
</gl-button>
</template>
</gl-card>
<gl-card>
<template #header>
<div class="row">
<div class="col-7">
<h3 class="gl-font-lg gl-display-inline">{{ s__('OnDemandScans|Site profiles') }}</h3>
</div>
<div class="col-5 gl-text-right">
<gl-button
:href="siteProfiles.length ? siteProfilesLibraryPath : null"
:disabled="!siteProfiles.length"
variant="success"
category="secondary"
size="small"
data-testid="manage-site-profiles-button"
>
{{ s__('OnDemandScans|Manage profiles') }}
</gl-button>
</div>
</div>
</template>
<gl-form-group v-if="siteProfiles.length">
<template #label>
{{ s__('OnDemandScans|Use existing site profile') }}
</template>
<gl-dropdown
v-model="form.dastSiteProfileId.value"
:text="siteProfileText"
class="mw-460"
data-testid="site-profiles-dropdown"
>
<gl-dropdown-item
v-for="siteProfile in siteProfiles"
:key="siteProfile.id"
:is-checked="form.dastSiteProfileId.value === siteProfile.id"
is-check-item
@click="setSiteProfile(siteProfile)"
>
{{ siteProfile.profileName }}
</gl-dropdown-item>
</gl-dropdown>
<template v-if="selectedSiteProfile">
<hr />
<div class="row" data-testid="site-profile-summary">
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Target URL') }}:</div>
<div class="col-md-9 gl-font-weight-bold">
{{ selectedSiteProfile.targetUrl }}
</div>
</div>
</div>
</div>
</template>
</gl-form-group>
<template v-else>
<p class="gl-text-gray-700">
{{
s__(
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile.',
)
}}
</p>
<gl-button
:href="newSiteProfilePath"
variant="success"
category="secondary"
data-testid="create-site-profile-link"
>
{{ s__('OnDemandScans|Create a new site profile') }}
</gl-button>
</template>
</gl-card>
<div class="gl-mt-6 gl-pt-6"> <div class="gl-mt-6 gl-pt-6">
<gl-button <gl-button
type="submit" type="submit"
variant="success" variant="success"
class="js-no-auto-disable" class="js-no-auto-disable"
:disabled="isSubmitDisabled" data-testid="on-demand-scan-submit-button"
:disabled="someFieldEmpty"
:loading="loading" :loading="loading"
> >
{{ s__('OnDemandScans|Run scan') }} {{ s__('OnDemandScans|Run scan') }}
......
...@@ -20,12 +20,10 @@ export const SCANNER_PROFILES_QUERY = { ...@@ -20,12 +20,10 @@ export const SCANNER_PROFILES_QUERY = {
field: 'dastScannerProfileId', field: 'dastScannerProfileId',
fetchQuery: dastScannerProfilesQuery, fetchQuery: dastScannerProfilesQuery,
fetchError: ERROR_FETCH_SCANNER_PROFILES, fetchError: ERROR_FETCH_SCANNER_PROFILES,
queryKind: 'scannerProfiles',
}; };
export const SITE_PROFILES_QUERY = { export const SITE_PROFILES_QUERY = {
field: 'dastSiteProfileId', field: 'dastSiteProfileId',
fetchQuery: dastSiteProfilesQuery, fetchQuery: dastSiteProfilesQuery,
fetchError: ERROR_FETCH_SITE_PROFILES, fetchError: ERROR_FETCH_SITE_PROFILES,
queryKind: 'siteProfiles',
}; };
import { merge } from 'lodash'; import { merge } from 'lodash';
import { mount, shallowMount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; import { GlForm, GlSkeletonLoader } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import OnDemandScansForm from 'ee/on_demand_scans/components/on_demand_scans_form.vue'; import OnDemandScansForm from 'ee/on_demand_scans/components/on_demand_scans_form.vue';
import OnDemandScansScannerProfileSelector from 'ee/on_demand_scans/components/profile_selector/scanner_profile_selector.vue';
import OnDemandScansSiteProfileSelector from 'ee/on_demand_scans/components/profile_selector/site_profile_selector.vue';
import dastOnDemandScanCreate from 'ee/on_demand_scans/graphql/dast_on_demand_scan_create.mutation.graphql'; import dastOnDemandScanCreate from 'ee/on_demand_scans/graphql/dast_on_demand_scan_create.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { scannerProfiles, siteProfiles } from '../mock_data';
const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-scans`; const helpPagePath = '/application_security/dast/index#on-demand-scans';
const projectPath = 'group/project'; const projectPath = 'group/project';
const defaultBranch = 'master'; const defaultBranch = 'master';
const scannerProfilesLibraryPath = '/security/configuration/dast_profiles#scanner-profiles'; const scannerProfilesLibraryPath = '/security/configuration/dast_profiles#scanner-profiles';
const siteProfilesLibraryPath = '/security/configuration/dast_profiles#site-profiles'; const siteProfilesLibraryPath = '/security/configuration/dast_profiles#site-profiles';
const newScannerProfilePath = '/security/configuration/dast_profiles/dast_scanner_profile/new'; const newScannerProfilePath = '/security/configuration/dast_profiles/dast_scanner_profile/new';
const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/security/configuration/dast_profiles`; const newSiteProfilePath = `/${projectPath}/-/security/configuration/dast_profiles`;
const defaultProps = { const defaultProps = {
helpPagePath, helpPagePath,
...@@ -20,52 +22,48 @@ const defaultProps = { ...@@ -20,52 +22,48 @@ const defaultProps = {
defaultBranch, defaultBranch,
}; };
const scannerProfiles = [ const defaultMocks = {
{ id: 1, profileName: 'My first scanner profile', spiderTimeout: 5, targetTimeout: 10 }, $apollo: {
{ id: 2, profileName: 'My second scanner profile', spiderTimeout: 20, targetTimeout: 150 }, mutate: jest.fn(),
]; queries: {
const siteProfiles = [ scannerProfiles: {},
{ id: 1, profileName: 'My first site profile', targetUrl: 'https://example.com' }, siteProfiles: {},
{ id: 2, profileName: 'My second site profile', targetUrl: 'https://foo.bar' }, },
]; addSmartQuery: jest.fn(),
const pipelineUrl = `${TEST_HOST}/${projectPath}/pipelines/123`; },
};
const pipelineUrl = `/${projectPath}/pipelines/123`;
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute, isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
redirectTo: jest.fn(), redirectTo: jest.fn(),
})); }));
describe('OnDemandScansApp', () => { describe('OnDemandScansForm', () => {
let wrapper; let subject;
const findForm = () => wrapper.find(GlForm); const findForm = () => subject.find(GlForm);
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`); const findByTestId = testId => subject.find(`[data-testid="${testId}"]`);
const findScannerProfilesDropdown = () => findByTestId('scanner-profiles-dropdown');
const findSiteProfilesDropdown = () => findByTestId('site-profiles-dropdown');
const findManageScannerProfilesButton = () => findByTestId('manage-scanner-profiles-button');
const findCreateNewScannerProfileLink = () => findByTestId('create-scanner-profile-link');
const findManageSiteProfilesButton = () => findByTestId('manage-site-profiles-button');
const findCreateNewSiteProfileLink = () => findByTestId('create-site-profile-link');
const findAlert = () => findByTestId('on-demand-scan-error'); const findAlert = () => findByTestId('on-demand-scan-error');
const findSubmitButton = () => findByTestId('on-demand-scan-submit-button');
const findCancelButton = () => findByTestId('on-demand-scan-cancel-button'); const findCancelButton = () => findByTestId('on-demand-scan-cancel-button');
const setFormData = () => {
subject.find(OnDemandScansScannerProfileSelector).vm.$emit('input', scannerProfiles[0].id);
subject.find(OnDemandScansSiteProfileSelector).vm.$emit('input', siteProfiles[0].id);
return subject.vm.$nextTick();
};
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} }); const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => { const subjectMounterFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn( subject = mountFn(
OnDemandScansForm, OnDemandScansForm,
merge( merge(
{}, {},
{ {
propsData: defaultProps, propsData: defaultProps,
mocks: { mocks: defaultMocks,
$apollo: {
mutate: jest.fn(),
queries: {
scannerProfiles: {},
siteProfiles: {},
},
},
},
provide: { provide: {
scannerProfilesLibraryPath, scannerProfilesLibraryPath,
siteProfilesLibraryPath, siteProfilesLibraryPath,
...@@ -82,221 +80,70 @@ describe('OnDemandScansApp', () => { ...@@ -82,221 +80,70 @@ describe('OnDemandScansApp', () => {
), ),
); );
}; };
const createComponent = wrapperFactory(); const mountSubject = subjectMounterFactory(mount);
const createFullComponent = wrapperFactory(mount); const mountShallowSubject = subjectMounterFactory();
beforeEach(() => {
createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); subject.destroy();
wrapper = null; subject = null;
}); });
it('renders properly', () => { it('renders properly', () => {
expect(wrapper.html()).not.toBe(''); mountSubject();
}); expect(subject.html()).not.toBe('');
describe('computed props', () => {
describe('formData', () => {
it('returns an object with a key:value mapping from the form object including the project path', () => {
wrapper.vm.form = {
siteProfileId: {
value: siteProfiles[0],
state: null,
feedback: '',
},
};
expect(wrapper.vm.formData).toEqual({
fullPath: projectPath,
siteProfileId: siteProfiles[0],
});
});
});
describe('formHasErrors', () => {
it('returns true if any of the fields are invalid', () => {
wrapper.vm.form = {
siteProfileId: {
value: siteProfiles[0],
state: false,
feedback: '',
},
foo: {
value: 'bar',
state: null,
},
};
expect(wrapper.vm.formHasErrors).toBe(true);
});
it('returns false if none of the fields are invalid', () => {
wrapper.vm.form = {
siteProfileId: {
value: siteProfiles[0],
state: null,
feedback: '',
},
foo: {
value: 'bar',
state: null,
},
};
expect(wrapper.vm.formHasErrors).toBe(false);
});
});
describe('someFieldEmpty', () => {
it('returns true if any of the fields are empty', () => {
wrapper.vm.form = {
siteProfileId: {
value: '',
state: false,
feedback: '',
},
foo: {
value: 'bar',
state: null,
},
};
expect(wrapper.vm.someFieldEmpty).toBe(true);
}); });
it('returns false if no field is empty', () => {
wrapper.vm.form = {
siteProfileId: {
value: siteProfiles[0],
state: null,
feedback: '',
},
foo: {
value: 'bar',
state: null,
},
};
expect(wrapper.vm.someFieldEmpty).toBe(false);
});
});
describe('isSubmitDisabled', () => {
it.each` it.each`
formHasErrors | someFieldEmpty | expected scannerProfilesLoading | siteProfilesLoading | isLoading
${true} | ${true} | ${true} ${true} | ${true} | ${true}
${true} | ${false} | ${true}
${false} | ${true} | ${true} ${false} | ${true} | ${true}
${true} | ${false} | ${true}
${false} | ${false} | ${false} ${false} | ${false} | ${false}
`( `(
'is $expected when formHasErrors is $formHasErrors and someFieldEmpty is $someFieldEmpty', 'sets loading state to $isLoading if scanner profiles loading is $scannerProfilesLoading and site profiles loading is $siteProfilesLoading',
({ formHasErrors, someFieldEmpty, expected }) => { ({ scannerProfilesLoading, siteProfilesLoading, isLoading }) => {
createComponent({ mountShallowSubject({
computed: { mocks: {
formHasErrors: () => formHasErrors, $apollo: {
someFieldEmpty: () => someFieldEmpty, queries: {
scannerProfiles: { loading: scannerProfilesLoading },
siteProfiles: { loading: siteProfilesLoading },
},
}, },
});
expect(wrapper.vm.isSubmitDisabled).toBe(expected);
}, },
);
});
});
describe.each`
profileType | manageProfilesButtonFinder | manageProfilesPath | createNewProfileButtonFinder | newProfilePath | dropdownFinder
${'scanner'} | ${findManageScannerProfilesButton} | ${scannerProfilesLibraryPath} | ${findCreateNewScannerProfileLink} | ${newScannerProfilePath} | ${findScannerProfilesDropdown}
${'site'} | ${findManageSiteProfilesButton} | ${siteProfilesLibraryPath} | ${findCreateNewSiteProfileLink} | ${newSiteProfilePath} | ${findSiteProfilesDropdown}
`(
'$profileType profiles',
({
profileType,
manageProfilesButtonFinder,
manageProfilesPath,
createNewProfileButtonFinder,
newProfilePath,
dropdownFinder,
}) => {
describe('while profiles are being fetched', () => {
beforeEach(() => {
createComponent({
mocks: { $apollo: { queries: { [`${profileType}Profiles`]: { loading: true } } } },
});
});
it('shows a skeleton loader', () => {
expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
});
});
describe('when profiles could not be fetched', () => {
beforeEach(() => {
createComponent();
wrapper.vm.showErrors(`ERROR_FETCH_${profileType.toUpperCase()}_PROFILES`);
}); });
it('shows a non-dismissible alert and no field', () => { expect(subject.find(GlSkeletonLoader).exists()).toBe(isLoading);
const alert = findAlert(); },
expect(alert.exists()).toBe(true);
expect(alert.props('dismissible')).toBe(false);
expect(alert.text()).toContain(
`Could not fetch ${profileType} profiles. Please refresh the page, or try again later.`,
); );
});
});
describe('when there are no profiles yet', () => {
beforeEach(() => {
createFullComponent();
});
it('disables the link to profiles library', () => { describe('submit button', () => {
expect(manageProfilesButtonFinder().props('disabled')).toBe(true); let submitButton;
});
it('shows a link to create a new profile', () => {
const link = createNewProfileButtonFinder();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(newProfilePath);
});
});
describe('when there are profiles', () => {
beforeEach(() => { beforeEach(() => {
createFullComponent({ mountShallowSubject({
data: { data: {
scannerProfiles, scannerProfiles,
siteProfiles, siteProfiles,
form: {
dastScannerProfileId: { value: scannerProfiles[0].id },
dastSiteProfileId: { value: siteProfiles[0].id },
}, },
},
});
}); });
submitButton = findSubmitButton();
it('enables link to profiles management', () => {
expect(manageProfilesButtonFinder().props('disabled')).toBe(false);
expect(manageProfilesButtonFinder().attributes('href')).toBe(manageProfilesPath);
}); });
it('shows a dropdown containing the profiles', () => { it('is disabled while some fields are empty', () => {
const dropdown = dropdownFinder(); expect(submitButton.props('disabled')).toBe(true);
expect(dropdown.exists()).toBe(true);
expect(dropdown.element.children).toHaveLength(siteProfiles.length);
}); });
it('when a profile is selected, its summary is displayed below the dropdow', () => { it('becomes enabled when form is valid', async () => {
const summary = wrapper.find(`[data-testid="${profileType}-profile-summary"]`); await setFormData();
expect(summary.exists()).toBe(true); expect(submitButton.props('disabled')).toBe(false);
}); });
}); });
},
);
describe('submission', () => { describe('submission', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ mountShallowSubject({
data: { data: {
scannerProfiles, scannerProfiles,
siteProfiles, siteProfiles,
...@@ -305,25 +152,24 @@ describe('OnDemandScansApp', () => { ...@@ -305,25 +152,24 @@ describe('OnDemandScansApp', () => {
}); });
describe('on success', () => { describe('on success', () => {
beforeEach(() => { beforeEach(async () => {
jest jest
.spyOn(wrapper.vm.$apollo, 'mutate') .spyOn(subject.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl, errors: [] } } }); .mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl, errors: [] } } });
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id); await setFormData();
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
it('sets loading state', () => { it('sets loading state', () => {
expect(wrapper.vm.loading).toBe(true); expect(subject.vm.loading).toBe(true);
}); });
it('triggers GraphQL mutation', () => { it('triggers GraphQL mutation', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ expect(subject.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: dastOnDemandScanCreate, mutation: dastOnDemandScanCreate,
variables: { variables: {
dastScannerProfileId: scannerProfiles[0].id, dastScannerProfileId: scannerProfiles[0].id,
dastSiteProfileId: siteProfiles[0], dastSiteProfileId: siteProfiles[0].id,
fullPath: projectPath, fullPath: projectPath,
}, },
}); });
...@@ -339,15 +185,14 @@ describe('OnDemandScansApp', () => { ...@@ -339,15 +185,14 @@ describe('OnDemandScansApp', () => {
}); });
describe('on top-level error', () => { describe('on top-level error', () => {
beforeEach(() => { beforeEach(async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(); jest.spyOn(subject.vm.$apollo, 'mutate').mockRejectedValue();
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id); await setFormData();
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
it('resets loading state', () => { it('resets loading state', () => {
expect(wrapper.vm.loading).toBe(false); expect(subject.vm.loading).toBe(false);
}); });
it('shows an alert', () => { it('shows an alert', () => {
...@@ -360,17 +205,16 @@ describe('OnDemandScansApp', () => { ...@@ -360,17 +205,16 @@ describe('OnDemandScansApp', () => {
describe('on errors as data', () => { describe('on errors as data', () => {
const errors = ['error#1', 'error#2', 'error#3']; const errors = ['error#1', 'error#2', 'error#3'];
beforeEach(() => { beforeEach(async () => {
jest jest
.spyOn(wrapper.vm.$apollo, 'mutate') .spyOn(subject.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl: null, errors } } }); .mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl: null, errors } } });
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id); await setFormData();
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
it('resets loading state', () => { it('resets loading state', () => {
expect(wrapper.vm.loading).toBe(false); expect(subject.vm.loading).toBe(false);
}); });
it('shows an alert with the returned errors', () => { it('shows an alert with the returned errors', () => {
...@@ -386,10 +230,11 @@ describe('OnDemandScansApp', () => { ...@@ -386,10 +230,11 @@ describe('OnDemandScansApp', () => {
describe('cancel', () => { describe('cancel', () => {
it('emits cancel event on click', () => { it('emits cancel event on click', () => {
jest.spyOn(wrapper.vm, '$emit'); mountShallowSubject();
jest.spyOn(subject.vm, '$emit');
findCancelButton().vm.$emit('click'); findCancelButton().vm.$emit('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('cancel'); expect(subject.vm.$emit).toHaveBeenCalledWith('cancel');
}); });
}); });
}); });
...@@ -17847,9 +17847,6 @@ msgstr "" ...@@ -17847,9 +17847,6 @@ msgstr ""
msgid "OnDemandScans|Scanner profile" msgid "OnDemandScans|Scanner profile"
msgstr "" msgstr ""
msgid "OnDemandScans|Scanner settings"
msgstr ""
msgid "OnDemandScans|Schedule or run scans immediately against target sites. Currently available on-demand scan type: DAST. %{helpLinkStart}More information%{helpLinkEnd}" msgid "OnDemandScans|Schedule or run scans immediately against target sites. Currently available on-demand scan type: DAST. %{helpLinkStart}More information%{helpLinkEnd}"
msgstr "" msgstr ""
...@@ -17859,9 +17856,6 @@ msgstr "" ...@@ -17859,9 +17856,6 @@ msgstr ""
msgid "OnDemandScans|Site profile" msgid "OnDemandScans|Site profile"
msgstr "" msgstr ""
msgid "OnDemandScans|Site profiles"
msgstr ""
msgid "OnDemandScans|Use existing scanner profile" msgid "OnDemandScans|Use existing scanner profile"
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