Remove validation from DAST site profile form

DAST site validation has been rethought and will not be part of the DAST
site profile form like it was originally designed to be. This is the
first step in a series of changes to move the validation to a dedicated
modal accessible from the DAST profiles library.

Here, we're simply removing all of the validation logic form the DAST
site profile form to clean things up.
parent a1780610
<script>
import { isEqual } from 'lodash';
import {
GlAlert,
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
GlModal,
GlToggle,
} from '@gitlab/ui';
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput, GlModal } from '@gitlab/ui';
import { initFormField } from 'ee/security_configuration/utils';
import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import { serializeFormObject } from '~/lib/utils/forms';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import validation from '~/vue_shared/directives/validation';
import DastSiteValidation from './dast_site_validation.vue';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from '../graphql/dast_site_token_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
import { DAST_SITE_VALIDATION_STATUS, DAST_SITE_VALIDATION_POLL_INTERVAL } from '../constants';
const { PENDING, INPROGRESS, PASSED, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default {
name: 'DastSiteProfileForm',
components: {
GlAlert,
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
GlModal,
GlToggle,
DastSiteValidation,
},
directives: {
validation: validation(),
},
mixins: [glFeatureFlagsMixin()],
props: {
fullPath: {
type: String,
......@@ -72,18 +51,12 @@ export default {
};
return {
fetchValidationTimeout: null,
form,
initialFormValues: serializeFormObject(form.fields),
isFetchingValidationStatus: false,
isValidatingSite: false,
isLoading: false,
hasAlert: false,
tokenId: null,
token: null,
isSiteValidationActive: false,
isSiteValidationTouched: false,
validationStatus: null,
errorMessage: '',
errors: [],
};
......@@ -92,9 +65,6 @@ export default {
isEdit() {
return Boolean(this.siteProfile?.id);
},
isSiteValidationDisabled() {
return !this.form.fields.targetUrl.state || this.validationStatusMatches(INPROGRESS);
},
i18n() {
const { isEdit } = this;
return {
......@@ -111,163 +81,18 @@ export default {
okTitle: __('Discard'),
cancelTitle: __('Cancel'),
},
siteValidation: {
validationStatusFetchError: s__(
'DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later.',
),
createTokenError: s__(
'DastProfiles|Could not create site validation token. Please refresh the page, or try again later.',
),
},
};
},
formTouched() {
return !isEqual(serializeFormObject(this.form.fields), this.initialFormValues);
},
isSubmitDisabled() {
return (
this.validationStatusMatches(INPROGRESS) ||
(this.isSiteValidationActive && !this.validationStatusMatches(PASSED))
);
},
showValidationSection() {
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 mounted() {
if (this.isEdit) {
this.form.showValidation = true;
if (this.glFeatures.securityOnDemandScansSiteValidation) {
await this.fetchValidationStatus();
this.isSiteValidationActive = this.validationStatusMatches(PASSED);
}
}
},
destroyed() {
clearTimeout(this.fetchValidationTimeout);
this.fetchValidationTimeout = null;
},
methods: {
async validateSite(validate) {
this.isSiteValidationActive = validate;
this.isSiteValidationTouched = true;
this.tokenId = null;
this.token = null;
if (!validate) {
this.validationStatus = null;
} else {
try {
this.isValidatingSite = true;
await this.fetchValidationStatus();
if (![PASSED, INPROGRESS].some(this.validationStatusMatches)) {
await this.createValidationToken();
}
} catch (exception) {
this.captureException(exception);
this.isSiteValidationActive = false;
} finally {
this.isValidatingSite = false;
}
}
},
validationStatusMatches(status) {
return this.validationStatus === status;
},
async fetchValidationStatus() {
this.isFetchingValidationStatus = true;
try {
const {
data: {
project: { dastSiteValidation },
},
} = await this.$apollo.query({
query: dastSiteValidationQuery,
variables: {
fullPath: this.fullPath,
targetUrl: this.form.fields.targetUrl.value,
},
fetchPolicy: fetchPolicies.NETWORK_ONLY,
});
this.validationStatus = dastSiteValidation?.status || null;
if (this.validationStatusMatches(INPROGRESS)) {
await new Promise(resolve => {
this.fetchValidationTimeout = setTimeout(resolve, DAST_SITE_VALIDATION_POLL_INTERVAL);
});
await this.fetchValidationStatus();
}
} catch (exception) {
this.showErrors({
message: this.i18n.siteValidation.validationStatusFetchError,
});
throw new Error(exception);
} finally {
this.isFetchingValidationStatus = false;
}
},
async createValidationToken() {
const errorMessage = this.i18n.siteValidation.createTokenError;
try {
const {
data: {
dastSiteTokenCreate: { id, token, errors = [] },
},
} = await this.$apollo.mutate({
mutation: dastSiteTokenCreateMutation,
variables: {
fullPath: this.fullPath,
targetUrl: this.form.fields.targetUrl.value,
},
});
if (errors.length) {
this.showErrors({ message: errorMessage, errors });
} else {
this.tokenId = id;
this.token = token;
}
} catch (exception) {
this.showErrors({ message: errorMessage });
throw new Error(exception);
}
},
onSubmit() {
this.form.showValidation = true;
......@@ -317,9 +142,6 @@ export default {
this.$refs[this.$options.modalId].show();
}
},
onValidationSuccess() {
this.validationStatus = PASSED;
},
discard() {
redirectTo(this.profilesLibraryPath);
},
......@@ -381,11 +203,6 @@ export default {
<gl-form-group
data-testid="target-url-input-group"
:invalid-feedback="form.fields.targetUrl.feedback"
:description="
isSiteValidationActive && !isValidatingSite
? s__('DastProfiles|Validation must be turned off to change the target URL')
: null
"
:label="s__('DastProfiles|Target URL')"
>
<gl-form-input
......@@ -397,44 +214,9 @@ export default {
required
type="url"
:state="form.fields.targetUrl.state"
:disabled="isSiteValidationActive"
/>
</gl-form-group>
<template v-if="glFeatures.securityOnDemandScansSiteValidation">
<gl-form-group :label="s__('DastProfiles|Validate target site')">
<template #description>
<p
v-if="siteValidationStatusDescription.text"
class="gl-mt-3"
:class="siteValidationStatusDescription.cssClass"
data-testid="siteValidationStatusDescription"
>
{{ siteValidationStatusDescription.text }}
</p>
</template>
<gl-toggle
data-testid="dast-site-validation-toggle"
:value="isSiteValidationActive"
:disabled="isSiteValidationDisabled"
:is-loading="
!isSiteValidationDisabled && (isFetchingValidationStatus || isValidatingSite)
"
@change="validateSite"
/>
</gl-form-group>
<gl-collapse :visible="showValidationSection">
<dast-site-validation
:full-path="fullPath"
:token-id="tokenId"
:token="token"
:target-url="form.fields.targetUrl.value"
@success="onValidationSuccess"
/>
</gl-collapse>
</template>
<hr />
<div class="gl-mt-6 gl-pt-6">
......@@ -443,7 +225,6 @@ export default {
variant="success"
class="js-no-auto-disable"
data-testid="dast-site-profile-form-submit-button"
:disabled="isSubmitDisabled"
:loading="isLoading"
>
{{ s__('DastProfiles|Save profile') }}
......
......@@ -7,14 +7,10 @@ import { GlForm, GlModal } from '@gitlab/ui';
import waitForPromises from 'jest/helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import DastSiteProfileForm from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_profile_form.vue';
import DastSiteValidation from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_validation.vue';
import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation.query.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 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 { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
import * as urlUtility from '~/lib/utils/url_utility';
const localVue = createLocalVue();
......@@ -25,8 +21,6 @@ const fullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/security/configuration/dast_profiles`;
const profileName = 'My DAST site profile';
const targetUrl = 'http://example.com';
const tokenId = '3455';
const token = '33988';
const defaultProps = {
profilesLibraryPath,
......@@ -36,10 +30,6 @@ const defaultProps = {
const defaultRequestHandlers = {
dastSiteProfileCreate: jest.fn().mockResolvedValue(responses.dastSiteProfileCreate()),
dastSiteProfileUpdate: jest.fn().mockResolvedValue(responses.dastSiteProfileUpdate()),
dastSiteTokenCreate: jest
.fn()
.mockResolvedValue(responses.dastSiteTokenCreate({ id: tokenId, token })),
dastSiteValidation: jest.fn().mockResolvedValue(responses.dastSiteValidation()),
};
describe('DastSiteProfileForm', () => {
......@@ -52,15 +42,12 @@ describe('DastSiteProfileForm', () => {
const findForm = () => wrapper.find(GlForm);
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
const findProfileNameInput = () => findByTestId('profile-name-input');
const findTargetUrlInputGroup = () => findByTestId('target-url-input-group');
const findTargetUrlInput = () => findByTestId('target-url-input');
const findSubmitButton = () => findByTestId('dast-site-profile-form-submit-button');
const findCancelButton = () => findByTestId('dast-site-profile-form-cancel-button');
const findCancelModal = () => wrapper.find(GlModal);
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const findAlert = () => findByTestId('dast-site-profile-form-alert');
const findSiteValidationToggle = () => findByTestId('dast-site-validation-toggle');
const findDastSiteValidation = () => wrapper.find(DastSiteValidation);
const setFieldValue = async (field, value) => {
await field.setValue(value);
......@@ -85,10 +72,6 @@ describe('DastSiteProfileForm', () => {
requestHandlers.dastSiteProfileUpdate,
);
mockClient.setRequestHandler(dastSiteTokenCreateMutation, requestHandlers.dastSiteTokenCreate);
mockClient.setRequestHandler(dastSiteValidationQuery, requestHandlers.dastSiteValidation);
return mockClient;
};
......@@ -149,139 +132,6 @@ describe('DastSiteProfileForm', () => {
});
});
describe('validation', () => {
const enableValidationToggle = async () => {
await setFieldValue(findTargetUrlInput(), targetUrl);
await findSiteValidationToggle().vm.$emit('change', true);
};
describe.each`
title | siteProfile
${'New site profile'} | ${null}
${'Edit site profile'} | ${siteProfileOne}
`('$title with feature flag disabled', ({ siteProfile }) => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: {
siteProfile,
},
});
});
it('does not render validation components', () => {
expect(findSiteValidationToggle().exists()).toBe(false);
expect(findDastSiteValidation().exists()).toBe(false);
});
it('does not check the target URLs validation status', () => {
expect(requestHandlers.dastSiteValidation).not.toHaveBeenCalled();
});
});
describe('with feature flag enabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
});
});
it('renders validation components', () => {
expect(findSiteValidationToggle().exists()).toBe(true);
expect(findDastSiteValidation().exists()).toBe(true);
});
it('toggle is disabled until target URL is valid', async () => {
expect(findSiteValidationToggle().props('disabled')).toBe(true);
await setFieldValue(findTargetUrlInput(), targetUrl);
expect(findSiteValidationToggle().props('disabled')).toBe(false);
});
it('disables target URL input when validation is enabled', async () => {
const targetUrlInputGroup = findTargetUrlInputGroup();
const targetUrlInput = findTargetUrlInput();
expect(targetUrlInputGroup.attributes('description')).toBeUndefined();
expect(targetUrlInput.attributes('disabled')).toBeUndefined();
await enableValidationToggle();
await waitForPromises();
expect(targetUrlInputGroup.text()).toContain(
'Validation must be turned off to change the target URL',
);
expect(targetUrlInput.attributes('disabled')).toBe('disabled');
});
it('checks the target URLs validation status when validation is enabled', async () => {
expect(requestHandlers.dastSiteValidation).not.toHaveBeenCalled();
await enableValidationToggle();
expect(requestHandlers.dastSiteValidation).toHaveBeenCalledWith({
fullPath,
targetUrl,
});
});
it('creates a site token if the target URL has not been validated', async () => {
expect(requestHandlers.dastSiteTokenCreate).not.toHaveBeenCalled();
await enableValidationToggle();
await waitForPromises();
expect(requestHandlers.dastSiteTokenCreate).toHaveBeenCalledWith({
fullPath,
targetUrl,
});
expect(findDastSiteValidation().props()).toMatchObject({
tokenId,
token,
});
});
it.each`
description | failingResponse | errorMessageStart
${'target URLs validation status can not be retrieved'} | ${'dastSiteValidation'} | ${'Could not retrieve site validation status'}
${'validation token can not be created'} | ${'dastSiteTokenCreate'} | ${'Could not create site validation token'}
`('shows an error if $description', async ({ failingResponse, errorMessageStart }) => {
respondWith({
[failingResponse]: jest.fn().mockRejectedValue(),
});
expect(findAlert().exists()).toBe(false);
await enableValidationToggle();
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(
`${errorMessageStart}. Please refresh the page, or try again later.`,
);
});
it('when validation section is opened and validation succeeds, section is collapsed', async () => {
expect(wrapper.vm.showValidationSection).toBe(false);
await enableValidationToggle();
await waitForPromises();
expect(wrapper.vm.showValidationSection).toBe(true);
await findDastSiteValidation().vm.$emit('success');
expect(wrapper.vm.showValidationSection).toBe(false);
});
});
});
describe.each`
title | siteProfile | mutationVars | mutationKind
${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'}
......@@ -414,84 +264,4 @@ describe('DastSiteProfileForm', () => {
});
});
});
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} | ${false} | ${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);
}
});
},
);
});
......@@ -8401,9 +8401,6 @@ msgstr ""
msgid "DastProfiles|Copy HTTP header to clipboard"
msgstr ""
msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr ""
......@@ -8428,9 +8425,6 @@ msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not update the scanner profile. Please try again."
msgstr ""
......@@ -8545,9 +8539,6 @@ msgstr ""
msgid "DastProfiles|Site is not validated yet, please follow the steps."
msgstr ""
msgid "DastProfiles|Site must be validated to run an active scan."
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
......@@ -8593,27 +8584,12 @@ msgstr ""
msgid "DastProfiles|Validate"
msgstr ""
msgid "DastProfiles|Validate target site"
msgstr ""
msgid "DastProfiles|Validating..."
msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method."
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"
msgstr ""
msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site."
msgstr ""
msgid "Data is still calculating..."
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