Commit 1a9d8318 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Kushal Pandya

DAST Site validation: Hook up with backend

* Adds queries and mutations to retrieve the validation status,
  validation token and mutation to validate a site
* Adds error handling
* Adds specs
parent d690637a
......@@ -17,6 +17,9 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 } from '../constants';
const initField = value => ({
value,
......@@ -67,10 +70,15 @@ export default {
form,
initialFormValues: extractFormValues(form),
isFetchingValidationStatus: false,
isValidatingSite: false,
loading: false,
showAlert: false,
tokenId: null,
token: null,
isSiteValid,
validateSite: isSiteValid,
errorMessage: '',
errors: [],
};
},
computed: {
......@@ -93,6 +101,14 @@ 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() {
......@@ -108,30 +124,35 @@ export default {
return (this.validateSite && !this.isSiteValid) || this.formHasErrors || this.someFieldEmpty;
},
showValidationSection() {
return this.validateSite && !this.isSiteValid && !this.isFetchingValidationStatus;
return this.validateSite && !this.isSiteValid && !this.isValidatingSite;
},
},
watch: {
async validateSite(validate) {
this.tokenId = null;
this.token = null;
if (!validate) {
this.isSiteValid = false;
} else {
// TODO: In the next iteration, this should be changed to:
// * Trigger a GraphQL query to retrieve the site's validation status
// * If the site is not validated, this should also trigger the dastSiteTokenCreate GraphQL
// mutation to create the validation token and pass it down to the validation component.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/238578
this.isFetchingValidationStatus = true;
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
this.isFetchingValidationStatus = false;
try {
this.isValidatingSite = true;
await this.fetchValidationStatus();
if (!this.isSiteValid) {
await this.createValidationToken();
}
} catch (exception) {
this.captureException(exception);
} finally {
this.isValidatingSite = false;
}
}
},
},
created() {
async created() {
if (this.isEdit) {
this.validateTargetUrl();
await this.fetchValidationStatus();
}
},
methods: {
......@@ -146,9 +167,64 @@ export default {
this.form.targetUrl.state = true;
this.form.targetUrl.feedback = null;
},
async fetchValidationStatus() {
this.isFetchingValidationStatus = true;
try {
const {
data: {
project: {
dastSiteValidation: { status },
},
},
} = await this.$apollo.query({
query: dastSiteValidationQuery,
variables: {
fullPath: this.fullPath,
targetUrl: this.form.targetUrl.value,
},
});
this.isSiteValid = status === DAST_SITE_VALIDATION_STATUS.VALID;
} catch (exception) {
this.showErrors({
message: this.i18n.siteValidation.validationStatusFetchError,
});
this.validateSite = false;
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: { projectFullPath: this.fullPath, targetUrl: this.form.targetUrl.value },
});
if (errors.length) {
this.showErrors({ message: errorMessage, errors });
} else {
this.tokenId = id;
this.token = token;
}
} catch (exception) {
this.showErrors({ message: errorMessage });
this.validateSite = false;
throw new Error(exception);
}
},
onSubmit() {
this.loading = true;
this.hideErrors();
const { errorMessage } = this.i18n;
const variables = {
fullPath: this.fullPath,
......@@ -168,16 +244,16 @@ export default {
},
}) => {
if (errors.length > 0) {
this.showErrors(errors);
this.showErrors({ message: errorMessage, errors });
this.loading = false;
} else {
redirectTo(this.profilesLibraryPath);
}
},
)
.catch(e => {
Sentry.captureException(e);
this.showErrors();
.catch(exception => {
this.showErrors({ message: errorMessage });
this.captureException(exception);
this.loading = false;
});
},
......@@ -191,11 +267,16 @@ export default {
discard() {
redirectTo(this.profilesLibraryPath);
},
showErrors(errors = []) {
captureException(exception) {
Sentry.captureException(exception);
},
showErrors({ message, errors = [] }) {
this.errorMessage = message;
this.errors = errors;
this.showAlert = true;
},
hideErrors() {
this.errorMessage = '';
this.errors = [];
this.showAlert = false;
},
......@@ -210,8 +291,14 @@ export default {
{{ i18n.title }}
</h2>
<gl-alert v-if="showAlert" variant="danger" class="gl-mb-5" @dismiss="hideErrors">
{{ i18n.errorMessage }}
<gl-alert
v-if="showAlert"
variant="danger"
class="gl-mb-5"
data-testid="dast-site-profile-form-alert"
@dismiss="hideErrors"
>
{{ errorMessage }}
<ul v-if="errors.length" class="gl-mt-3 gl-mb-0">
<li v-for="error in errors" :key="error" v-text="error"></li>
</ul>
......@@ -232,7 +319,7 @@ export default {
data-testid="target-url-input-group"
:invalid-feedback="form.targetUrl.feedback"
:description="
validateSite
validateSite && !isValidatingSite
? s__('DastProfiles|Validation must be turned off to change the target URL')
: null
"
......@@ -267,13 +354,15 @@ export default {
v-model="validateSite"
data-testid="dast-site-validation-toggle"
:disabled="!form.targetUrl.state"
:is-loading="isFetchingValidationStatus"
:is-loading="isFetchingValidationStatus || isValidatingSite"
/>
</gl-form-group>
<gl-collapse :visible="showValidationSection">
<dast-site-validation
token="asd"
:full-path="fullPath"
:token-id="tokenId"
:token="token"
:target-url="form.targetUrl.value"
@success="isSiteValid = true"
/>
......
<script>
import * as Sentry from '@sentry/browser';
import {
GlAlert,
GlButton,
......@@ -12,7 +13,13 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import download from '~/lib/utils/downloader';
import { DAST_SITE_VALIDATION_METHOD_TEXT_FILE, DAST_SITE_VALIDATION_METHODS } from '../constants';
import {
DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
DAST_SITE_VALIDATION_METHODS,
DAST_SITE_VALIDATION_STATUS,
} from '../constants';
import dastSiteValidationCreateMutation from '../graphql/dast_site_validation_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
export default {
name: 'DastSiteValidation',
......@@ -28,18 +35,59 @@ export default {
GlInputGroupText,
GlLoadingIcon,
},
apollo: {
dastSiteValidation: {
query: dastSiteValidationQuery,
variables() {
return {
fullPath: this.fullPath,
targetUrl: this.targetUrl,
};
},
manual: true,
result({
data: {
project: {
dastSiteValidation: { status },
},
},
}) {
if (status === DAST_SITE_VALIDATION_STATUS.VALID) {
this.onSuccess();
}
},
skip() {
return !(this.isCreatingValidation || this.isValidating);
},
pollInterval: 1000,
error(e) {
this.onError(e);
},
},
},
props: {
fullPath: {
type: String,
required: true,
},
targetUrl: {
type: String,
required: true,
},
tokenId: {
type: String,
required: false,
default: null,
},
token: {
type: String,
required: true,
required: false,
default: null,
},
},
data() {
return {
isCreatingValidation: false,
isValidating: false,
hasValidationError: false,
validationMethod: DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
......@@ -66,20 +114,45 @@ export default {
download({ fileName: this.textFileName, fileData: btoa(this.token) });
},
async validate() {
// TODO: Trigger the dastSiteValidationCreate GraphQL mutation.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/238578
this.hasValidationError = false;
this.isCreatingValidation = true;
this.isValidating = true;
try {
await new Promise(resolve => {
setTimeout(resolve, 1000);
const {
data: {
dastSiteValidationCreate: { status, errors },
},
} = await this.$apollo.mutate({
mutation: dastSiteValidationCreateMutation,
variables: {
projectFullPath: this.fullPath,
dastSiteTokenId: this.tokenId,
strategy: this.validationMethod,
},
});
this.isValidating = false;
this.$emit('success');
} catch (_) {
this.hasValidationError = true;
this.isValidating = false;
if (errors?.length) {
this.onError();
} else if (status === DAST_SITE_VALIDATION_STATUS.VALID) {
this.onSuccess();
} else {
this.isCreatingValidation = false;
}
} catch (exception) {
this.onError(exception);
}
},
onSuccess() {
this.isCreatingValidation = false;
this.isValidating = false;
this.$emit('success');
},
onError(exception = null) {
if (exception !== null) {
Sentry.captureException(exception);
}
this.isCreatingValidation = false;
this.isValidating = false;
this.hasValidationError = true;
},
},
validationMethodOptions: Object.values(DAST_SITE_VALIDATION_METHODS),
......
import { s__ } from '~/locale';
export const DAST_SITE_VALIDATION_METHOD_TEXT_FILE = 'DAST_SITE_VALIDATION_METHOD_TEXT_FILE';
export const DAST_SITE_VALIDATION_METHOD_TEXT_FILE = 'TEXT_FILE';
export const DAST_SITE_VALIDATION_METHODS = {
DAST_SITE_VALIDATION_METHOD_TEXT_FILE: {
[DAST_SITE_VALIDATION_METHOD_TEXT_FILE]: {
value: DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
text: s__('DastProfiles|Text file validation'),
i18n: {
......@@ -10,3 +10,8 @@ export const DAST_SITE_VALIDATION_METHODS = {
},
},
};
export const DAST_SITE_VALIDATION_STATUS = {
VALID: 'PASSED_VALIDATION',
INVALID: 'FAILED_VALIDATION',
};
mutation dastSiteTokenCreate($projectFullPath: ID!, $targetUrl: String!) {
dastSiteTokenCreate(input: { projectFullPath: $projectFullPath, targetUrl: $targetUrl }) {
token
errors
}
}
query project($fullPath: ID!, $targetUrl: String!) {
project(fullPath: $fullPath) {
dastSiteValidation(targetUrl: $targetUrl) {
status
}
}
}
mutation dastSiteValidationCreate(
$projectFullPath: ID!
$dastSiteTokenId: DastSiteTokenID!
$validationStrategy: DastSiteValidationStrategyEnum
) {
dastSiteValidationCreate(
input: {
projectFullPath: $projectFullPath
dastSiteTokenId: $dastSiteTokenId
strategy: $validationStrategy
}
) {
status
errors
}
}
import merge from 'lodash/merge';
import VueApollo from 'vue-apollo';
import { within } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlForm, GlModal } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { createMockClient } from 'mock-apollo-client';
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/dast_site_profiles_form/components/dast_site_profile_form.vue';
import DastSiteValidation from 'ee/dast_site_profiles_form/components/dast_site_validation.vue';
import dastSiteValidationQuery from 'ee/dast_site_profiles_form/graphql/dast_site_validation.query.graphql';
import dastSiteProfileCreateMutation from 'ee/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from 'ee/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from 'ee/dast_site_profiles_form/graphql/dast_site_token_create.mutation.graphql';
import * as responses from 'ee_jest/dast_site_profiles_form/mock_data/apollo_mock';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
......@@ -14,18 +20,34 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const localVue = createLocalVue();
localVue.use(VueApollo);
const fullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/on_demand_scans/profiles`;
const profileName = 'My DAST site profile';
const targetUrl = 'http://example.com';
const tokenId = '3455';
const token = '33988';
const defaultProps = {
profilesLibraryPath,
fullPath,
};
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', () => {
let wrapper;
let apolloProvider;
let requestHandlers;
const withinComponent = () => within(wrapper.element);
......@@ -39,27 +61,58 @@ describe('DastSiteProfileForm', () => {
wrapper.find('[data-testid="dast-site-profile-form-cancel-button"]');
const findCancelModal = () => wrapper.find(GlModal);
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const findAlert = () => wrapper.find(GlAlert);
const findAlert = () => wrapper.find('[data-testid="dast-site-profile-form-alert"]');
const findSiteValidationToggle = () =>
wrapper.find('[data-testid="dast-site-validation-toggle"]');
const findDastSiteValidation = () => wrapper.find(DastSiteValidation);
const mockClientFactory = handlers => {
const mockClient = createMockClient();
requestHandlers = {
...defaultRequestHandlers,
...handlers,
};
mockClient.setRequestHandler(
dastSiteProfileCreateMutation,
requestHandlers.dastSiteProfileCreate,
);
mockClient.setRequestHandler(
dastSiteProfileUpdateMutation,
requestHandlers.dastSiteProfileUpdate,
);
mockClient.setRequestHandler(dastSiteTokenCreateMutation, requestHandlers.dastSiteTokenCreate);
mockClient.setRequestHandler(dastSiteValidationQuery, requestHandlers.dastSiteValidation);
return mockClient;
};
const respondWith = handlers => {
apolloProvider.defaultClient = mockClientFactory(handlers);
};
const componentFactory = (mountFn = shallowMount) => options => {
wrapper = mountFn(
DastSiteProfileForm,
merge(
{},
{
propsData: defaultProps,
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
},
options,
),
apolloProvider = new VueApollo({
defaultClient: mockClientFactory(),
});
const mountOpts = merge(
{},
{
propsData: defaultProps,
},
options,
{
localVue,
apolloProvider,
},
);
wrapper = mountFn(DastSiteProfileForm, mountOpts);
};
const createComponent = componentFactory();
const createFullComponent = componentFactory(mount);
......@@ -67,6 +120,7 @@ describe('DastSiteProfileForm', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
apolloProvider = null;
});
it('renders properly', () => {
......@@ -125,6 +179,11 @@ describe('DastSiteProfileForm', () => {
});
describe('validation', () => {
const enableValidationToggle = async () => {
await findTargetUrlInput().vm.$emit('input', targetUrl);
await findSiteValidationToggle().vm.$emit('change', true);
};
describe('with feature flag disabled', () => {
beforeEach(() => {
createComponent({
......@@ -165,12 +224,12 @@ describe('DastSiteProfileForm', () => {
it('disables target URL input when validation is enabled', async () => {
const targetUrlInputGroup = findTargetUrlInputGroup();
const targetUrlInput = findTargetUrlInput();
await targetUrlInput.vm.$emit('input', targetUrl);
expect(targetUrlInputGroup.attributes('description')).toBeUndefined();
expect(targetUrlInput.attributes('disabled')).toBeUndefined();
await findSiteValidationToggle().vm.$emit('change', true);
await enableValidationToggle();
await waitForPromises();
expect(targetUrlInputGroup.attributes('description')).toBe(
'Validation must be turned off to change the target URL',
......@@ -178,16 +237,59 @@ describe('DastSiteProfileForm', () => {
expect(targetUrlInput.attributes('disabled')).toBe('true');
});
// TODO: refactor this test case when actual GraphQL mutations are implemented
// See https://gitlab.com/gitlab-org/gitlab/-/issues/238578
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({
projectFullPath: 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 () => {
jest.useFakeTimers();
expect(wrapper.vm.showValidationSection).toBe(false);
findTargetUrlInput().vm.$emit('input', targetUrl);
await findSiteValidationToggle().vm.$emit('change', true);
jest.runAllTimers();
await wrapper.vm.$nextTick();
await enableValidationToggle();
await waitForPromises();
expect(wrapper.vm.showValidationSection).toBe(true);
......@@ -199,10 +301,10 @@ describe('DastSiteProfileForm', () => {
});
describe.each`
title | siteProfile | mutation | mutationVars | mutationKind
${'New site profile'} | ${null} | ${dastSiteProfileCreateMutation} | ${{}} | ${'dastSiteProfileCreate'}
${'Edit site profile'} | ${{ id: 1, name: 'foo', targetUrl: 'bar' }} | ${dastSiteProfileUpdateMutation} | ${{ id: 1 }} | ${'dastSiteProfileUpdate'}
`('$title', ({ siteProfile, title, mutation, mutationVars, mutationKind }) => {
title | siteProfile | mutationVars | mutationKind
${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'}
${'Edit site profile'} | ${{ id: 1, name: 'foo', targetUrl: 'bar' }} | ${{ id: 1 }} | ${'dastSiteProfileUpdate'}
`('$title', ({ siteProfile, title, mutationVars, mutationKind }) => {
beforeEach(() => {
createFullComponent({
propsData: {
......@@ -220,13 +322,8 @@ describe('DastSiteProfileForm', () => {
});
describe('submission', () => {
const createdProfileId = 30203;
describe('on success', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { [mutationKind]: { id: createdProfileId } } });
findProfileNameInput().vm.$emit('input', profileName);
findTargetUrlInput().vm.$emit('input', targetUrl);
submitForm();
......@@ -237,14 +334,11 @@ describe('DastSiteProfileForm', () => {
});
it('triggers GraphQL mutation', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation,
variables: {
profileName,
targetUrl,
fullPath,
...mutationVars,
},
expect(requestHandlers[mutationKind]).toHaveBeenCalledWith({
profileName,
targetUrl,
fullPath,
...mutationVars,
});
});
......@@ -259,10 +353,15 @@ describe('DastSiteProfileForm', () => {
describe('on top-level error', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue();
respondWith({
[mutationKind]: jest.fn().mockRejectedValue(new Error('GraphQL Network Error')),
});
const input = findTargetUrlInput();
input.vm.$emit('input', targetUrl);
submitForm();
return waitForPromises();
});
it('resets loading state', () => {
......@@ -278,12 +377,15 @@ describe('DastSiteProfileForm', () => {
const errors = ['error#1', 'error#2', 'error#3'];
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { [mutationKind]: { errors } } });
respondWith({
[mutationKind]: jest.fn().mockResolvedValue(responses[mutationKind](errors)),
});
const input = findTargetUrlInput();
input.vm.$emit('input', targetUrl);
submitForm();
return waitForPromises();
});
it('resets loading state', () => {
......
import merge from 'lodash/merge';
import VueApollo from 'vue-apollo';
import { within } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { createMockClient } from 'mock-apollo-client';
import { GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'jest/helpers/wait_for_promises';
import DastSiteValidation from 'ee/dast_site_profiles_form/components/dast_site_validation.vue';
import dastSiteValidationCreateMutation from 'ee/dast_site_profiles_form/graphql/dast_site_validation_create.mutation.graphql';
import dastSiteValidationQuery from 'ee/dast_site_profiles_form/graphql/dast_site_validation.query.graphql';
import * as responses from 'ee_jest/dast_site_profiles_form/mock_data/apollo_mock';
import download from '~/lib/utils/downloader';
jest.mock('~/lib/utils/downloader');
// TODO: stop using fake timers once GraphQL mutations are implemented.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/238578
jest.useFakeTimers();
const localVue = createLocalVue();
localVue.use(VueApollo);
const fullPath = 'group/project';
const targetUrl = 'https://example.com/';
const token = 'validation-token-123';
const defaultProps = {
fullPath,
targetUrl,
token,
};
const defaultRequestHandlers = {
dastSiteValidation: jest.fn().mockResolvedValue(responses.dastSiteValidation()),
dastSiteValidationCreate: jest.fn().mockResolvedValue(responses.dastSiteValidationCreate()),
};
describe('DastSiteValidation', () => {
let wrapper;
let apolloProvider;
let requestHandlers;
const mockClientFactory = handlers => {
const mockClient = createMockClient();
requestHandlers = {
...defaultRequestHandlers,
...handlers,
};
mockClient.setRequestHandler(dastSiteValidationQuery, requestHandlers.dastSiteValidation);
mockClient.setRequestHandler(
dastSiteValidationCreateMutation,
requestHandlers.dastSiteValidationCreate,
);
return mockClient;
};
const respondWith = handlers => {
apolloProvider.defaultClient = mockClientFactory(handlers);
};
const componentFactory = (mountFn = shallowMount) => options => {
apolloProvider = new VueApollo({
defaultClient: mockClientFactory(),
});
wrapper = mountFn(
DastSiteValidation,
merge(
......@@ -30,6 +71,10 @@ describe('DastSiteValidation', () => {
propsData: defaultProps,
},
options,
{
localVue,
apolloProvider,
},
),
);
};
......@@ -41,6 +86,10 @@ describe('DastSiteValidation', () => {
wrapper.find('[data-testid="download-dast-text-file-validation-button"]');
const findValidateButton = () => wrapper.find('[data-testid="validate-dast-site-button"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findErrorMessage = () =>
withinComponent().queryByText(
/validation failed, please make sure that you follow the steps above with the choosen method./i,
);
afterEach(() => {
wrapper.destroy();
......@@ -88,24 +137,45 @@ describe('DastSiteValidation', () => {
describe('validation', () => {
beforeEach(() => {
createComponent();
findValidateButton().vm.$emit('click');
});
it('while validating, shows a loading state', () => {
expect(findLoadingIcon().exists()).toBe(true);
expect(wrapper.text()).toContain('Validating...');
});
describe('success', () => {
beforeEach(() => {
findValidateButton().vm.$emit('click');
});
it('on success, emits success event', async () => {
jest.spyOn(wrapper.vm, '$emit');
jest.runAllTimers();
await wrapper.vm.$nextTick();
it('while validating, shows a loading state', () => {
expect(findLoadingIcon().exists()).toBe(true);
expect(wrapper.text()).toContain('Validating...');
});
it('on success, emits success event', async () => {
await waitForPromises();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('success');
expect(wrapper.emitted('success')).toHaveLength(1);
});
});
// TODO: test error handling once GraphQL mutations are implemented.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/238578
it.todo('on error, shows error state');
describe.each`
errorKind | errorResponse
${'top level error'} | ${() => Promise.reject(new Error('GraphQL Network Error'))}
${'errors as data'} | ${() => Promise.resolve(responses.dastSiteValidationCreate(['error#1', 'error#2']))}
`('$errorKind', ({ errorResponse }) => {
beforeEach(() => {
respondWith({
dastSiteValidationCreate: errorResponse,
});
});
it('on error, shows error state', async () => {
expect(findErrorMessage()).toBe(null);
findValidateButton().vm.$emit('click');
await waitForPromises();
expect(findErrorMessage()).not.toBe(null);
});
});
});
});
export const dastSiteProfileCreate = (errors = []) => ({
data: { dastSiteProfileCreate: { id: '3083', errors } },
});
export const dastSiteProfileUpdate = (errors = []) => ({
data: { dastSiteProfileUpdate: { id: '3083', errors } },
});
export const dastSiteValidation = (status = 'FAILED_VALIDATION') => ({
data: { project: { dastSiteValidation: { status, id: '1' } } },
});
export const dastSiteValidationCreate = (errors = []) => ({
data: { dastSiteValidationCreate: { status: 'PASSED_VALIDATION', id: '1', errors } },
});
export const dastSiteTokenCreate = ({ id = '1', token = '1', errors = [] }) => ({
data: {
dastSiteTokenCreate: {
id,
token,
errors,
},
},
});
......@@ -7798,6 +7798,9 @@ msgstr ""
msgid "DastProfiles|Are you sure you want to delete this profile?"
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 ""
......@@ -7822,6 +7825,9 @@ 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 site profile. Please try again."
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