diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5146a44de839ff7369afe3630d164614a3807e3f..90cb8749d48216a571406f4cde0b2666e231c307 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -41,6 +41,10 @@ class ProjectsController < Projects::ApplicationController push_frontend_feature_flag(:service_desk_custom_address, @project) end + before_action only: [:edit] do + push_frontend_feature_flag(:approval_suggestions, @project) + end + layout :determine_layout def index diff --git a/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue b/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue index 4e05b4b229039c98c6a27916af9814cad500f1fc..370e9be76c48f20b96c4615c74f4871d731c5037 100644 --- a/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue +++ b/ee/app/assets/javascripts/approvals/components/modal_rule_create.vue @@ -25,7 +25,12 @@ export default { rule: 'data', }), title() { - return this.rule ? __('Update approval rule') : __('Add approval rule'); + return !this.rule || this.defaultRuleName + ? __('Add approval rule') + : __('Update approval rule'); + }, + defaultRuleName() { + return this.rule?.defaultRuleName; }, }, methods: { @@ -47,6 +52,11 @@ export default { size="sm" @ok.prevent="submit" > - + diff --git a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue index c252daaedc8cd4a1a00157de33e9362501f43ca0..146d581abc0418ce9e33338a2cee7f9012c4fd28 100644 --- a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue +++ b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue @@ -1,13 +1,16 @@ - - - - {{ hasNamedRule ? name : members }} - - {{ members }} - - {{ branches }} - {{ approvalsRequired }} - - - - - - - - {{ rule.name }} - - - - - - - - - - - - + + + + + {{ hasNamedRule ? name : members }} + + {{ members }} + + {{ branches }} + {{ approvalsRequired }} + - - + + + + + {{ rule.name }} + + + + + + + + + + + + + + + + + + + diff --git a/ee/app/assets/javascripts/approvals/components/rule_form.vue b/ee/app/assets/javascripts/approvals/components/rule_form.vue index a8b2643adae32bd11ec2e824fbf79b735e61cf61..17f5e53fa3a4ad7308942845eeada740e3ee24f4 100644 --- a/ee/app/assets/javascripts/approvals/components/rule_form.vue +++ b/ee/app/assets/javascripts/approvals/components/rule_form.vue @@ -1,4 +1,5 @@ + + + + + + + {{ rule.name }} + + + + {{ content }} + + + + + + + {{ __('Enable') }} + + + + + + + {{ rule.name }} + + + + {{ content }} + + + + + + diff --git a/ee/app/assets/javascripts/approvals/components/security_configuration/unconfigured_security_rules.vue b/ee/app/assets/javascripts/approvals/components/security_configuration/unconfigured_security_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..2b698cb11d745c29919c17d8ee48ac643e06050d --- /dev/null +++ b/ee/app/assets/javascripts/approvals/components/security_configuration/unconfigured_security_rules.vue @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + diff --git a/ee/app/assets/javascripts/approvals/constants.js b/ee/app/assets/javascripts/approvals/constants.js index 127a97e27aa160992758046da62f46fe412b42f7..cdfc6e179af516b2ffc9c53a5a54ffc1a35cc873 100644 --- a/ee/app/assets/javascripts/approvals/constants.js +++ b/ee/app/assets/javascripts/approvals/constants.js @@ -14,6 +14,15 @@ export const RULE_NAME_ANY_APPROVER = 'All Members'; export const VULNERABILITY_CHECK_NAME = 'Vulnerability-Check'; export const LICENSE_CHECK_NAME = 'License-Check'; +export const JOB_TYPES = { + SAST: 'sast', + DAST: 'dast', + DEPENDENCY_SCANNING: 'dependency_scanning', + SECRET_DETECTION: 'secret_detection', + COVERAGE_FUZZING: 'coverage_fuzzing', + LICENSE_SCANNING: 'license_scanning', +}; + export const APPROVAL_RULE_CONFIGS = { [VULNERABILITY_CHECK_NAME]: { title: __('Vulnerability-Check'), diff --git a/ee/app/assets/javascripts/approvals/mount_project_settings.js b/ee/app/assets/javascripts/approvals/mount_project_settings.js index 91cd298738c020e03d3dad00060af1325ff6df63..d08a630ca682deadd29db5ab2f365485a267bb64 100644 --- a/ee/app/assets/javascripts/approvals/mount_project_settings.js +++ b/ee/app/assets/javascripts/approvals/mount_project_settings.js @@ -12,6 +12,8 @@ export default function mountProjectSettingsApprovals(el) { return null; } + const { vulnerabilityCheckHelpPagePath, licenseCheckHelpPagePath } = el.dataset; + const store = createStore(projectSettingsModule(), { ...el.dataset, prefix: 'project-settings', @@ -22,6 +24,10 @@ export default function mountProjectSettingsApprovals(el) { return new Vue({ el, store, + provide: { + vulnerabilityCheckHelpPagePath, + licenseCheckHelpPagePath, + }, render(h) { return h(ProjectSettingsApp); }, diff --git a/ee/app/assets/javascripts/approvals/stores/index.js b/ee/app/assets/javascripts/approvals/stores/index.js index b17514eab74678f0cb7ba9b621973d04299ca357..3a915ca27becad5bd0c864e37e6825084e73d78d 100644 --- a/ee/app/assets/javascripts/approvals/stores/index.js +++ b/ee/app/assets/javascripts/approvals/stores/index.js @@ -1,5 +1,6 @@ import Vuex from 'vuex'; import modalModule from '~/vuex_shared/modules/modal'; +import securityConfigurationModule from 'ee/security_configuration/modules/configuration'; import state from './state'; export const createStoreOptions = (approvalsModule, settings) => ({ @@ -8,6 +9,9 @@ export const createStoreOptions = (approvalsModule, settings) => ({ ...(approvalsModule ? { approvals: approvalsModule } : {}), createModal: modalModule(), deleteModal: modalModule(), + securityConfiguration: securityConfigurationModule({ + securityConfigurationPath: settings?.securityConfigurationPath || '', + }), }, }); diff --git a/ee/app/assets/javascripts/security_configuration/modules/configuration/actions.js b/ee/app/assets/javascripts/security_configuration/modules/configuration/actions.js index 72fd566d60537a2aa3664e38abc9578d58b01854..d705d5ab142de933a5f4a1e813b8523961f18a61 100644 --- a/ee/app/assets/javascripts/security_configuration/modules/configuration/actions.js +++ b/ee/app/assets/javascripts/security_configuration/modules/configuration/actions.js @@ -2,9 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as Sentry from '@sentry/browser'; import * as types from './mutation_types'; -export const setSecurityConfigurationEndpoint = ({ commit }, endpoint) => - commit(types.SET_SECURITY_CONFIGURATION_ENDPOINT, endpoint); - +// eslint-disable-next-line import/prefer-default-export export const fetchSecurityConfiguration = ({ commit, state }) => { if (!state.securityConfigurationPath) { return commit(types.RECEIVE_SECURITY_CONFIGURATION_ERROR); diff --git a/ee/app/assets/javascripts/security_configuration/modules/configuration/index.js b/ee/app/assets/javascripts/security_configuration/modules/configuration/index.js index 68c81bb45096f154036c07678c400afaec9bb260..b9849719c571f7efa4233f94ef0b37127e56b8eb 100644 --- a/ee/app/assets/javascripts/security_configuration/modules/configuration/index.js +++ b/ee/app/assets/javascripts/security_configuration/modules/configuration/index.js @@ -1,10 +1,10 @@ -import state from './state'; +import createState from './state'; import mutations from './mutations'; import * as actions from './actions'; -export default { +export default ({ securityConfigurationPath = '' }) => ({ namespaced: true, - state, + state: createState({ securityConfigurationPath }), mutations, actions, -}; +}); diff --git a/ee/app/assets/javascripts/security_configuration/modules/configuration/mutation_types.js b/ee/app/assets/javascripts/security_configuration/modules/configuration/mutation_types.js index d5d664f6f2c8c286350faec3ef6c72d56798d436..e4c19eb65bb76401bb2642b69b430a4592ed680d 100644 --- a/ee/app/assets/javascripts/security_configuration/modules/configuration/mutation_types.js +++ b/ee/app/assets/javascripts/security_configuration/modules/configuration/mutation_types.js @@ -1,5 +1,3 @@ -export const SET_SECURITY_CONFIGURATION_ENDPOINT = 'SET_SECURITY_CONFIGURATION_ENDPOINT'; - export const REQUEST_SECURITY_CONFIGURATION = 'REQUEST_SECURITY_CONFIGURATION'; export const RECEIVE_SECURITY_CONFIGURATION_SUCCESS = 'RECEIVE_SECURITY_CONFIGURATION_SUCCESS'; export const RECEIVE_SECURITY_CONFIGURATION_ERROR = 'RECEIVE_SECURITY_CONFIGURATION_ERROR'; diff --git a/ee/app/assets/javascripts/security_configuration/modules/configuration/mutations.js b/ee/app/assets/javascripts/security_configuration/modules/configuration/mutations.js index a45bbbdaa838c7dee31722e111a71f941887678b..4eaf4ad9362b1a7abd69b94f671e33597adc95f1 100644 --- a/ee/app/assets/javascripts/security_configuration/modules/configuration/mutations.js +++ b/ee/app/assets/javascripts/security_configuration/modules/configuration/mutations.js @@ -1,9 +1,6 @@ import * as types from './mutation_types'; export default { - [types.SET_SECURITY_CONFIGURATION_ENDPOINT](state, payload) { - state.securityConfigurationPath = payload; - }, [types.REQUEST_SECURITY_CONFIGURATION](state) { state.isLoading = true; state.errorLoading = false; diff --git a/ee/app/assets/javascripts/security_configuration/modules/configuration/state.js b/ee/app/assets/javascripts/security_configuration/modules/configuration/state.js index 872f40846ad9497590279d41ae0c1b50244722f3..50229b4258b77f00ad38b36a7098edfb163f87bb 100644 --- a/ee/app/assets/javascripts/security_configuration/modules/configuration/state.js +++ b/ee/app/assets/javascripts/security_configuration/modules/configuration/state.js @@ -1,5 +1,5 @@ -export default () => ({ - securityConfigurationPath: '', +export default ({ securityConfigurationPath }) => ({ + securityConfigurationPath, isLoading: false, errorLoading: false, configuration: {}, diff --git a/ee/app/helpers/ee/projects_helper.rb b/ee/app/helpers/ee/projects_helper.rb index 4c0b948f0f8c50d3d041612a68d5479f57a88c7e..21b07f0f0bcc887d6add34366f86425a3457c07d 100644 --- a/ee/app/helpers/ee/projects_helper.rb +++ b/ee/app/helpers/ee/projects_helper.rb @@ -94,6 +94,24 @@ def remove_project_message(project) { date: date } end + def approvals_app_data(project = @project) + { data: { 'project_id': project.id, + 'can_edit': can_modify_approvers.to_s, + 'project_path': expose_path(api_v4_projects_path(id: project.id)), + 'settings_path': expose_path(api_v4_projects_approval_settings_path(id: project.id)), + 'rules_path': expose_path(api_v4_projects_approval_settings_rules_path(id: project.id)), + 'allow_multi_rule': project.multiple_approval_rules_available?.to_s, + 'eligible_approvers_docs_path': help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'), + 'security_approvals_help_page_path': help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate'), + 'security_configuration_path': project_security_configuration_path(project), + 'vulnerability_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-security-approvals-within-a-project'), + 'license_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-license-approvals-within-a-project') } } + end + + def can_modify_approvers(project = @project) + can?(current_user, :modify_approvers_rules, project) + end + def permanent_delete_message(project) message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc.') html_escape(message) % remove_message_data(project) diff --git a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml index f3ffd3c4fe557bfa04d3eda8fa4d1f3a57933f74..c44b203b26aafa92fadf073a6bdf9c8c48519d40 100644 --- a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml +++ b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml @@ -1,19 +1,11 @@ - can_override_approvers = project.can_override_approvers? -- can_modify_approvers = can?(current_user, :modify_approvers_rules, @project) - can_modify_merge_request_author_settings = can?(current_user, :modify_merge_request_author_setting, @project) - can_modify_merge_request_committer_settings = can?(current_user, :modify_merge_request_committer_setting, @project) .form-group = form.label :approver_ids, class: 'label-bold' do = _("Approval rules") - #js-mr-approvals-settings{ data: { 'project_id': @project.id, - 'can_edit': can_modify_approvers.to_s, - 'project_path': expose_path(api_v4_projects_path(id: @project.id)), - 'settings_path': expose_path(api_v4_projects_approval_settings_path(id: @project.id)), - 'rules_path': expose_path(api_v4_projects_approval_settings_rules_path(id: @project.id)), - 'allow_multi_rule': @project.multiple_approval_rules_available?.to_s, - 'eligible_approvers_docs_path': help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'), - 'security_approvals_help_page_path': help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')} } + #js-mr-approvals-settings{ approvals_app_data } .text-center.gl-mt-3 = sprite_icon('spinner', size: 24, css_class: 'gl-spinner') diff --git a/ee/spec/frontend/approvals/components/modal_rule_create_spec.js b/ee/spec/frontend/approvals/components/modal_rule_create_spec.js index 0ad0f05a0bce181f9755f89a309af9084cd59dc4..d8579d79a0c935ea12c20a317571e443d4a734bb 100644 --- a/ee/spec/frontend/approvals/components/modal_rule_create_spec.js +++ b/ee/spec/frontend/approvals/components/modal_rule_create_spec.js @@ -14,6 +14,11 @@ localVue.use(Vuex); describe('Approvals ModalRuleCreate', () => { let createModalState; let wrapper; + let modal; + let form; + + const findModal = () => wrapper.find(GlModalVuex); + const findForm = () => wrapper.find(RuleForm); const factory = (options = {}) => { const store = new Vuex.Store({ @@ -49,15 +54,13 @@ describe('Approvals ModalRuleCreate', () => { describe('without data', () => { beforeEach(() => { createModalState.data = null; + factory(); + modal = findModal(); + form = findForm(); }); it('renders modal', () => { - factory(); - - const modal = wrapper.find(GlModalVuex); - expect(modal.exists()).toBe(true); - expect(modal.props('modalModule')).toEqual(MODAL_MODULE); expect(modal.props('modalId')).toEqual(TEST_MODAL_ID); expect(modal.attributes('title')).toEqual('Add approval rule'); @@ -65,22 +68,12 @@ describe('Approvals ModalRuleCreate', () => { }); it('renders form', () => { - factory(); - - const modal = wrapper.find(GlModalVuex); - const form = modal.find(RuleForm); - expect(form.exists()).toBe(true); expect(form.props('initRule')).toEqual(null); }); it('when modal emits ok, submits form', () => { - factory(); - - const form = wrapper.find(RuleForm); form.vm.submit = jest.fn(); - - const modal = wrapper.find(GlModalVuex); modal.vm.$emit('ok', new Event('ok')); expect(form.vm.submit).toHaveBeenCalled(); @@ -90,27 +83,50 @@ describe('Approvals ModalRuleCreate', () => { describe('with data', () => { beforeEach(() => { createModalState.data = TEST_RULE; + factory(); + modal = findModal(); + form = findForm(); }); it('renders modal', () => { - factory(); - - const modal = wrapper.find(GlModalVuex); - expect(modal.exists()).toBe(true); - expect(modal.attributes('title')).toEqual('Update approval rule'); expect(modal.attributes('ok-title')).toEqual('Update approval rule'); }); it('renders form', () => { - factory(); + expect(form.exists()).toBe(true); + expect(form.props('initRule')).toEqual(TEST_RULE); + }); + }); - const modal = wrapper.find(GlModalVuex); - const form = modal.find(RuleForm); + describe('with approvalSuggestions feature flag', () => { + beforeEach(() => { + createModalState.data = { ...TEST_RULE, defaultRuleName: 'Vulnerability-Check' }; + factory({ + provide: { + glFeatures: { approvalSuggestions: true }, + }, + }); + modal = findModal(); + form = findForm(); + }); + + it('renders add rule modal', () => { + expect(modal.exists()).toBe(true); + expect(modal.attributes('title')).toEqual('Add approval rule'); + expect(modal.attributes('ok-title')).toEqual('Add approval rule'); + }); + + it('renders form with defaultRuleName', () => { + expect(form.props().defaultRuleName).toBe('Vulnerability-Check'); expect(form.exists()).toBe(true); - expect(form.props('initRule')).toEqual(TEST_RULE); + }); + + it('renders the form when passing in an existing rule', () => { + expect(form.exists()).toBe(true); + expect(form.props('initRule')).toEqual(createModalState.data); }); }); }); diff --git a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js index e4aef5046b48f07f1b83b444a56003296f29683c..0474c48500ff989f63df39ea1c1998878cc61d21 100644 --- a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js +++ b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js @@ -5,6 +5,7 @@ import projectSettingsModule from 'ee/approvals/stores/modules/project_settings' import ProjectRules from 'ee/approvals/components/project_settings/project_rules.vue'; import RuleInput from 'ee/approvals/components/mr_edit/rule_input.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; +import UnconfiguredSecurityRules from 'ee/approvals/components/security_configuration/unconfigured_security_rules.vue'; import { createProjectRules } from '../../mocks'; const TEST_RULES = createProjectRules(); @@ -29,11 +30,12 @@ describe('Approvals ProjectRules', () => { let wrapper; let store; - const factory = (props = {}) => { + const factory = (props = {}, options = {}) => { wrapper = mount(localVue.extend(ProjectRules), { propsData: props, store: new Vuex.Store(store), localVue, + ...options, }); }; @@ -121,5 +123,38 @@ describe('Approvals ProjectRules', () => { expect(nameCell.find('.js-help').exists()).toBeFalsy(); }); + + it('should not render the unconfigured-security-rules component', () => { + expect(wrapper.contains(UnconfiguredSecurityRules)).toBe(false); + }); }); + + describe.each([true, false])( + 'when the approvalSuggestions feature flag is %p', + approvalSuggestions => { + beforeEach(() => { + const rules = createProjectRules(); + rules[0].name = 'Vulnerability-Check'; + store.modules.approvals.state.rules = rules; + store.state.settings.allowMultiRule = true; + }); + + beforeEach(() => { + factory( + {}, + { + provide: { + glFeatures: { approvalSuggestions }, + }, + }, + ); + }); + + it(`should ${ + approvalSuggestions ? '' : 'not' + } render the unconfigured-security-rules component`, () => { + expect(wrapper.contains(UnconfiguredSecurityRules)).toBe(approvalSuggestions); + }); + }, + ); }); diff --git a/ee/spec/frontend/approvals/components/rule_form_spec.js b/ee/spec/frontend/approvals/components/rule_form_spec.js index 35402d78bb0d5516e90c710865c74b6e4377993e..2fde554ee887453546a867cf5d097e0690812197 100644 --- a/ee/spec/frontend/approvals/components/rule_form_spec.js +++ b/ee/spec/frontend/approvals/components/rule_form_spec.js @@ -39,13 +39,13 @@ describe('EE Approvals RuleForm', () => { let store; let actions; - const createComponent = (props = {}) => { + const createComponent = (props = {}, options = {}) => { wrapper = shallowMount(localVue.extend(RuleForm), { propsData: props, store: new Vuex.Store(store), localVue, provide: { - glFeatures: { scopedApprovalRules: true }, + glFeatures: { scopedApprovalRules: true, ...options.provide?.glFeatures }, }, }); }; @@ -482,6 +482,38 @@ describe('EE Approvals RuleForm', () => { }); }); + describe('with approvalSuggestions enabled', () => { + describe.each` + defaultRuleName | expectedDisabledAttribute + ${'Vulnerability-Check'} | ${'disabled'} + ${'License-Check'} | ${'disabled'} + ${'Foo Bar Baz'} | ${undefined} + `( + 'with defaultRuleName set to $defaultRuleName', + ({ defaultRuleName, expectedDisabledAttribute }) => { + beforeEach(() => { + createComponent( + { + initRule: null, + defaultRuleName, + }, + { + provide: { + glFeatures: { approvalSuggestions: true }, + }, + }, + ); + }); + + it(`it ${ + expectedDisabledAttribute ? 'disables' : 'does not disable' + } the name text field`, () => { + expect(findNameInput().attributes('disabled')).toBe(expectedDisabledAttribute); + }); + }, + ); + }); + describe('with new License-Check rule', () => { beforeEach(() => { createComponent({ @@ -494,6 +526,18 @@ describe('EE Approvals RuleForm', () => { }); }); + describe('with new Vulnerability-Check rule', () => { + beforeEach(() => { + createComponent({ + initRule: { ...TEST_RULE, id: null, name: 'Vulnerability-Check' }, + }); + }); + + it('does not disable the name text field', () => { + expect(findNameInput().attributes('disabled')).toBe(undefined); + }); + }); + describe('with editing the License-Check rule', () => { beforeEach(() => { createComponent({ @@ -505,6 +549,18 @@ describe('EE Approvals RuleForm', () => { expect(findNameInput().attributes('disabled')).toBe('disabled'); }); }); + + describe('with editing the Vulnerability-Check rule', () => { + beforeEach(() => { + createComponent({ + initRule: { ...TEST_RULE, name: 'Vulnerability-Check' }, + }); + }); + + it('disables the name text field', () => { + expect(findNameInput().attributes('disabled')).toBe('disabled'); + }); + }); }); describe('when allow only single rule', () => { diff --git a/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rule_spec.js b/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rule_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5fdefc4631b0b348f1197a2efb30df143ef1b9c6 --- /dev/null +++ b/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rule_spec.js @@ -0,0 +1,88 @@ +import Vuex from 'vuex'; +import { LICENSE_CHECK_NAME, VULNERABILITY_CHECK_NAME } from 'ee/approvals/constants'; +import UnconfiguredSecurityRule from 'ee/approvals/components/security_configuration/unconfigured_security_rule.vue'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlSprintf, GlButton } from '@gitlab/ui'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('UnconfiguredSecurityRule component', () => { + let wrapper; + let description; + + const findDescription = () => wrapper.find(GlSprintf); + const findButton = () => wrapper.find(GlButton); + + const vulnCheckRule = { + name: VULNERABILITY_CHECK_NAME, + description: 'vuln-check description without enable button', + enableDescription: 'vuln-check description with enable button', + docsPath: 'docs/vuln-check', + }; + + const licenseCheckRule = { + name: LICENSE_CHECK_NAME, + description: 'license-check description without enable button', + enableDescription: 'license-check description with enable button', + docsPath: 'docs/license-check', + }; + + const createWrapper = (props = {}, options = {}) => { + wrapper = mount(UnconfiguredSecurityRule, { + localVue, + propsData: { + ...props, + }, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + rule | ruleName | descriptionText + ${licenseCheckRule} | ${licenseCheckRule.name} | ${licenseCheckRule.enableDescription} + ${vulnCheckRule} | ${vulnCheckRule.name} | ${vulnCheckRule.enableDescription} + `('with a configured job that is eligible for $ruleName', ({ rule, descriptionText }) => { + beforeEach(() => { + createWrapper({ + rule: { ...rule, hasConfiguredJob: true }, + }); + description = findDescription(); + }); + + it('should render the row with the enable decription and enable button', () => { + expect(description.exists()).toBe(true); + expect(description.text()).toBe(descriptionText); + expect(findButton().exists()).toBe(true); + }); + + it('should emit the "enable" event when the button is clicked', () => { + findButton().trigger('click'); + expect(wrapper.emitted('enable')).toEqual([[]]); + }); + }); + + describe.each` + rule | ruleName | descriptionText + ${licenseCheckRule} | ${licenseCheckRule.name} | ${licenseCheckRule.description} + ${vulnCheckRule} | ${vulnCheckRule.name} | ${vulnCheckRule.description} + `('with a unconfigured job that is eligible for $ruleName', ({ rule, descriptionText }) => { + beforeEach(() => { + createWrapper({ + rule: { ...rule, hasConfiguredJob: false }, + }); + description = findDescription(); + }); + + it('should render the row with the decription and no button', () => { + expect(description.exists()).toBe(true); + expect(description.text()).toBe(descriptionText); + expect(findButton().exists()).toBe(false); + }); + }); +}); diff --git a/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rules_spec.js b/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rules_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e092f3f77f3c454e6f6c91d66c91a0b8e4d3397b --- /dev/null +++ b/ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rules_spec.js @@ -0,0 +1,81 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { GlSkeletonLoading } from '@gitlab/ui'; +import UnconfiguredSecurityRules from 'ee/approvals/components/security_configuration/unconfigured_security_rules.vue'; +import UnconfiguredSecurityRule from 'ee/approvals/components/security_configuration/unconfigured_security_rule.vue'; +import { createStoreOptions } from 'ee/approvals/stores'; +import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('UnconfiguredSecurityRules component', () => { + let wrapper; + let store; + + const TEST_PROJECT_ID = '7'; + + const createWrapper = (props = {}) => { + wrapper = shallowMount(UnconfiguredSecurityRules, { + localVue, + store, + propsData: { + ...props, + }, + provide: { + vulnerabilityCheckHelpPagePath: '', + licenseCheckHelpPagePath: '', + }, + }); + }; + + beforeEach(() => { + store = new Vuex.Store( + createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID }), + ); + jest.spyOn(store, 'dispatch'); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when created ', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should fetch the security configuration', () => { + expect(store.dispatch).toHaveBeenCalledWith( + 'securityConfiguration/fetchSecurityConfiguration', + undefined, + ); + }); + + it('should render a unconfigured-security-rule component for every security rule ', () => { + expect(wrapper.findAll(UnconfiguredSecurityRule).length).toBe(2); + }); + }); + + describe.each` + approvalsLoading | securityConfigurationLoading | shouldRender + ${false} | ${false} | ${false} + ${true} | ${false} | ${true} + ${false} | ${true} | ${true} + ${true} | ${true} | ${true} + `( + 'while approvalsLoading is $approvalsLoading and securityConfigurationLoading is $securityConfigurationLoading', + ({ approvalsLoading, securityConfigurationLoading, shouldRender }) => { + beforeEach(() => { + createWrapper(); + store.state.approvals.isLoading = approvalsLoading; + store.state.securityConfiguration.isLoading = securityConfigurationLoading; + }); + + it(`should ${shouldRender ? '' : 'not'} render the loading skeleton`, () => { + expect(wrapper.contains(GlSkeletonLoading)).toBe(shouldRender); + }); + }, + ); +}); diff --git a/ee/spec/frontend/security_configuration/modules/configuration/actions_spec.js b/ee/spec/frontend/security_configuration/modules/configuration/actions_spec.js index 2ed0bece29de28abdbb6a625bb33eb0dba455059..393b7573162c56f7d6519d3e3e9b6b10839d061f 100644 --- a/ee/spec/frontend/security_configuration/modules/configuration/actions_spec.js +++ b/ee/spec/frontend/security_configuration/modules/configuration/actions_spec.js @@ -11,25 +11,8 @@ describe('security configuration module actions', () => { let state; beforeEach(() => { - state = createState(); - }); - - describe('setSecurityConfigurationEndpoint', () => { - const securityConfigurationPath = 123; - - it('should commit the SET_SECURITY_CONFIGURATION_ENDPOINT mutation', async () => { - await testAction( - actions.setSecurityConfigurationEndpoint, - securityConfigurationPath, - state, - [ - { - type: types.SET_SECURITY_CONFIGURATION_ENDPOINT, - payload: securityConfigurationPath, - }, - ], - [], - ); + state = createState({ + securityConfigurationPath: `${TEST_HOST}/-/security/configuration.json`, }); }); @@ -38,7 +21,6 @@ describe('security configuration module actions', () => { const configuration = {}; beforeEach(() => { - state.securityConfigurationPath = `${TEST_HOST}/-/security/configuration.json`; mock = new MockAdapter(axios); }); diff --git a/ee/spec/frontend/security_configuration/modules/configuration/mutation_spec.js b/ee/spec/frontend/security_configuration/modules/configuration/mutation_spec.js index db8df89805d925372b66345a88d29fb00de79190..4f393b98e3ec00f1c52eaf63ca7f582578a644ac 100644 --- a/ee/spec/frontend/security_configuration/modules/configuration/mutation_spec.js +++ b/ee/spec/frontend/security_configuration/modules/configuration/mutation_spec.js @@ -8,15 +8,6 @@ describe('security configuration module mutations', () => { state = {}; }); - describe('SET_SECURITY_CONFIGURATION_ENDPOINT', () => { - const securityConfigurationPath = 123; - - it(`should set the securityConfigurationPath to ${securityConfigurationPath}`, () => { - mutations[types.SET_SECURITY_CONFIGURATION_ENDPOINT](state, securityConfigurationPath); - expect(state.securityConfigurationPath).toBe(securityConfigurationPath); - }); - }); - describe('REQUEST_SECURITY_CONFIGURATION', () => { it('should set the isLoading to true', () => { mutations[types.REQUEST_SECURITY_CONFIGURATION](state); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9da13a1fd2df2175c5026bf588ec72ae9ed8b134..3749c1814aa6df31e6d78c7ca32b26183fb72a0c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21348,6 +21348,18 @@ msgstr "" msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})" msgstr "" +msgid "SecurityApprovals|License Scanning must be enabled. %{linkStart}More information%{linkEnd}" +msgstr "" + +msgid "SecurityApprovals|One or more of the security scanners must be enabled. %{linkStart}More information%{linkEnd}" +msgstr "" + +msgid "SecurityApprovals|Requires approval for vulnerabilties of Critical, High, or Unknown severity. %{linkStart}More information%{linkEnd}" +msgstr "" + +msgid "SecurityApprovals|Requires license policy rules for licenses of Allowed, or Denied. %{linkStart}More information%{linkEnd}" +msgstr "" + msgid "SecurityConfiguration|An error occurred while creating the merge request." msgstr ""