Commit 476b71f3 authored by Alexander Turinske's avatar Alexander Turinske

Add ability to create scan execution policy MR

- when security project exists and is linked,
  create policy and open MR for modifications
  to scan_policy.yml
- create new mutations to create scan_execution policy
- create utils file for mutations
- update policy editor layout to show alert on error
- add tests
parent 8a6f6da1
mutation createMergeRequest($input: MergeRequestCreateInput!) {
mergeRequestCreate(input: $input) {
mergeRequest {
iid
}
errors
}
}
<script> <script>
import { GlFormGroup, GlFormSelect } from '@gitlab/ui'; import { GlAlert, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentPicker from '../environment_picker.vue'; import EnvironmentPicker from '../environment_picker.vue';
...@@ -9,6 +9,7 @@ import ScanExecutionPolicyEditor from './scan_execution_policy/scan_execution_po ...@@ -9,6 +9,7 @@ import ScanExecutionPolicyEditor from './scan_execution_policy/scan_execution_po
export default { export default {
components: { components: {
GlAlert,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
EnvironmentPicker, EnvironmentPicker,
...@@ -17,6 +18,10 @@ export default { ...@@ -17,6 +18,10 @@ export default {
}, },
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
props: { props: {
assignedPolicyProject: {
type: Object,
required: true,
},
existingPolicy: { existingPolicy: {
type: Object, type: Object,
required: false, required: false,
...@@ -25,6 +30,7 @@ export default { ...@@ -25,6 +30,7 @@ export default {
}, },
data() { data() {
return { return {
error: '',
policyType: POLICY_KIND_OPTIONS.network.value, policyType: POLICY_KIND_OPTIONS.network.value,
}; };
}, },
...@@ -44,6 +50,9 @@ export default { ...@@ -44,6 +50,9 @@ export default {
}, },
methods: { methods: {
...mapActions('threatMonitoring', ['fetchEnvironments']), ...mapActions('threatMonitoring', ['fetchEnvironments']),
setError(error) {
this.error = error;
},
updatePolicyType(type) { updatePolicyType(type) {
this.policyType = type; this.policyType = type;
}, },
...@@ -54,6 +63,9 @@ export default { ...@@ -54,6 +63,9 @@ export default {
<template> <template>
<section class="policy-editor"> <section class="policy-editor">
<gl-alert v-if="error" dissmissable="true" variant="danger" @dismiss="setError('')">
{{ error }}
</gl-alert>
<header class="gl-pb-5"> <header class="gl-pb-5">
<h3>{{ s__('NetworkPolicies|Policy description') }}</h3> <h3>{{ s__('NetworkPolicies|Policy description') }}</h3>
</header> </header>
...@@ -69,6 +81,11 @@ export default { ...@@ -69,6 +81,11 @@ export default {
</gl-form-group> </gl-form-group>
<environment-picker v-if="shouldShowEnvironmentPicker" /> <environment-picker v-if="shouldShowEnvironmentPicker" />
</div> </div>
<component :is="policyComponent" :existing-policy="existingPolicy" /> <component
:is="policyComponent"
:existing-policy="existingPolicy"
:assigned-policy-project="assignedPolicyProject"
@error="setError($event)"
/>
</section> </section>
</template> </template>
...@@ -23,6 +23,11 @@ export default { ...@@ -23,6 +23,11 @@ export default {
required: false, required: false,
default: EDITOR_MODE_RULE, default: EDITOR_MODE_RULE,
}, },
disableUpdate: {
type: Boolean,
required: false,
default: false,
},
editorModes: { editorModes: {
type: Array, type: Array,
required: false, required: false,
...@@ -132,6 +137,7 @@ export default { ...@@ -132,6 +137,7 @@ export default {
variant="success" variant="success"
data-testid="save-policy" data-testid="save-policy"
:loading="isUpdatingPolicy" :loading="isUpdatingPolicy"
:disabled="disableUpdate"
@click="savePolicy" @click="savePolicy"
> >
<slot name="save-button-text"> <slot name="save-button-text">
......
import { s__ } from '~/locale';
export const DEFAULT_MR_TITLE = s__('SecurityOrchestration|Update scan execution policies');
export const GRAPHQL_ERROR_MESSAGE = s__(
'SecurityOrchestration|There was a problem creating the new security policy',
);
export { fromYaml } from './from_yaml'; export { fromYaml } from './from_yaml';
export * from './constants';
export * from './utils';
export const DEFAULT_SCAN_EXECUTION_POLICY = `type: scan_execution_policy export const DEFAULT_SCAN_EXECUTION_POLICY = `type: scan_execution_policy
name: '' name: ''
description: '' description: ''
......
import assignPolicyProject from 'ee/threat_monitoring/graphql/mutations/assign_policy_project.mutation.graphql';
import createPolicyProject from 'ee/threat_monitoring/graphql/mutations/create_policy_project.mutation.graphql';
import createScanExecutionPolicy from 'ee/threat_monitoring/graphql/mutations/create_scan_execution_policy.mutation.graphql';
import { gqClient } from 'ee/threat_monitoring/utils';
import createMergeRequestMutation from '~/graphql_shared/mutations/create_merge_request.mutation.graphql';
import { DEFAULT_MR_TITLE } from './constants';
/**
* Checks if an error exists and throws it if it does
* @param {Object} payload contains the errors if they exist
*/
const checkForErrors = ({ errors }) => {
if (errors?.length) {
throw new Error(errors);
}
};
/**
* Creates a new security policy project and assigns it to the current project
* @param {String} projectPath
* @returns {Object} contains the new security policy project and any errors
*/
const assignSecurityPolicyProject = async (projectPath) => {
const {
data: {
securityPolicyProjectCreate: { project, errors: createErrors },
},
} = await gqClient.mutate({
mutation: createPolicyProject,
variables: {
projectPath,
},
});
checkForErrors({ errors: createErrors });
const {
data: {
securityPolicyProjectAssign: { errors: assignErrors },
},
} = await gqClient.mutate({
mutation: assignPolicyProject,
variables: {
projectPath,
id: project.id,
},
});
return { ...project, errors: assignErrors };
};
/**
* Creates a merge request for the changes to the policy file
* @param {Object} payload contains the path to the project, the branch to merge on the project, and the branch to merge into
* @returns {Object} contains the id of the merge request and any errors
*/
const createMergeRequest = async ({ projectPath, sourceBranch, targetBranch }) => {
const input = {
projectPath,
sourceBranch,
targetBranch,
title: DEFAULT_MR_TITLE,
};
const {
data: {
mergeRequestCreate: {
mergeRequest: { iid: id },
errors,
},
},
} = await gqClient.mutate({
mutation: createMergeRequestMutation,
variables: { input },
});
return { id, errors };
};
/**
* Creates a new security policy on the security policy project's policy file
* @param {Object} payload contains the path to the project and the policy yaml value
* @returns {Object} contains the branch containing the updated policy file and any errors
*/
const updatePolicy = async ({ projectPath, yamlEditorValue }) => {
const {
data: {
scanExecutionPolicyCommit: { branch, errors },
},
} = await gqClient.mutate({
mutation: createScanExecutionPolicy,
variables: {
projectPath,
policyYaml: yamlEditorValue,
},
});
return { branch, errors };
};
/**
* Updates the assigned security policy project's policy file with the new policy yaml or creates one (project or file) if one does not exist
* @param {Object} payload contains the currently assigned security policy project (if one exists), the path to the project, and the policy yaml value
* @returns {Object} contains the currently assigned security policy project and the created merge request
*/
export const savePolicy = async ({ assignedPolicyProject, projectPath, yamlEditorValue }) => {
let currentAssignedPolicyProject = assignedPolicyProject;
if (!currentAssignedPolicyProject.fullPath) {
currentAssignedPolicyProject = await assignSecurityPolicyProject(projectPath);
}
checkForErrors(currentAssignedPolicyProject);
const newPolicyCommitBranch = await updatePolicy({
projectPath: currentAssignedPolicyProject.fullPath,
yamlEditorValue,
});
checkForErrors(newPolicyCommitBranch);
const mergeRequest = await createMergeRequest({
projectPath: currentAssignedPolicyProject.fullPath,
sourceBranch: newPolicyCommitBranch.branch,
targetBranch: currentAssignedPolicyProject.branch,
});
checkForErrors(mergeRequest);
return { currentAssignedPolicyProject, mergeRequest };
};
<script> <script>
import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils'; import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants'; import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants';
import PolicyEditorLayout from '../policy_editor_layout.vue'; import PolicyEditorLayout from '../policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY, fromYaml } from './lib'; import { DEFAULT_SCAN_EXECUTION_POLICY, fromYaml, GRAPHQL_ERROR_MESSAGE, savePolicy } from './lib';
export default { export default {
DEFAULT_EDITOR_MODE: EDITOR_MODE_YAML, DEFAULT_EDITOR_MODE: EDITOR_MODE_YAML,
...@@ -14,8 +15,12 @@ export default { ...@@ -14,8 +15,12 @@ export default {
components: { components: {
PolicyEditorLayout, PolicyEditorLayout,
}, },
inject: ['threatMonitoringPath', 'projectId'], inject: ['disableScanExecutionUpdate', 'projectId', 'projectPath'],
props: { props: {
assignedPolicyProject: {
type: Object,
required: true,
},
existingPolicy: { existingPolicy: {
type: Object, type: Object,
required: false, required: false,
...@@ -28,6 +33,8 @@ export default { ...@@ -28,6 +33,8 @@ export default {
: DEFAULT_SCAN_EXECUTION_POLICY; : DEFAULT_SCAN_EXECUTION_POLICY;
return { return {
error: '',
isCreatingMR: false,
policy: fromYaml(yamlEditorValue), policy: fromYaml(yamlEditorValue),
yamlEditorValue, yamlEditorValue,
}; };
...@@ -38,6 +45,34 @@ export default { ...@@ -38,6 +45,34 @@ export default {
}, },
}, },
methods: { methods: {
async handleSavePolicy() {
this.$emit('error', '');
this.isCreatingMR = true;
try {
const { currentAssignedPolicyProject, mergeRequest } = await savePolicy({
assignedPolicyProject: this.assignedPolicyProject,
projectPath: this.projectPath,
yamlEditorValue: this.yamlEditorValue,
});
visitUrl(
joinPaths(
gon.relative_url_root || '/',
currentAssignedPolicyProject.fullPath,
'/-/merge_requests',
mergeRequest.id,
),
);
} catch (e) {
if (e.message.toLowerCase().includes('graphql')) {
this.$emit('error', GRAPHQL_ERROR_MESSAGE);
} else {
this.$emit('error', e.message);
}
this.isCreatingMR = false;
}
},
updateYaml(manifest) { updateYaml(manifest) {
this.yamlEditorValue = manifest; this.yamlEditorValue = manifest;
}, },
...@@ -48,10 +83,13 @@ export default { ...@@ -48,10 +83,13 @@ export default {
<template> <template>
<policy-editor-layout <policy-editor-layout
:default-editor-mode="$options.DEFAULT_EDITOR_MODE" :default-editor-mode="$options.DEFAULT_EDITOR_MODE"
:disable-update="disableScanExecutionUpdate"
:editor-modes="$options.EDITOR_MODES" :editor-modes="$options.EDITOR_MODES"
:is-editing="isEditing" :is-editing="isEditing"
:is-updating-policy="isCreatingMR"
:policy-name="policy.name" :policy-name="policy.name"
:yaml-editor-value="yamlEditorValue" :yaml-editor-value="yamlEditorValue"
@save-policy="handleSavePolicy"
@update-yaml="updateYaml" @update-yaml="updateYaml"
> >
<template #save-button-text> <template #save-button-text>
......
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const DEFAULT_ASSIGNED_POLICY_PROJECT = { fullPath: '', branch: '' };
export const INVALID_CURRENT_ENVIRONMENT_NAME = ''; export const INVALID_CURRENT_ENVIRONMENT_NAME = '';
export const PREDEFINED_NETWORK_POLICIES = [ export const PREDEFINED_NETWORK_POLICIES = [
......
mutation assignPolicyProject($projectPath: ID!, $id: ProjectID!) {
securityPolicyProjectAssign(input: { projectPath: $projectPath, securityPolicyProjectId: $id }) {
errors
}
}
mutation createPolicyProject($projectPath: ID!) {
securityPolicyProjectCreate(input: { projectPath: $projectPath }) {
project {
fullPath
id
}
errors
}
}
mutation updatePolicy(
$projectPath: ID!
$mode: MutationOperationMode = APPEND
$policyYaml: String!
) {
scanExecutionPolicyCommit(
input: { projectPath: $projectPath, operationMode: $mode, policyYaml: $policyYaml }
) {
branch
errors
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import PolicyEditorApp from './components/policy_editor/policy_editor.vue'; import PolicyEditorApp from './components/policy_editor/policy_editor.vue';
import { DEFAULT_ASSIGNED_POLICY_PROJECT } from './constants';
import createStore from './store'; import createStore from './store';
import { gqClient } from './utils';
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: gqClient,
}); });
export default () => { export default () => {
const el = document.querySelector('#js-policy-builder-app'); const el = document.querySelector('#js-policy-builder-app');
const { const {
assignedPolicyProject,
disableScanExecutionUpdate,
environmentsEndpoint, environmentsEndpoint,
configureAgentHelpPath, configureAgentHelpPath,
createAgentHelpPath, createAgentHelpPath,
...@@ -36,7 +40,16 @@ export default () => { ...@@ -36,7 +40,16 @@ export default () => {
store.dispatch('threatMonitoring/setCurrentEnvironmentId', parseInt(environmentId, 10)); store.dispatch('threatMonitoring/setCurrentEnvironmentId', parseInt(environmentId, 10));
} }
const props = policy ? { existingPolicy: JSON.parse(policy) } : {}; const policyProject = JSON.parse(assignedPolicyProject);
const props = {
assignedPolicyProject: policyProject
? convertObjectPropsToCamelCase(policyProject)
: DEFAULT_ASSIGNED_POLICY_PROJECT,
};
if (policy) {
props.existingPolicy = JSON.parse(policy);
}
return new Vue({ return new Vue({
el, el,
...@@ -44,6 +57,7 @@ export default () => { ...@@ -44,6 +57,7 @@ export default () => {
provide: { provide: {
configureAgentHelpPath, configureAgentHelpPath,
createAgentHelpPath, createAgentHelpPath,
disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate),
projectId, projectId,
projectPath, projectPath,
threatMonitoringPath, threatMonitoringPath,
......
import createGqClient from '~/lib/graphql';
import { POLICY_KINDS } from './components/constants'; import { POLICY_KINDS } from './components/constants';
/** /**
...@@ -36,3 +37,8 @@ export const getPolicyKind = (yaml = '') => { ...@@ -36,3 +37,8 @@ export const getPolicyKind = (yaml = '') => {
export const removeUnnecessaryDashes = (manifest) => { export const removeUnnecessaryDashes = (manifest) => {
return manifest.replace('---\n', ''); return manifest.replace('---\n', '');
}; };
/**
* Create GraphQL Client for threat monitoring
*/
export const gqClient = createGqClient();
...@@ -27,7 +27,11 @@ module PolicyHelper ...@@ -27,7 +27,11 @@ module PolicyHelper
private private
def details(project) def details(project)
disable_scan_execution_update = !can_update_security_orchestration_policy_project?(project)
{ {
assigned_policy_project: assigned_policy_project(project).to_json,
disable_scan_execution_update: disable_scan_execution_update.to_s,
network_policies_endpoint: project_security_network_policies_path(project), network_policies_endpoint: project_security_network_policies_path(project),
configure_agent_help_path: help_page_url('user/clusters/agent/repository.html'), configure_agent_help_path: help_page_url('user/clusters/agent/repository.html'),
create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'), create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'),
......
...@@ -7,6 +7,11 @@ module Projects::Security::PoliciesHelper ...@@ -7,6 +7,11 @@ module Projects::Security::PoliciesHelper
orchestration_policy_configuration = project.security_orchestration_policy_configuration orchestration_policy_configuration = project.security_orchestration_policy_configuration
security_policy_management_project = orchestration_policy_configuration.security_policy_management_project security_policy_management_project = orchestration_policy_configuration.security_policy_management_project
{ id: security_policy_management_project.to_global_id.to_s, name: security_policy_management_project.name } {
id: security_policy_management_project.to_global_id.to_s,
name: security_policy_management_project.name,
full_path: security_policy_management_project.full_path,
branch: security_policy_management_project.default_branch_or_main
}
end end
end end
...@@ -67,6 +67,7 @@ describe('PolicyEditorLayout component', () => { ...@@ -67,6 +67,7 @@ describe('PolicyEditorLayout component', () => {
it('does display custom save button text', () => { it('does display custom save button text', () => {
const saveButton = findSavePolicyButton(); const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true); expect(saveButton.exists()).toBe(true);
expect(saveButton.attributes('disabled')).toBe(undefined);
expect(saveButton.text()).toBe('Create policy'); expect(saveButton.text()).toBe('Create policy');
}); });
}); });
...@@ -122,4 +123,12 @@ describe('PolicyEditorLayout component', () => { ...@@ -122,4 +123,12 @@ describe('PolicyEditorLayout component', () => {
expect(wrapper.emitted('update-yaml')).toStrictEqual([[newManifest]]); expect(wrapper.emitted('update-yaml')).toStrictEqual([[newManifest]]);
}); });
}); });
describe('disabled actions', () => {
it('disables the save button', async () => {
factory({ propsData: { disableUpdate: true } });
expect(findSavePolicyButton().exists()).toBe(true);
expect(findSavePolicyButton().attributes('disabled')).toBe('true');
});
});
}); });
import { GlFormSelect } from '@gitlab/ui'; import { GlAlert, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import EnvironmentPicker from 'ee/threat_monitoring/components/environment_picker.vue'; import EnvironmentPicker from 'ee/threat_monitoring/components/environment_picker.vue';
import { POLICY_KIND_OPTIONS } from 'ee/threat_monitoring/components/policy_editor/constants'; import { POLICY_KIND_OPTIONS } from 'ee/threat_monitoring/components/policy_editor/constants';
import NetworkPolicyEditor from 'ee/threat_monitoring/components/policy_editor/network_policy/network_policy_editor.vue'; import NetworkPolicyEditor from 'ee/threat_monitoring/components/policy_editor/network_policy/network_policy_editor.vue';
import PolicyEditor from 'ee/threat_monitoring/components/policy_editor/policy_editor.vue'; import PolicyEditor from 'ee/threat_monitoring/components/policy_editor/policy_editor.vue';
import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/threat_monitoring/constants';
import createStore from 'ee/threat_monitoring/store'; import createStore from 'ee/threat_monitoring/store';
import { mockL3Manifest } from '../../mocks/mock_data'; import { mockL3Manifest } from '../../mocks/mock_data';
...@@ -11,6 +12,7 @@ describe('PolicyEditor component', () => { ...@@ -11,6 +12,7 @@ describe('PolicyEditor component', () => {
let store; let store;
let wrapper; let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findEnvironmentPicker = () => wrapper.findComponent(EnvironmentPicker); const findEnvironmentPicker = () => wrapper.findComponent(EnvironmentPicker);
const findFormSelect = () => wrapper.findComponent(GlFormSelect); const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findNeworkPolicyEditor = () => wrapper.findComponent(NetworkPolicyEditor); const findNeworkPolicyEditor = () => wrapper.findComponent(NetworkPolicyEditor);
...@@ -21,7 +23,10 @@ describe('PolicyEditor component', () => { ...@@ -21,7 +23,10 @@ describe('PolicyEditor component', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve()); jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = shallowMount(PolicyEditor, { wrapper = shallowMount(PolicyEditor, {
propsData, propsData: {
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
...propsData,
},
provide, provide,
store, store,
stubs: { GlFormSelect }, stubs: { GlFormSelect },
...@@ -35,8 +40,13 @@ describe('PolicyEditor component', () => { ...@@ -35,8 +40,13 @@ describe('PolicyEditor component', () => {
describe('default', () => { describe('default', () => {
beforeEach(factory); beforeEach(factory);
it('renders the environment picker', () => { it.each`
expect(findEnvironmentPicker().exists()).toBe(true); component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'NetworkPolicyEditor component'} | ${'does display'} | ${findNeworkPolicyEditor} | ${true}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
}); });
it('renders the disabled form select', () => { it('renders the disabled form select', () => {
...@@ -46,8 +56,13 @@ describe('PolicyEditor component', () => { ...@@ -46,8 +56,13 @@ describe('PolicyEditor component', () => {
expect(formSelect.attributes('disabled')).toBe('true'); expect(formSelect.attributes('disabled')).toBe('true');
}); });
it('renders the "NetworkPolicyEditor" component', () => { it('shows an alert when "error" is emitted from the component', async () => {
expect(findNeworkPolicyEditor().exists()).toBe(true); const errorMessage = 'test';
findNeworkPolicyEditor().vm.$emit('error', errorMessage);
await wrapper.vm.$nextTick();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(errorMessage);
}); });
}); });
......
import { savePolicy } from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib/utils';
import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/threat_monitoring/constants';
import assignPolicyProject from 'ee/threat_monitoring/graphql/mutations/assign_policy_project.mutation.graphql';
import createPolicyProject from 'ee/threat_monitoring/graphql/mutations/create_policy_project.mutation.graphql';
import createScanExecutionPolicy from 'ee/threat_monitoring/graphql/mutations/create_scan_execution_policy.mutation.graphql';
import { gqClient } from 'ee/threat_monitoring/utils';
import createMergeRequestMutation from '~/graphql_shared/mutations/create_merge_request.mutation.graphql';
jest.mock('ee/threat_monitoring/utils');
const defaultAssignedPolicyProject = { fullPath: 'path/to/policy-project', branch: 'main' };
const newAssignedPolicyProject = { fullPath: 'path/to/new-project', branch: 'main' };
const projectPath = 'path/to/current-project';
const yamlEditorValue = 'some yaml';
const createSavePolicyInput = (assignedPolicyProject = defaultAssignedPolicyProject) => ({
assignedPolicyProject,
projectPath,
yamlEditorValue,
});
const error = 'There was an error';
const mockApolloResponses = (shouldReject) => {
return ({ mutation }) => {
if (mutation === createPolicyProject) {
return Promise.resolve({
data: {
securityPolicyProjectCreate: {
project: newAssignedPolicyProject,
errors: [],
},
},
});
} else if (mutation === assignPolicyProject) {
return Promise.resolve({
data: {
securityPolicyProjectAssign: { errors: [] },
},
});
} else if (mutation === createScanExecutionPolicy) {
return Promise.resolve({
data: {
scanExecutionPolicyCommit: {
branch: 'new-branch',
errors: shouldReject ? [error] : [],
},
},
});
} else if (mutation === createMergeRequestMutation) {
return Promise.resolve({
data: { mergeRequestCreate: { mergeRequest: { iid: '01' }, errors: [] } },
});
}
return Promise.resolve();
};
};
describe('savePolicy', () => {
it('returns the policy project and merge request on success when a policy project does not exist', async () => {
gqClient.mutate.mockImplementation(mockApolloResponses());
const { currentAssignedPolicyProject, mergeRequest } = await savePolicy(
createSavePolicyInput(DEFAULT_ASSIGNED_POLICY_PROJECT),
);
expect(currentAssignedPolicyProject).toStrictEqual({ ...newAssignedPolicyProject, errors: [] });
expect(mergeRequest).toStrictEqual({ id: '01', errors: [] });
});
it('returns the policy project and merge request on success when a policy project does exist', async () => {
gqClient.mutate.mockImplementation(mockApolloResponses());
const { currentAssignedPolicyProject, mergeRequest } = await savePolicy(
createSavePolicyInput(),
);
expect(currentAssignedPolicyProject).toStrictEqual(defaultAssignedPolicyProject);
expect(mergeRequest).toStrictEqual({ id: '01', errors: [] });
});
it('throws when an error is detected', async () => {
gqClient.mutate.mockImplementation(mockApolloResponses(true));
await expect(savePolicy(createSavePolicyInput())).rejects.toThrowError(error);
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue'; import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY } from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib'; import {
DEFAULT_SCAN_EXECUTION_POLICY,
savePolicy,
} from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
import ScanExecutionPolicyEditor from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/scan_execution_policy_editor.vue'; import ScanExecutionPolicyEditor from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/scan_execution_policy_editor.vue';
import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/threat_monitoring/constants';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
jest.mock('ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib', () => ({
DEFAULT_SCAN_EXECUTION_POLICY: jest.requireActual(
'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib',
).DEFAULT_SCAN_EXECUTION_POLICY,
fromYaml: jest.requireActual(
'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib',
).fromYaml,
savePolicy: jest.fn().mockResolvedValue({
currentAssignedPolicyProject: { fullPath: 'tests' },
mergeRequest: { id: '2' },
}),
}));
describe('ScanExecutionPolicyEditor', () => { describe('ScanExecutionPolicyEditor', () => {
let wrapper; let wrapper;
const defaultProjectPath = 'path/to/project';
const factory = ({ propsData = {} } = {}) => { const factory = ({ propsData = {} } = {}) => {
wrapper = shallowMount(ScanExecutionPolicyEditor, { wrapper = shallowMount(ScanExecutionPolicyEditor, {
propsData, propsData: {
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
...propsData,
},
provide: { provide: {
threatMonitoringPath: '', disableScanExecutionUpdate: false,
projectId: 1, projectId: 1,
projectPath: defaultProjectPath,
}, },
}); });
}; };
...@@ -34,4 +62,18 @@ describe('ScanExecutionPolicyEditor', () => { ...@@ -34,4 +62,18 @@ describe('ScanExecutionPolicyEditor', () => {
await findPolicyEditorLayout().vm.$emit('update-yaml', newManifest); await findPolicyEditorLayout().vm.$emit('update-yaml', newManifest);
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest); expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest);
}); });
it('saves the policy when "savePolicy" is emitted', async () => {
findPolicyEditorLayout().vm.$emit('save-policy');
await wrapper.vm.$nextTick();
expect(savePolicy).toHaveBeenCalledTimes(1);
expect(savePolicy).toHaveBeenCalledWith({
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
projectPath: defaultProjectPath,
yamlEditorValue: DEFAULT_SCAN_EXECUTION_POLICY,
});
await wrapper.vm.$nextTick();
expect(visitUrl).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith('/tests/-/merge_requests/2');
});
}); });
...@@ -17,6 +17,8 @@ RSpec.describe PolicyHelper do ...@@ -17,6 +17,8 @@ RSpec.describe PolicyHelper do
let(:base_data) do let(:base_data) do
{ {
assigned_policy_project: "null",
disable_scan_execution_update: "false",
network_policies_endpoint: kind_of(String), network_policies_endpoint: kind_of(String),
configure_agent_help_path: kind_of(String), configure_agent_help_path: kind_of(String),
create_agent_help_path: kind_of(String), create_agent_help_path: kind_of(String),
...@@ -28,6 +30,13 @@ RSpec.describe PolicyHelper do ...@@ -28,6 +30,13 @@ RSpec.describe PolicyHelper do
end end
describe '#policy_details' do describe '#policy_details' do
let(:owner) { project.owner }
before do
allow(helper).to receive(:current_user) { owner }
allow(helper).to receive(:can?).with(owner, :update_security_orchestration_policy_project, project) { true }
end
context 'when a new policy is being created' do context 'when a new policy is being created' do
subject { helper.policy_details(project) } subject { helper.policy_details(project) }
......
...@@ -29442,6 +29442,12 @@ msgstr "" ...@@ -29442,6 +29442,12 @@ msgstr ""
msgid "SecurityOrchestration|Select security project" msgid "SecurityOrchestration|Select security project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|There was a problem creating the new security policy"
msgstr ""
msgid "SecurityOrchestration|Update scan execution policies"
msgstr ""
msgid "SecurityPolicies|+%{count} more" msgid "SecurityPolicies|+%{count} more"
msgstr "" msgstr ""
......
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