Commit 6c4006d7 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '287827-update-compliance-framework-form' into 'master'

Create the update form for compliance frameworks

See merge request gitlab-org/gitlab!52479
parents 1d73ba55 bbcd0057
<script>
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import SharedForm from './shared_form.vue';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql';
export default {
components: {
SharedForm,
},
props: {
graphqlFieldName: {
type: String,
required: true,
},
groupEditPath: {
type: String,
required: true,
},
groupPath: {
type: String,
required: true,
},
id: {
type: String,
required: false,
default: null,
},
},
data() {
return {
complianceFramework: {},
errorMessage: '',
};
},
apollo: {
complianceFramework: {
query: getComplianceFrameworkQuery,
variables() {
return {
fullPath: this.groupPath,
complianceFramework: convertToGraphQLId(this.graphqlFieldName, this.id),
};
},
update(data) {
const complianceFrameworks = data.namespace?.complianceFrameworks?.nodes || [];
if (!complianceFrameworks.length) {
this.setError(new Error(this.$options.i18n.fetchError), this.$options.i18n.fetchError);
return {};
}
const { id, name, description, color } = complianceFrameworks[0];
return {
id,
name,
description,
color,
};
},
error(error) {
this.setError(error, this.$options.i18n.fetchError);
},
},
},
computed: {
isLoading() {
return this.$apollo.loading;
},
isFormReady() {
return Object.keys(this.complianceFramework).length > 0 && !this.isLoading;
},
},
methods: {
setError(error, userFriendlyText) {
this.errorMessage = userFriendlyText;
Sentry.captureException(error);
},
async onSubmit(formData) {
try {
const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation,
variables: {
input: {
id: this.complianceFramework.id,
params: {
name: formData.name,
description: formData.description,
color: formData.color,
},
},
},
});
const [error] = data?.updateComplianceFramework?.errors || [];
if (error) {
this.setError(new Error(error), error);
} else {
visitUrl(this.groupEditPath);
}
} catch (e) {
this.setError(e, this.$options.i18n.saveError);
}
},
},
i18n: {
fetchError: s__(
'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page',
),
saveError: s__(
'ComplianceFrameworks|Unable to save this compliance framework. Please try again',
),
},
};
</script>
<template>
<shared-form
:group-edit-path="groupEditPath"
:loading="isLoading"
:render-form="isFormReady"
:error="errorMessage"
:compliance-framework="complianceFramework"
@submit="onSubmit"
/>
</template>
query getComplianceFramework($fullPath: ID!) {
query getComplianceFramework(
$fullPath: ID!
$complianceFramework: ComplianceManagementFrameworkID
) {
namespace(fullPath: $fullPath) {
id
name
complianceFrameworks {
complianceFrameworks(id: $complianceFramework) {
nodes {
id
name
......
mutation updateComplianceFramework($input: UpdateComplianceFrameworkInput!) {
updateComplianceFramework(input: $input) {
clientMutationId
errors
}
}
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import CreateForm from './components/create_form.vue';
import EditForm from './components/edit_form.vue';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
......@@ -15,14 +16,19 @@ const createComplianceFrameworksFormApp = (el) => {
return false;
}
const { groupEditPath, groupPath } = el.dataset;
const { groupEditPath, groupPath, graphqlFieldName = null, frameworkId: id = null } = el.dataset;
return new Vue({
el,
apolloProvider,
render(createElement) {
const element = CreateForm;
const props = { groupEditPath, groupPath };
let element = CreateForm;
let props = { groupEditPath, groupPath };
if (id) {
element = EditForm;
props = { ...props, graphqlFieldName, id };
}
return createElement(element, {
props,
......
import VueApollo from 'vue-apollo';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_frameworks/graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/queries/update_compliance_framework.mutation.graphql';
import EditForm from 'ee/groups/settings/compliance_frameworks/components/edit_form.vue';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import {
validFetchOneResponse,
emptyFetchResponse,
frameworkFoundResponse,
validUpdateResponse,
errorUpdateResponse,
} from '../mock_data';
import * as Sentry from '~/sentry/wrapper';
const localVue = createLocalVue();
localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility');
describe('Form', () => {
let wrapper;
const sentryError = new Error('Network error');
const sentrySaveError = new Error('Invalid values given');
const propsData = {
graphqlFieldName: 'field',
groupPath: 'group-1',
groupEditPath: 'group-1/edit',
id: '1',
scopedLabelsHelpPath: 'help/scoped-labels',
};
const fetchOne = jest.fn().mockResolvedValue(validFetchOneResponse);
const fetchEmpty = jest.fn().mockResolvedValue(emptyFetchResponse);
const fetchLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
const fetchWithErrors = jest.fn().mockRejectedValue(sentryError);
const update = jest.fn().mockResolvedValue(validUpdateResponse);
const updateWithNetworkErrors = jest.fn().mockRejectedValue(sentryError);
const updateWithErrors = jest.fn().mockResolvedValue(errorUpdateResponse);
const findForm = () => wrapper.findComponent(SharedForm);
function createMockApolloProvider(requestHandlers) {
localVue.use(VueApollo);
return createMockApollo(requestHandlers);
}
function createComponent(requestHandlers = []) {
return shallowMount(EditForm, {
localVue,
apolloProvider: createMockApolloProvider(requestHandlers),
propsData,
});
}
afterEach(() => {
wrapper.destroy();
});
describe('loading', () => {
beforeEach(() => {
wrapper = createComponent([[getComplianceFrameworkQuery, fetchLoading]]);
});
it('passes the loading state to the form', () => {
expect(findForm().props('loading')).toBe(true);
expect(findForm().props('renderForm')).toBe(false);
});
});
describe('on load', () => {
it('queries for existing framework data and passes to the form', async () => {
wrapper = createComponent([[getComplianceFrameworkQuery, fetchOne]]);
await waitForPromises();
expect(fetchOne).toHaveBeenCalledTimes(1);
expect(findForm().props('complianceFramework')).toMatchObject(frameworkFoundResponse);
expect(findForm().props('renderForm')).toBe(true);
});
it('passes the error to the form if the existing framework query returns no data', async () => {
jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchEmpty]]);
await waitForPromises();
expect(fetchEmpty).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false);
expect(findForm().props('error')).toBe(
'Error fetching compliance frameworks data. Please refresh the page',
);
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(
new Error('Error fetching compliance frameworks data. Please refresh the page'),
);
});
it('passes the error to the form if the existing framework query fails', async () => {
jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([[getComplianceFrameworkQuery, fetchWithErrors]]);
await waitForPromises();
expect(fetchWithErrors).toHaveBeenCalledTimes(1);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(false);
expect(findForm().props('error')).toBe(
'Error fetching compliance frameworks data. Please refresh the page',
);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(sentryError);
});
});
describe('onSubmit', () => {
const name = 'Test';
const description = 'Test description';
const color = '#000000';
const updateProps = {
input: {
id: 'gid://gitlab/ComplianceManagement::Framework/1',
params: {
name,
description,
color,
},
},
};
it('passes the error to the form when saving causes an exception and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithNetworkErrors],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(updateWithNetworkErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe(
'Unable to save this compliance framework. Please try again',
);
expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(sentryError);
});
it('passes the errors to the form when saving fails and does not redirect', async () => {
jest.spyOn(Sentry, 'captureException');
wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, updateWithErrors],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(updateWithErrors).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).not.toHaveBeenCalled();
expect(findForm().props('error')).toBe('Invalid values given');
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(sentrySaveError);
});
it('saves inputted values and redirects', async () => {
wrapper = createComponent([
[getComplianceFrameworkQuery, fetchOne],
[updateComplianceFrameworkMutation, update],
]);
await waitForPromises();
findForm().vm.$emit('submit', { name, description, color });
await waitForPromises();
expect(update).toHaveBeenCalledWith(updateProps);
expect(findForm().props('loading')).toBe(false);
expect(findForm().props('renderForm')).toBe(true);
expect(visitUrl).toHaveBeenCalledWith(propsData.groupEditPath);
});
});
});
import { createWrapper } from '@vue/test-utils';
import { createComplianceFrameworksFormApp } from 'ee/groups/settings/compliance_frameworks/init_form';
import CreateForm from 'ee/groups/settings/compliance_frameworks/components/create_form.vue';
import EditForm from 'ee/groups/settings/compliance_frameworks/components/edit_form.vue';
import { suggestedLabelColors } from './mock_data';
describe('createComplianceFrameworksFormApp', () => {
let wrapper;
let el;
const groupEditPath = 'group-1/edit';
const groupPath = 'group-1';
const graphqlFieldName = 'field';
const testId = '1';
const findFormApp = (form) => wrapper.find(form);
const setUpDocument = (id = null) => {
el = document.createElement('div');
el.setAttribute('data-group-edit-path', groupEditPath);
el.setAttribute('data-group-path', groupPath);
if (id) {
el.setAttribute('data-graphql-field-name', graphqlFieldName);
el.setAttribute('data-framework-id', id);
}
document.body.appendChild(el);
wrapper = createWrapper(createComplianceFrameworksFormApp(el));
};
beforeEach(() => {
gon.suggested_label_colors = suggestedLabelColors;
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
el.remove();
el = null;
});
describe('CreateForm', () => {
beforeEach(() => {
setUpDocument();
});
it('parses and passes props', () => {
expect(findFormApp(CreateForm).props()).toMatchObject({
groupEditPath,
groupPath,
});
});
});
describe('EditForm', () => {
beforeEach(() => {
setUpDocument(testId);
});
it('parses and passes props', () => {
expect(findFormApp(EditForm).props()).toMatchObject({
groupEditPath,
groupPath,
id: testId,
});
});
});
});
export const suggestedLabelColors = {
'#000000': 'Black',
'#0033CC': 'UA blue',
'#428BCA': 'Moderate blue',
'#44AD8E': 'Lime green',
};
export const validFetchResponse = {
data: {
namespace: {
......@@ -46,7 +53,28 @@ export const frameworkFoundResponse = {
name: 'GDPR',
description: 'General Data Protection Regulation',
color: '#1aaa55',
parsedId: 1,
};
export const validFetchOneResponse = {
data: {
namespace: {
id: 'gid://gitlab/Group/1',
name: 'Group 1',
complianceFrameworks: {
nodes: [
{
id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR',
description: 'General Data Protection Regulation',
color: '#1aaa55',
__typename: 'ComplianceFramework',
},
],
__typename: 'ComplianceFrameworkConnection',
},
__typename: 'Namespace',
},
},
};
export const validCreateResponse = {
......@@ -74,3 +102,23 @@ export const errorCreateResponse = {
},
},
};
export const validUpdateResponse = {
data: {
updateComplianceFramework: {
clientMutationId: null,
errors: [],
__typename: 'UpdateComplianceFrameworkPayload',
},
},
};
export const errorUpdateResponse = {
data: {
updateComplianceFramework: {
clientMutationId: null,
errors: ['Invalid values given'],
__typename: 'UpdateComplianceFrameworkPayload',
},
},
};
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