diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section_new.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section_new.vue new file mode 100644 index 0000000000000000000000000000000000000000..79e776fe7066976e4c865cb1d21587f85c80db23 --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section_new.vue @@ -0,0 +1,48 @@ +<script> +import { REQUIRE_APPROVAL_TYPE } from '../lib'; +import ApproverAction from './approver_action.vue'; +import BotCommentAction from './bot_comment_action.vue'; + +export default { + name: 'ActionSection', + components: { + ApproverAction, + BotCommentAction, + }, + props: { + errors: { + type: Array, + required: false, + default: () => [], + }, + initAction: { + type: Object, + required: true, + }, + existingApprovers: { + type: Object, + required: true, + }, + }, + computed: { + isApproverAction() { + return this.initAction.type === REQUIRE_APPROVAL_TYPE; + }, + }, +}; +</script> + +<template> + <approver-action + v-if="isApproverAction" + class="gl-mb-4" + :init-action="initAction" + :errors="errors.action" + :existing-approvers="existingApprovers" + @error="$emit('error')" + @updateApprovers="$emit('updateApprovers', $event)" + @changed="$emit('changed', $event)" + @remove="$emit('remove')" + /> + <bot-comment-action v-else class="gl-mb-4" :init-action="initAction" @remove="$emit('remove')" /> +</template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue similarity index 96% rename from ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section.vue rename to ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue index b447a44f4d0e547037e905df019d4e12012721dd..2a1b5781696ab439ff49f13dbc64ab8952f756d2 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_section.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue @@ -10,12 +10,13 @@ import { actionHasType, } from '../lib/actions'; import SectionLayout from '../../section_layout.vue'; -import ActionApprovers from './action_approvers.vue'; +import ApproverSelectionWrapper from './approver_selection_wrapper.vue'; export default { + name: 'ApproverAction', components: { GlAlert, - ActionApprovers, + ApproverSelectionWrapper, SectionLayout, }, inject: ['namespaceId'], @@ -137,7 +138,7 @@ export default { </gl-alert> <section-layout content-classes="gl-py-5 gl-pr-5 gl-bg-white" @remove="$emit('remove')"> <template #content> - <action-approvers + <approver-selection-wrapper v-for="({ id, type }, i) in approverTypeTracker" :key="id" :approver-index="i" diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_approvers.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper.vue similarity index 100% rename from ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/action_approvers.vue rename to ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper.vue diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action.vue new file mode 100644 index 0000000000000000000000000000000000000000..9d5e6d9ff0709fd804168d3abb6793490f90a033 --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action.vue @@ -0,0 +1,18 @@ +<script> +import SectionLayout from '../../section_layout.vue'; +import { BOT_COMMENT_TYPE } from '../lib'; + +export default { + name: 'BotCommentAction', + components: { + SectionLayout, + }, + BOT_COMMENT_TYPE, +}; +</script> + +<template> + <section-layout @remove="$emit('remove')"> + <template #content>{{ $options.BOT_COMMENT_TYPE }}</template> + </section-layout> +</template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/editor_component.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/editor_component.vue index 049d821511c8f7703e87c377da76ff52cb4c9c7d..ec74a22a3d73a4a414eb47ea91c9058c70a31f67 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/editor_component.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/editor_component.vue @@ -4,6 +4,7 @@ import { isEmpty } from 'lodash'; import { GlAlert, GlEmptyState, GlButton } from '@gitlab/ui'; import { joinPaths, visitUrl, setUrlFragment } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { isGroup, isProject } from 'ee/security_orchestration/components/utils'; import { ADD_ACTION_LABEL, @@ -22,7 +23,8 @@ import EditorLayout from '../editor_layout.vue'; import { assignSecurityPolicyProject, modifyPolicy } from '../utils'; import DimDisableContainer from '../dim_disable_container.vue'; import SettingsSection from './settings/settings_section.vue'; -import ActionSection from './action/action_section.vue'; +import ActionSection from './action/action_section_new.vue'; +import ApproverAction from './action/approver_action.vue'; import RuleSection from './rule/rule_section.vue'; import { @@ -75,6 +77,7 @@ export default { }, components: { ActionSection, + ApproverAction, DimDisableContainer, GlAlert, GlButton, @@ -83,6 +86,7 @@ export default { RuleSection, SettingsSection, }, + mixins: [glFeatureFlagsMixin()], inject: [ 'disableScanPolicyUpdate', 'policyEditorEmptyStateSvgPath', @@ -138,6 +142,9 @@ export default { disableUpdate() { return !this.hasParsingError && this.hasEmptyActions && this.hasEmptySettings; }, + isProject() { + return isProject(this.namespaceType); + }, settings() { return buildSettingsList({ settings: this.policy.approval_settings, @@ -198,6 +205,9 @@ export default { description: this.$options.i18n.settingWarningDescription, }; }, + showBotCommentAction() { + return this.isProject && this.glFeatures.approvalPolicyDisableBotComment; + }, }, watch: { invalidBranches(branches) { @@ -216,9 +226,10 @@ export default { this.$set(this.policy, 'actions', [buildApprovalAction()]); this.updateYamlEditorValue(this.policy); }, - removeAction() { + removeAction(index) { const { actions, ...newPolicy } = this.policy; - this.policy = newPolicy; + actions.splice(index, 1); + this.policy = { ...newPolicy, ...(actions.length ? { actions } : {}) }; this.updateYamlEditorValue(this.policy); this.updatePolicyApprovers({}); }, @@ -340,7 +351,7 @@ export default { if (this.isActiveRuleMode) { this.hasParsingError = this.invalidForRuleMode(); - if (!this.hasEmptyRules && isProject(this.namespaceType) && this.rulesHaveBranches) { + if (!this.hasEmptyRules && this.isProject && this.rulesHaveBranches) { this.invalidBranches = await getInvalidBranches({ branches: this.allBranches, projectId: this.namespaceId, @@ -430,21 +441,37 @@ export default { </template> <div v-if="Boolean(policy.actions)"> - <action-section - v-for="(action, index) in policy.actions" - :key="action.id" - :data-testid="`action-${index}`" - class="gl-mb-4" - :init-action="action" - :errors="errors.action" - :existing-approvers="existingApprovers" - @error="handleParsingError" - @updateApprovers="updatePolicyApprovers" - @changed="updateAction(index, $event)" - @remove="removeAction" - /> + <div v-if="showBotCommentAction"> + <action-section + v-for="(action, index) in policy.actions" + :key="action.id" + :data-testid="`action-${index}`" + class="gl-mb-4" + :init-action="action" + :errors="errors.action" + :existing-approvers="existingApprovers" + @error="handleParsingError" + @updateApprovers="updatePolicyApprovers" + @changed="updateAction(index, $event)" + @remove="removeAction(index)" + /> + </div> + <div v-else> + <approver-action + v-for="(action, index) in policy.actions" + :key="action.id" + :data-testid="`action-${index}`" + class="gl-mb-4" + :init-action="action" + :errors="errors.action" + :existing-approvers="existingApprovers" + @error="handleParsingError" + @updateApprovers="updatePolicyApprovers" + @changed="updateAction(index, $event)" + @remove="removeAction(index)" + /> + </div> </div> - <div v-else class="gl-bg-gray-10 gl-rounded-base gl-p-5 gl-mb-5"> <gl-button variant="link" data-testid="add-action" icon="plus" @click="addAction"> {{ $options.i18n.ADD_ACTION_LABEL }} diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/actions.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/actions.js index 8fb8bae933a522f19a770f105315052de5b285b9..12585f321aa887b8daf556abb82057b44199aa5c 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/actions.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/actions.js @@ -108,6 +108,10 @@ export const MULTIPLE_APPROVER_TYPES_HUMANIZED_TEMPLATE = s__('SecurityOrchestra export const DEFAULT_APPROVER_DROPDOWN_TEXT = s__('SecurityOrchestration|Choose approver type'); +export const REQUIRE_APPROVAL_TYPE = 'require_approval'; + +export const BOT_COMMENT_TYPE = 'send_bot_message'; + export const buildApprovalAction = () => { - return { type: 'require_approval', approvals_required: 1, id: uniqueId('action_') }; + return { type: REQUIRE_APPROVAL_TYPE, approvals_required: 1, id: uniqueId('action_') }; }; diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/index.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/index.js index 8005819ff2963fb4a5822f3bf892596ee7879eae..fb52ed7cdd3e6e760a5d58427a663031227c994d 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/index.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/index.js @@ -1,12 +1,7 @@ export { createPolicyObject, fromYaml } from './from_yaml'; export { policyToYaml } from './to_yaml'; export * from './rules'; -export { - approversOutOfSync, - APPROVER_TYPE_DICT, - APPROVER_TYPE_LIST_ITEMS, - buildApprovalAction, -} from './actions'; +export * from './actions'; export * from './settings'; export * from './vulnerability_states'; export * from './filters'; diff --git a/ee/app/controllers/projects/security/policies_controller.rb b/ee/app/controllers/projects/security/policies_controller.rb index aa96b95dde73e02ec1aeac271ded5be73106df8a..41b96e50844d0bd59f19e5a3e890b9d0df249b2b 100644 --- a/ee/app/controllers/projects/security/policies_controller.rb +++ b/ee/app/controllers/projects/security/policies_controller.rb @@ -15,6 +15,7 @@ class PoliciesController < Projects::ApplicationController push_frontend_feature_flag(:compliance_pipeline_in_policies, project) push_frontend_feature_flag(:pipeline_execution_policy_type, project.group) if project.group push_frontend_feature_flag(:security_policies_breaking_changes, project) + push_frontend_feature_flag(:approval_policy_disable_bot_comment, project) end feature_category :security_policy_management diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_new_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_new_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..12fafa6d249ce7c1f1c74558e09771bb00b4ce97 --- /dev/null +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_new_spec.js @@ -0,0 +1,71 @@ +import { shallowMount } from '@vue/test-utils'; +import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section_new.vue'; +import ApproverAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue'; +import BotCommentAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action.vue'; +import { + BOT_COMMENT_TYPE, + REQUIRE_APPROVAL_TYPE, +} from 'ee/security_orchestration/components/policy_editor/scan_result/lib'; + +describe('ActionSection', () => { + let wrapper; + + const defaultProps = { + initAction: { type: REQUIRE_APPROVAL_TYPE }, + existingApprovers: {}, + }; + const factory = ({ props = {} } = {}) => { + wrapper = shallowMount(ActionSection, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findApproverAction = () => wrapper.findComponent(ApproverAction); + const findBotCommentAction = () => wrapper.findComponent(BotCommentAction); + + describe('Approval Action', () => { + beforeEach(() => { + factory(); + }); + + it('renders an approver action for that type of action', () => { + expect(findApproverAction().exists()).toBe(true); + expect(findBotCommentAction().exists()).toBe(false); + }); + + describe('events', () => { + it('passes through the "error" event', () => { + findApproverAction().vm.$emit('error'); + expect(wrapper.emitted('error')).toEqual([[]]); + }); + + it('passes through the "update-approvers" event', () => { + const event = 'event'; + findApproverAction().vm.$emit('updateApprovers', event); + expect(wrapper.emitted('updateApprovers')).toEqual([[event]]); + }); + + it('passes through the "changed" event', () => { + const event = 'event'; + findApproverAction().vm.$emit('changed', event); + expect(wrapper.emitted('changed')).toEqual([[event]]); + }); + + it('passes through the "remove" event', () => { + findApproverAction().vm.$emit('remove'); + expect(wrapper.emitted('remove')).toEqual([[]]); + }); + }); + }); + + describe('Bot Comment Action', () => { + it('renders a bot comment action for that type of action', () => { + factory({ props: { initAction: { type: BOT_COMMENT_TYPE } } }); + expect(findBotCommentAction().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(false); + }); + }); +}); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_action_spec.js similarity index 90% rename from ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_spec.js rename to ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_action_spec.js index 3d6c614e3d64d6b82bcee6c27bcfb80ef7acc831..e7d220d2cadcb6506531d13ef8e8819757448963 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_section_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_action_spec.js @@ -2,12 +2,12 @@ import { nextTick } from 'vue'; import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { GROUP_TYPE, USER_TYPE, ROLE_TYPE } from 'ee/security_orchestration/constants'; -import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section.vue'; -import ActionApprovers from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_approvers.vue'; +import ApproverAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue'; +import ApproverSelectionWrapper from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper.vue'; import { APPROVER_TYPE_LIST_ITEMS } from 'ee/security_orchestration/components/policy_editor/scan_result/lib/actions'; import SectionLayout from 'ee/security_orchestration/components/policy_editor/section_layout.vue'; -describe('ActionSection', () => { +describe('ApproverAction', () => { let wrapper; const APPROVERS_IDS = [1, 2, 3]; @@ -59,7 +59,7 @@ describe('ActionSection', () => { }; const createWrapper = (propsData = {}) => { - wrapper = shallowMount(ActionSection, { + wrapper = shallowMount(ApproverAction, { propsData: { initAction: DEFAULT_ACTION, existingApprovers: {}, @@ -71,8 +71,8 @@ describe('ActionSection', () => { }); }; - const findActionApprover = () => wrapper.findComponent(ActionApprovers); - const findAllActionApprovers = () => wrapper.findAllComponents(ActionApprovers); + const findActionApprover = () => wrapper.findComponent(ApproverSelectionWrapper); + const findAllApproverSelectionWrapper = () => wrapper.findAllComponents(ApproverSelectionWrapper); const findAllAlerts = () => wrapper.findAllComponents(GlAlert); const findSectionLayout = () => wrapper.findComponent(SectionLayout); @@ -97,9 +97,9 @@ describe('ActionSection', () => { }); it('creates a new approver on "addApproverType"', async () => { - expect(findAllActionApprovers()).toHaveLength(1); + expect(findAllApproverSelectionWrapper()).toHaveLength(1); await emit('addApproverType'); - expect(findAllActionApprovers()).toHaveLength(2); + expect(findAllApproverSelectionWrapper()).toHaveLength(2); }); it('does not render alert', () => { @@ -237,7 +237,7 @@ describe('ActionSection', () => { APPROVER_TYPE_LIST_ITEMS.filter((t) => t.value !== USER_TYPE), ); await emit('addApproverType'); - findAllActionApprovers().at(0).vm.$emit('removeApproverType', USER_TYPE); + findAllApproverSelectionWrapper().at(0).vm.$emit('removeApproverType', USER_TYPE); await nextTick(); expect(findActionApprover().props('availableTypes')).toEqual( expect.arrayContaining(APPROVER_TYPE_LIST_ITEMS), @@ -281,7 +281,7 @@ describe('ActionSection', () => { }); it('renders the user select when there are existing user approvers', () => { - expect(findAllActionApprovers()).toHaveLength(1); + expect(findAllApproverSelectionWrapper()).toHaveLength(1); expect(findActionApprover().props('approverType')).toBe(USER_TYPE); }); }); @@ -295,7 +295,7 @@ describe('ActionSection', () => { }); it('renders the group select when there are existing group approvers', () => { - expect(findAllActionApprovers()).toHaveLength(1); + expect(findAllApproverSelectionWrapper()).toHaveLength(1); expect(findActionApprover().props('approverType')).toBe(GROUP_TYPE); }); }); @@ -309,9 +309,9 @@ describe('ActionSection', () => { }); it('renders the user select with only the user approvers', () => { - expect(findAllActionApprovers()).toHaveLength(2); - expect(findAllActionApprovers().at(0).props('approverType')).toBe(GROUP_TYPE); - expect(findAllActionApprovers().at(1).props('approverType')).toBe(USER_TYPE); + expect(findAllApproverSelectionWrapper()).toHaveLength(2); + expect(findAllApproverSelectionWrapper().at(0).props('approverType')).toBe(GROUP_TYPE); + expect(findAllApproverSelectionWrapper().at(1).props('approverType')).toBe(USER_TYPE); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_approvers_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper_spec.js similarity index 96% rename from ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_approvers_spec.js rename to ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper_spec.js index 36f722d328fd76b1f23c958781328a545d1cf9cb..4ae4f179b3708c4828c7c57a9ec7534e8f882a08 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/action_approvers_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper_spec.js @@ -3,7 +3,7 @@ import { GlForm, GlFormInput, GlCollapsibleListbox, GlSprintf } from '@gitlab/ui import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SectionLayout from 'ee/security_orchestration/components/policy_editor/section_layout.vue'; import { GROUP_TYPE, USER_TYPE } from 'ee/security_orchestration/constants'; -import ActionApprovers from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_approvers.vue'; +import ApproverSelectionWrapper from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_selection_wrapper.vue'; import GroupSelect from 'ee/security_orchestration/components/policy_editor/scan_result/action/group_select.vue'; import UserSelect from 'ee/security_orchestration/components/policy_editor/scan_result/action/user_select.vue'; import { @@ -18,11 +18,11 @@ const DEFAULT_ACTION = { type: 'require_approval', }; -describe('ActionApprovers', () => { +describe('ApproverSelectionWrapper', () => { let wrapper; const factory = ({ propsData = {}, stubs = {} } = {}) => { - wrapper = shallowMountExtended(ActionApprovers, { + wrapper = shallowMountExtended(ApproverSelectionWrapper, { propsData: { availableTypes: APPROVER_TYPE_LIST_ITEMS, approverIndex: 0, diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..18e02b635536ff565ece09502e48fd32c21ee969 --- /dev/null +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action_spec.js @@ -0,0 +1,18 @@ +import { shallowMount } from '@vue/test-utils'; +import SectionLayout from 'ee/security_orchestration/components/policy_editor/section_layout.vue'; +import BotCommentAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/bot_comment_action.vue'; + +describe('BotCommentAction', () => { + let wrapper; + + const factory = () => { + wrapper = shallowMount(BotCommentAction); + }; + + const findSectionLayout = () => wrapper.findComponent(SectionLayout); + + it('renders', () => { + factory(); + expect(findSectionLayout().exists()).toBe(true); + }); +}); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/editor_component_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/editor_component_spec.js index 95f2f20a9d67da3b3f62459cd81097a7d4733b14..6e26ad7a47d50babef4bb00611fec1b18228fe4f 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/editor_component_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/editor_component_spec.js @@ -22,6 +22,7 @@ import { mockDefaultBranchesScanResultObject, mockDeprecatedScanResultManifest, mockDeprecatedScanResultObject, + mockBotCommentScanResultObject, } from 'ee_jest/security_orchestration/mocks/mock_scan_result_policy_data'; import { unsupportedManifest, @@ -47,7 +48,8 @@ import { PARSING_ERROR_MESSAGE, } from 'ee/security_orchestration/components/policy_editor/constants'; import DimDisableContainer from 'ee/security_orchestration/components/policy_editor/dim_disable_container.vue'; -import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section.vue'; +import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section_new.vue'; +import ApproverAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue'; import RuleSection from 'ee/security_orchestration/components/policy_editor/scan_result/rule/rule_section.vue'; jest.mock('lodash/uniqueId'); @@ -137,6 +139,8 @@ describe('EditorComponent', () => { const findPolicyEditorLayout = () => wrapper.findComponent(EditorLayout); const findActionSection = () => wrapper.findComponent(ActionSection); const findAllActionSections = () => wrapper.findAllComponents(ActionSection); + const findApproverAction = () => wrapper.findComponent(ApproverAction); + const findAllApproverActions = () => wrapper.findAllComponents(ApproverAction); const findAddActionButton = () => wrapper.findByTestId('add-action'); const findAddRuleButton = () => wrapper.findByTestId('add-rule'); const findAllDisabledComponents = () => wrapper.findAllComponents(DimDisableContainer); @@ -195,12 +199,6 @@ describe('EditorComponent', () => { expect(findAddRuleButton().exists()).toBe(true); }); - it('displays the initial action', () => { - factory(); - expect(findAllActionSections()).toHaveLength(1); - expect(findActionSection().props('existingApprovers')).toEqual(scanResultPolicyApprovers); - }); - describe('when a user is not an owner of the project', () => { it('displays the empty state with the appropriate properties', () => { factory({ provide: { disableScanPolicyUpdate: true } }); @@ -221,7 +219,7 @@ describe('EditorComponent', () => { mockDefaultBranchesScanResultManifest, ); expect(findAllRuleSections()).toHaveLength(1); - expect(findAllActionSections()).toHaveLength(1); + expect(findAllApproverActions()).toHaveLength(1); }); it('displays a scan result policy', () => { @@ -231,7 +229,7 @@ describe('EditorComponent', () => { mockDeprecatedScanResultManifest, ); expect(findAllRuleSections()).toHaveLength(1); - expect(findAllActionSections()).toHaveLength(1); + expect(findAllApproverActions()).toHaveLength(1); }); }); }); @@ -392,17 +390,27 @@ describe('EditorComponent', () => { }); }); - describe('action section', () => { + describe('action section when the "approvalPolicyDisableBotComment" feature is off', () => { + describe('rendering', () => { + it('displays the approver action when the "approvalPolicyDisableBotComment" feature is off', () => { + factory(); + expect(findAllApproverActions()).toHaveLength(1); + expect(findApproverAction().props('existingApprovers')).toEqual( + scanResultPolicyApprovers, + ); + }); + }); + describe('add', () => { it('hides the add button when actions exist', () => { factory(); - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findAddActionButton().exists()).toBe(false); }); it('shows the add button when actions do not exist', () => { factoryWithExistingPolicy({ hasActions: false }); - expect(findActionSection().exists()).toBe(false); + expect(findApproverAction().exists()).toBe(false); expect(findAddActionButton().exists()).toBe(true); }); }); @@ -410,6 +418,120 @@ describe('EditorComponent', () => { describe('remove', () => { it('removes the initial action', async () => { factory(); + expect(findApproverAction().exists()).toBe(true); + expect(findPolicyEditorLayout().props('policy')).toHaveProperty('actions'); + await findApproverAction().vm.$emit('remove'); + expect(findApproverAction().exists()).toBe(false); + expect(findPolicyEditorLayout().props('policy')).not.toHaveProperty('actions'); + }); + + it('removes the action approvers when the action is removed', async () => { + factory(); + await findApproverAction().vm.$emit( + 'changed', + mockDefaultBranchesScanResultObject.actions[0], + ); + await findApproverAction().vm.$emit('remove'); + await findAddActionButton().vm.$emit('click'); + expect(findPolicyEditorLayout().props('policy').actions).toEqual([ + { + approvals_required: 1, + type: 'require_approval', + id: 'action_0', + }, + ]); + expect(findApproverAction().props('existingApprovers')).toEqual({}); + }); + }); + + describe('update', () => { + beforeEach(() => { + factory(); + }); + + it('updates policy action when edited', async () => { + const UPDATED_ACTION = { type: 'required_approval', group_approvers_ids: [1] }; + await findApproverAction().vm.$emit('changed', UPDATED_ACTION); + + expect(findApproverAction().props('initAction')).toEqual(UPDATED_ACTION); + }); + + it('updates the policy approvers', async () => { + const newApprover = ['owner']; + + await findApproverAction().vm.$emit('updateApprovers', { + ...scanResultPolicyApprovers, + role: newApprover, + }); + + expect(findApproverAction().props('existingApprovers')).toMatchObject({ + role: newApprover, + }); + }); + + it('creates an error when the action section emits one', async () => { + await findApproverAction().vm.$emit('error'); + verifiesParsingError(); + }); + }); + }); + + describe('action section when the "approvalPolicyDisableBotComment" feature is on', () => { + describe('rendering', () => { + afterAll(() => { + uniqueId.mockRestore(); + }); + + it('displays the action section on the project-level when the "approvalPolicyDisableBotComment" feature is on', () => { + factory({ + glFeatures: { approvalPolicyDisableBotComment: true }, + provide: { namespaceType: NAMESPACE_TYPES.PROJECT }, + }); + expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(false); + }); + + it('displays the approver action on the group-level when the "approvalPolicyDisableBotComment" feature is on', () => { + factory({ + glFeatures: { approvalPolicyDisableBotComment: true }, + provide: { namespaceType: NAMESPACE_TYPES.GROUP }, + }); + expect(findActionSection().exists()).toBe(false); + expect(findApproverAction().exists()).toBe(true); + }); + + it('displays multiple action sections', () => { + uniqueId + .mockImplementationOnce(jest.fn((prefix) => `${prefix}0`)) + .mockImplementationOnce(jest.fn((prefix) => `${prefix}1`)); + factoryWithExistingPolicy({ + glFeatures: { approvalPolicyDisableBotComment: true }, + policy: mockBotCommentScanResultObject, + }); + expect(findAllActionSections()).toHaveLength(2); + }); + }); + + describe('add', () => { + it('hides the add button when actions exist', () => { + factory({ glFeatures: { approvalPolicyDisableBotComment: true } }); + expect(findActionSection().exists()).toBe(true); + expect(findAddActionButton().exists()).toBe(false); + }); + + it('shows the add button when actions do not exist', () => { + factoryWithExistingPolicy({ + hasActions: false, + glFeatures: { approvalPolicyDisableBotComment: true }, + }); + expect(findActionSection().exists()).toBe(false); + expect(findAddActionButton().exists()).toBe(true); + }); + }); + + describe('remove', () => { + it('removes the initial action', async () => { + factory({ glFeatures: { approvalPolicyDisableBotComment: true } }); expect(findActionSection().exists()).toBe(true); expect(findPolicyEditorLayout().props('policy')).toHaveProperty('actions'); await findActionSection().vm.$emit('remove'); @@ -418,7 +540,7 @@ describe('EditorComponent', () => { }); it('removes the action approvers when the action is removed', async () => { - factory(); + factory({ glFeatures: { approvalPolicyDisableBotComment: true } }); await findActionSection().vm.$emit( 'changed', mockDefaultBranchesScanResultObject.actions[0], @@ -438,7 +560,7 @@ describe('EditorComponent', () => { describe('update', () => { beforeEach(() => { - factory(); + factory({ glFeatures: { approvalPolicyDisableBotComment: true } }); }); it('updates policy action when edited', async () => { @@ -534,7 +656,7 @@ describe('EditorComponent', () => { await findPolicyEditorLayout().vm.$emit('save-policy'); await waitForPromises(); - expect(findActionSection().props('errors')).toEqual(error.cause); + expect(findApproverAction().props('errors')).toEqual(error.cause); expect(wrapper.emitted('error')).toStrictEqual([['']]); }); @@ -545,7 +667,7 @@ describe('EditorComponent', () => { await findPolicyEditorLayout().vm.$emit('save-policy'); await waitForPromises(); - expect(findActionSection().props('errors')).toEqual([]); + expect(findApproverAction().props('errors')).toEqual([]); expect(wrapper.emitted('error')).toStrictEqual([[''], [error.message]]); }); @@ -556,7 +678,7 @@ describe('EditorComponent', () => { await findPolicyEditorLayout().vm.$emit('save-policy'); await waitForPromises(); - expect(findActionSection().props('errors')).toEqual([]); + expect(findApproverAction().props('errors')).toEqual([]); expect(wrapper.emitted('error')).toStrictEqual([[''], [error.message]]); }); @@ -567,7 +689,7 @@ describe('EditorComponent', () => { await findPolicyEditorLayout().vm.$emit('save-policy'); await waitForPromises(); - expect(findActionSection().props('errors')).toEqual([approverCause]); + expect(findApproverAction().props('errors')).toEqual([approverCause]); expect(wrapper.emitted('error')).toStrictEqual([[''], ['There was an error']]); }); }); @@ -581,7 +703,7 @@ describe('EditorComponent', () => { await findPolicyEditorLayout().vm.$emit('save-policy'); await waitForPromises(); - expect(findActionSection().props('errors')).toEqual([]); + expect(findApproverAction().props('errors')).toEqual([]); expect(wrapper.emitted('error')).toStrictEqual([[''], [error.message]]); }); }); @@ -823,8 +945,7 @@ describe('EditorComponent', () => { describe('displays the danger alert when there are no actions and no settings', () => { beforeEach(() => { factoryWithExistingPolicy({ - hasActions: false, - policy: { approval_settings: { [BLOCK_BRANCH_MODIFICATION]: false } }, + policy: { actions: [], approval_settings: { [BLOCK_BRANCH_MODIFICATION]: false } }, }); }); diff --git a/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js b/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js index 3a85d93a8cc37e8dadc827c3ccdd70327c65b5c1..316f1d1c5d7b7e2a0bd31f7b1dbf8b897492cc01 100644 --- a/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js +++ b/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js @@ -58,6 +58,23 @@ export const mockDefaultBranchesScanResultObject = { ], }; +export const mockBotCommentScanResultObject = { + type: 'approval_policy', + name: 'Add bot comment', + description: '', + enabled: true, + rules: [], + actions: [ + { + type: 'require_approval', + approvals_required: 1, + }, + { + type: 'send_bot_comment', + }, + ], +}; + export const mockDeprecatedScanResultManifest = `type: scan_result_policy name: critical vulnerability CS approvals description: This policy enforces critical vulnerability CS approvals diff --git a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/actions_spec.js b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/actions_spec.js index 1ca2aec30d475c2e926366cac07df369a263633f..87016941fe51b1b69bd67f1d320bec2978be7b78 100644 --- a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/actions_spec.js +++ b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/actions_spec.js @@ -1,7 +1,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import * as urlUtils from '~/lib/utils/url_utility'; import App from 'ee/security_orchestration/components/policy_editor/app.vue'; -import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section.vue'; +import ApproverAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue'; import GroupSelect from 'ee/security_orchestration/components/policy_editor/scan_result/action/group_select.vue'; import RoleSelect from 'ee/security_orchestration/components/policy_editor/scan_result/action/role_select.vue'; import UserSelect from 'ee/security_orchestration/components/policy_editor/scan_result/action/user_select.vue'; @@ -52,7 +52,7 @@ describe('Scan result policy actions', () => { const findApprovalsInput = () => wrapper.findByTestId('approvals-required-input'); const findAvailableTypeListBox = () => wrapper.findByTestId('available-types'); - const findActionSection = () => wrapper.findComponent(ActionSection); + const findApproverAction = () => wrapper.findComponent(ApproverAction); const findGroupSelect = () => wrapper.findComponent(GroupSelect); const findRoleSelect = () => wrapper.findComponent(RoleSelect); const findUserSelect = () => wrapper.findComponent(UserSelect); @@ -63,7 +63,7 @@ describe('Scan result policy actions', () => { }); it('should render action section', () => { - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findYamlPreview(wrapper).text()).toContain( 'actions:\n - type: require_approval\n approvals_required: 1', ); @@ -83,9 +83,9 @@ describe('Scan result policy actions', () => { const DEVELOPER = 'developer'; const verifyRuleMode = () => { - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findRoleSelect().exists()).toBe(true); - expect(findActionSection().props('initAction').role_approvers).toEqual([DEVELOPER]); + expect(findApproverAction().props('initAction').role_approvers).toEqual([DEVELOPER]); }; await findAvailableTypeListBox().vm.$emit('select', ROLE_TYPE); @@ -103,9 +103,9 @@ describe('Scan result policy actions', () => { it('selects user approvers', async () => { const verifyRuleMode = () => { - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findUserSelect().exists()).toBe(true); - expect(findActionSection().props('initAction').user_approvers_ids).toEqual([USER.id]); + expect(findApproverAction().props('initAction').user_approvers_ids).toEqual([USER.id]); }; await findAvailableTypeListBox().vm.$emit('select', USER_TYPE); @@ -123,9 +123,9 @@ describe('Scan result policy actions', () => { it('selects group approvers', async () => { const verifyRuleMode = () => { - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findGroupSelect().exists()).toBe(true); - expect(findActionSection().props('initAction').group_approvers_ids).toEqual([GROUP.id]); + expect(findApproverAction().props('initAction').group_approvers_ids).toEqual([GROUP.id]); }; await findAvailableTypeListBox().vm.$emit('select', GROUP_TYPE); diff --git a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/scan_result_spec.js b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/scan_result_spec.js index b930bc137bc768c6207f9fa447557e9700b2e30c..64a8da364de682a30e7c7ea1feff3f1bf988e52b 100644 --- a/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/scan_result_spec.js +++ b/ee/spec/frontend_integration/security_orchestration/policy_editor/scan_result/scan_result_spec.js @@ -3,7 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import App from 'ee/security_orchestration/components/policy_editor/app.vue'; import SettingsSection from 'ee/security_orchestration/components/policy_editor/scan_result/settings/settings_section.vue'; import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/security_orchestration/constants'; -import ActionSection from 'ee/security_orchestration/components/policy_editor/scan_result/action/action_section.vue'; +import ApproverAction from 'ee/security_orchestration/components/policy_editor/scan_result/action/approver_action.vue'; import RuleSection from 'ee/security_orchestration/components/policy_editor/scan_result/rule/rule_section.vue'; import { DEFAULT_PROVIDE } from '../mocks/mocks'; @@ -31,7 +31,7 @@ describe('Policy Editor', () => { wrapper.findByTestId('select-policy-approval_policy'); const findYamlPreview = () => wrapper.findByTestId('rule-editor-preview'); const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const findActionSection = () => wrapper.findComponent(ActionSection); + const findApproverAction = () => wrapper.findComponent(ApproverAction); const findRuleSection = () => wrapper.findComponent(RuleSection); const findSettingsSection = () => wrapper.findComponent(SettingsSection); @@ -47,7 +47,7 @@ describe('Policy Editor', () => { describe('rendering', () => { it('renders the page correctly', () => { expect(findEmptyState().exists()).toBe(false); - expect(findActionSection().exists()).toBe(true); + expect(findApproverAction().exists()).toBe(true); expect(findRuleSection().exists()).toBe(true); expect(findSettingsSection().exists()).toBe(true); expect(findYamlPreview().exists()).toBe(true);