Commit 5895a3e6 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '321281-secret-detection-configure-mvc' into 'master'

Configure Secret Detection via an MR [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!57869
parents cce5ca05 d4dc3a22
......@@ -6,3 +6,4 @@ filenames:
- ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/dast_failed_site_validations.query.graphql
- app/assets/javascripts/repository/queries/blob_info.query.graphql
- ee/app/assets/javascripts/security_configuration/graphql/configure_dependency_scanning.mutation.graphql
- ee/app/assets/javascripts/security_configuration/graphql/configure_secret_detection.mutation.graphql
import { s__ } from '~/locale';
import { REPORT_TYPE_DEPENDENCY_SCANNING } from '~/vue_shared/security_reports/constants';
import {
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import configureDependencyScanningMutation from '../graphql/configure_dependency_scanning.mutation.graphql';
import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
export const SMALL = 'SMALL';
export const MEDIUM = 'MEDIUM';
......@@ -23,4 +27,8 @@ export const featureToMutationMap = {
type: 'configureDependencyScanning',
mutation: configureDependencyScanningMutation,
},
[REPORT_TYPE_SECRET_DETECTION]: {
type: 'configureSecretDetection',
mutation: configureSecretDetectionMutation,
},
};
......@@ -4,6 +4,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import ManageDastProfiles from './manage_dast_profiles.vue';
import ManageGeneric from './manage_generic.vue';
......@@ -12,6 +13,7 @@ import ManageViaMr from './manage_via_mr.vue';
const scannerComponentMap = {
[REPORT_TYPE_DAST_PROFILES]: ManageDastProfiles,
[REPORT_TYPE_DEPENDENCY_SCANNING]: ManageViaMr,
[REPORT_TYPE_SECRET_DETECTION]: ManageViaMr,
};
export default {
......@@ -23,6 +25,9 @@ export default {
if (!this.glFeatures.secDependencyScanningUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_DEPENDENCY_SCANNING];
}
if (!this.glFeatures.secSecretDetectionUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_SECRET_DETECTION];
}
return scannerComponentMapCopy;
},
manageComponent() {
......
mutation configureSecretDetection($fullPath: ID!) {
configureSecretDetection(fullPath: $fullPath) {
successPath
errors
}
}
......@@ -15,6 +15,7 @@ module EE
before_action only: [:show] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
push_frontend_feature_flag(:sec_dependency_scanning_ui_enable, project, default_enabled: :yaml)
push_frontend_feature_flag(:sec_secret_detection_ui_enable, project, default_enabled: :yaml)
end
before_action only: [:auto_fix] do
......
---
name: sec_secret_detection_ui_enable
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57869
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326049
milestone: '13.11'
type: development
group: group::static analysis
default_enabled: false
const buildConfigureDependencyScanningMock = ({
export const buildConfigureSecurityFeatureMockFactory = (mutationType) => ({
successPath = 'testSuccessPath',
errors = [],
} = {}) => ({
data: {
configureDependencyScanning: {
[mutationType]: {
successPath,
errors,
__typename: 'ConfigureDependencyScanningPayload',
__typename: `${mutationType}Payload`,
},
},
});
export const configureDependencyScanningSuccess = buildConfigureDependencyScanningMock();
export const configureDependencyScanningNoSuccessPath = buildConfigureDependencyScanningMock({
successPath: '',
});
export const configureDependencyScanningError = buildConfigureDependencyScanningMock({
errors: ['foo'],
});
......@@ -6,6 +6,7 @@ import ManageViaMr from 'ee/security_configuration/components/manage_via_mr.vue'
import {
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import { generateFeatures } from './helpers';
......@@ -21,6 +22,7 @@ describe('ManageFeature component', () => {
provide: {
glFeatures: {
secDependencyScanningUiEnable: true,
secSecretDetectionUiEnable: true,
},
},
...options,
......@@ -53,6 +55,7 @@ describe('ManageFeature component', () => {
type | expectedComponent
${REPORT_TYPE_DAST_PROFILES} | ${ManageDastProfiles}
${REPORT_TYPE_DEPENDENCY_SCANNING} | ${ManageViaMr}
${REPORT_TYPE_SECRET_DETECTION} | ${ManageViaMr}
${'foo'} | ${ManageGeneric}
`('given a $type feature', ({ type, expectedComponent }) => {
let feature;
......@@ -77,6 +80,7 @@ describe('ManageFeature component', () => {
it.each`
type | featureFlag
${REPORT_TYPE_DEPENDENCY_SCANNING} | ${'secDependencyScanningUiEnable'}
${REPORT_TYPE_SECRET_DETECTION} | ${'secSecretDetectionUiEnable'}
`('renders generic component for $type if $featureFlag is disabled', ({ type, featureFlag }) => {
const [feature] = generateFeatures(1, { type });
createComponent({
......
......@@ -4,16 +4,16 @@ 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 { redirectTo } from '~/lib/utils/url_utility';
import { REPORT_TYPE_DEPENDENCY_SCANNING } from '~/vue_shared/security_reports/constants';
import {
configureDependencyScanningSuccess,
configureDependencyScanningNoSuccessPath,
configureDependencyScanningError,
} from './apollo_mocks';
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');
......@@ -24,107 +24,122 @@ describe('ManageViaMr component', () => {
const findButton = () => wrapper.findComponent(GlButton);
const successHandler = async () => configureDependencyScanningSuccess;
const noSuccessPathHandler = async () => configureDependencyScanningNoSuccessPath;
const errorHandler = async () => configureDependencyScanningError;
const pendingHandler = () => new Promise(() => {});
function createMockApolloProvider(handler) {
const requestHandlers = [[configureDependencyScanningMutation, handler]];
return createMockApollo(requestHandlers);
}
function createComponent({ mockApollo, isFeatureConfigured = false } = {}) {
wrapper = extendedWrapper(
mount(ManageViaMr, {
apolloProvider: mockApollo,
propsData: {
feature: {
name: 'Dependency Scanning',
configured: isFeatureConfigured,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
},
},
}),
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,
);
}
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);
});
});
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,
propsData: {
feature: {
name: featureName,
configured: isFeatureConfigured,
type: featureType,
},
},
}),
);
}
describe('when feature is not configured', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo, isFeatureConfigured: false });
afterEach(() => {
wrapper.destroy();
});
it('it does render a button', () => {
expect(findButton().exists()).toBe(true);
});
});
describe('when feature is configured', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo, isFeatureConfigured: true });
});
describe('given a pending response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(pendingHandler);
createComponent({ mockApollo });
it('it does not render a button', () => {
expect(findButton().exists()).toBe(false);
});
});
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('when feature is not configured', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo, isFeatureConfigured: false });
});
describe('given a successful response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo });
it('it does render a button', () => {
expect(findButton().exists()).toBe(true);
});
});
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 () => {
const button = findButton();
expect(button.props('loading')).toBe(false);
await button.trigger('click');
expect(button.props('loading')).toBe(true);
});
});
});
describe.each`
handler | message
${noSuccessPathHandler} | ${'Dependency Scanning merge request creation mutation failed'}
${errorHandler} | ${'foo'}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(handler);
createComponent({ mockApollo });
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);
});
});
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);
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