Commit 9bf37d57 authored by David Pisek's avatar David Pisek Committed by Enrique Alcántara

DAST on-demand site profile: Handle validation states

Adds logic to handle pending and in-progress states when a profile
gets edited.
parent 8c78a0cc
...@@ -13,13 +13,16 @@ import { ...@@ -13,13 +13,16 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { isAbsolute, redirectTo } from '~/lib/utils/url_utility'; import { isAbsolute, redirectTo } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DastSiteValidation from './dast_site_validation.vue'; import DastSiteValidation from './dast_site_validation.vue';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql'; import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql'; import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from '../graphql/dast_site_token_create.mutation.graphql'; import dastSiteTokenCreateMutation from '../graphql/dast_site_token_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql'; import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
import { DAST_SITE_VALIDATION_STATUS } from '../constants'; import { DAST_SITE_VALIDATION_STATUS, DAST_SITE_VALIDATION_POLL_INTERVAL } from '../constants';
const { PENDING, INPROGRESS, PASSED, FAILED } = DAST_SITE_VALIDATION_STATUS;
const initField = value => ({ const initField = value => ({
value, value,
...@@ -61,22 +64,25 @@ export default { ...@@ -61,22 +64,25 @@ export default {
}, },
data() { data() {
const { name = '', targetUrl = '' } = this.siteProfile || {}; const { name = '', targetUrl = '' } = this.siteProfile || {};
const isSiteValid = false;
const form = { const form = {
profileName: initField(name), profileName: initField(name),
targetUrl: initField(targetUrl), targetUrl: initField(targetUrl),
}; };
return { return {
fetchValidationTimeout: null,
form, form,
initialFormValues: extractFormValues(form), initialFormValues: extractFormValues(form),
isFetchingValidationStatus: false, isFetchingValidationStatus: false,
isValidatingSite: false, isValidatingSite: false,
loading: false, isLoading: false,
showAlert: false, hasAlert: false,
tokenId: null, tokenId: null,
token: null, token: null,
isSiteValid, isSiteValidationActive: false,
validateSite: isSiteValid, isSiteValidationTouched: false,
validationStatus: null,
errorMessage: '', errorMessage: '',
errors: [], errors: [],
}; };
...@@ -85,6 +91,9 @@ export default { ...@@ -85,6 +91,9 @@ export default {
isEdit() { isEdit() {
return Boolean(this.siteProfile?.id); return Boolean(this.siteProfile?.id);
}, },
isSiteValidationDisabled() {
return !this.form.targetUrl.state || this.validationStatusMatches(INPROGRESS);
},
i18n() { i18n() {
const { isEdit } = this; const { isEdit } = this;
return { return {
...@@ -121,44 +130,93 @@ export default { ...@@ -121,44 +130,93 @@ export default {
return Object.values(this.form).some(({ value }) => !value); return Object.values(this.form).some(({ value }) => !value);
}, },
isSubmitDisabled() { isSubmitDisabled() {
return (this.validateSite && !this.isSiteValid) || this.formHasErrors || this.someFieldEmpty; return (
(this.isSiteValidationActive && !this.validationStatusMatches(PASSED)) ||
this.formHasErrors ||
this.someFieldEmpty ||
this.validationStatusMatches(INPROGRESS)
);
}, },
showValidationSection() { showValidationSection() {
return this.validateSite && !this.isSiteValid && !this.isValidatingSite; return (
this.isSiteValidationActive &&
!this.isValidatingSite &&
![INPROGRESS, PASSED].some(this.validationStatusMatches)
);
},
siteValidationStatusDescription() {
const descriptions = {
[PENDING]: { text: s__('DastProfiles|Site must be validated to run an active scan.') },
[INPROGRESS]: {
text: s__('DastProfiles|Validation is in progress...'),
},
[PASSED]: {
text: s__(
'DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site.',
),
cssClass: 'gl-text-green-500',
},
[FAILED]: {
text: s__('DastProfiles|Validation failed. Please try again.'),
cssClass: 'gl-text-red-500',
dismissed: this.isSiteValidationTouched,
},
};
const defaultDescription = descriptions[PENDING];
const currentStatusDescription = descriptions[this.validationStatus];
return currentStatusDescription && !currentStatusDescription.dismissed
? currentStatusDescription
: defaultDescription;
},
},
async created() {
if (this.isEdit) {
this.validateTargetUrl();
if (this.glFeatures.securityOnDemandScansSiteValidation) {
await this.fetchValidationStatus();
if ([PASSED, INPROGRESS].some(this.validationStatusMatches)) {
this.isSiteValidationActive = true;
}
}
}
}, },
destroyed() {
clearTimeout(this.fetchValidationTimeout);
this.fetchValidationTimeout = null;
}, },
watch: { methods: {
async validateSite(validate) { async validateSite(validate) {
this.isSiteValidationActive = validate;
this.isSiteValidationTouched = true;
this.tokenId = null; this.tokenId = null;
this.token = null; this.token = null;
if (!validate) { if (!validate) {
this.isSiteValid = false; this.validationStatus = null;
} else { } else {
try { try {
this.isValidatingSite = true; this.isValidatingSite = true;
await this.fetchValidationStatus(); await this.fetchValidationStatus();
if (!this.isSiteValid) { if (![PASSED, INPROGRESS].some(this.validationStatusMatches)) {
await this.createValidationToken(); await this.createValidationToken();
} }
} catch (exception) { } catch (exception) {
this.captureException(exception); this.captureException(exception);
this.isSiteValidationActive = false;
} finally { } finally {
this.isValidatingSite = false; this.isValidatingSite = false;
} }
} }
}, },
validationStatusMatches(status) {
return this.validationStatus === status;
}, },
async created() {
if (this.isEdit) {
this.validateTargetUrl();
if (this.glFeatures.securityOnDemandScansSiteValidation) {
await this.fetchValidationStatus();
}
}
},
methods: {
validateTargetUrl() { validateTargetUrl() {
if (!isAbsolute(this.form.targetUrl.value)) { if (!isAbsolute(this.form.targetUrl.value)) {
this.form.targetUrl.state = false; this.form.targetUrl.state = false;
...@@ -186,14 +244,20 @@ export default { ...@@ -186,14 +244,20 @@ export default {
fullPath: this.fullPath, fullPath: this.fullPath,
targetUrl: this.form.targetUrl.value, targetUrl: this.form.targetUrl.value,
}, },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
}); });
this.isSiteValid = status === DAST_SITE_VALIDATION_STATUS.VALID; this.validationStatus = status;
if (this.validationStatusMatches(INPROGRESS)) {
this.fetchValidationTimeout = setTimeout(
this.fetchValidationStatus,
DAST_SITE_VALIDATION_POLL_INTERVAL,
);
}
} catch (exception) { } catch (exception) {
this.showErrors({ this.showErrors({
message: this.i18n.siteValidation.validationStatusFetchError, message: this.i18n.siteValidation.validationStatusFetchError,
}); });
this.validateSite = false;
throw new Error(exception); throw new Error(exception);
} finally { } finally {
this.isFetchingValidationStatus = false; this.isFetchingValidationStatus = false;
...@@ -219,13 +283,12 @@ export default { ...@@ -219,13 +283,12 @@ export default {
} }
} catch (exception) { } catch (exception) {
this.showErrors({ message: errorMessage }); this.showErrors({ message: errorMessage });
this.validateSite = false;
throw new Error(exception); throw new Error(exception);
} }
}, },
onSubmit() { onSubmit() {
this.loading = true; this.isLoading = true;
this.hideErrors(); this.hideErrors();
const { errorMessage } = this.i18n; const { errorMessage } = this.i18n;
...@@ -248,7 +311,7 @@ export default { ...@@ -248,7 +311,7 @@ export default {
}) => { }) => {
if (errors.length > 0) { if (errors.length > 0) {
this.showErrors({ message: errorMessage, errors }); this.showErrors({ message: errorMessage, errors });
this.loading = false; this.isLoading = false;
} else { } else {
redirectTo(this.profilesLibraryPath); redirectTo(this.profilesLibraryPath);
} }
...@@ -257,7 +320,7 @@ export default { ...@@ -257,7 +320,7 @@ export default {
.catch(exception => { .catch(exception => {
this.showErrors({ message: errorMessage }); this.showErrors({ message: errorMessage });
this.captureException(exception); this.captureException(exception);
this.loading = false; this.isLoading = false;
}); });
}, },
onCancelClicked() { onCancelClicked() {
...@@ -267,6 +330,9 @@ export default { ...@@ -267,6 +330,9 @@ export default {
this.$refs[this.$options.modalId].show(); this.$refs[this.$options.modalId].show();
} }
}, },
onValidationSuccess() {
this.validationStatus = PASSED;
},
discard() { discard() {
redirectTo(this.profilesLibraryPath); redirectTo(this.profilesLibraryPath);
}, },
...@@ -276,12 +342,12 @@ export default { ...@@ -276,12 +342,12 @@ export default {
showErrors({ message, errors = [] }) { showErrors({ message, errors = [] }) {
this.errorMessage = message; this.errorMessage = message;
this.errors = errors; this.errors = errors;
this.showAlert = true; this.hasAlert = true;
}, },
hideErrors() { hideErrors() {
this.errorMessage = ''; this.errorMessage = '';
this.errors = []; this.errors = [];
this.showAlert = false; this.hasAlert = false;
}, },
}, },
modalId: 'deleteDastProfileModal', modalId: 'deleteDastProfileModal',
...@@ -295,7 +361,7 @@ export default { ...@@ -295,7 +361,7 @@ export default {
</h2> </h2>
<gl-alert <gl-alert
v-if="showAlert" v-if="hasAlert"
variant="danger" variant="danger"
class="gl-mb-5" class="gl-mb-5"
data-testid="dast-site-profile-form-alert" data-testid="dast-site-profile-form-alert"
...@@ -322,7 +388,7 @@ export default { ...@@ -322,7 +388,7 @@ export default {
data-testid="target-url-input-group" data-testid="target-url-input-group"
:invalid-feedback="form.targetUrl.feedback" :invalid-feedback="form.targetUrl.feedback"
:description=" :description="
validateSite && !isValidatingSite isSiteValidationActive && !isValidatingSite
? s__('DastProfiles|Validation must be turned off to change the target URL') ? s__('DastProfiles|Validation must be turned off to change the target URL')
: null : null
" "
...@@ -334,7 +400,7 @@ export default { ...@@ -334,7 +400,7 @@ export default {
data-testid="target-url-input" data-testid="target-url-input"
type="url" type="url"
:state="form.targetUrl.state" :state="form.targetUrl.state"
:disabled="validateSite" :disabled="isSiteValidationActive"
@input="validateTargetUrl" @input="validateTargetUrl"
/> />
</gl-form-group> </gl-form-group>
...@@ -342,22 +408,23 @@ export default { ...@@ -342,22 +408,23 @@ export default {
<template v-if="glFeatures.securityOnDemandScansSiteValidation"> <template v-if="glFeatures.securityOnDemandScansSiteValidation">
<gl-form-group :label="s__('DastProfiles|Validate target site')"> <gl-form-group :label="s__('DastProfiles|Validate target site')">
<template #description> <template #description>
<p v-if="!isSiteValid" class="gl-mt-3"> <p
{{ s__('DastProfiles|Site must be validated to run an active scan.') }} v-if="siteValidationStatusDescription.text"
</p> class="gl-mt-3"
<p v-else class="gl-text-green-500 gl-mt-3"> :class="siteValidationStatusDescription.cssClass"
{{ data-testid="siteValidationStatusDescription"
s__( >
'DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site.', {{ siteValidationStatusDescription.text }}
)
}}
</p> </p>
</template> </template>
<gl-toggle <gl-toggle
v-model="validateSite"
data-testid="dast-site-validation-toggle" data-testid="dast-site-validation-toggle"
:disabled="!form.targetUrl.state" :value="isSiteValidationActive"
:is-loading="isFetchingValidationStatus || isValidatingSite" :disabled="isSiteValidationDisabled"
:is-loading="
!isSiteValidationDisabled && (isFetchingValidationStatus || isValidatingSite)
"
@change="validateSite"
/> />
</gl-form-group> </gl-form-group>
...@@ -367,7 +434,7 @@ export default { ...@@ -367,7 +434,7 @@ export default {
:token-id="tokenId" :token-id="tokenId"
:token="token" :token="token"
:target-url="form.targetUrl.value" :target-url="form.targetUrl.value"
@success="isSiteValid = true" @success="onValidationSuccess"
/> />
</gl-collapse> </gl-collapse>
</template> </template>
...@@ -381,7 +448,7 @@ export default { ...@@ -381,7 +448,7 @@ export default {
class="js-no-auto-disable" class="js-no-auto-disable"
data-testid="dast-site-profile-form-submit-button" data-testid="dast-site-profile-form-submit-button"
:disabled="isSubmitDisabled" :disabled="isSubmitDisabled"
:loading="loading" :loading="isLoading"
> >
{{ s__('DastProfiles|Save profile') }} {{ s__('DastProfiles|Save profile') }}
</gl-button> </gl-button>
......
...@@ -14,10 +14,12 @@ import { ...@@ -14,10 +14,12 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import { cleanLeadingSeparator, joinPaths, stripPathTail } from '~/lib/utils/url_utility'; import { cleanLeadingSeparator, joinPaths, stripPathTail } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import { import {
DAST_SITE_VALIDATION_METHOD_TEXT_FILE, DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
DAST_SITE_VALIDATION_METHODS, DAST_SITE_VALIDATION_METHODS,
DAST_SITE_VALIDATION_STATUS, DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_POLL_INTERVAL,
} from '../constants'; } from '../constants';
import dastSiteValidationCreateMutation from '../graphql/dast_site_validation_create.mutation.graphql'; import dastSiteValidationCreateMutation from '../graphql/dast_site_validation_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql'; import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
...@@ -53,14 +55,19 @@ export default { ...@@ -53,14 +55,19 @@ export default {
}, },
}, },
}) { }) {
if (status === DAST_SITE_VALIDATION_STATUS.VALID) { if (status === DAST_SITE_VALIDATION_STATUS.PASSED) {
this.onSuccess(); this.onSuccess();
} }
if (status === DAST_SITE_VALIDATION_STATUS.FAILED) {
this.onError();
}
}, },
skip() { skip() {
return !(this.isCreatingValidation || this.isValidating); return !(this.isCreatingValidation || this.isValidating);
}, },
pollInterval: 1000, pollInterval: DAST_SITE_VALIDATION_POLL_INTERVAL,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
error(e) { error(e) {
this.onError(e); this.onError(e);
}, },
...@@ -159,7 +166,7 @@ export default { ...@@ -159,7 +166,7 @@ export default {
}); });
if (errors?.length) { if (errors?.length) {
this.onError(); this.onError();
} else if (status === DAST_SITE_VALIDATION_STATUS.VALID) { } else if (status === DAST_SITE_VALIDATION_STATUS.PASSED) {
this.onSuccess(); this.onSuccess();
} else { } else {
this.isCreatingValidation = false; this.isCreatingValidation = false;
......
...@@ -12,6 +12,10 @@ export const DAST_SITE_VALIDATION_METHODS = { ...@@ -12,6 +12,10 @@ export const DAST_SITE_VALIDATION_METHODS = {
}; };
export const DAST_SITE_VALIDATION_STATUS = { export const DAST_SITE_VALIDATION_STATUS = {
VALID: 'PASSED_VALIDATION', PENDING: 'PENDING_VALIDATION',
INVALID: 'FAILED_VALIDATION', INPROGRESS: 'INPROGRESS_VALIDATION',
PASSED: 'PASSED_VALIDATION',
FAILED: 'FAILED_VALIDATION',
}; };
export const DAST_SITE_VALIDATION_POLL_INTERVAL = 1000;
...@@ -12,17 +12,15 @@ import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profile ...@@ -12,17 +12,15 @@ import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profile
import dastSiteProfileCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql'; import dastSiteProfileCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql'; import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_token_create.mutation.graphql'; import dastSiteTokenCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_token_create.mutation.graphql';
import { siteProfiles } from 'ee_jest/on_demand_scans/mock_data';
import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock'; import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock';
import { redirectTo } from '~/lib/utils/url_utility'; import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
import * as urlUtility from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
redirectTo: jest.fn(),
}));
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const [siteProfileOne] = siteProfiles;
const fullPath = 'group/project'; const fullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/security/configuration/dast_profiles`; const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/security/configuration/dast_profiles`;
const profileName = 'My DAST site profile'; const profileName = 'My DAST site profile';
...@@ -52,18 +50,16 @@ describe('DastSiteProfileForm', () => { ...@@ -52,18 +50,16 @@ describe('DastSiteProfileForm', () => {
const withinComponent = () => within(wrapper.element); const withinComponent = () => within(wrapper.element);
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findProfileNameInput = () => wrapper.find('[data-testid="profile-name-input"]'); const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
const findTargetUrlInputGroup = () => wrapper.find('[data-testid="target-url-input-group"]'); const findProfileNameInput = () => findByTestId('profile-name-input');
const findTargetUrlInput = () => wrapper.find('[data-testid="target-url-input"]'); const findTargetUrlInputGroup = () => findByTestId('target-url-input-group');
const findSubmitButton = () => const findTargetUrlInput = () => findByTestId('target-url-input');
wrapper.find('[data-testid="dast-site-profile-form-submit-button"]'); const findSubmitButton = () => findByTestId('dast-site-profile-form-submit-button');
const findCancelButton = () => const findCancelButton = () => findByTestId('dast-site-profile-form-cancel-button');
wrapper.find('[data-testid="dast-site-profile-form-cancel-button"]');
const findCancelModal = () => wrapper.find(GlModal); const findCancelModal = () => wrapper.find(GlModal);
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} }); const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const findAlert = () => wrapper.find('[data-testid="dast-site-profile-form-alert"]'); const findAlert = () => findByTestId('dast-site-profile-form-alert');
const findSiteValidationToggle = () => const findSiteValidationToggle = () => findByTestId('dast-site-validation-toggle');
wrapper.find('[data-testid="dast-site-validation-toggle"]');
const findDastSiteValidation = () => wrapper.find(DastSiteValidation); const findDastSiteValidation = () => wrapper.find(DastSiteValidation);
const mockClientFactory = handlers => { const mockClientFactory = handlers => {
...@@ -95,9 +91,9 @@ describe('DastSiteProfileForm', () => { ...@@ -95,9 +91,9 @@ describe('DastSiteProfileForm', () => {
apolloProvider.defaultClient = mockClientFactory(handlers); apolloProvider.defaultClient = mockClientFactory(handlers);
}; };
const componentFactory = (mountFn = shallowMount) => options => { const componentFactory = (mountFn = shallowMount) => (options, handlers) => {
apolloProvider = new VueApollo({ apolloProvider = new VueApollo({
defaultClient: mockClientFactory(), defaultClient: mockClientFactory(handlers),
}); });
const mountOpts = merge( const mountOpts = merge(
...@@ -187,7 +183,7 @@ describe('DastSiteProfileForm', () => { ...@@ -187,7 +183,7 @@ describe('DastSiteProfileForm', () => {
describe.each` describe.each`
title | siteProfile title | siteProfile
${'New site profile'} | ${null} ${'New site profile'} | ${null}
${'Edit site profile'} | ${{ id: 1, name: 'foo', targetUrl: 'bar' }} ${'Edit site profile'} | ${siteProfileOne}
`('$title with feature flag disabled', ({ siteProfile }) => { `('$title with feature flag disabled', ({ siteProfile }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
...@@ -314,7 +310,7 @@ describe('DastSiteProfileForm', () => { ...@@ -314,7 +310,7 @@ describe('DastSiteProfileForm', () => {
describe.each` describe.each`
title | siteProfile | mutationVars | mutationKind title | siteProfile | mutationVars | mutationKind
${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'} ${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'}
${'Edit site profile'} | ${{ id: 1, name: 'foo', targetUrl: 'bar' }} | ${{ id: 1 }} | ${'dastSiteProfileUpdate'} ${'Edit site profile'} | ${siteProfileOne} | ${{ id: siteProfileOne.id }} | ${'dastSiteProfileUpdate'}
`('$title', ({ siteProfile, title, mutationVars, mutationKind }) => { `('$title', ({ siteProfile, title, mutationVars, mutationKind }) => {
beforeEach(() => { beforeEach(() => {
createFullComponent({ createFullComponent({
...@@ -322,6 +318,8 @@ describe('DastSiteProfileForm', () => { ...@@ -322,6 +318,8 @@ describe('DastSiteProfileForm', () => {
siteProfile, siteProfile,
}, },
}); });
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
}); });
it('sets the correct title', () => { it('sets the correct title', () => {
...@@ -354,7 +352,7 @@ describe('DastSiteProfileForm', () => { ...@@ -354,7 +352,7 @@ describe('DastSiteProfileForm', () => {
}); });
it('redirects to the profiles library', () => { it('redirects to the profiles library', () => {
expect(redirectTo).toHaveBeenCalledWith(profilesLibraryPath); expect(urlUtility.redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
}); });
it('does not show an alert', () => { it('does not show an alert', () => {
...@@ -418,7 +416,7 @@ describe('DastSiteProfileForm', () => { ...@@ -418,7 +416,7 @@ describe('DastSiteProfileForm', () => {
describe('form unchanged', () => { describe('form unchanged', () => {
it('redirects to the profiles library', () => { it('redirects to the profiles library', () => {
findCancelButton().vm.$emit('click'); findCancelButton().vm.$emit('click');
expect(redirectTo).toHaveBeenCalledWith(profilesLibraryPath); expect(urlUtility.redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
}); });
}); });
...@@ -436,9 +434,89 @@ describe('DastSiteProfileForm', () => { ...@@ -436,9 +434,89 @@ describe('DastSiteProfileForm', () => {
it('redirects to the profiles library if confirmed', () => { it('redirects to the profiles library if confirmed', () => {
findCancelModal().vm.$emit('ok'); findCancelModal().vm.$emit('ok');
expect(redirectTo).toHaveBeenCalledWith(profilesLibraryPath); expect(urlUtility.redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
});
});
});
});
describe.each`
givenValidationStatus | expectedDescription | shouldShowDefaultDescriptionAfterToggle | shouldHaveSiteValidationActivated | shouldHaveSiteValidationDisabled | shouldPoll
${DAST_SITE_VALIDATION_STATUS.PENDING} | ${'Site must be validated to run an active scan.'} | ${false} | ${false} | ${false} | ${false}
${DAST_SITE_VALIDATION_STATUS.INPROGRESS} | ${'Validation is in progress...'} | ${false} | ${true} | ${true} | ${true}
${DAST_SITE_VALIDATION_STATUS.PASSED} | ${'Validation succeeded. Both active and passive scans can be run against the target site.'} | ${false} | ${true} | ${false} | ${false}
${DAST_SITE_VALIDATION_STATUS.FAILED} | ${'Validation failed. Please try again.'} | ${true} | ${false} | ${false} | ${false}
`(
'when editing an existing profile and the validation status is "$givenValidationStatus"',
({
givenValidationStatus,
expectedDescription,
shouldHaveSiteValidationActivated,
shouldShowDefaultDescriptionAfterToggle,
shouldHaveSiteValidationDisabled,
shouldPoll,
}) => {
let dastSiteValidationHandler;
beforeEach(() => {
dastSiteValidationHandler = jest
.fn()
.mockResolvedValue(responses.dastSiteValidation(givenValidationStatus));
createFullComponent(
{
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
propsData: {
siteProfile: siteProfileOne,
},
},
{
dastSiteValidation: dastSiteValidationHandler,
},
);
return wrapper.vm.$nextTick();
});
it('shows the correct status text', () => {
expect(findByTestId('siteValidationStatusDescription').text()).toBe(expectedDescription);
});
it(`shows the correct status text after the validation toggle has been changed`, async () => {
const defaultDescription = 'Site must be validated to run an active scan.';
findSiteValidationToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
expect(findByTestId('siteValidationStatusDescription').text()).toBe(
shouldShowDefaultDescriptionAfterToggle ? defaultDescription : expectedDescription,
);
}); });
it('sets the validation toggle to the correct state', () => {
expect(findSiteValidationToggle().props()).toMatchObject({
value: shouldHaveSiteValidationActivated,
disabled: shouldHaveSiteValidationDisabled,
}); });
}); });
it(`should ${shouldPoll ? '' : 'not '}poll the validation status`, async () => {
jest.useFakeTimers();
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(1);
jest.runOnlyPendingTimers();
await waitForPromises();
if (shouldPoll) {
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(2);
} else {
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(1);
}
}); });
},
);
}); });
...@@ -9,6 +9,7 @@ import DastSiteValidation from 'ee/security_configuration/dast_site_profiles_for ...@@ -9,6 +9,7 @@ import DastSiteValidation from 'ee/security_configuration/dast_site_profiles_for
import dastSiteValidationCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation_create.mutation.graphql'; import dastSiteValidationCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation_create.mutation.graphql';
import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation.query.graphql'; import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation.query.graphql';
import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock'; import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock';
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
jest.mock('~/lib/utils/downloader'); jest.mock('~/lib/utils/downloader');
...@@ -184,7 +185,7 @@ describe('DastSiteValidation', () => { ...@@ -184,7 +185,7 @@ describe('DastSiteValidation', () => {
createComponent(); createComponent();
}); });
describe('success', () => { describe('passed', () => {
beforeEach(() => { beforeEach(() => {
findValidateButton().vm.$emit('click'); findValidateButton().vm.$emit('click');
}); });
...@@ -210,6 +211,24 @@ describe('DastSiteValidation', () => { ...@@ -210,6 +211,24 @@ describe('DastSiteValidation', () => {
}); });
}); });
describe('failed', () => {
beforeEach(() => {
respondWith({
dastSiteValidation: () =>
Promise.resolve(responses.dastSiteValidation(DAST_SITE_VALIDATION_STATUS.FAILED)),
});
});
it('shows failure message', async () => {
expect(findErrorMessage()).toBe(null);
findValidateButton().vm.$emit('click');
await waitForPromises();
expect(findErrorMessage()).not.toBe(null);
});
});
describe.each` describe.each`
errorKind | errorResponse errorKind | errorResponse
${'top level error'} | ${() => Promise.reject(new Error('GraphQL Network Error'))} ${'top level error'} | ${() => Promise.reject(new Error('GraphQL Network Error'))}
......
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
export const dastSiteProfileCreate = (errors = []) => ({ export const dastSiteProfileCreate = (errors = []) => ({
data: { dastSiteProfileCreate: { id: '3083', errors } }, data: { dastSiteProfileCreate: { id: '3083', errors } },
}); });
...@@ -6,12 +8,14 @@ export const dastSiteProfileUpdate = (errors = []) => ({ ...@@ -6,12 +8,14 @@ export const dastSiteProfileUpdate = (errors = []) => ({
data: { dastSiteProfileUpdate: { id: '3083', errors } }, data: { dastSiteProfileUpdate: { id: '3083', errors } },
}); });
export const dastSiteValidation = (status = 'FAILED_VALIDATION') => ({ export const dastSiteValidation = (status = DAST_SITE_VALIDATION_STATUS.PENDING) => ({
data: { project: { dastSiteValidation: { status, id: '1' } } }, data: { project: { dastSiteValidation: { status, id: '1' } } },
}); });
export const dastSiteValidationCreate = (errors = []) => ({ export const dastSiteValidationCreate = (errors = []) => ({
data: { dastSiteValidationCreate: { status: 'PASSED_VALIDATION', id: '1', errors } }, data: {
dastSiteValidationCreate: { status: DAST_SITE_VALIDATION_STATUS.PASSED, id: '1', errors },
},
}); });
export const dastSiteTokenCreate = ({ id = '1', token = '1', errors = [] }) => ({ export const dastSiteTokenCreate = ({ id = '1', token = '1', errors = [] }) => ({
......
...@@ -8178,6 +8178,12 @@ msgstr "" ...@@ -8178,6 +8178,12 @@ msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method." msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method."
msgstr "" msgstr ""
msgid "DastProfiles|Validation failed. Please try again."
msgstr ""
msgid "DastProfiles|Validation is in progress..."
msgstr ""
msgid "DastProfiles|Validation must be turned off to change the target URL" msgid "DastProfiles|Validation must be turned off to change the target URL"
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