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 { ...@@ -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