Commit 76e89467 authored by Jannik Lehmann's avatar Jannik Lehmann Committed by Mark Florian

Refactor manage_via_mr Component

This commit solves 326249.
The security Configuration Table for CE and EE has been refactored.
Both of those tables support the ability to enable scanners via Merge Request.
Previously this was handled separately leading to duplicate code.
parent 021ca831
<script>
import { GlLink, GlTable, GlAlert } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import ManageViaMR from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
......@@ -11,8 +12,8 @@ import {
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
import ManageSast from './manage_sast.vue';
import { scanners } from './scanners_constants';
import { scanners } from './constants';
import Upgrade from './upgrade.vue';
const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
......@@ -40,7 +41,7 @@ export default {
},
getComponentForItem(item) {
const COMPONENTS = {
[REPORT_TYPE_SAST]: ManageSast,
[REPORT_TYPE_SAST]: ManageViaMR,
[REPORT_TYPE_DAST]: Upgrade,
[REPORT_TYPE_DAST_PROFILES]: Upgrade,
[REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
......@@ -49,7 +50,6 @@ export default {
[REPORT_TYPE_API_FUZZING]: Upgrade,
[REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
};
return COMPONENTS[item.type];
},
},
......@@ -95,7 +95,12 @@ export default {
</template>
<template #cell(manage)="{ item }">
<component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
<component
:is="getComponentForItem(item)"
:feature="item"
:data-testid="item.type"
@error="onError"
/>
</template>
</gl-table>
</div>
......
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
......@@ -134,3 +135,18 @@ export const scanners = [
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
export const featureToMutationMap = {
[REPORT_TYPE_SAST]: {
mutationId: 'configureSast',
getMutationPayload: (projectPath) => ({
mutation: configureSastMutation,
variables: {
input: {
projectPath,
configuration: { global: [], pipeline: [], analyzers: [] },
},
},
}),
},
};
<script>
import { GlButton } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
export default {
components: {
GlButton,
},
inject: {
projectPath: {
from: 'projectPath',
default: '',
},
},
data() {
return {
isLoading: false,
};
},
methods: {
async mutate() {
this.isLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: configureSastMutation,
variables: {
input: {
projectPath: this.projectPath,
configuration: { global: [], pipeline: [], analyzers: [] },
},
},
});
const { errors, successPath } = data.configureSast;
if (errors.length > 0) {
throw new Error(errors[0]);
}
if (!successPath) {
throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
}
redirectTo(successPath);
} catch (e) {
this.$emit('error', e.message);
this.isLoading = false;
}
},
},
};
</script>
<template>
<gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
s__('SecurityConfiguration|Configure via merge request')
}}</gl-button>
</template>
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { UPGRADE_CTA } from './scanners_constants';
import { UPGRADE_CTA } from './constants';
export default {
components: {
......
<script>
import { GlButton } from '@gitlab/ui';
import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants';
import { redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
import apolloProvider from '../graphql/provider';
import { featureToMutationMap } from './constants';
import apolloProvider from '../provider';
export default {
apolloProvider,
components: {
GlButton,
},
inject: {
projectPath: {
from: 'projectPath',
default: '',
},
},
inject: ['projectPath'],
props: {
feature: {
type: Object,
......@@ -36,15 +31,9 @@ export default {
async mutate() {
this.isLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: this.featureSettings.mutation,
variables: {
input: {
projectPath: this.projectPath,
},
},
});
const { errors, successPath } = data[this.featureSettings.type];
const mutation = this.featureSettings;
const { data } = await this.$apollo.mutate(mutation.getMutationPayload(this.projectPath));
const { errors, successPath } = data[mutation.mutationId];
if (errors.length > 0) {
throw new Error(errors[0]);
......
......@@ -2,7 +2,7 @@
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { scanners } from '~/security_configuration/components/scanners_constants';
import { scanners } from '~/security_configuration/components/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AutoFixSettings from './auto_fix_settings.vue';
......
import { s__ } from '~/locale';
import { featureToMutationMap as featureToMutationMapCE } from '~/security_configuration/components/constants';
import {
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_SECRET_DETECTION,
......@@ -23,12 +24,27 @@ export const CUSTOM_VALUE_MESSAGE = s__(
);
export const featureToMutationMap = {
...featureToMutationMapCE,
[REPORT_TYPE_DEPENDENCY_SCANNING]: {
type: 'configureDependencyScanning',
mutation: configureDependencyScanningMutation,
mutationId: 'configureDependencyScanning',
getMutationPayload: (projectPath) => ({
mutation: configureDependencyScanningMutation,
variables: {
input: {
projectPath,
},
},
}),
},
[REPORT_TYPE_SECRET_DETECTION]: {
type: 'configureSecretDetection',
mutation: configureSecretDetectionMutation,
mutationId: 'configureSecretDetection',
getMutationPayload: (projectPath) => ({
mutation: configureSecretDetectionMutation,
variables: {
input: {
projectPath,
},
},
}),
},
};
<script>
import { propsUnion } from '~/vue_shared/components/lib/utils/props_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import {
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_DEPENDENCY_SCANNING,
......@@ -8,7 +9,6 @@ import {
} from '~/vue_shared/security_reports/constants';
import ManageDastProfiles from './manage_dast_profiles.vue';
import ManageGeneric from './manage_generic.vue';
import ManageViaMr from './manage_via_mr.vue';
const scannerComponentMap = {
[REPORT_TYPE_DAST_PROFILES]: ManageDastProfiles,
......
......@@ -18,8 +18,8 @@ export const initSecurityConfiguration = (el) => {
containerScanningHelpPath,
dependencyScanningHelpPath,
toggleAutofixSettingEndpoint,
gitlabCiHistoryPath,
projectPath,
gitlabCiHistoryPath,
} = el.dataset;
return new Vue({
......
......@@ -5,7 +5,7 @@ import SecurityConfigurationApp from 'ee/security_configuration/components/app.v
import ConfigurationTable from 'ee/security_configuration/components/configuration_table.vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import stubChildren from 'helpers/stub_children';
import { scanners } from '~/security_configuration/components/scanners_constants';
import { scanners } from '~/security_configuration/components/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { generateFeatures } from './helpers';
......
import { scanners } from '~/security_configuration/components/scanners_constants';
import { scanners } from '~/security_configuration/components/constants';
export const generateFeatures = (n, overrides = {}) => {
return [...Array(n).keys()].map((i) => ({
......
......@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import ManageDastProfiles from 'ee/security_configuration/components/manage_dast_profiles.vue';
import ManageFeature from 'ee/security_configuration/components/manage_feature.vue';
import ManageGeneric from 'ee/security_configuration/components/manage_generic.vue';
import ManageViaMr from 'ee/security_configuration/components/manage_via_mr.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import {
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_DEPENDENCY_SCANNING,
......
......@@ -10,9 +10,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import { makeEntities, makeSastCiConfiguration } from '../../helpers';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
jest.mock('~/lib/utils/url_utility');
const projectPath = 'group/project';
const sastAnalyzersDocumentationPath = '/help/sast/analyzers';
......
......@@ -2,18 +2,18 @@ import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import ManageViaMr from 'ee/security_configuration/components/manage_via_mr.vue';
import configureDependencyScanningMutation from 'ee/security_configuration/graphql/configure_dependency_scanning.mutation.graphql';
import configureSecretDetectionMutation from 'ee/security_configuration/graphql/configure_secret_detection.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { buildConfigureSecurityFeatureMockFactory } from 'jest/vue_shared/security_reports/components/apollo_mocks';
import { redirectTo } from '~/lib/utils/url_utility';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import {
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
jest.mock('~/lib/utils/url_utility');
......@@ -23,15 +23,12 @@ describe('ManageViaMr component', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
describe.each`
featureName | featureType | mutation | mutationType
${'Dependency Scanning'} | ${REPORT_TYPE_DEPENDENCY_SCANNING} | ${configureDependencyScanningMutation} | ${'configureDependencyScanning'}
${'Secret Detection'} | ${REPORT_TYPE_SECRET_DETECTION} | ${configureSecretDetectionMutation} | ${'configureSecretDetection'}
`('$featureType', ({ featureName, featureType, mutation, mutationType }) => {
const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(
mutationType,
);
featureName | featureType | mutation | mutationId
${'SECRET_DETECTION'} | ${REPORT_TYPE_DEPENDENCY_SCANNING} | ${configureDependencyScanningMutation} | ${'configureDependencyScanning'}
${'DEPENDENCY_SCANNING'} | ${REPORT_TYPE_SECRET_DETECTION} | ${configureSecretDetectionMutation} | ${'configureSecretDetection'}
`('$featureType', ({ featureName, mutation, featureType, mutationId }) => {
const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(mutationId);
const successHandler = async () => buildConfigureSecurityFeatureMock();
const noSuccessPathHandler = async () =>
buildConfigureSecurityFeatureMock({
......@@ -53,11 +50,14 @@ describe('ManageViaMr component', () => {
wrapper = extendedWrapper(
mount(ManageViaMr, {
apolloProvider: mockApollo,
provide: {
projectPath: 'testProjectPath',
},
propsData: {
feature: {
name: featureName,
configured: isFeatureConfigured,
type: featureType,
configured: isFeatureConfigured,
},
},
}),
......
......@@ -28495,9 +28495,6 @@ msgstr ""
msgid "SecurityConfiguration|Configure via Merge Request"
msgstr ""
msgid "SecurityConfiguration|Configure via merge request"
msgstr ""
msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later."
msgstr ""
......@@ -28534,9 +28531,6 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration"
msgstr ""
msgid "SecurityConfiguration|SAST merge request creation mutation failed"
msgstr ""
msgid "SecurityConfiguration|Security Control"
msgstr ""
......
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
import { scanners, UPGRADE_CTA } from '~/security_configuration/components/scanners_constants';
import { scanners, UPGRADE_CTA } from '~/security_configuration/components/constants';
import {
REPORT_TYPE_SAST,
......@@ -12,7 +12,13 @@ describe('Configuration Table Component', () => {
let wrapper;
const createComponent = () => {
wrapper = extendedWrapper(mount(ConfigurationTable, {}));
wrapper = extendedWrapper(
mount(ConfigurationTable, {
provide: {
projectPath: 'testProjectPath',
},
}),
);
};
const findHelpLinks = () => wrapper.findAll('[data-testid="help-link"]');
......@@ -30,8 +36,10 @@ describe('Configuration Table Component', () => {
expect(wrapper.text()).toContain(scanner.name);
expect(wrapper.text()).toContain(scanner.description);
if (scanner.type === REPORT_TYPE_SAST) {
expect(wrapper.findByTestId(scanner.type).text()).toBe('Configure via merge request');
} else if (scanner.type !== REPORT_TYPE_SECRET_DETECTION) {
expect(wrapper.findByTestId(scanner.type).text()).toBe('Configure via Merge Request');
} else if (scanner.type === REPORT_TYPE_SECRET_DETECTION) {
expect(wrapper.findByTestId(scanner.type).exists()).toBe(false);
} else {
expect(wrapper.findByTestId(scanner.type).text()).toMatchInterpolatedText(UPGRADE_CTA);
}
});
......
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility';
import ManageSast from '~/security_configuration/components/manage_sast.vue';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
Vue.use(VueApollo);
describe('Manage Sast Component', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
const successHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const noSuccessPathHandler = async () => {
return {
data: {
configureSast: {
successPath: '',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const errorHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: ['foo'],
__typename: 'ConfigureSastPayload',
},
},
};
};
const pendingHandler = () => new Promise(() => {});
function createMockApolloProvider(handler) {
const requestHandlers = [[configureSastMutation, handler]];
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
wrapper = extendedWrapper(
mount(ManageSast, {
apolloProvider: mockApollo,
}),
);
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render Button with correct text', () => {
createComponent();
expect(findButton().text()).toContain('Configure via merge request');
});
describe('given a successful response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo });
});
it('should call redirect helper with correct value', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
// This is done for UX reasons. If the loading prop is set to false
// on success, then there's a period where the button is clickable
// again. Instead, we want the button to display a loading indicator
// for the remainder of the lifetime of the page (i.e., until the
// browser can start painting the new page it's been redirected to).
expect(findButton().props().loading).toBe(true);
});
});
describe('given a pending response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(pendingHandler);
createComponent({ mockApollo });
});
it('renders spinner correctly', async () => {
expect(findButton().props('loading')).toBe(false);
await wrapper.trigger('click');
await waitForPromises();
expect(findButton().props('loading')).toBe(true);
});
});
describe.each`
handler | message
${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'}
${errorHandler} | ${'foo'}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(handler);
createComponent({ mockApollo });
});
it('should catch and emit error', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[message]]);
expect(findButton().props('loading')).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import { UPGRADE_CTA } from '~/security_configuration/components/scanners_constants';
import { UPGRADE_CTA } from '~/security_configuration/components/constants';
import Upgrade from '~/security_configuration/components/upgrade.vue';
const TEST_URL = 'http://www.example.test';
......
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility';
import configureSast from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
describe('ManageViaMr component', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
describe.each`
featureName | featureType | mutation | mutationId
${'SAST'} | ${REPORT_TYPE_SAST} | ${configureSast} | ${'configureSast'}
`('$featureType', ({ featureName, mutation, featureType, mutationId }) => {
const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(mutationId);
const successHandler = async () => buildConfigureSecurityFeatureMock();
const noSuccessPathHandler = async () =>
buildConfigureSecurityFeatureMock({
successPath: '',
});
const errorHandler = async () =>
buildConfigureSecurityFeatureMock({
errors: ['foo'],
});
const pendingHandler = () => new Promise(() => {});
function createMockApolloProvider(handler) {
const requestHandlers = [[mutation, handler]];
return createMockApollo(requestHandlers);
}
function createComponent({ mockApollo, isFeatureConfigured = false } = {}) {
wrapper = extendedWrapper(
mount(ManageViaMr, {
apolloProvider: mockApollo,
provide: {
projectPath: 'testProjectPath',
},
propsData: {
feature: {
name: featureName,
type: featureType,
configured: isFeatureConfigured,
},
},
}),
);
}
afterEach(() => {
wrapper.destroy();
});
describe('when feature is configured', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo, isFeatureConfigured: true });
});
it('it does not render a button', () => {
expect(findButton().exists()).toBe(false);
});
});
describe('when feature is not configured', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo, isFeatureConfigured: false });
});
it('it does render a button', () => {
expect(findButton().exists()).toBe(true);
});
});
describe('given a pending response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(pendingHandler);
createComponent({ mockApollo });
});
it('renders spinner correctly', async () => {
const button = findButton();
expect(button.props('loading')).toBe(false);
await button.trigger('click');
expect(button.props('loading')).toBe(true);
});
});
describe('given a successful response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo });
});
it('should call redirect helper with correct value', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
// This is done for UX reasons. If the loading prop is set to false
// on success, then there's a period where the button is clickable
// again. Instead, we want the button to display a loading indicator
// for the remainder of the lifetime of the page (i.e., until the
// browser can start painting the new page it's been redirected to).
expect(findButton().props().loading).toBe(true);
});
});
describe.each`
handler | message
${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
${errorHandler} | ${'foo'}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(handler);
createComponent({ mockApollo });
});
it('should catch and emit error', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[message]]);
expect(findButton().props('loading')).toBe(false);
});
});
});
});
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