Commit bbcd0057 authored by Robert Hunt's avatar Robert Hunt

Created the update form for compliance frameworks

This form contains all the processes which are necessary to update
a compliance framework. This form makes use of the shared form to show
the necessary fields

This form gets the compliance framework via GraphQL using the ID. On
success it populates the fields, on failure it shows an error alert.

This form saves the compliance framework via GraphQL. On success it
redirects to the general settings page. On failure, it shows an error
alert
parent 95e42fe5
<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