Commit d690637a authored by Simon Knox's avatar Simon Knox

Merge branch '235610-ondemand-scans-use-existing-scanner-profiles' into 'master'

DAST On-Demand Scans - Use existing scanner profiles for scans implementation - Frontend

See merge request gitlab-org/gitlab!40414
parents 88f43c7d dd40f342
...@@ -25,14 +25,6 @@ export default { ...@@ -25,14 +25,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
profilesLibraryPath: {
type: String,
required: true,
},
newSiteProfilePath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -49,8 +41,6 @@ export default { ...@@ -49,8 +41,6 @@ export default {
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:project-path="projectPath" :project-path="projectPath"
:default-branch="defaultBranch" :default-branch="defaultBranch"
:profiles-library-path="profilesLibraryPath"
:new-site-profile-path="newSiteProfilePath"
@cancel="showForm = false" @cancel="showForm = false"
/> />
</template> </template>
......
...@@ -14,17 +14,22 @@ import { ...@@ -14,17 +14,22 @@ import {
GlSprintf, GlSprintf,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import dastScannerProfilesQuery from 'ee/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastSiteProfilesQuery from 'ee/dast_profiles/graphql/dast_site_profiles.query.graphql'; import dastSiteProfilesQuery from 'ee/dast_profiles/graphql/dast_site_profiles.query.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql'; import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql';
import { SCAN_TYPES } from '../constants';
const ERROR_RUN_SCAN = 'ERROR_RUN_SCAN'; const ERROR_RUN_SCAN = 'ERROR_RUN_SCAN';
const ERROR_FETCH_SCANNER_PROFILES = 'ERROR_FETCH_SCANNER_PROFILES';
const ERROR_FETCH_SITE_PROFILES = 'ERROR_FETCH_SITE_PROFILES'; const ERROR_FETCH_SITE_PROFILES = 'ERROR_FETCH_SITE_PROFILES';
const ERROR_MESSAGES = { const ERROR_MESSAGES = {
[ERROR_RUN_SCAN]: s__('OnDemandScans|Could not run the scan. Please try again.'), [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.',
),
[ERROR_FETCH_SITE_PROFILES]: s__( [ERROR_FETCH_SITE_PROFILES]: s__(
'OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later.', 'OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later.',
), ),
...@@ -53,7 +58,27 @@ export default { ...@@ -53,7 +58,27 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin()],
apollo: { apollo: {
scannerProfiles: {
query: dastScannerProfilesQuery,
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);
},
skip() {
return !this.glFeatures.securityOnDemandScansScannerProfiles;
},
},
siteProfiles: { siteProfiles: {
query: dastSiteProfilesQuery, query: dastSiteProfilesQuery,
variables() { variables() {
...@@ -84,21 +109,29 @@ export default { ...@@ -84,21 +109,29 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
profilesLibraryPath: { },
type: String, inject: {
required: true, scannerProfilesLibraryPath: {
default: '',
},
siteProfilesLibraryPath: {
default: '',
},
newScannerProfilePath: {
default: '',
}, },
newSiteProfilePath: { newSiteProfilePath: {
type: String, default: '',
required: true,
}, },
}, },
data() { data() {
return { return {
scannerProfiles: [],
siteProfiles: [], siteProfiles: [],
form: { form: {
scanType: initField(SCAN_TYPES.PASSIVE), ...(this.glFeatures.securityOnDemandScansScannerProfiles
branch: initField(this.defaultBranch), ? { dastScannerProfileId: initField(null) }
: {}),
dastSiteProfileId: initField(null), dastSiteProfileId: initField(null),
}, },
loading: false, loading: false,
...@@ -112,10 +145,12 @@ export default { ...@@ -112,10 +145,12 @@ export default {
return ERROR_MESSAGES[this.errorType] || null; return ERROR_MESSAGES[this.errorType] || null;
}, },
isLoadingProfiles() { isLoadingProfiles() {
return this.$apollo.queries.siteProfiles.loading; return ['scanner', 'site'].some(
profileType => this.$apollo.queries[`${profileType}Profiles`].loading,
);
}, },
failedToLoadProfiles() { failedToLoadProfiles() {
return [ERROR_FETCH_SITE_PROFILES].includes(this.errorType); return [ERROR_FETCH_SCANNER_PROFILES, ERROR_FETCH_SITE_PROFILES].includes(this.errorType);
}, },
formData() { formData() {
return { return {
...@@ -132,12 +167,24 @@ export default { ...@@ -132,12 +167,24 @@ export default {
isSubmitDisabled() { isSubmitDisabled() {
return this.formHasErrors || this.someFieldEmpty; return this.formHasErrors || this.someFieldEmpty;
}, },
selectedScannerProfile() {
const selectedScannerProfile = this.form.dastScannerProfileId.value;
return selectedScannerProfile === null
? null
: this.scannerProfiles.find(({ id }) => id === selectedScannerProfile);
},
selectedSiteProfile() { selectedSiteProfile() {
const selectedSiteProfileId = this.form.dastSiteProfileId.value; const selectedSiteProfileId = this.form.dastSiteProfileId.value;
return selectedSiteProfileId === null return selectedSiteProfileId === null
? null ? null
: this.siteProfiles.find(({ id }) => id === selectedSiteProfileId); : this.siteProfiles.find(({ id }) => id === selectedSiteProfileId);
}, },
scannerProfileText() {
const { selectedScannerProfile } = this;
return selectedScannerProfile
? selectedScannerProfile.profileName
: s__('OnDemandScans|Select one of the existing profiles');
},
siteProfileText() { siteProfileText() {
const { selectedSiteProfile } = this; const { selectedSiteProfile } = this;
return selectedSiteProfile return selectedSiteProfile
...@@ -146,6 +193,9 @@ export default { ...@@ -146,6 +193,9 @@ export default {
}, },
}, },
methods: { methods: {
setScannerProfile({ id }) {
this.form.dastScannerProfileId.value = id;
},
setSiteProfile({ id }) { setSiteProfile({ id }) {
this.form.dastSiteProfileId.value = id; this.form.dastSiteProfileId.value = id;
}, },
...@@ -239,9 +289,105 @@ export default { ...@@ -239,9 +289,105 @@ export default {
<template v-else-if="!failedToLoadProfiles"> <template v-else-if="!failedToLoadProfiles">
<gl-card> <gl-card>
<template #header> <template #header>
<h3 class="gl-font-lg gl-display-inline">{{ s__('OnDemandScans|Scanner settings') }}</h3> <div class="row">
<div class="col-7">
<h3 class="gl-font-lg gl-display-inline">
{{ s__('OnDemandScans|Scanner settings') }}
</h3>
</div>
<div v-if="glFeatures.securityOnDemandScansScannerProfiles" 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> </template>
<template v-if="glFeatures.securityOnDemandScansScannerProfiles">
<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>
</template>
<template v-else>
<gl-form-group class="gl-mt-4"> <gl-form-group class="gl-mt-4">
<template #label> <template #label>
{{ s__('OnDemandScans|Scan mode') }} {{ s__('OnDemandScans|Scan mode') }}
...@@ -267,6 +413,7 @@ export default { ...@@ -267,6 +413,7 @@ export default {
</template> </template>
{{ defaultBranch }} {{ defaultBranch }}
</gl-form-group> </gl-form-group>
</template>
</gl-card> </gl-card>
<gl-card> <gl-card>
...@@ -277,7 +424,7 @@ export default { ...@@ -277,7 +424,7 @@ export default {
</div> </div>
<div class="col-5 gl-text-right"> <div class="col-5 gl-text-right">
<gl-button <gl-button
:href="siteProfiles.length ? profilesLibraryPath : null" :href="siteProfiles.length ? siteProfilesLibraryPath : null"
:disabled="!siteProfiles.length" :disabled="!siteProfiles.length"
variant="success" variant="success"
category="secondary" category="secondary"
......
export const SCAN_TYPES = {
PASSIVE: 'PASSIVE',
};
mutation dastOnDemandScanCreate($fullPath: ID!, $dastSiteProfileId: DastSiteProfileID!) { mutation dastOnDemandScanCreate(
dastOnDemandScanCreate(input: { fullPath: $fullPath, dastSiteProfileId: $dastSiteProfileId }) { $fullPath: ID!
$dastScannerProfileId: DastScannerProfileID
$dastSiteProfileId: DastSiteProfileID!
) {
dastOnDemandScanCreate(
input: {
fullPath: $fullPath
dastScannerProfileId: $dastScannerProfileId
dastSiteProfileId: $dastSiteProfileId
}
) {
pipelineUrl pipelineUrl
errors errors
} }
......
...@@ -13,13 +13,21 @@ export default () => { ...@@ -13,13 +13,21 @@ export default () => {
emptyStateSvgPath, emptyStateSvgPath,
projectPath, projectPath,
defaultBranch, defaultBranch,
profilesLibraryPath, scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newSiteProfilePath, newSiteProfilePath,
newScannerProfilePath,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
provide: {
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newScannerProfilePath,
newSiteProfilePath,
},
render(h) { render(h) {
return h(OnDemandScansApp, { return h(OnDemandScansApp, {
props: { props: {
...@@ -27,8 +35,6 @@ export default () => { ...@@ -27,8 +35,6 @@ export default () => {
emptyStateSvgPath, emptyStateSvgPath,
projectPath, projectPath,
defaultBranch, defaultBranch,
profilesLibraryPath,
newSiteProfilePath,
}, },
}); });
}, },
......
...@@ -4,6 +4,7 @@ module Projects ...@@ -4,6 +4,7 @@ module Projects
class OnDemandScansController < Projects::ApplicationController class OnDemandScansController < Projects::ApplicationController
before_action do before_action do
authorize_read_on_demand_scans! authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_scanner_profiles)
end end
def index def index
......
...@@ -7,7 +7,9 @@ module Projects::OnDemandScansHelper ...@@ -7,7 +7,9 @@ module Projects::OnDemandScansHelper
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'), 'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch, 'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace, 'project-path' => project.path_with_namespace,
'profiles-library-path' => project_profiles_path(project), 'scanner-profiles-library-path' => project_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_dast_scanner_profile_path(project),
'new-site-profile-path' => new_project_dast_site_profile_path(project) 'new-site-profile-path' => new_project_dast_site_profile_path(project)
} }
end end
......
...@@ -9,7 +9,6 @@ const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-sca ...@@ -9,7 +9,6 @@ const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-sca
const projectPath = 'group/project'; const projectPath = 'group/project';
const defaultBranch = 'master'; const defaultBranch = 'master';
const emptyStateSvgPath = `${TEST_HOST}/assets/illustrations/alert-management-empty-state.svg`; const emptyStateSvgPath = `${TEST_HOST}/assets/illustrations/alert-management-empty-state.svg`;
const profilesLibraryPath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`;
const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`; const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`;
describe('OnDemandScansApp', () => { describe('OnDemandScansApp', () => {
...@@ -39,7 +38,6 @@ describe('OnDemandScansApp', () => { ...@@ -39,7 +38,6 @@ describe('OnDemandScansApp', () => {
projectPath, projectPath,
defaultBranch, defaultBranch,
emptyStateSvgPath, emptyStateSvgPath,
profilesLibraryPath,
newSiteProfilePath, newSiteProfilePath,
}, },
}, },
...@@ -85,8 +83,6 @@ describe('OnDemandScansApp', () => { ...@@ -85,8 +83,6 @@ describe('OnDemandScansApp', () => {
helpPagePath, helpPagePath,
projectPath, projectPath,
defaultBranch, defaultBranch,
profilesLibraryPath,
newSiteProfilePath,
}); });
}); });
......
...@@ -9,17 +9,21 @@ import { redirectTo } from '~/lib/utils/url_utility'; ...@@ -9,17 +9,21 @@ import { redirectTo } from '~/lib/utils/url_utility';
const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-scans`; const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-scans`;
const projectPath = 'group/project'; const projectPath = 'group/project';
const defaultBranch = 'master'; const defaultBranch = 'master';
const profilesLibraryPath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`; const scannerProfilesLibraryPath = '/on_demand_scans/profiles#scanner-profiles';
const siteProfilesLibraryPath = '/on_demand_scans/profiles#site-profiles';
const newScannerProfilePath = '/on_demand_scans/profiles/dast_scanner_profile/new';
const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`; const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`;
const defaultProps = { const defaultProps = {
helpPagePath, helpPagePath,
projectPath, projectPath,
defaultBranch, defaultBranch,
profilesLibraryPath,
newSiteProfilePath,
}; };
const scannerProfiles = [
{ id: 1, profileName: 'My first scanner profile', spiderTimeout: 5, targetTimeout: 10 },
{ id: 2, profileName: 'My second scanner profile', spiderTimeout: 20, targetTimeout: 150 },
];
const siteProfiles = [ const siteProfiles = [
{ id: 1, profileName: 'My first site profile', targetUrl: 'https://example.com' }, { id: 1, profileName: 'My first site profile', targetUrl: 'https://example.com' },
{ id: 2, profileName: 'My second site profile', targetUrl: 'https://foo.bar' }, { id: 2, profileName: 'My second site profile', targetUrl: 'https://foo.bar' },
...@@ -35,13 +39,15 @@ describe('OnDemandScansApp', () => { ...@@ -35,13 +39,15 @@ describe('OnDemandScansApp', () => {
let wrapper; let wrapper;
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findSiteProfilesDropdown = () => wrapper.find('[data-testid="site-profiles-dropdown"]'); const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
const findManageSiteProfilesButton = () => const findScannerProfilesDropdown = () => findByTestId('scanner-profiles-dropdown');
wrapper.find('[data-testid="manage-site-profiles-button"]'); const findSiteProfilesDropdown = () => findByTestId('site-profiles-dropdown');
const findCreateNewSiteProfileLink = () => const findManageScannerProfilesButton = () => findByTestId('manage-scanner-profiles-button');
wrapper.find('[data-testid="create-site-profile-link"]'); const findCreateNewScannerProfileLink = () => findByTestId('create-scanner-profile-link');
const findAlert = () => wrapper.find('[data-testid="on-demand-scan-error"]'); const findManageSiteProfilesButton = () => findByTestId('manage-site-profiles-button');
const findCancelButton = () => wrapper.find('[data-testid="on-demand-scan-cancel-button"]'); const findCreateNewSiteProfileLink = () => findByTestId('create-site-profile-link');
const findAlert = () => findByTestId('on-demand-scan-error');
const findCancelButton = () => findByTestId('on-demand-scan-cancel-button');
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} }); const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => { const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
...@@ -55,10 +61,18 @@ describe('OnDemandScansApp', () => { ...@@ -55,10 +61,18 @@ describe('OnDemandScansApp', () => {
$apollo: { $apollo: {
mutate: jest.fn(), mutate: jest.fn(),
queries: { queries: {
scannerProfiles: {},
siteProfiles: {}, siteProfiles: {},
}, },
}, },
}, },
provide: {
glFeatures: { securityOnDemandScansScannerProfiles: true },
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newScannerProfilePath,
newSiteProfilePath,
},
}, },
options, options,
{ {
...@@ -189,10 +203,60 @@ describe('OnDemandScansApp', () => { ...@@ -189,10 +203,60 @@ describe('OnDemandScansApp', () => {
}); });
}); });
describe('site profiles', () => { describe('scanner profiles with feature flag disabled', () => {
describe('while site profiles are being fetched', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading: true } } } } }); createComponent({
provide: {
glFeatures: { securityOnDemandScansScannerProfiles: false },
},
});
});
it('shows static scanner settings and no scanner profiles component', () => {
expect(findScannerProfilesDropdown().exists()).toBe(false);
expect(findManageScannerProfilesButton().exists()).toBe(false);
expect(findCreateNewScannerProfileLink().exists()).toBe(false);
expect(wrapper.text()).toContain('Passive');
expect(wrapper.text()).toContain('master');
});
it('when submitting the form, GraphQL query does not include scanner data', async () => {
wrapper.vm.siteProfiles = siteProfiles;
await wrapper.vm.$nextTick();
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl, errors: [] } } });
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: dastOnDemandScanCreate,
variables: {
dastSiteProfileId: siteProfiles[0],
fullPath: projectPath,
},
});
});
});
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', () => { it('shows a skeleton loader', () => {
...@@ -200,10 +264,10 @@ describe('OnDemandScansApp', () => { ...@@ -200,10 +264,10 @@ describe('OnDemandScansApp', () => {
}); });
}); });
describe('when site profiles could not be fetched', () => { describe('when profiles could not be fetched', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
return wrapper.vm.showErrors('ERROR_FETCH_SITE_PROFILES'); wrapper.vm.showErrors(`ERROR_FETCH_${profileType.toUpperCase()}_PROFILES`);
}); });
it('shows a non-dismissible alert and no field', () => { it('shows a non-dismissible alert and no field', () => {
...@@ -211,57 +275,66 @@ describe('OnDemandScansApp', () => { ...@@ -211,57 +275,66 @@ describe('OnDemandScansApp', () => {
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.props('dismissible')).toBe(false); expect(alert.props('dismissible')).toBe(false);
expect(alert.text()).toContain( expect(alert.text()).toContain(
'Could not fetch site profiles. Please refresh the page, or try again later.', `Could not fetch ${profileType} profiles. Please refresh the page, or try again later.`,
); );
}); });
}); });
describe('when there are no site profiles yet', () => { describe('when there are no profiles yet', () => {
beforeEach(() => { beforeEach(() => {
createFullComponent(); createFullComponent();
}); });
it('disables the link to manage site profiles', () => { it('disables the link to profiles library', () => {
expect(findManageSiteProfilesButton().props('disabled')).toBe(true); expect(manageProfilesButtonFinder().props('disabled')).toBe(true);
}); });
it('shows a link to create a new site profile', () => { it('shows a link to create a new profile', () => {
const link = findCreateNewSiteProfileLink(); const link = createNewProfileButtonFinder();
expect(link.exists()).toBe(true); expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(newSiteProfilePath); expect(link.attributes('href')).toBe(newProfilePath);
}); });
}); });
describe('when there are site profiles', () => { describe('when there are profiles', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createFullComponent({
data: { data: {
scannerProfiles,
siteProfiles, siteProfiles,
form: {
dastScannerProfileId: { value: scannerProfiles[0].id },
dastSiteProfileId: { value: siteProfiles[0].id },
},
}, },
}); });
}); });
it('shows a dropdown containing the site profiles', () => { it('enables link to profiles management', () => {
const dropdown = findSiteProfilesDropdown(); expect(manageProfilesButtonFinder().props('disabled')).toBe(false);
expect(manageProfilesButtonFinder().attributes('href')).toBe(manageProfilesPath);
});
it('shows a dropdown containing the profiles', () => {
const dropdown = dropdownFinder();
expect(dropdown.exists()).toBe(true); expect(dropdown.exists()).toBe(true);
expect(dropdown.element.children).toHaveLength(siteProfiles.length); expect(dropdown.element.children).toHaveLength(siteProfiles.length);
}); });
it('when a site profile is selected, its summary is displayed below the dropdown', async () => { it('when a profile is selected, its summary is displayed below the dropdow', () => {
wrapper.vm.form.dastSiteProfileId.value = siteProfiles[0].id; const summary = wrapper.find(`[data-testid="${profileType}-profile-summary"]`);
await wrapper.vm.$nextTick();
const summary = wrapper.find('[data-testid="site-profile-summary"]');
expect(summary.exists()).toBe(true); expect(summary.exists()).toBe(true);
expect(summary.text()).toContain(siteProfiles[0].targetUrl);
});
}); });
}); });
},
);
describe('submission', () => { describe('submission', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
data: { data: {
scannerProfiles,
siteProfiles, siteProfiles,
}, },
}); });
...@@ -272,6 +345,7 @@ describe('OnDemandScansApp', () => { ...@@ -272,6 +345,7 @@ describe('OnDemandScansApp', () => {
jest jest
.spyOn(wrapper.vm.$apollo, 'mutate') .spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl, errors: [] } } }); .mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl, errors: [] } } });
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id);
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]); findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
...@@ -284,8 +358,7 @@ describe('OnDemandScansApp', () => { ...@@ -284,8 +358,7 @@ describe('OnDemandScansApp', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: dastOnDemandScanCreate, mutation: dastOnDemandScanCreate,
variables: { variables: {
scanType: 'PASSIVE', dastScannerProfileId: scannerProfiles[0].id,
branch: 'master',
dastSiteProfileId: siteProfiles[0], dastSiteProfileId: siteProfiles[0],
fullPath: projectPath, fullPath: projectPath,
}, },
...@@ -304,6 +377,7 @@ describe('OnDemandScansApp', () => { ...@@ -304,6 +377,7 @@ describe('OnDemandScansApp', () => {
describe('on top-level error', () => { describe('on top-level error', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue();
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id);
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]); findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
...@@ -326,6 +400,7 @@ describe('OnDemandScansApp', () => { ...@@ -326,6 +400,7 @@ describe('OnDemandScansApp', () => {
jest jest
.spyOn(wrapper.vm.$apollo, 'mutate') .spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl: null, errors } } }); .mockResolvedValue({ data: { dastOnDemandScanCreate: { pipelineUrl: null, errors } } });
findScannerProfilesDropdown().vm.$emit('input', scannerProfiles[0].id);
findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]); findSiteProfilesDropdown().vm.$emit('input', siteProfiles[0]);
submitForm(); submitForm();
}); });
......
...@@ -12,7 +12,9 @@ RSpec.describe Projects::OnDemandScansHelper do ...@@ -12,7 +12,9 @@ RSpec.describe Projects::OnDemandScansHelper do
'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'), 'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch, 'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace, 'project-path' => project.path_with_namespace,
'profiles-library-path' => project_profiles_path(project), 'scanner-profiles-library-path' => project_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_dast_scanner_profile_path(project),
'new-site-profile-path' => new_project_dast_site_profile_path(project) 'new-site-profile-path' => new_project_dast_site_profile_path(project)
) )
end end
......
...@@ -7870,6 +7870,9 @@ msgstr "" ...@@ -7870,6 +7870,9 @@ msgstr ""
msgid "DastProfiles|No profiles created yet" msgid "DastProfiles|No profiles created yet"
msgstr "" msgstr ""
msgid "DastProfiles|Passive"
msgstr ""
msgid "DastProfiles|Please enter a valid URL format, ex: http://www.example.com/home" msgid "DastProfiles|Please enter a valid URL format, ex: http://www.example.com/home"
msgstr "" msgstr ""
...@@ -7885,6 +7888,9 @@ msgstr "" ...@@ -7885,6 +7888,9 @@ msgstr ""
msgid "DastProfiles|Save profile" msgid "DastProfiles|Save profile"
msgstr "" msgstr ""
msgid "DastProfiles|Scan mode"
msgstr ""
msgid "DastProfiles|Scanner Profile" msgid "DastProfiles|Scanner Profile"
msgstr "" msgstr ""
...@@ -17281,12 +17287,18 @@ msgstr "" ...@@ -17281,12 +17287,18 @@ msgstr ""
msgid "OnDemandScans|Attached branch is where the scan job runs." msgid "OnDemandScans|Attached branch is where the scan job runs."
msgstr "" msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
msgid "OnDemandScans|Could not run the scan. Please try again." msgid "OnDemandScans|Could not run the scan. Please try again."
msgstr "" msgstr ""
msgid "OnDemandScans|Create a new scanner profile"
msgstr ""
msgid "OnDemandScans|Create a new site profile" msgid "OnDemandScans|Create a new site profile"
msgstr "" msgstr ""
...@@ -17299,6 +17311,9 @@ msgstr "" ...@@ -17299,6 +17311,9 @@ msgstr ""
msgid "OnDemandScans|New on-demand DAST scan" msgid "OnDemandScans|New on-demand DAST scan"
msgstr "" msgstr ""
msgid "OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile."
msgstr ""
msgid "OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile." msgid "OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile."
msgstr "" msgstr ""
...@@ -17332,6 +17347,9 @@ msgstr "" ...@@ -17332,6 +17347,9 @@ msgstr ""
msgid "OnDemandScans|Site profiles" msgid "OnDemandScans|Site profiles"
msgstr "" msgstr ""
msgid "OnDemandScans|Use existing scanner profile"
msgstr ""
msgid "OnDemandScans|Use existing site profile" msgid "OnDemandScans|Use existing site 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