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
......@@ -14,10 +14,12 @@ import {
} from '@gitlab/ui';
import download from '~/lib/utils/downloader';
import { cleanLeadingSeparator, joinPaths, stripPathTail } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import {
DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
DAST_SITE_VALIDATION_METHODS,
DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_POLL_INTERVAL,
} from '../constants';
import dastSiteValidationCreateMutation from '../graphql/dast_site_validation_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
......@@ -53,14 +55,19 @@ export default {
},
},
}) {
if (status === DAST_SITE_VALIDATION_STATUS.VALID) {
if (status === DAST_SITE_VALIDATION_STATUS.PASSED) {
this.onSuccess();
}
if (status === DAST_SITE_VALIDATION_STATUS.FAILED) {
this.onError();
}
},
skip() {
return !(this.isCreatingValidation || this.isValidating);
},
pollInterval: 1000,
pollInterval: DAST_SITE_VALIDATION_POLL_INTERVAL,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
error(e) {
this.onError(e);
},
......@@ -159,7 +166,7 @@ export default {
});
if (errors?.length) {
this.onError();
} else if (status === DAST_SITE_VALIDATION_STATUS.VALID) {
} else if (status === DAST_SITE_VALIDATION_STATUS.PASSED) {
this.onSuccess();
} else {
this.isCreatingValidation = false;
......
......@@ -12,6 +12,10 @@ export const DAST_SITE_VALIDATION_METHODS = {
};
export const DAST_SITE_VALIDATION_STATUS = {
VALID: 'PASSED_VALIDATION',
INVALID: 'FAILED_VALIDATION',
PENDING: 'PENDING_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
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 { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
redirectTo: jest.fn(),
}));
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();
localVue.use(VueApollo);
const [siteProfileOne] = siteProfiles;
const fullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/security/configuration/dast_profiles`;
const profileName = 'My DAST site profile';
......@@ -52,18 +50,16 @@ describe('DastSiteProfileForm', () => {
const withinComponent = () => within(wrapper.element);
const findForm = () => wrapper.find(GlForm);
const findProfileNameInput = () => wrapper.find('[data-testid="profile-name-input"]');
const findTargetUrlInputGroup = () => wrapper.find('[data-testid="target-url-input-group"]');
const findTargetUrlInput = () => wrapper.find('[data-testid="target-url-input"]');
const findSubmitButton = () =>
wrapper.find('[data-testid="dast-site-profile-form-submit-button"]');
const findCancelButton = () =>
wrapper.find('[data-testid="dast-site-profile-form-cancel-button"]');
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 = () => wrapper.find('[data-testid="dast-site-profile-form-alert"]');
const findSiteValidationToggle = () =>
wrapper.find('[data-testid="dast-site-validation-toggle"]');
const findAlert = () => findByTestId('dast-site-profile-form-alert');
const findSiteValidationToggle = () => findByTestId('dast-site-validation-toggle');
const findDastSiteValidation = () => wrapper.find(DastSiteValidation);
const mockClientFactory = handlers => {
......@@ -95,9 +91,9 @@ describe('DastSiteProfileForm', () => {
apolloProvider.defaultClient = mockClientFactory(handlers);
};
const componentFactory = (mountFn = shallowMount) => options => {
const componentFactory = (mountFn = shallowMount) => (options, handlers) => {
apolloProvider = new VueApollo({
defaultClient: mockClientFactory(),
defaultClient: mockClientFactory(handlers),
});
const mountOpts = merge(
......@@ -187,7 +183,7 @@ describe('DastSiteProfileForm', () => {
describe.each`
title | siteProfile
${'New site profile'} | ${null}
${'Edit site profile'} | ${{ id: 1, name: 'foo', targetUrl: 'bar' }}
${'Edit site profile'} | ${siteProfileOne}
`('$title with feature flag disabled', ({ siteProfile }) => {
beforeEach(() => {
createComponent({
......@@ -314,7 +310,7 @@ describe('DastSiteProfileForm', () => {
describe.each`
title | siteProfile | mutationVars | mutationKind
${'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 }) => {
beforeEach(() => {
createFullComponent({
......@@ -322,6 +318,8 @@ describe('DastSiteProfileForm', () => {
siteProfile,
},
});
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
});
it('sets the correct title', () => {
......@@ -354,7 +352,7 @@ describe('DastSiteProfileForm', () => {
});
it('redirects to the profiles library', () => {
expect(redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
expect(urlUtility.redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
});
it('does not show an alert', () => {
......@@ -418,7 +416,7 @@ describe('DastSiteProfileForm', () => {
describe('form unchanged', () => {
it('redirects to the profiles library', () => {
findCancelButton().vm.$emit('click');
expect(redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
expect(urlUtility.redirectTo).toHaveBeenCalledWith(profilesLibraryPath);
});
});
......@@ -436,9 +434,89 @@ describe('DastSiteProfileForm', () => {
it('redirects to the profiles library if confirmed', () => {
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
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 * 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';
jest.mock('~/lib/utils/downloader');
......@@ -184,7 +185,7 @@ describe('DastSiteValidation', () => {
createComponent();
});
describe('success', () => {
describe('passed', () => {
beforeEach(() => {
findValidateButton().vm.$emit('click');
});
......@@ -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`
errorKind | errorResponse
${'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 = []) => ({
data: { dastSiteProfileCreate: { id: '3083', errors } },
});
......@@ -6,12 +8,14 @@ export const dastSiteProfileUpdate = (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' } } },
});
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 = [] }) => ({
......
......@@ -8178,6 +8178,12 @@ msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen 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 ""
......
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