Commit 9b7c462e authored by Savas Vedova's avatar Savas Vedova Committed by Miguel Rincon

Submit vulnerability form

This commit is part of a commit series which introduces a form
to manually create a vulnerability. It adds the submit vulnerability
button and handles the form submission.
parent 4211bf92
mutation vulnerabilityCreate($input: VulnerabilityCreateInput!) {
vulnerabilityCreate(input: $input) {
errors
vulnerability {
id
vulnerabilityPath
}
}
}
import { __ } from '~/locale';
export const REQUIRED_FIELD = __('This field is required.');
<script> <script>
import { GlForm } from '@gitlab/ui'; import { GlForm, GlButton, GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_PROJECT } from '~/graphql_shared/constants';
import { redirectTo } from '~/lib/utils/url_utility';
import createVulnerabilityMutation from 'ee/security_dashboard/graphql/mutations/vulnerability_create.mutation.graphql';
import SectionDetails from './section_details.vue'; import SectionDetails from './section_details.vue';
import SectionIdentifiers from './section_identifiers.vue'; import SectionIdentifiers from './section_identifiers.vue';
import SectionName from './section_name.vue'; import SectionName from './section_name.vue';
...@@ -10,11 +15,14 @@ export default { ...@@ -10,11 +15,14 @@ export default {
name: 'NewVulnerabilityForm', name: 'NewVulnerabilityForm',
components: { components: {
GlForm, GlForm,
GlButton,
GlAlert,
SectionDetails, SectionDetails,
SectionIdentifiers, SectionIdentifiers,
SectionName, SectionName,
SectionSolution, SectionSolution,
}, },
inject: ['projectId'],
data() { data() {
return { return {
form: { form: {
...@@ -25,18 +33,165 @@ export default { ...@@ -25,18 +33,165 @@ export default {
detectionMethod: '', detectionMethod: '',
identifiers: [], identifiers: [],
}, },
validation: {
severity: null,
status: null,
name: null,
identifiers: [],
},
submitting: false,
errors: [],
}; };
}, },
computed: {
shouldShowAlert() {
return this.errors.length > 0;
},
},
watch: {
shouldShowAlert(newValue) {
if (newValue) {
this.scrollTop();
}
},
},
methods: { methods: {
scrollTop() {
this.$nextTick(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
},
async submitForm() {
this.errors = this.validateFormValues();
if (this.errors.length > 0) {
if (this.shouldShowAlert) {
this.scrollTop();
}
return;
}
this.submitting = true;
try {
const { data } = await this.$apollo.mutate({
mutation: createVulnerabilityMutation,
variables: {
input: {
project: convertToGraphQLId(TYPE_PROJECT, this.projectId),
name: this.form.vulnerabilityName,
description: this.form.vulnerabilityDesc,
severity: this.form.severity.toUpperCase(),
state: this.form.status.toUpperCase(),
identifiers: this.form.identifiers,
solution: this.form.solution,
// The scanner needs to be hardcoded because of two reasons:
// 1. It's a required field in the backend.
// 2. We expect that the manually created vulnerabilities are the ones that the scanners cannot catch.
// So most likely the scanner will be left out even if we present an option to choose which one.
scanner: {
id: 'gitlab-manual-vulnerability-report',
name: 'manually-created-vulnerability',
url: 'https://gitlab.com',
version: '1.0',
vendor: {
name: 'GitLab',
},
},
},
},
});
if (data.vulnerabilityCreate.vulnerability?.vulnerabilityPath) {
redirectTo(data.vulnerabilityCreate.vulnerability.vulnerabilityPath);
return;
}
if (data.vulnerabilityCreate.errors) {
this.errors = data.vulnerabilityCreate.errors;
} else {
throw new Error(this.$options.i18n.submitError);
}
} catch (error) {
this.errors = [this.$options.i18n.submitError];
Sentry.captureException({ error, component: this.$options.name });
}
this.submitting = false;
},
validateFormValues() {
const errors = [];
const {
vulnerabilityName,
vulnerabilityState,
vulnerabilitySeverity,
vulnerabilityIdentifiers,
} = this.$options.i18n.errors;
this.validation = {};
if (!this.form.vulnerabilityName) {
this.validation.name = false;
errors.push(vulnerabilityName);
}
if (!this.form.status) {
this.validation.status = false;
errors.push(vulnerabilityState);
}
if (!this.form.severity) {
this.validation.severity = false;
errors.push(vulnerabilitySeverity);
}
if (!this.form.identifiers?.length) {
this.validation.identifiers = [{ identifierCode: false, identifierUrl: false }];
errors.push(vulnerabilityIdentifiers);
} else {
this.validation.identifiers = [];
this.validation.identifiers = this.form.identifiers.map((item) => ({
identifierCode: Boolean(item.name),
identifierUrl: Boolean(item.url),
}));
if (this.validation.identifiers.find((i) => !i.identifierUrl || !i.identifierCode)) {
errors.push(vulnerabilityIdentifiers);
}
}
return errors;
},
updateFormValues(values) { updateFormValues(values) {
this.form = { ...this.form, ...values }; this.form = { ...this.form, ...values };
// If there are previous errors, revalidate the form.
if (this.errors.length) {
this.validateFormValues();
}
},
dismissAlert() {
this.errors = [];
}, },
}, },
i18n: { i18n: {
title: s__('VulnerabilityManagement|Add vulnerability finding'), title: s__('VulnerabilityManagement|Add vulnerability finding'),
submitVulnerability: s__('VulnerabilityManagement|Submit vulnerability'),
submitError: s__('VulnerabilityManagement|Something went wrong while creating vulnerability'),
description: s__( description: s__(
'VulnerabilityManagement|Manually add a vulnerability entry into the vulnerability report.', 'VulnerabilityManagement|Manually add a vulnerability entry into the vulnerability report.',
), ),
errors: {
vulnerabilityName: s__('VulnerabilityManagement|Name is a required field'),
vulnerabilitySeverity: s__('VulnerabilityManagement|Severity is a required field'),
vulnerabilityState: s__('VulnerabilityManagement|Status is a required field'),
vulnerabilityIdentifiers: s__('VulnerabilityManagement|At least one identifier is required'),
},
}, },
}; };
</script> </script>
...@@ -51,11 +206,28 @@ export default { ...@@ -51,11 +206,28 @@ export default {
{{ $options.i18n.description }} {{ $options.i18n.description }}
</p> </p>
</header> </header>
<gl-form class="gl-p-4 gl-w-85p" @submit.prevent> <gl-form @submit.prevent="submitForm">
<section-name @change="updateFormValues" /> <gl-alert v-if="shouldShowAlert" variant="danger" dismissible @dismiss="dismissAlert">
<section-details @change="updateFormValues" /> <ul v-if="errors.length > 1" class="gl-mb-0 gl-pl-5">
<section-identifiers @change="updateFormValues" /> <li v-for="error in errors" :key="error">{{ error }}</li>
<section-solution @change="updateFormValues" /> </ul>
<span v-else>{{ errors[0] }}</span>
</gl-alert>
<div class="gl-p-4 gl-w-85p">
<section-name :validation-state="validation" @change="updateFormValues" />
<section-details :validation-state="validation" @change="updateFormValues" />
<section-identifiers :validation-state="validation" @change="updateFormValues" />
<section-solution @change="updateFormValues" />
</div>
<div class="gl-mt-5 gl-pt-5 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1">
<gl-button
type="submit"
variant="confirm"
class="js-no-auto-disable"
:disabled="submitting"
>{{ $options.i18n.submitVulnerability }}</gl-button
>
</div>
</gl-form> </gl-form>
</div> </div>
</template> </template>
...@@ -11,6 +11,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba ...@@ -11,6 +11,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants'; import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import { SEVERITY_LEVELS, DETECTION_METHODS } from 'ee/security_dashboard/store/constants'; import { SEVERITY_LEVELS, DETECTION_METHODS } from 'ee/security_dashboard/store/constants';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import * as i18n from './i18n';
export default { export default {
components: { components: {
...@@ -22,6 +23,13 @@ export default { ...@@ -22,6 +23,13 @@ export default {
GlFormRadioGroup, GlFormRadioGroup,
SeverityBadge, SeverityBadge,
}, },
props: {
validationState: {
type: Object,
required: false,
default: () => ({}),
},
},
data() { data() {
return { return {
// Note: The cvss field is disabled during the MVC because the backend implementation // Note: The cvss field is disabled during the MVC because the backend implementation
...@@ -33,6 +41,8 @@ export default { ...@@ -33,6 +41,8 @@ export default {
statusId: '', statusId: '',
severity: '', severity: '',
detectionMethod: -1, detectionMethod: -1,
severityState: null,
statusState: null,
}; };
}, },
computed: { computed: {
...@@ -98,6 +108,7 @@ export default { ...@@ -98,6 +108,7 @@ export default {
critical: [9.0, 10.0], critical: [9.0, 10.0],
}, },
i18n: { i18n: {
requiredField: i18n.REQUIRED_FIELD,
title: s__('Vulnerability|Details'), title: s__('Vulnerability|Details'),
description: s__( description: s__(
'Vulnerability|Information related how the vulnerability was discovered and its impact to the system.', 'Vulnerability|Information related how the vulnerability was discovered and its impact to the system.',
...@@ -152,6 +163,8 @@ export default { ...@@ -152,6 +163,8 @@ export default {
<div class="gl-display-flex gl-mb-6"> <div class="gl-display-flex gl-mb-6">
<gl-form-group <gl-form-group
:label="$options.i18n.severity.label" :label="$options.i18n.severity.label"
:state="validationState.severity"
:invalid-feedback="$options.i18n.requiredField"
label-for="form-severity" label-for="form-severity"
class="gl-mr-6 gl-mb-0" class="gl-mr-6 gl-mb-0"
> >
...@@ -175,7 +188,11 @@ export default { ...@@ -175,7 +188,11 @@ export default {
<gl-form-input id="form-cvss" v-model="cvss" class="gl-mb-2" type="text" /> <gl-form-input id="form-cvss" v-model="cvss" class="gl-mb-2" type="text" />
</gl-form-group> </gl-form-group>
</div> </div>
<gl-form-group :label="$options.i18n.status.label"> <gl-form-group
:label="$options.i18n.status.label"
:state="validationState.status"
:invalid-feedback="$options.i18n.requiredField"
>
<p>{{ $options.i18n.status.description }}</p> <p>{{ $options.i18n.status.description }}</p>
<gl-form-radio-group :checked="statusId" @change="emitChanges"> <gl-form-radio-group :checked="statusId" @change="emitChanges">
<label <label
......
<script> <script>
import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import * as i18n from './i18n';
export default { export default {
components: { components: {
...@@ -8,25 +9,52 @@ export default { ...@@ -8,25 +9,52 @@ export default {
GlFormInput, GlFormInput,
GlButton, GlButton,
}, },
id: 0, props: {
validationState: {
type: Object,
required: false,
default: () => ({}),
},
},
data() { data() {
return { return {
identifiers: [{ identifierCode: '', identifierUrl: '', id: this.$options.id }], identifiers: [{ identifierCode: '', identifierUrl: '' }],
}; };
}, },
methods: { methods: {
emitChanges() { emitChanges() {
this.$emit('change', { identifiers: this.identifiers }); this.$emit('change', {
identifiers: this.identifiers.map((i) => ({
name: i.identifierCode,
url: i.identifierUrl,
})),
});
}, },
addIdentifier() { addIdentifier() {
this.$options.id += 1; this.identifiers.push({ identifierCode: '', identifierUrl: '' });
this.identifiers.push({ identifierCode: '', identifierUrl: '', id: this.$options.id }); },
removeIdentifier(index) {
this.identifiers.splice(index, 1);
this.emitChanges();
}, },
removeIdentifier(id) { // null is when the user didn't input anything yet
this.identifiers = this.identifiers.filter((i) => i.id !== id); // false when the user provided invalid input and
// true when the validation passes
validationStateIdentifierUrl(index) {
return this.validationState.identifiers?.[index]?.identifierUrl ?? null;
},
validationStateIdentifierCode(index) {
return this.validationState.identifiers?.[index]?.identifierCode ?? null;
},
rowHasError(index) {
return (
this.validationStateIdentifierUrl(index) === false ||
this.validationStateIdentifierCode(index) === false
);
}, },
}, },
i18n: { i18n: {
requiredField: i18n.REQUIRED_FIELD,
title: s__('Vulnerability|Identifiers'), title: s__('Vulnerability|Identifiers'),
description: s__( description: s__(
'Vulnerability|Enter the associated CVE or CWE entries for this vulnerability.', 'Vulnerability|Enter the associated CVE or CWE entries for this vulnerability.',
...@@ -50,41 +78,48 @@ export default { ...@@ -50,41 +78,48 @@ export default {
</p> </p>
</header> </header>
<div <div
v-for="identifier in identifiers" v-for="(identifier, index) in identifiers"
:key="identifier.id" :key="index"
data-testid="identifier-row" data-testid="identifier-row"
class="gl-display-flex gl-mb-6" class="gl-display-flex gl-mb-6"
> >
<gl-form-group <gl-form-group
:label="$options.i18n.identifierCode" :label="$options.i18n.identifierCode"
:label-for="`form-identifier-code-${identifier.id}`" :label-for="`form-identifier-code-${index}`"
:state="validationStateIdentifierCode(index)"
:invalid-feedback="$options.i18n.requiredField"
class="gl-mr-6 gl-mb-0" class="gl-mr-6 gl-mb-0"
> >
<gl-form-input <gl-form-input
:id="`form-identifier-code-${identifier.id}`" :id="`form-identifier-code-${index}`"
v-model="identifier.identifierCode" v-model.trim="identifier.identifierCode"
:state="validationStateIdentifierCode(index)"
type="text" type="text"
@change="emitChanges" @change="emitChanges"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
:label="$options.i18n.identifierUrl" :label="$options.i18n.identifierUrl"
:label-for="`form-identifier-url-${identifier.id}`" :state="validationStateIdentifierUrl(index)"
:invalid-feedback="$options.i18n.requiredField"
:label-for="`form-identifier-url-${index}`"
class="gl-flex-grow-1 gl-mb-0" class="gl-flex-grow-1 gl-mb-0"
> >
<gl-form-input <gl-form-input
:id="`form-identifier-url-${identifier.id}`" :id="`form-identifier-url-${index}`"
v-model="identifier.identifierUrl" v-model.trim="identifier.identifierUrl"
:state="validationStateIdentifierUrl(index)"
type="text" type="text"
@change="emitChanges" @change="emitChanges"
/> />
</gl-form-group> </gl-form-group>
<gl-button <gl-button
v-if="identifier.id > 0" v-if="index > 0"
class="gl-align-self-end gl-ml-4 gl-shadow-none!" class="gl-ml-4 gl-shadow-none!"
:class="rowHasError(index) ? 'gl-align-self-center' : 'gl-align-self-end'"
icon="remove" icon="remove"
:aria-label="$options.i18n.removeIdentifierRow" :aria-label="$options.i18n.removeIdentifierRow"
@click="removeIdentifier(identifier.id)" @click="removeIdentifier(index)"
/> />
<!-- <!--
The first row does not contain a remove button and this creates The first row does not contain a remove button and this creates
...@@ -98,12 +133,8 @@ export default { ...@@ -98,12 +133,8 @@ export default {
icon="remove" icon="remove"
/> />
</div> </div>
<gl-button <gl-button category="secondary" variant="confirm" @click="addIdentifier">{{
data-testid="add-identifier-row" $options.i18n.addIdentifier
category="secondary" }}</gl-button>
variant="confirm"
@click="addIdentifier"
>{{ $options.i18n.addIdentifier }}</gl-button
>
</section> </section>
</template> </template>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import * as i18n from './i18n';
export default { export default {
components: { components: {
...@@ -11,6 +12,13 @@ export default { ...@@ -11,6 +12,13 @@ export default {
MarkdownField, MarkdownField,
}, },
inject: ['markdownDocsPath', 'markdownPreviewPath'], inject: ['markdownDocsPath', 'markdownPreviewPath'],
props: {
validationState: {
type: Object,
required: false,
default: () => ({}),
},
},
data() { data() {
return { return {
isSubmitting: false, isSubmitting: false,
...@@ -27,6 +35,7 @@ export default { ...@@ -27,6 +35,7 @@ export default {
}, },
}, },
i18n: { i18n: {
requiredField: i18n.REQUIRED_FIELD,
vulnerabilityName: { vulnerabilityName: {
label: __('Name'), label: __('Name'),
description: s__( description: s__(
...@@ -47,12 +56,15 @@ export default { ...@@ -47,12 +56,15 @@ export default {
<gl-form-group <gl-form-group
:label="$options.i18n.vulnerabilityName.label" :label="$options.i18n.vulnerabilityName.label"
:description="$options.i18n.vulnerabilityName.description" :description="$options.i18n.vulnerabilityName.description"
:state="validationState.name"
:invalid-feedback="$options.i18n.requiredField"
label-for="form-vulnerability-name" label-for="form-vulnerability-name"
class="gl-mb-6" class="gl-mb-6"
> >
<gl-form-input <gl-form-input
id="form-vulnerability-name" id="form-vulnerability-name"
v-model="vulnerabilityName" v-model.trim="vulnerabilityName"
:state="validationState.name"
type="text" type="text"
@change="emitChanges" @change="emitChanges"
/> />
...@@ -76,7 +88,7 @@ export default { ...@@ -76,7 +88,7 @@ export default {
<template #textarea> <template #textarea>
<gl-form-textarea <gl-form-textarea
id="form-vulnerability-desc" id="form-vulnerability-desc"
v-model="vulnerabilityDesc" v-model.trim="vulnerabilityDesc"
rows="8" rows="8"
class="gl-shadow-none! gl-px-0! gl-py-4! gl-h-auto!" class="gl-shadow-none! gl-px-0! gl-py-4! gl-h-auto!"
:aria-label="$options.i18n.vulnerabilityDesc.description" :aria-label="$options.i18n.vulnerabilityDesc.description"
......
...@@ -13,6 +13,7 @@ export default (el) => { ...@@ -13,6 +13,7 @@ export default (el) => {
provide: { provide: {
markdownDocsPath: el.dataset.markdownDocsPath, markdownDocsPath: el.dataset.markdownDocsPath,
markdownPreviewPath: el.dataset.markdownPreviewPath, markdownPreviewPath: el.dataset.markdownPreviewPath,
projectId: el.dataset.projectId,
}, },
render: (h) => h(App), render: (h) => h(App),
}); });
......
...@@ -4,4 +4,6 @@ ...@@ -4,4 +4,6 @@
- page_title _("Add vulnerability finding") - page_title _("Add vulnerability finding")
- add_page_specific_style 'page_bundles/security_dashboard' - add_page_specific_style 'page_bundles/security_dashboard'
#js-vulnerability-new{ data: { markdown_docs_path: help_page_path('user/markdown'), markdown_preview_path: preview_markdown_path(@project) } } #js-vulnerability-new{ data: { markdown_docs_path: help_page_path('user/markdown'),
markdown_preview_path: preview_markdown_path(@project),
project_id: @project.id } }
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlDropdown, GlDropdownItem, GlFormRadio } from '@gitlab/ui'; import { GlFormGroup, GlDropdown, GlDropdownItem, GlFormRadio } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import SectionDetails from 'ee/vulnerabilities/components/new_vulnerability/section_details.vue'; import SectionDetails from 'ee/vulnerabilities/components/new_vulnerability/section_details.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
...@@ -7,6 +7,8 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba ...@@ -7,6 +7,8 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba
describe('New vulnerability - Section Details', () => { describe('New vulnerability - Section Details', () => {
let wrapper; let wrapper;
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
const findDetectionMethodItem = (at) => const findDetectionMethodItem = (at) =>
wrapper.findAllComponents(GlDropdown).at(0).findAllComponents(GlDropdownItem).at(at); wrapper.findAllComponents(GlDropdown).at(0).findAllComponents(GlDropdownItem).at(at);
...@@ -78,4 +80,28 @@ describe('New vulnerability - Section Details', () => { ...@@ -78,4 +80,28 @@ describe('New vulnerability - Section Details', () => {
status: value, status: value,
}); });
}); });
it('does not display invalid state by default', () => {
expect(findFormGroup(1).attributes('aria-invalid')).toBeUndefined();
expect(findFormGroup(0).attributes('aria-invalid')).toBeUndefined();
});
it('handles form validation', async () => {
wrapper.setProps({
validationState: {
severity: false,
status: false,
},
});
await nextTick();
// severity input
expect(wrapper.findAllByRole('alert').at(0).text()).toBe('This field is required.');
expect(findFormGroup(1).attributes('aria-invalid')).toBe('true');
// status input
expect(wrapper.findAllByRole('alert').at(1).text()).toBe('This field is required.');
expect(findFormGroup(2).attributes('aria-invalid')).toBe('true');
});
}); });
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlFormGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import SectionIdentifiers from 'ee/vulnerabilities/components/new_vulnerability/section_identifiers.vue'; import SectionIdentifiers from 'ee/vulnerabilities/components/new_vulnerability/section_identifiers.vue';
...@@ -6,7 +7,11 @@ describe('New vulnerability - Section Identifiers', () => { ...@@ -6,7 +7,11 @@ describe('New vulnerability - Section Identifiers', () => {
let wrapper; let wrapper;
const createWrapper = () => { const createWrapper = () => {
return mountExtended(SectionIdentifiers); return mountExtended(SectionIdentifiers, {
propsData: {
validationState: { identifiers: [{ identifierCode: false }] },
},
});
}; };
beforeEach(() => { beforeEach(() => {
...@@ -18,28 +23,56 @@ describe('New vulnerability - Section Identifiers', () => { ...@@ -18,28 +23,56 @@ describe('New vulnerability - Section Identifiers', () => {
}); });
const findIdentifierRows = () => wrapper.findAllByTestId('identifier-row'); const findIdentifierRows = () => wrapper.findAllByTestId('identifier-row');
const findFormGroup = (index) => wrapper.findAllComponents(GlFormGroup).at(index);
const findIdentifierCodeInput = () => wrapper.findByLabelText('Identifier code');
const findIdentifierUrlInput = () => wrapper.findByLabelText('Identifier URL');
it('does not display a warning when the validation state is emtpy', async () => {
wrapper.setProps({
validationState: {
identifiers: [],
},
});
await nextTick();
expect(findFormGroup(1).attributes('aria-invalid')).toBeUndefined();
expect(findFormGroup(0).attributes('aria-invalid')).toBeUndefined();
});
describe.each` it('displays a warning when the validation fails', async () => {
labelText wrapper.setProps({
${'Identifier code'} validationState: {
${'Identifier URL'} identifiers: [{ identifierCode: false, identifierUrl: false }],
`('for input $labelText', ({ labelText }) => { },
it(`displays the input with the correct label: "${labelText}"`, () => {
expect(wrapper.findByLabelText(labelText).exists()).toBe(true);
}); });
it('emits change even when input changes', () => { await nextTick();
wrapper.findByLabelText(labelText).trigger('change');
expect(wrapper.emitted('change')[0][0]).toEqual({ expect(findFormGroup(0).attributes('aria-invalid')).toBe('true');
identifiers: [{ identifierCode: '', identifierUrl: '', id: 0 }], expect(wrapper.findAllByRole('alert').at(0).text()).toBe('This field is required.');
});
expect(findFormGroup(1).attributes('aria-invalid')).toBe('true');
expect(wrapper.findAllByRole('alert').at(1).text()).toBe('This field is required.');
});
it('emits change event when input changes', () => {
const codeInput = findIdentifierCodeInput();
const urlInput = findIdentifierUrlInput();
codeInput.setValue('cve-23');
urlInput.setValue('https://gitlab.com');
codeInput.trigger('change');
expect(wrapper.emitted('change')[0][0]).toEqual({
identifiers: [{ name: 'cve-23', url: 'https://gitlab.com' }],
}); });
}); });
it('adds and removes identifier rows', async () => { it('adds and removes identifier rows', async () => {
expect(findIdentifierRows()).toHaveLength(1); expect(findIdentifierRows()).toHaveLength(1);
wrapper.findByTestId('add-identifier-row').trigger('click'); wrapper.findByRole('button', { name: 'Add another identifier' }).trigger('click');
await nextTick(); await nextTick();
expect(findIdentifierRows()).toHaveLength(2); expect(findIdentifierRows()).toHaveLength(2);
......
import { GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { nextTick } from 'vue';
import { GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import SectionName from 'ee/vulnerabilities/components/new_vulnerability/section_name.vue'; import SectionName from 'ee/vulnerabilities/components/new_vulnerability/section_name.vue';
...@@ -9,6 +10,8 @@ describe('New vulnerability - Section Name', () => { ...@@ -9,6 +10,8 @@ describe('New vulnerability - Section Name', () => {
let wrapper; let wrapper;
const findFormGroup = (index) => wrapper.findAllComponents(GlFormGroup).at(index);
const createWrapper = () => { const createWrapper = () => {
return mountExtended(SectionName, { return mountExtended(SectionName, {
provide: { provide: {
...@@ -50,14 +53,33 @@ describe('New vulnerability - Section Name', () => { ...@@ -50,14 +53,33 @@ describe('New vulnerability - Section Name', () => {
}); });
it.each` it.each`
field | component | value field | component | fieldKey | fieldValue
${'Name'} | ${GlFormInput} | ${{ vulnerabilityName: 'CVE 2021', vulnerabilityDesc: '' }} ${'Name'} | ${GlFormInput} | ${'vulnerabilityName'} | ${'CVE 2021'}
${'Description'} | ${GlFormTextarea} | ${{ vulnerabilityName: '', vulnerabilityDesc: 'Password leak' }} ${'Description'} | ${GlFormTextarea} | ${'vulnerabilityDesc'} | ${'Password leak'}
`('emits the changes: $field ', async ({ component, value }) => { `('emits the changes: $field ', async ({ component, fieldKey, fieldValue }) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details wrapper.findComponent(component).setValue(fieldValue);
// eslint-disable-next-line no-restricted-syntax wrapper.findComponent(component).vm.$emit('change', fieldValue);
wrapper.setData(value); expect(wrapper.emitted('change')[0][0]).toEqual({
wrapper.findComponent(component).vm.$emit('change', value); vulnerabilityName: '',
expect(wrapper.emitted('change')[0][0]).toEqual(value); vulnerabilityDesc: '',
[fieldKey]: fieldValue,
});
});
it('does not display invalid state by default', () => {
expect(findFormGroup(0).attributes('aria-invalid')).toBeUndefined();
});
it('handles form validation', async () => {
wrapper.setProps({
validationState: {
name: false,
},
});
await nextTick();
expect(wrapper.findByRole('alert').text()).toBe('This field is required.');
expect(findFormGroup(0).attributes('aria-invalid')).toBe('true');
}); });
}); });
...@@ -40167,6 +40167,9 @@ msgstr "" ...@@ -40167,6 +40167,9 @@ msgstr ""
msgid "VulnerabilityManagement|An unverified non-confirmed finding" msgid "VulnerabilityManagement|An unverified non-confirmed finding"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|At least one identifier is required"
msgstr ""
msgid "VulnerabilityManagement|Change status" msgid "VulnerabilityManagement|Change status"
msgstr "" msgstr ""
...@@ -40182,6 +40185,9 @@ msgstr "" ...@@ -40182,6 +40185,9 @@ msgstr ""
msgid "VulnerabilityManagement|Manually add a vulnerability entry into the vulnerability report." msgid "VulnerabilityManagement|Manually add a vulnerability entry into the vulnerability report."
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Name is a required field"
msgstr ""
msgid "VulnerabilityManagement|Needs triage" msgid "VulnerabilityManagement|Needs triage"
msgstr "" msgstr ""
...@@ -40197,6 +40203,12 @@ msgstr "" ...@@ -40197,6 +40203,12 @@ msgstr ""
msgid "VulnerabilityManagement|Select a method" msgid "VulnerabilityManagement|Select a method"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Severity is a required field"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while creating vulnerability"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later." msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
msgstr "" msgstr ""
...@@ -40221,6 +40233,12 @@ msgstr "" ...@@ -40221,6 +40233,12 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state." msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Status is a required field"
msgstr ""
msgid "VulnerabilityManagement|Submit vulnerability"
msgstr ""
msgid "VulnerabilityManagement|Summary, detailed description, steps to reproduce, etc." msgid "VulnerabilityManagement|Summary, detailed description, steps to reproduce, etc."
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