Commit 4efe88d4 authored by Dave Pisek's avatar Dave Pisek

Use GraphQL endpoint for sec training mutation

This change moves from a client-side mutation to a server-side mutation
for the security training mutation.

It also does a slight refactor due to a small GraphQL schema difference
between the client-side resolver and the backend.
parent c2440e5e
......@@ -49,7 +49,7 @@ export default {
data() {
return {
errorMessage: '',
toggleLoading: false,
providerLoadingId: null,
securityTrainingProviders: [],
hasTouchedConfiguration: false,
};
......@@ -89,37 +89,29 @@ export default {
Sentry.captureException(e);
}
},
toggleProvider(selectedProviderId) {
const toggledProviders = this.securityTrainingProviders.map((provider) => ({
...provider,
...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
}));
toggleProvider(provider) {
const { isEnabled } = provider;
const toggledIsEnabled = !isEnabled;
const enabledProviderIds = toggledProviders
.filter(({ isEnabled }) => isEnabled)
.map(({ id }) => id);
const { isEnabled: selectedProviderIsEnabled } = toggledProviders.find(
(provider) => provider.id === selectedProviderId,
);
this.trackProviderToggle(selectedProviderId, selectedProviderIsEnabled);
this.storeEnabledProviders(enabledProviderIds);
this.trackProviderToggle(provider.id, toggledIsEnabled);
this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
},
async storeEnabledProviders(enabledProviderIds) {
this.toggleLoading = true;
async storeProvider({ id, isEnabled, isPrimary }) {
this.providerLoadingId = id;
try {
const {
data: {
configureSecurityTrainingProviders: { errors = [] },
securityTrainingUpdate: { errors = [] },
},
} = await this.$apollo.mutate({
mutation: configureSecurityTrainingProvidersMutation,
variables: {
input: {
enabledProviders: enabledProviderIds,
fullPath: this.projectFullPath,
projectPath: this.projectFullPath,
providerId: id,
isEnabled,
isPrimary,
},
},
});
......@@ -133,7 +125,7 @@ export default {
} catch {
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
} finally {
this.toggleLoading = false;
this.providerLoadingId = null;
}
},
trackProviderToggle(providerId, providerIsEnabled) {
......@@ -166,25 +158,21 @@ export default {
</gl-skeleton-loader>
</div>
<ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
<li
v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
:key="id"
class="gl-mb-6"
>
<li v-for="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6">
<gl-card>
<div class="gl-display-flex">
<gl-toggle
:value="isEnabled"
:value="provider.isEnabled"
:label="__('Training mode')"
label-position="hidden"
:is-loading="toggleLoading"
@change="toggleProvider(id)"
:is-loading="providerLoadingId === provider.id"
@change="toggleProvider(provider)"
/>
<div class="gl-ml-5">
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
<p>
{{ description }}
<gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
{{ provider.description }}
<gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link>
</p>
</div>
</div>
......
mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
configureSecurityTrainingProviders(input: $input) @client {
mutation updateSecurityTraining($input: SecurityTrainingUpdateInput!) {
securityTrainingUpdate(input: $input) {
errors
securityTrainingProviders {
training {
id
isEnabled
isPrimary
}
}
}
......@@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) {
name
id
description
isPrimary
isEnabled
url
}
......
......@@ -5,7 +5,6 @@ import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import { augmentFeatures } from './utils';
import tempResolvers from './resolver';
export const initSecurityConfiguration = (el) => {
if (!el) {
......@@ -15,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(tempResolvers),
defaultClient: createDefaultClient(),
});
const {
......
import produce from 'immer';
import { __ } from '~/locale';
import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql';
// Note: this is behind a feature flag and only a placeholder
// until the actual GraphQL fields have been added
// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
export default {
Query: {
securityTrainingProviders() {
return [
{
__typename: 'SecurityTrainingProvider',
id: 101,
name: __('Kontra'),
description: __('Interactive developer security education.'),
url: 'https://application.security/',
isEnabled: false,
},
{
__typename: 'SecurityTrainingProvider',
id: 102,
name: __('SecureCodeWarrior'),
description: __('Security training with guide and learning pathways.'),
url: 'https://www.securecodewarrior.com/',
isEnabled: true,
},
];
},
},
Mutation: {
configureSecurityTrainingProviders: (
_,
{ input: { enabledProviders, primaryProvider, fullPath } },
{ cache },
) => {
const sourceData = cache.readQuery({
query: securityTrainingProvidersQuery,
variables: {
fullPath,
},
});
const data = produce(sourceData.project, (draftData) => {
/* eslint-disable no-param-reassign */
draftData.securityTrainingProviders.forEach((provider) => {
provider.isPrimary = provider.id === primaryProvider;
provider.isEnabled =
provider.id === primaryProvider || enabledProviders.includes(provider.id);
});
});
return {
__typename: 'configureSecurityTrainingProvidersPayload',
securityTrainingProviders: data.securityTrainingProviders,
};
},
},
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import tempResolvers from '~/security_configuration/resolver';
Vue.use(VueApollo);
const defaultClient = createDefaultClient({
...tempResolvers,
});
const defaultClient = createDefaultClient();
export default new VueApollo({
defaultClient,
......
......@@ -19717,9 +19717,6 @@ msgstr ""
msgid "Integrations|can't exceed %{recipients_limit}"
msgstr ""
msgid "Interactive developer security education."
msgstr ""
msgid "Interactive mode"
msgstr ""
......@@ -21058,9 +21055,6 @@ msgstr ""
msgid "Ki"
msgstr ""
msgid "Kontra"
msgstr ""
msgid "Kroki"
msgstr ""
......@@ -32018,9 +32012,6 @@ msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
msgid "SecureCodeWarrior"
msgstr ""
msgid "Security"
msgstr ""
......@@ -32045,9 +32036,6 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
msgid "Security training with guide and learning pathways."
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
msgstr ""
......
......@@ -19,6 +19,8 @@ import {
dismissUserCalloutErrorResponse,
securityTrainingProviders,
securityTrainingProvidersResponse,
updateSecurityTrainingProvidersResponse,
updateSecurityTrainingProvidersErrorResponse,
testProjectPath,
textProviderIds,
} from '../mock_data';
......@@ -29,18 +31,22 @@ describe('TrainingProviderList component', () => {
let wrapper;
let apolloProvider;
const createApolloProvider = ({ resolvers, handlers = [] } = {}) => {
const createApolloProvider = ({ handlers = [] } = {}) => {
const defaultHandlers = [
[
securityTrainingProvidersQuery,
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
],
[
configureSecurityTrainingProvidersMutation,
jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse),
],
];
// make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors
const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
apolloProvider = createMockApollo(mergedHandlers, resolvers);
apolloProvider = createMockApollo(mergedHandlers);
};
const createComponent = () => {
......@@ -62,7 +68,7 @@ describe('TrainingProviderList component', () => {
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]);
afterEach(() => {
wrapper.destroy();
......@@ -146,9 +152,9 @@ describe('TrainingProviderList component', () => {
beforeEach(async () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForMutationToBeLoaded();
await waitForQueryToBeLoaded();
toggleFirstProvider();
await toggleFirstProvider();
});
it.each`
......@@ -166,7 +172,14 @@ describe('TrainingProviderList component', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: configureSecurityTrainingProvidersMutation,
variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
variables: {
input: {
providerId: textProviderIds[0],
isEnabled: true,
isPrimary: false,
projectPath: testProjectPath,
},
},
}),
);
});
......@@ -264,14 +277,12 @@ describe('TrainingProviderList component', () => {
describe('when storing training provider configurations', () => {
beforeEach(async () => {
createApolloProvider({
resolvers: {
Mutation: {
configureSecurityTrainingProviders: () => ({
errors: ['something went wrong!'],
securityTrainingProviders: [],
}),
},
},
handlers: [
[
configureSecurityTrainingProvidersMutation,
jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse),
],
],
});
createComponent();
......
......@@ -9,6 +9,7 @@ export const securityTrainingProviders = [
description: 'Interactive developer security education',
url: 'https://www.example.org/security/training',
isEnabled: false,
isPrimary: false,
},
{
id: textProviderIds[1],
......@@ -16,6 +17,7 @@ export const securityTrainingProviders = [
description: 'Security training with guide and learning pathways.',
url: 'https://www.vendornametwo.com/',
isEnabled: true,
isPrimary: false,
},
];
......@@ -51,3 +53,26 @@ export const dismissUserCalloutErrorResponse = {
},
},
};
export const updateSecurityTrainingProvidersResponse = {
data: {
securityTrainingUpdate: {
errors: [],
training: {
id: 101,
name: 'Acme',
isEnabled: true,
isPrimary: false,
},
},
},
};
export const updateSecurityTrainingProvidersErrorResponse = {
data: {
securityTrainingUpdate: {
errors: ['something went wrong!'],
training: null,
},
},
};
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