Commit b405e234 authored by David Pisek's avatar David Pisek Committed by Savas Vedova

Add error handling to security training config

This commit adds error handling to the security-training
providers configuration page.
parent 28981f57
<script>
import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import { GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql';
const i18n = {
providerQueryErrorMessage: __(
'Could not fetch training providers. Please refresh the page, or try again later.',
),
configMutationErrorMessage: __(
'Could not save configuration. Please refresh the page, or try again later.',
),
};
export default {
components: {
GlAlert,
GlCard,
GlToggle,
GlLink,
......@@ -14,10 +25,14 @@ export default {
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
error() {
this.errorMessage = this.$options.i18n.providerQueryErrorMessage;
},
},
},
data() {
return {
errorMessage: '',
toggleLoading: false,
securityTrainingProviders: [],
};
......@@ -34,17 +49,21 @@ export default {
...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
}));
this.storeEnabledProviders(toggledProviders);
},
storeEnabledProviders(toggledProviders) {
const enabledProviderIds = toggledProviders
.filter(({ isEnabled }) => isEnabled)
.map(({ id }) => id);
this.storeEnabledProviders(toggledProviders, enabledProviderIds);
},
async storeEnabledProviders(toggledProviders, enabledProviderIds) {
this.toggleLoading = true;
return this.$apollo
.mutate({
try {
const {
data: {
configureSecurityTrainingProviders: { errors = [] },
},
} = await this.$apollo.mutate({
mutation: configureSecurityTrainingProvidersMutation,
variables: {
input: {
......@@ -52,16 +71,28 @@ export default {
fullPath: this.projectPath,
},
},
})
.then(() => {
this.toggleLoading = false;
});
if (errors.length > 0) {
// throwing an error here means we can handle scenarios within the `catch` block below
throw new Error();
}
} catch {
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
} finally {
this.toggleLoading = false;
}
},
},
i18n,
};
</script>
<template>
<div>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-6">
{{ errorMessage }}
</gl-alert>
<div
v-if="isLoading"
class="gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
......@@ -98,4 +129,5 @@ export default {
</gl-card>
</li>
</ul>
</div>
</template>
mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
configureSecurityTrainingProviders(input: $input) @client {
errors
securityTrainingProviders {
id
isEnabled
......
......@@ -9853,6 +9853,9 @@ msgstr ""
msgid "Could not fetch policy because existing policy YAML is invalid"
msgstr ""
msgid "Could not fetch training providers. Please refresh the page, or try again later."
msgstr ""
msgid "Could not find design."
msgstr ""
......@@ -9889,6 +9892,9 @@ msgstr ""
msgid "Could not revoke project access token %{project_access_token_name}."
msgstr ""
msgid "Could not save configuration. Please refresh the page, or try again later."
msgstr ""
msgid "Could not save group ID"
msgstr ""
......
import { GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
......@@ -8,7 +8,7 @@ import configureSecurityTrainingProvidersMutation from '~/security_configuration
import waitForPromises from 'helpers/wait_for_promises';
import {
securityTrainingProviders,
mockResolvers,
createMockResolvers,
testProjectPath,
textProviderIds,
} from '../mock_data';
......@@ -17,37 +17,42 @@ Vue.use(VueApollo);
describe('TrainingProviderList component', () => {
let wrapper;
let mockApollo;
let mockSecurityTrainingProvidersData;
let apolloProvider;
const createComponent = () => {
mockApollo = createMockApollo([], mockResolvers);
const createApolloProvider = ({ resolvers } = {}) => {
apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
};
const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
provide: {
projectPath: testProjectPath,
},
apolloProvider: mockApollo,
apolloProvider,
});
};
const waitForQueryToBeLoaded = () => waitForPromises();
const waitForMutationToBeLoaded = waitForQueryToBeLoaded;
const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
const findFirstToggle = () => findToggles().at(0);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
mockSecurityTrainingProvidersData = jest.fn();
mockSecurityTrainingProvidersData.mockResolvedValue(securityTrainingProviders);
createComponent();
});
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
afterEach(() => {
wrapper.destroy();
mockApollo = null;
apolloProvider = null;
});
describe('with a successful response', () => {
beforeEach(() => {
createApolloProvider();
createComponent();
});
describe('when loading', () => {
......@@ -95,35 +100,94 @@ describe('TrainingProviderList component', () => {
});
});
describe('success mutation', () => {
const firstToggle = () => findToggles().at(0);
describe('storing training provider settings', () => {
beforeEach(async () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForQueryToBeLoaded();
await waitForMutationToBeLoaded();
firstToggle().vm.$emit('change');
toggleFirstProvider();
});
it.each`
loading | wait | desc
${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
`('$desc', async ({ loading, wait }) => {
if (wait) {
await waitForMutationToBeLoaded();
}
expect(findFirstToggle().props('isLoading')).toBe(loading);
});
it('calls mutation when toggle is changed', () => {
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith(
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: configureSecurityTrainingProvidersMutation,
variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
}),
);
});
});
});
it.each`
loading | wait | desc
${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
`('$desc', async ({ loading, wait }) => {
if (wait) {
await waitForPromises();
}
expect(firstToggle().props('isLoading')).toBe(loading);
describe('with errors', () => {
const expectErrorAlertToExist = () => {
expect(findErrorAlert().props()).toMatchObject({
dismissible: false,
variant: 'danger',
});
};
describe('when fetching training providers', () => {
beforeEach(async () => {
createApolloProvider({
resolvers: {
Query: {
securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
},
},
});
createComponent();
await waitForQueryToBeLoaded();
});
it('shows an non-dismissible error alert', () => {
expectErrorAlertToExist();
});
it('shows an error description', () => {
expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.providerQueryErrorMessage);
});
});
describe('when storing training provider configurations', () => {
beforeEach(async () => {
createApolloProvider({
resolvers: {
Mutation: {
configureSecurityTrainingProviders: () => ({
errors: ['something went wrong!'],
securityTrainingProviders: [],
}),
},
},
});
createComponent();
await waitForQueryToBeLoaded();
toggleFirstProvider();
await waitForMutationToBeLoaded();
});
it('shows an non-dismissible error alert', () => {
expectErrorAlertToExist();
});
it('shows an error description', () => {
expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage);
});
});
});
});
......@@ -25,10 +25,15 @@ export const securityTrainingProvidersResponse = {
},
};
export const mockResolvers = {
const defaultMockResolvers = {
Query: {
securityTrainingProviders() {
return securityTrainingProviders;
},
},
};
export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
...defaultMockResolvers,
...customMockResolvers,
});
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