Commit 6b3ab4fa authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '292696-add-compliance-pipeline-configuration-location-field-to-forms' into 'master'

Add 'compliance pipeline configuration location' to compliance frameworks form

See merge request gitlab-org/gitlab!53887
parents 8b3468a2 78e72982
...@@ -21,6 +21,11 @@ export default { ...@@ -21,6 +21,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pipelineConfigurationFullPathEnabled: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -44,7 +49,7 @@ export default { ...@@ -44,7 +49,7 @@ export default {
this.errorMessage = ''; this.errorMessage = '';
try { try {
const { name, description, color } = this.formData; const { name, description, pipelineConfigurationFullPath, color } = this.formData;
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: createComplianceFrameworkMutation, mutation: createComplianceFrameworkMutation,
variables: { variables: {
...@@ -53,6 +58,7 @@ export default { ...@@ -53,6 +58,7 @@ export default {
params: { params: {
name, name,
description, description,
pipelineConfigurationFullPath,
color, color,
}, },
}, },
...@@ -80,8 +86,10 @@ export default { ...@@ -80,8 +86,10 @@ export default {
<form-status :loading="isLoading" :error="errorMessage"> <form-status :loading="isLoading" :error="errorMessage">
<shared-form <shared-form
:group-edit-path="groupEditPath" :group-edit-path="groupEditPath"
:pipeline-configuration-full-path-enabled="pipelineConfigurationFullPathEnabled"
:name.sync="formData.name" :name.sync="formData.name"
:description.sync="formData.description" :description.sync="formData.description"
:pipeline-configuration-full-path.sync="formData.pipelineConfigurationFullPath"
:color.sync="formData.color" :color.sync="formData.color"
@submit="onSubmit" @submit="onSubmit"
/> />
......
...@@ -33,6 +33,11 @@ export default { ...@@ -33,6 +33,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
pipelineConfigurationFullPathEnabled: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -85,11 +90,12 @@ export default { ...@@ -85,11 +90,12 @@ export default {
return initialiseFormData(); return initialiseFormData();
} }
const { name, description, color } = complianceFrameworks[0]; const { name, description, pipelineConfigurationFullPath, color } = complianceFrameworks[0];
return { return {
name, name,
description, description,
pipelineConfigurationFullPath,
color, color,
}; };
}, },
...@@ -106,7 +112,7 @@ export default { ...@@ -106,7 +112,7 @@ export default {
this.saveErrorMessage = ''; this.saveErrorMessage = '';
try { try {
const { name, description, color } = this.formData; const { name, description, pipelineConfigurationFullPath, color } = this.formData;
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation, mutation: updateComplianceFrameworkMutation,
variables: { variables: {
...@@ -115,6 +121,7 @@ export default { ...@@ -115,6 +121,7 @@ export default {
params: { params: {
name, name,
description, description,
pipelineConfigurationFullPath,
color, color,
}, },
}, },
...@@ -143,8 +150,10 @@ export default { ...@@ -143,8 +150,10 @@ export default {
<shared-form <shared-form
v-if="showForm" v-if="showForm"
:group-edit-path="groupEditPath" :group-edit-path="groupEditPath"
:pipeline-configuration-full-path-enabled="pipelineConfigurationFullPathEnabled"
:name.sync="formData.name" :name.sync="formData.name"
:description.sync="formData.description" :description.sync="formData.description"
:pipeline-configuration-full-path.sync="formData.pipelineConfigurationFullPath"
:color.sync="formData.color" :color.sync="formData.color"
@submit="onSubmit" @submit="onSubmit"
/> />
......
...@@ -5,6 +5,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; ...@@ -5,6 +5,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { validateHexColor } from '~/lib/utils/color_utils'; import { validateHexColor } from '~/lib/utils/color_utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
import { fetchPipelineConfigurationFileExists, validatePipelineConfirmationFormat } from '../utils';
export default { export default {
components: { components: {
...@@ -36,6 +37,21 @@ export default { ...@@ -36,6 +37,21 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
pipelineConfigurationFullPathEnabled: {
type: Boolean,
required: false,
default: false,
},
pipelineConfigurationFullPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
pipelineConfigurationFileExists: true,
};
}, },
computed: { computed: {
isValidColor() { isValidColor() {
...@@ -55,18 +71,50 @@ export default { ...@@ -55,18 +71,50 @@ export default {
return Boolean(this.description); return Boolean(this.description);
}, },
isValidPipelineConfiguration() {
if (!this.pipelineConfigurationFullPath) {
return null;
}
return this.isValidPipelineConfigurationFormat && this.pipelineConfigurationFileExists;
},
isValidPipelineConfigurationFormat() {
return validatePipelineConfirmationFormat(this.pipelineConfigurationFullPath);
},
disableSubmitBtn() { disableSubmitBtn() {
return !this.isValidName || !this.isValidDescription || !this.isValidColor; return (
!this.isValidName ||
!this.isValidDescription ||
!this.isValidColor ||
this.isValidPipelineConfiguration === false
);
},
pipelineConfigurationFeedbackMessage() {
if (!this.isValidPipelineConfigurationFormat) {
return this.$options.i18n.pipelineConfigurationInputInvalidFormat;
}
return this.$options.i18n.pipelineConfigurationInputUnknownFile;
}, },
scopedLabelsHelpPath() { scopedLabelsHelpPath() {
return helpPagePath('user/project/labels.md', { anchor: 'scoped-labels' }); return helpPagePath('user/project/labels.md', { anchor: 'scoped-labels' });
}, },
}, },
async created() {
if (this.pipelineConfigurationFullPath) {
this.pipelineConfigurationFileExists = await fetchPipelineConfigurationFileExists(
this.pipelineConfigurationFullPath,
);
}
},
methods: { methods: {
onSubmit() { onSubmit() {
const { name, description, color } = this; this.$emit('submit');
},
async updatePipelineConfiguration(path) {
this.pipelineConfigurationFileExists = await fetchPipelineConfigurationFileExists(path);
this.$emit('submit', { name, description, color }); this.$emit('update:pipelineConfigurationFullPath', path);
}, },
}, },
i18n: { i18n: {
...@@ -77,6 +125,21 @@ export default { ...@@ -77,6 +125,21 @@ export default {
titleInputInvalid: __('A title is required'), titleInputInvalid: __('A title is required'),
descriptionInputLabel: __('Description'), descriptionInputLabel: __('Description'),
descriptionInputInvalid: __('A description is required'), descriptionInputInvalid: __('A description is required'),
pipelineConfigurationInputLabel: s__(
'ComplianceFrameworks|Compliance pipeline configuration location (optional)',
),
pipelineConfigurationInputSubLabel: s__(
'ComplianceFrameworks|Combines with the CI configuration at runtime.',
),
pipelineConfigurationInputDescription: s__(
'ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name',
),
pipelineConfigurationInputInvalidFormat: s__(
'ComplianceFrameworks|Invalid format: it should follow the format [PATH].y(a)ml@[GROUP]/[PROJECT]',
),
pipelineConfigurationInputUnknownFile: s__(
'ComplianceFrameworks|Could not find this configuration location, please try a different location',
),
colorInputLabel: __('Background color'), colorInputLabel: __('Background color'),
submitBtnText: __('Save changes'), submitBtnText: __('Save changes'),
cancelBtnText: __('Cancel'), cancelBtnText: __('Cancel'),
...@@ -125,6 +188,25 @@ export default { ...@@ -125,6 +188,25 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="pipelineConfigurationFullPathEnabled"
:label="$options.i18n.pipelineConfigurationInputLabel"
:description="$options.i18n.pipelineConfigurationInputDescription"
:invalid-feedback="pipelineConfigurationFeedbackMessage"
:state="isValidPipelineConfiguration"
data-testid="pipeline-configuration-input-group"
>
<p class="col-form-label gl-font-weight-normal!">
{{ $options.i18n.pipelineConfigurationInputSubLabel }}
</p>
<gl-form-input
:value="pipelineConfigurationFullPath"
:state="isValidPipelineConfiguration"
data-testid="pipeline-configuration-input"
@input="updatePipelineConfiguration"
/>
</gl-form-group>
<color-picker <color-picker
:value="color" :value="color"
:label="$options.i18n.colorInputLabel" :label="$options.i18n.colorInputLabel"
......
...@@ -7,3 +7,6 @@ export const FETCH_ERROR = s__( ...@@ -7,3 +7,6 @@ export const FETCH_ERROR = s__(
export const SAVE_ERROR = s__( export const SAVE_ERROR = s__(
'ComplianceFrameworks|Unable to save this compliance framework. Please try again', 'ComplianceFrameworks|Unable to save this compliance framework. Please try again',
); );
// Check that it matches the format [FILE].y(a)ml@[GROUP]/[PROJECT]
export const PIPELINE_CONFIGURATION_PATH_FORMAT = /^([^@]*\.ya?ml)@([^/]*)\/(.*)$/;
...@@ -11,6 +11,7 @@ query getComplianceFramework( ...@@ -11,6 +11,7 @@ query getComplianceFramework(
name name
description description
color color
pipelineConfigurationFullPath
} }
} }
} }
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CreateForm from './components/create_form.vue'; import CreateForm from './components/create_form.vue';
import EditForm from './components/edit_form.vue'; import EditForm from './components/edit_form.vue';
...@@ -16,14 +17,24 @@ const createComplianceFrameworksFormApp = (el) => { ...@@ -16,14 +17,24 @@ const createComplianceFrameworksFormApp = (el) => {
return false; return false;
} }
const { groupEditPath, groupPath, graphqlFieldName = null, frameworkId: id = null } = el.dataset; const {
groupEditPath,
groupPath,
pipelineConfigurationFullPathEnabled,
graphqlFieldName = null,
frameworkId: id = null,
} = el.dataset;
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
render(createElement) { render(createElement) {
let element = CreateForm; let element = CreateForm;
let props = { groupEditPath, groupPath }; let props = {
groupEditPath,
groupPath,
pipelineConfigurationFullPathEnabled: parseBoolean(pipelineConfigurationFullPathEnabled),
};
if (id) { if (id) {
element = EditForm; element = EditForm;
......
import httpStatus from '~/lib/utils/http_status';
import Api from '~/api';
import { PIPELINE_CONFIGURATION_PATH_FORMAT } from './constants';
export const initialiseFormData = () => ({ export const initialiseFormData = () => ({
name: null, name: null,
description: null, description: null,
pipelineConfigurationFullPath: null, pipelineConfigurationFullPath: null,
color: null, color: null,
}); });
export const getPipelineConfigurationPathParts = (path) => {
const [, file, group, project] = path.match(PIPELINE_CONFIGURATION_PATH_FORMAT) || [];
return { file, group, project };
};
export const validatePipelineConfirmationFormat = (path) =>
PIPELINE_CONFIGURATION_PATH_FORMAT.test(path);
export const fetchPipelineConfigurationFileExists = async (path) => {
const { file, group, project } = getPipelineConfigurationPathParts(path);
if (!file || !group || !project) {
return false;
}
try {
const { status } = await Api.getRawFile(`${group}/${project}`, file);
return status === httpStatus.OK;
} catch (e) {
return false;
}
};
...@@ -24,6 +24,7 @@ describe('CreateForm', () => { ...@@ -24,6 +24,7 @@ describe('CreateForm', () => {
const propsData = { const propsData = {
groupPath: 'group-1', groupPath: 'group-1',
groupEditPath: 'group-1/edit', groupEditPath: 'group-1/edit',
pipelineConfigurationFullPathEnabled: true,
}; };
const sentryError = new Error('Network error'); const sentryError = new Error('Network error');
...@@ -50,11 +51,12 @@ describe('CreateForm', () => { ...@@ -50,11 +51,12 @@ describe('CreateForm', () => {
}); });
} }
async function submitForm(name, description, color) { async function submitForm(name, description, pipelineConfiguration, color) {
await waitForPromises(); await waitForPromises();
findForm().vm.$emit('update:name', name); findForm().vm.$emit('update:name', name);
findForm().vm.$emit('update:description', description); findForm().vm.$emit('update:description', description);
findForm().vm.$emit('update:pipelineConfigurationFullPath', pipelineConfiguration);
findForm().vm.$emit('update:color', color); findForm().vm.$emit('update:color', color);
findForm().vm.$emit('submit'); findForm().vm.$emit('submit');
...@@ -78,6 +80,7 @@ describe('CreateForm', () => { ...@@ -78,6 +80,7 @@ describe('CreateForm', () => {
describe('onSubmit', () => { describe('onSubmit', () => {
const name = 'Test'; const name = 'Test';
const description = 'Test description'; const description = 'Test description';
const pipelineConfigurationFullPath = 'file.yml@group/project';
const color = '#000000'; const color = '#000000';
const creationProps = { const creationProps = {
input: { input: {
...@@ -85,6 +88,7 @@ describe('CreateForm', () => { ...@@ -85,6 +88,7 @@ describe('CreateForm', () => {
params: { params: {
name, name,
description, description,
pipelineConfigurationFullPath,
color, color,
}, },
}, },
...@@ -94,7 +98,7 @@ describe('CreateForm', () => { ...@@ -94,7 +98,7 @@ describe('CreateForm', () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithNetworkErrors]]); wrapper = createComponent([[createComplianceFrameworkMutation, createWithNetworkErrors]]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(createWithNetworkErrors).toHaveBeenCalledWith(creationProps); expect(createWithNetworkErrors).toHaveBeenCalledWith(creationProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
...@@ -107,7 +111,7 @@ describe('CreateForm', () => { ...@@ -107,7 +111,7 @@ describe('CreateForm', () => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[createComplianceFrameworkMutation, createWithErrors]]); wrapper = createComponent([[createComplianceFrameworkMutation, createWithErrors]]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(createWithErrors).toHaveBeenCalledWith(creationProps); expect(createWithErrors).toHaveBeenCalledWith(creationProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
...@@ -119,7 +123,7 @@ describe('CreateForm', () => { ...@@ -119,7 +123,7 @@ describe('CreateForm', () => {
it('saves inputted values and redirects', async () => { it('saves inputted values and redirects', async () => {
wrapper = createComponent([[createComplianceFrameworkMutation, create]]); wrapper = createComponent([[createComplianceFrameworkMutation, create]]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(create).toHaveBeenCalledWith(creationProps); expect(create).toHaveBeenCalledWith(creationProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
......
...@@ -32,6 +32,7 @@ describe('EditForm', () => { ...@@ -32,6 +32,7 @@ describe('EditForm', () => {
groupEditPath: 'group-1/edit', groupEditPath: 'group-1/edit',
groupPath: 'group-1', groupPath: 'group-1',
id: '1', id: '1',
pipelineConfigurationFullPathEnabled: true,
}; };
const sentryError = new Error('Network error'); const sentryError = new Error('Network error');
...@@ -63,11 +64,12 @@ describe('EditForm', () => { ...@@ -63,11 +64,12 @@ describe('EditForm', () => {
}); });
} }
async function submitForm(name, description, color) { async function submitForm(name, description, pipelineConfiguration, color) {
await waitForPromises(); await waitForPromises();
findForm().vm.$emit('update:name', name); findForm().vm.$emit('update:name', name);
findForm().vm.$emit('update:description', description); findForm().vm.$emit('update:description', description);
findForm().vm.$emit('update:pipelineConfigurationFullPath', pipelineConfiguration);
findForm().vm.$emit('update:color', color); findForm().vm.$emit('update:color', color);
findForm().vm.$emit('submit'); findForm().vm.$emit('submit');
...@@ -96,10 +98,12 @@ describe('EditForm', () => { ...@@ -96,10 +98,12 @@ describe('EditForm', () => {
expect(fetchOne).toHaveBeenCalledTimes(1); expect(fetchOne).toHaveBeenCalledTimes(1);
expect(findForm().props()).toStrictEqual({ expect(findForm().props()).toStrictEqual({
name: frameworkFoundResponse.name,
description: frameworkFoundResponse.description,
color: frameworkFoundResponse.color, color: frameworkFoundResponse.color,
description: frameworkFoundResponse.description,
groupEditPath: propsData.groupEditPath, groupEditPath: propsData.groupEditPath,
name: frameworkFoundResponse.name,
pipelineConfigurationFullPath: frameworkFoundResponse.pipelineConfigurationFullPath,
pipelineConfigurationFullPathEnabled: true,
}); });
expect(findForm().exists()).toBe(true); expect(findForm().exists()).toBe(true);
}); });
...@@ -133,6 +137,7 @@ describe('EditForm', () => { ...@@ -133,6 +137,7 @@ describe('EditForm', () => {
describe('onSubmit', () => { describe('onSubmit', () => {
const name = 'Test'; const name = 'Test';
const description = 'Test description'; const description = 'Test description';
const pipelineConfigurationFullPath = 'file.yml@group/project';
const color = '#000000'; const color = '#000000';
const updateProps = { const updateProps = {
input: { input: {
...@@ -140,6 +145,7 @@ describe('EditForm', () => { ...@@ -140,6 +145,7 @@ describe('EditForm', () => {
params: { params: {
name, name,
description, description,
pipelineConfigurationFullPath,
color, color,
}, },
}, },
...@@ -152,7 +158,7 @@ describe('EditForm', () => { ...@@ -152,7 +158,7 @@ describe('EditForm', () => {
[updateComplianceFrameworkMutation, updateWithNetworkErrors], [updateComplianceFrameworkMutation, updateWithNetworkErrors],
]); ]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps); expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
...@@ -168,7 +174,7 @@ describe('EditForm', () => { ...@@ -168,7 +174,7 @@ describe('EditForm', () => {
[updateComplianceFrameworkMutation, updateWithErrors], [updateComplianceFrameworkMutation, updateWithErrors],
]); ]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(updateWithErrors).toHaveBeenCalledWith(updateProps); expect(updateWithErrors).toHaveBeenCalledWith(updateProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
...@@ -183,7 +189,7 @@ describe('EditForm', () => { ...@@ -183,7 +189,7 @@ describe('EditForm', () => {
[updateComplianceFrameworkMutation, update], [updateComplianceFrameworkMutation, update],
]); ]);
await submitForm(name, description, color); await submitForm(name, description, pipelineConfigurationFullPath, color);
expect(update).toHaveBeenCalledWith(updateProps); expect(update).toHaveBeenCalledWith(updateProps);
expect(findFormStatus().props('loading')).toBe(false); expect(findFormStatus().props('loading')).toBe(false);
......
...@@ -3,11 +3,12 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; ...@@ -3,11 +3,12 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import List from 'ee/groups/settings/compliance_frameworks/components/list.vue'; import List from 'ee/groups/settings/compliance_frameworks/components/list.vue';
import EmptyState from 'ee/groups/settings/compliance_frameworks/components/list_empty_state.vue';
import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue'; import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue';
import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_frameworks/graphql/queries/get_compliance_framework.query.graphql'; import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_frameworks/graphql/queries/get_compliance_framework.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { PIPELINE_CONFIGURATION_PATH_FORMAT } from 'ee/groups/settings/compliance_frameworks/constants';
import EmptyState from 'ee/groups/settings/compliance_frameworks/components/list_empty_state.vue';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { validFetchResponse, emptyFetchResponse } from '../mock_data'; import { validFetchResponse, emptyFetchResponse } from '../mock_data';
...@@ -160,6 +161,9 @@ describe('List', () => { ...@@ -160,6 +161,9 @@ describe('List', () => {
parsedId: expect.any(Number), parsedId: expect.any(Number),
name: expect.any(String), name: expect.any(String),
description: expect.any(String), description: expect.any(String),
pipelineConfigurationFullPath: expect.stringMatching(
PIPELINE_CONFIGURATION_PATH_FORMAT,
),
color: expect.stringMatching(/^#([0-9A-F]{3}){1,2}$/i), color: expect.stringMatching(/^#([0-9A-F]{3}){1,2}$/i),
}, },
}), }),
......
import { GlForm, GlSprintf } from '@gitlab/ui'; import { GlForm, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import waitForPromises from 'helpers/wait_for_promises';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
import * as Utils from 'ee/groups/settings/compliance_frameworks/utils';
import { GlFormGroup, GlFormInput } from '../stubs';
import { frameworkFoundResponse, suggestedLabelColors } from '../mock_data'; import { frameworkFoundResponse, suggestedLabelColors } from '../mock_data';
describe('SharedForm', () => { describe('SharedForm', () => {
let wrapper; let wrapper;
const defaultPropsData = { groupEditPath: 'group-1' }; const defaultPropsData = { groupEditPath: 'group-1', pipelineConfigurationFullPathEnabled: true };
const findForm = () => wrapper.findComponent(GlForm); const findForm = () => wrapper.findComponent(GlForm);
const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]'); const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]');
const findNameInput = () => wrapper.find('[data-testid="name-input"]'); const findNameInput = () => wrapper.find('[data-testid="name-input"]');
const findDescriptionGroup = () => wrapper.find('[data-testid="description-input-group"]'); const findDescriptionGroup = () => wrapper.find('[data-testid="description-input-group"]');
const findDescriptionInput = () => wrapper.find('[data-testid="description-input"]'); const findDescriptionInput = () => wrapper.find('[data-testid="description-input"]');
const findPipelineConfigurationGroup = () =>
wrapper.find('[data-testid="pipeline-configuration-input-group"]');
const findPipelineConfigurationInput = () =>
wrapper.find('[data-testid="pipeline-configuration-input"]');
const findColorPicker = () => wrapper.findComponent(ColorPicker); const findColorPicker = () => wrapper.findComponent(ColorPicker);
const findSubmitBtn = () => wrapper.find('[data-testid="submit-btn"]'); const findSubmitBtn = () => wrapper.find('[data-testid="submit-btn"]');
const findCancelBtn = () => wrapper.find('[data-testid="cancel-btn"]'); const findCancelBtn = () => wrapper.find('[data-testid="cancel-btn"]');
...@@ -28,15 +34,7 @@ describe('SharedForm', () => { ...@@ -28,15 +34,7 @@ describe('SharedForm', () => {
}, },
stubs: { stubs: {
GlFormGroup, GlFormGroup,
GlFormInput: { GlFormInput,
name: 'gl-form-input-stub',
props: ['state'],
template: `
<div>
<slot></slot>
</div>
`,
},
GlSprintf, GlSprintf,
}, },
}); });
...@@ -47,9 +45,7 @@ describe('SharedForm', () => { ...@@ -47,9 +45,7 @@ describe('SharedForm', () => {
}); });
afterEach(() => { afterEach(() => {
if (wrapper) { wrapper.destroy();
wrapper.destroy();
}
}); });
describe('Fields', () => { describe('Fields', () => {
...@@ -58,6 +54,7 @@ describe('SharedForm', () => { ...@@ -58,6 +54,7 @@ describe('SharedForm', () => {
expect(findNameInput()).toExist(); expect(findNameInput()).toExist();
expect(findDescriptionInput()).toExist(); expect(findDescriptionInput()).toExist();
expect(findPipelineConfigurationInput()).toExist();
expect(findColorPicker()).toExist(); expect(findColorPicker()).toExist();
expect(findSubmitBtn()).toExist(); expect(findSubmitBtn()).toExist();
expect(findCancelBtn()).toExist(); expect(findCancelBtn()).toExist();
...@@ -68,6 +65,15 @@ describe('SharedForm', () => { ...@@ -68,6 +65,15 @@ describe('SharedForm', () => {
expect(findNameGroup().text()).toContain('Use :: to create a scoped set (eg. SOX::AWS)'); expect(findNameGroup().text()).toContain('Use :: to create a scoped set (eg. SOX::AWS)');
}); });
it.each([true, false])(
'renders the pipeline configuration correctly when enabled is %s',
(enabled) => {
wrapper = createComponent({ pipelineConfigurationFullPathEnabled: enabled });
expect(findPipelineConfigurationGroup().exists()).toBe(enabled);
},
);
}); });
describe('Validation', () => { describe('Validation', () => {
...@@ -95,6 +101,47 @@ describe('SharedForm', () => { ...@@ -95,6 +101,47 @@ describe('SharedForm', () => {
expect(findDescriptionInput().props('state')).toBe(validity); expect(findDescriptionInput().props('state')).toBe(validity);
}); });
it.each`
pipelineConfigurationFullPath | message
${'foobar'} | ${'Invalid format: it should follow the format [PATH].y(a)ml@[GROUP]/[PROJECT]'}
${'foo.yml@bar/baz'} | ${'Could not find this configuration location, please try a different location'}
`(
'sets the correct invalid message to the group',
async ({ pipelineConfigurationFullPath, message }) => {
jest.spyOn(Utils, 'fetchPipelineConfigurationFileExists').mockReturnValue(false);
wrapper = createComponent({ pipelineConfigurationFullPath });
await waitForPromises();
expect(findPipelineConfigurationGroup().attributes('invalid-feedback')).toBe(message);
},
);
it.each`
pipelineConfigurationFullPath | validity
${null} | ${null}
${''} | ${null}
${'foobar'} | ${false}
${'foo.yml@bar/zab'} | ${false}
${'foo.yaml@bar/baz'} | ${true}
${'foo.yml@bar/baz'} | ${true}
`(
'sets the correct state for the input and group when pipeline configuration is $pipelineConfigurationFullPath',
async ({ pipelineConfigurationFullPath, validity }) => {
jest
.spyOn(Utils, 'fetchPipelineConfigurationFileExists')
.mockReturnValue(Boolean(validity));
wrapper = createComponent({ pipelineConfigurationFullPath });
await waitForPromises();
expect(findPipelineConfigurationGroup().props('state')).toBe(validity);
expect(findPipelineConfigurationInput().props('state')).toBe(validity);
},
);
it.each` it.each`
color | validity color | validity
${null} | ${null} ${null} | ${null}
...@@ -110,17 +157,27 @@ describe('SharedForm', () => { ...@@ -110,17 +157,27 @@ describe('SharedForm', () => {
}); });
it.each` it.each`
name | description | color | disabled name | description | color | pipelineConfigurationFullPath | disabled
${null} | ${null} | ${null} | ${'true'} ${null} | ${null} | ${null} | ${null} | ${'true'}
${''} | ${null} | ${null} | ${'true'} ${'Foo'} | ${null} | ${null} | ${null} | ${'true'}
${null} | ${''} | ${null} | ${'true'} ${null} | ${'Bar'} | ${null} | ${null} | ${'true'}
${null} | ${null} | ${''} | ${'true'} ${null} | ${null} | ${'#000'} | ${null} | ${'true'}
${'Foo'} | ${null} | ${''} | ${'true'} ${null} | ${null} | ${null} | ${'foo.yml@bar/zab'} | ${'true'}
${'Foo'} | ${'Bar'} | ${'#000'} | ${undefined} ${'Foo'} | ${''} | ${''} | ${''} | ${'true'}
${'Foo'} | ${'Bar'} | ${'#000'} | ${''} | ${undefined}
${'Foo'} | ${'Bar'} | ${'#000'} | ${'foo.yml@bar/baz'} | ${undefined}
`( `(
'should set the submit buttons disabled attribute to $disabled', 'should set the submit buttons disabled attribute to $disabled when name: $name, description: $description, color: $color, pipelineConfigurationFullPath: $pipelineConfigurationFullPath',
({ name, description, color, disabled }) => { async ({ name, description, color, pipelineConfigurationFullPath, disabled }) => {
wrapper = createComponent({ name, description, color }); if (pipelineConfigurationFullPath?.includes('zab')) {
jest.spyOn(Utils, 'fetchPipelineConfigurationFileExists').mockReturnValue(false);
} else {
jest.spyOn(Utils, 'fetchPipelineConfigurationFileExists').mockReturnValue(true);
}
wrapper = createComponent({ name, description, color, pipelineConfigurationFullPath });
await waitForPromises();
expect(findSubmitBtn().attributes('disabled')).toBe(disabled); expect(findSubmitBtn().attributes('disabled')).toBe(disabled);
}, },
...@@ -129,30 +186,33 @@ describe('SharedForm', () => { ...@@ -129,30 +186,33 @@ describe('SharedForm', () => {
describe('Updating data', () => { describe('Updating data', () => {
it('updates the initial form data when the props are updated', async () => { it('updates the initial form data when the props are updated', async () => {
const { name, description, color } = frameworkFoundResponse; const { name, description, pipelineConfigurationFullPath, color } = frameworkFoundResponse;
wrapper = createComponent(); wrapper = createComponent();
expect(findNameInput().attributes('value')).toBe(undefined); expect(findNameInput().props('value')).toBe(null);
expect(findDescriptionInput().attributes('value')).toBe(undefined); expect(findDescriptionInput().props('value')).toBe(null);
expect(findColorPicker().attributes('value')).toBe(undefined); expect(findPipelineConfigurationInput().props('value')).toBe(null);
expect(findColorPicker().props('value')).toBe(null);
await wrapper.setProps({ name, description, color }); await wrapper.setProps({ name, description, pipelineConfigurationFullPath, color });
expect(findNameInput().attributes('value')).toBe(name); expect(findNameInput().props('value')).toBe(name);
expect(findDescriptionInput().attributes('value')).toBe(description); expect(findDescriptionInput().props('value')).toBe(description);
expect(findColorPicker().attributes('value')).toBe(color); expect(findPipelineConfigurationInput().props('value')).toBe(pipelineConfigurationFullPath);
expect(findColorPicker().props('value')).toBe(color);
}); });
}); });
describe('On form submission', () => { describe('On form submission', () => {
it('emits a submit event', async () => { it('emits a submit event', async () => {
const { name, description, color } = frameworkFoundResponse; jest.spyOn(Utils, 'fetchPipelineConfigurationFileExists').mockReturnValue(true);
wrapper = createComponent({ name, description, color });
const { name, description, pipelineConfigurationFullPath, color } = frameworkFoundResponse;
wrapper = createComponent({ name, description, pipelineConfigurationFullPath, color });
await findForm().vm.$emit('submit', { preventDefault: () => {} }); await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1); expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([{ name, description, color }]);
}); });
}); });
}); });
...@@ -11,6 +11,7 @@ describe('createComplianceFrameworksFormApp', () => { ...@@ -11,6 +11,7 @@ describe('createComplianceFrameworksFormApp', () => {
const groupEditPath = 'group-1/edit'; const groupEditPath = 'group-1/edit';
const groupPath = 'group-1'; const groupPath = 'group-1';
const pipelineConfigurationFullPathEnabled = true;
const graphqlFieldName = 'field'; const graphqlFieldName = 'field';
const testId = '1'; const testId = '1';
...@@ -20,6 +21,7 @@ describe('createComplianceFrameworksFormApp', () => { ...@@ -20,6 +21,7 @@ describe('createComplianceFrameworksFormApp', () => {
el = document.createElement('div'); el = document.createElement('div');
el.setAttribute('data-group-edit-path', groupEditPath); el.setAttribute('data-group-edit-path', groupEditPath);
el.setAttribute('data-group-path', groupPath); el.setAttribute('data-group-path', groupPath);
el.setAttribute('data-pipeline-configuration-full-path-enabled', 'true');
if (id) { if (id) {
el.setAttribute('data-graphql-field-name', graphqlFieldName); el.setAttribute('data-graphql-field-name', graphqlFieldName);
...@@ -51,6 +53,7 @@ describe('createComplianceFrameworksFormApp', () => { ...@@ -51,6 +53,7 @@ describe('createComplianceFrameworksFormApp', () => {
expect(findFormApp(CreateForm).props()).toStrictEqual({ expect(findFormApp(CreateForm).props()).toStrictEqual({
groupEditPath, groupEditPath,
groupPath, groupPath,
pipelineConfigurationFullPathEnabled,
}); });
}); });
}); });
...@@ -66,6 +69,7 @@ describe('createComplianceFrameworksFormApp', () => { ...@@ -66,6 +69,7 @@ describe('createComplianceFrameworksFormApp', () => {
groupEditPath, groupEditPath,
groupPath, groupPath,
id: testId, id: testId,
pipelineConfigurationFullPathEnabled,
}); });
}); });
}); });
......
...@@ -16,6 +16,7 @@ export const validFetchResponse = { ...@@ -16,6 +16,7 @@ export const validFetchResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/1', id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR', name: 'GDPR',
description: 'General Data Protection Regulation', description: 'General Data Protection Regulation',
pipelineConfigurationFullPath: 'file.yml@group/project',
color: '#1aaa55', color: '#1aaa55',
__typename: 'ComplianceFramework', __typename: 'ComplianceFramework',
}, },
...@@ -23,6 +24,7 @@ export const validFetchResponse = { ...@@ -23,6 +24,7 @@ export const validFetchResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/2', id: 'gid://gitlab/ComplianceManagement::Framework/2',
name: 'PCI-DSS', name: 'PCI-DSS',
description: 'Payment Card Industry-Data Security Standard', description: 'Payment Card Industry-Data Security Standard',
pipelineConfigurationFullPath: 'file.yml@group/project',
color: '#6666c4', color: '#6666c4',
__typename: 'ComplianceFramework', __typename: 'ComplianceFramework',
}, },
...@@ -52,6 +54,7 @@ export const frameworkFoundResponse = { ...@@ -52,6 +54,7 @@ export const frameworkFoundResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/1', id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR', name: 'GDPR',
description: 'General Data Protection Regulation', description: 'General Data Protection Regulation',
pipelineConfigurationFullPath: 'file.yml@group/project',
color: '#1aaa55', color: '#1aaa55',
}; };
...@@ -66,6 +69,7 @@ export const validFetchOneResponse = { ...@@ -66,6 +69,7 @@ export const validFetchOneResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/1', id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR', name: 'GDPR',
description: 'General Data Protection Regulation', description: 'General Data Protection Regulation',
pipelineConfigurationFullPath: 'file.yml@group/project',
color: '#1aaa55', color: '#1aaa55',
__typename: 'ComplianceFramework', __typename: 'ComplianceFramework',
}, },
...@@ -84,6 +88,7 @@ export const validCreateResponse = { ...@@ -84,6 +88,7 @@ export const validCreateResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/1', id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR', name: 'GDPR',
description: 'General Data Protection Regulation', description: 'General Data Protection Regulation',
pipelineConfigurationFullPath: 'file.yml@group/project',
color: '#1aaa55', color: '#1aaa55',
__typename: 'ComplianceFramework', __typename: 'ComplianceFramework',
}, },
......
export const GlFormGroup = {
name: 'gl-form-group-stub',
props: ['state'],
template: `
<div>
<slot name="label"></slot>
<slot></slot>
<slot name="description"></slot>
</div>`,
};
export const GlFormInput = {
name: 'gl-form-input-stub',
props: ['state', 'disabled', 'value'],
template: `
<div>
<slot></slot>
</div>`,
};
import MockAdapter from 'axios-mock-adapter';
import httpStatus from '~/lib/utils/http_status';
import * as Utils from 'ee/groups/settings/compliance_frameworks/utils'; import * as Utils from 'ee/groups/settings/compliance_frameworks/utils';
import axios from '~/lib/utils/axios_utils';
const GET_RAW_FILE_ENDPOINT = /\/api\/(.*)\/projects\/bar%2Fbaz\/repository\/files\/foo\.ya?ml\/raw/;
describe('Utils', () => { describe('Utils', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('initialiseFormData', () => { describe('initialiseFormData', () => {
it('returns the initial form data object', () => { it('returns the initial form data object', () => {
expect(Utils.initialiseFormData()).toStrictEqual({ expect(Utils.initialiseFormData()).toStrictEqual({
...@@ -11,4 +26,59 @@ describe('Utils', () => { ...@@ -11,4 +26,59 @@ describe('Utils', () => {
}); });
}); });
}); });
describe('getPipelineConfigurationPathParts', () => {
it.each`
path | parts
${''} | ${{ file: undefined, group: undefined, project: undefined }}
${'abc'} | ${{ file: undefined, group: undefined, project: undefined }}
${'foo@bar/baz'} | ${{ file: undefined, group: undefined, project: undefined }}
${'foo.pdf@bar/baz'} | ${{ file: undefined, group: undefined, project: undefined }}
${'foo.yml@bar/baz'} | ${{ file: 'foo.yml', group: 'bar', project: 'baz' }}
`('should return the correct object when $path is given', ({ path, parts }) => {
expect(Utils.getPipelineConfigurationPathParts(path)).toStrictEqual(parts);
});
});
describe('validatePipelineConfirmationFormat', () => {
it.each`
path | valid
${null} | ${false}
${''} | ${false}
${'abc'} | ${false}
${'foo@bar/baz'} | ${false}
${'foo.pdf@bar/baz'} | ${false}
${'foo.yaml@bar/baz'} | ${true}
${'foo.yml@bar/baz'} | ${true}
`('should validate to $valid when path is $path', ({ path, valid }) => {
expect(Utils.validatePipelineConfirmationFormat(path)).toBe(valid);
});
});
describe('fetchPipelineConfigurationFileExists', () => {
it.each`
path | returns
${''} | ${false}
${'abc'} | ${false}
${'foo@bar/baz'} | ${false}
${'foo.pdf@bar/baz'} | ${false}
${'foo.yaml@bar/baz'} | ${true}
${'foo.yml@bar/baz'} | ${true}
`('should return $returns when the path is $path', async ({ path, returns }) => {
mock.onGet(GET_RAW_FILE_ENDPOINT).reply(returns ? httpStatus.OK : httpStatus.NOT_FOUND, {});
expect(await Utils.fetchPipelineConfigurationFileExists(path)).toBe(returns);
});
it.each`
response | returns
${httpStatus.OK} | ${true}
${httpStatus.NO_CONTENT} | ${false}
${httpStatus.NOT_FOUND} | ${false}
`('should return $returns when the response is $response', async ({ response, returns }) => {
mock.onGet(GET_RAW_FILE_ENDPOINT).reply(response, {});
expect(await Utils.fetchPipelineConfigurationFileExists('foo.yml@bar/baz')).toBe(returns);
});
});
}); });
...@@ -7525,9 +7525,21 @@ msgstr "" ...@@ -7525,9 +7525,21 @@ msgstr ""
msgid "ComplianceFrameworks|All" msgid "ComplianceFrameworks|All"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|Combines with the CI configuration at runtime."
msgstr ""
msgid "ComplianceFrameworks|Compliance pipeline configuration location (optional)"
msgstr ""
msgid "ComplianceFrameworks|Could not find this configuration location, please try a different location"
msgstr ""
msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page" msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|Invalid format: it should follow the format [PATH].y(a)ml@[GROUP]/[PROJECT]"
msgstr ""
msgid "ComplianceFrameworks|Once you have created a compliance framework it will appear here." msgid "ComplianceFrameworks|Once you have created a compliance framework it will appear here."
msgstr "" msgstr ""
...@@ -7543,6 +7555,9 @@ msgstr "" ...@@ -7543,6 +7555,9 @@ msgstr ""
msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})" msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name"
msgstr ""
msgid "ComplianceFramework|GDPR" msgid "ComplianceFramework|GDPR"
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