Commit 96ff3966 authored by Thong Kuah's avatar Thong Kuah

Merge branch...

Merge branch '332017-remove-status-checks-from-approval-rules-and-update-feature-specs' into 'master'

Remove status checks from approval rules and update feature specs

See merge request gitlab-org/gitlab!62682
parents 669b9785 4b869c11
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
},
props: {
approverTypeOptions: {
type: Array,
required: true,
},
},
data() {
return {
selected: null,
};
},
computed: {
dropdownText() {
return this.selected.text;
},
},
created() {
const [firstOption] = this.approverTypeOptions;
this.onSelect(firstOption);
},
methods: {
isSelectedType(type) {
return this.selected.type === type;
},
onSelect(option) {
this.selected = option;
this.$emit('input', option.type);
},
},
};
</script>
<template>
<gl-dropdown class="gl-w-full gl-dropdown-menu-full-width" :text="dropdownText">
<gl-dropdown-item
v-for="option in approverTypeOptions"
:key="option.type"
:is-check-item="true"
:is-checked="isSelectedType(option.type)"
@click="onSelect(option)"
>
<span>{{ option.text }}</span>
</gl-dropdown-item>
</gl-dropdown>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { n__, s__, __ } from '~/locale';
import { n__, __ } from '~/locale';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import { RULE_TYPE_EXTERNAL_APPROVAL } from '../constants';
const i18n = {
cancelButtonText: __('Cancel'),
regularRule: {
primaryButtonText: __('Remove approvers'),
modalTitle: __('Remove approvers?'),
removeWarningText: (i) =>
......@@ -16,12 +14,6 @@ const i18n = {
'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{strongStart}%{count} members%{strongEnd}. Approvals from these members are not revoked.',
i,
),
},
externalRule: {
primaryButtonText: s__('StatusCheck|Remove status check'),
modalTitle: s__('StatusCheck|Remove status check?'),
removeWarningText: s__('StatusCheck|You are about to remove the %{name} status check.'),
},
};
export default {
......@@ -39,9 +31,6 @@ export default {
...mapState('deleteModal', {
rule: 'data',
}),
isExternalApprovalRule() {
return this.rule?.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
},
approversCount() {
return this.rule.approvers.length;
},
......@@ -52,34 +41,20 @@ export default {
this.rule.approvers.length,
);
},
modalTitle() {
return this.isExternalApprovalRule
? i18n.externalRule.modalTitle
: i18n.regularRule.modalTitle;
},
modalText() {
return this.isExternalApprovalRule
? i18n.externalRule.removeWarningText
: i18n.regularRule.removeWarningText(this.approversCount);
return i18n.removeWarningText(this.approversCount);
},
primaryButtonProps() {
const text = this.isExternalApprovalRule
? i18n.externalRule.primaryButtonText
: i18n.regularRule.primaryButtonText;
return {
text,
text: i18n.primaryButtonText,
attributes: [{ variant: 'danger' }],
};
},
},
methods: {
...mapActions(['deleteRule', 'deleteExternalApprovalRule']),
...mapActions(['deleteRule']),
submit() {
if (this.rule.externalUrl) {
this.deleteExternalApprovalRule(this.rule.id);
} else {
this.deleteRule(this.rule.id);
}
},
},
cancelButtonProps: {
......@@ -93,7 +68,7 @@ export default {
<gl-modal-vuex
modal-module="deleteModal"
:modal-id="modalId"
:title="modalTitle"
:title="$options.i18n.modalTitle"
:action-primary="primaryButtonProps"
:action-cancel="$options.cancelButtonProps"
@ok.prevent="submit"
......
......@@ -4,11 +4,7 @@ import RuleName from 'ee/approvals/components/rule_name.vue';
import { n__, sprintf } from '~/locale';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
RULE_TYPE_EXTERNAL_APPROVAL,
RULE_TYPE_ANY_APPROVER,
RULE_TYPE_REGULAR,
} from '../../constants';
import { RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR } from '../../constants';
import EmptyRule from '../empty_rule.vue';
import RuleInput from '../mr_edit/rule_input.vue';
......@@ -16,11 +12,9 @@ import RuleBranches from '../rule_branches.vue';
import RuleControls from '../rule_controls.vue';
import Rules from '../rules.vue';
import UnconfiguredSecurityRules from '../security_configuration/unconfigured_security_rules.vue';
import StatusChecksIcon from '../status_checks_icon.vue';
export default {
components: {
StatusChecksIcon,
RuleControls,
Rules,
UserAvatarList,
......@@ -101,9 +95,6 @@ export default {
return canEdit && (!allowMultiRule || !rule.hasSource);
},
isExternalApprovalRule({ ruleType }) {
return ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
},
},
};
</script>
......@@ -141,14 +132,13 @@ export default {
class="js-members"
:class="settings.allowMultiRule ? 'd-none d-sm-table-cell' : null"
>
<status-checks-icon v-if="isExternalApprovalRule(rule)" :url="rule.externalUrl" />
<user-avatar-list v-else :items="rule.approvers" :img-size="24" empty-text="" />
<user-avatar-list :items="rule.approvers" :img-size="24" empty-text="" />
</td>
<td v-if="settings.allowMultiRule" class="js-branches">
<rule-branches :rule="rule" />
</td>
<td class="js-approvals-required">
<rule-input v-if="!isExternalApprovalRule(rule)" :rule="rule" />
<rule-input :rule="rule" />
</td>
<td class="text-nowrap px-2 w-0 js-controls">
<rule-controls v-if="canEdit(rule)" :rule="rule" />
......
......@@ -3,18 +3,8 @@ import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { groupBy, isEqual, isNumber } from 'lodash';
import { mapState, mapActions } from 'vuex';
import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue';
import { isSafeURL } from '~/lib/utils/url_utility';
import { sprintf, __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
ANY_BRANCH,
TYPE_USER,
TYPE_GROUP,
TYPE_HIDDEN_GROUPS,
RULE_TYPE_EXTERNAL_APPROVAL,
RULE_TYPE_USER_OR_GROUP_APPROVER,
} from '../constants';
import ApproverTypeSelect from './approver_type_select.vue';
import { ANY_BRANCH, TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from '../constants';
import ApproversList from './approvers_list.vue';
import ApproversSelect from './approvers_select.vue';
......@@ -29,14 +19,12 @@ function mapServerResponseToValidationErrors(messages) {
export default {
components: {
ApproverTypeSelect,
ApproversList,
ApproversSelect,
GlFormGroup,
GlFormInput,
ProtectedBranchesSelector,
},
mixins: [glFeatureFlagsMixin()],
props: {
initRule: {
type: Object,
......@@ -59,7 +47,6 @@ export default {
name: this.defaultRuleName,
approvalsRequired: 1,
minApprovalsRequired: 0,
externalUrl: null,
approvers: [],
approversToAdd: [],
branches: [],
......@@ -68,15 +55,11 @@ export default {
isFallback: false,
containsHiddenGroups: false,
serverValidationErrors: [],
ruleType: null,
...this.getInitialData(),
};
},
computed: {
...mapState(['settings']),
isExternalApprovalRule() {
return this.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
},
rule() {
// If we are creating a new rule with a suggested approval name
return this.defaultRuleName ? null : this.initRule;
......@@ -96,17 +79,6 @@ export default {
groupIds() {
return this.groups.map((x) => x.id);
},
invalidStatusChecksUrl() {
if (this.serverValidationErrors.includes('External url has already been taken')) {
return this.$options.i18n.validations.externalUrlTaken;
}
if (!this.externalUrl || !isSafeURL(this.externalUrl)) {
return this.$options.i18n.validations.invalidUrl;
}
return '';
},
invalidName() {
if (this.isMultiSubmission) {
if (this.serverValidationErrors.includes('name has already been taken')) {
......@@ -162,9 +134,6 @@ export default {
this.isValidApprovers
);
},
isValidExternalApprovalRule() {
return this.isValidName && this.isValidBranches && this.isValidStatusChecksUrl;
},
isValidName() {
return !this.showValidation || !this.invalidName;
},
......@@ -177,9 +146,6 @@ export default {
isValidApprovers() {
return !this.showValidation || !this.invalidApprovers;
},
isValidStatusChecksUrl() {
return !this.showValidation || !this.invalidStatusChecksUrl;
},
isMultiSubmission() {
return this.settings.allowMultiRule && !this.isFallbackSubmission;
},
......@@ -191,14 +157,6 @@ export default {
isPersisted() {
return this.initRule && this.initRule.id;
},
showApproverTypeSelect() {
return (
this.glFeatures.ffComplianceApprovalGates &&
!this.isEditing &&
!this.isMrEdit &&
!READONLY_NAMES.includes(this.name)
);
},
showName() {
return !this.settings.lockedApprovalsRuleName;
},
......@@ -229,15 +187,6 @@ export default {
isEditing() {
return Boolean(this.initRule);
},
externalRuleSubmissionData() {
const { id, name, protectedBranchIds } = this.submissionData;
return {
id,
name,
protectedBranchIds,
externalUrl: this.externalUrl,
};
},
},
watch: {
approversToAdd(value) {
......@@ -248,15 +197,7 @@ export default {
},
},
methods: {
...mapActions([
'putFallbackRule',
'putExternalApprovalRule',
'postExternalApprovalRule',
'postRule',
'putRule',
'deleteRule',
'postRegularRule',
]),
...mapActions(['putFallbackRule', 'postRule', 'putRule', 'deleteRule', 'postRegularRule']),
addSelection() {
if (!this.approversToAdd.length) {
return;
......@@ -277,9 +218,7 @@ export default {
this.serverValidationErrors = [];
this.showValidation = true;
const valid = this.isExternalApprovalRule ? this.isValidExternalApprovalRule : this.isValid;
if (!valid) {
if (!this.isValid) {
submission = Promise.resolve;
} else if (this.isFallbackSubmission) {
submission = this.submitFallback;
......@@ -292,24 +231,15 @@ export default {
try {
await submission();
} catch (failureResponse) {
if (this.isExternalApprovalRule) {
this.serverValidationErrors = failureResponse?.response?.data?.message || [];
} else {
this.serverValidationErrors = mapServerResponseToValidationErrors(
failureResponse?.response?.data?.message || {},
);
}
}
},
/**
* Submit the rule, by either put-ing or post-ing.
*/
submitRule() {
if (this.isExternalApprovalRule) {
const data = this.externalRuleSubmissionData;
return data.id ? this.putExternalApprovalRule(data) : this.postExternalApprovalRule(data);
}
const data = this.submissionData;
if (!this.settings.allowMultiRule && this.settings.prefix === 'mr-edit') {
......@@ -328,7 +258,7 @@ export default {
* Submit as a single rule. This is determined by the settings.
*/
submitSingleRule() {
if (!this.approvers.length && !this.isExternalApprovalRule) {
if (!this.approvers.length) {
return this.submitEmptySingleRule();
}
......@@ -355,16 +285,6 @@ export default {
};
}
if (this.initRule.ruleType === RULE_TYPE_EXTERNAL_APPROVAL) {
return {
name: this.initRule.name || '',
externalUrl: this.initRule.externalUrl,
branches: this.initRule.protectedBranches || [],
ruleType: this.initRule.ruleType,
approvers: [],
};
}
const { containsHiddenGroups = false, removeHiddenGroups = false } = this.initRule;
const users = this.initRule.users.map((x) => ({ ...x, type: TYPE_USER }));
......@@ -375,7 +295,6 @@ export default {
name: this.initRule.name || '',
approvalsRequired: this.initRule.approvalsRequired || 0,
minApprovalsRequired: this.initRule.minApprovalsRequired || 0,
ruleType: this.initRule.ruleType,
containsHiddenGroups,
approvers: groups
.concat(users)
......@@ -388,9 +307,6 @@ export default {
},
i18n: {
form: {
addStatusChecks: s__('StatusCheck|API to check'),
statusChecks: s__('StatusCheck|Status to check'),
statusChecksDescription: s__('StatusCheck|Invoke an external API as part of the approvals'),
approvalsRequiredLabel: s__('ApprovalRule|Approvals required'),
approvalTypeLabel: s__('ApprovalRule|Approver Type'),
approversLabel: s__('ApprovalRule|Add approvers'),
......@@ -411,14 +327,8 @@ export default {
branchesRequired: __('Please select a valid target branch'),
ruleNameTaken: __('Rule name is already taken.'),
ruleNameMissing: __('Please provide a name'),
externalUrlTaken: __('External url has already been taken'),
invalidUrl: __('Please provide a valid URL'),
},
},
approverTypeOptions: [
{ type: RULE_TYPE_USER_OR_GROUP_APPROVER, text: s__('ApprovalRule|Users or groups') },
{ type: RULE_TYPE_EXTERNAL_APPROVAL, text: s__('ApprovalRule|Status check') },
],
};
</script>
......@@ -455,13 +365,6 @@ export default {
:selected-branches="branches"
/>
</gl-form-group>
<gl-form-group v-if="showApproverTypeSelect" :label="$options.i18n.form.approvalTypeLabel">
<approver-type-select
v-model="ruleType"
:approver-type-options="$options.approverTypeOptions"
/>
</gl-form-group>
<template v-if="!isExternalApprovalRule">
<gl-form-group
:label="$options.i18n.form.approvalsRequiredLabel"
:state="isValidApprovalsRequired"
......@@ -493,24 +396,7 @@ export default {
data-qa-selector="member_select_field"
/>
</gl-form-group>
</template>
<gl-form-group
v-if="isExternalApprovalRule"
:label="$options.i18n.form.addStatusChecks"
:description="$options.i18n.form.statusChecksDescription"
:state="isValidStatusChecksUrl"
:invalid-feedback="invalidStatusChecksUrl"
data-testid="status-checks-url-group"
>
<gl-form-input
v-model="externalUrl"
:state="isValidStatusChecksUrl"
type="url"
data-qa-selector="external_url_field"
data-testid="status-checks-url"
/>
</gl-form-group>
<div v-if="!isExternalApprovalRule" class="bordered-box overflow-auto h-12em">
<div class="bordered-box overflow-auto h-12em">
<approvers-list v-model="approvers" />
</div>
</form>
......
<script>
import { GlIcon, GlPopover } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { s__ } from '~/locale';
export default {
components: {
GlIcon,
GlPopover,
},
props: {
url: {
type: String,
required: true,
},
},
computed: {
iconId() {
return uniqueId('status-checks-icon-');
},
containerId() {
return uniqueId('status-checks-icon-container-');
},
},
i18n: {
title: s__('StatusCheck|Status to check'),
},
};
</script>
<template>
<div :id="containerId">
<gl-icon :id="iconId" name="api" />
<gl-popover
:target="iconId"
:container="containerId"
placement="top"
:title="$options.i18n.title"
triggers="hover focus"
:content="url"
/>
</div>
</template>
......@@ -15,9 +15,7 @@ export const RULE_TYPE_REGULAR = 'regular';
export const RULE_TYPE_REPORT_APPROVER = 'report_approver';
export const RULE_TYPE_CODE_OWNER = 'code_owner';
export const RULE_TYPE_ANY_APPROVER = 'any_approver';
export const RULE_TYPE_EXTERNAL_APPROVAL = 'external_approval';
export const RULE_NAME_ANY_APPROVER = 'All Members';
export const RULE_TYPE_USER_OR_GROUP_APPROVER = 'user_or_group';
export const VULNERABILITY_CHECK_NAME = 'Vulnerability-Check';
export const LICENSE_CHECK_NAME = 'License-Check';
......
import {
RULE_TYPE_REGULAR,
RULE_TYPE_ANY_APPROVER,
RULE_TYPE_EXTERNAL_APPROVAL,
} from './constants';
import { RULE_TYPE_REGULAR, RULE_TYPE_ANY_APPROVER } from './constants';
const visibleTypes = new Set([RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR]);
......@@ -24,17 +20,10 @@ function withDefaultEmptyRule(rules = []) {
ruleType: RULE_TYPE_ANY_APPROVER,
protectedBranches: [],
overridden: false,
external_url: null,
},
];
}
export const mapExternalApprovalRuleRequest = (req) => ({
name: req.name,
protected_branch_ids: req.protectedBranchIds,
external_url: req.externalUrl,
});
export const mapApprovalRuleRequest = (req) => ({
name: req.name,
approvals_required: req.approvalsRequired,
......@@ -61,16 +50,6 @@ export const mapApprovalRuleResponse = (res) => ({
ruleType: res.rule_type,
protectedBranches: res.protected_branches,
overridden: res.overridden,
externalUrl: res.external_url,
});
export const mapExternalApprovalRuleResponse = (res) => ({
...mapApprovalRuleResponse(res),
ruleType: RULE_TYPE_EXTERNAL_APPROVAL,
});
export const mapExternalApprovalResponse = (res) => ({
rules: res.map(mapExternalApprovalRuleResponse),
});
export const mapApprovalSettingsResponse = (res) => ({
......
import {
mapExternalApprovalRuleRequest,
mapApprovalRuleRequest,
mapApprovalSettingsResponse,
mapApprovalFallbackRuleRequest,
mapExternalApprovalResponse,
} from 'ee/approvals/mappers';
import { joinRuleResponses } from 'ee/approvals/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from '../base/mutation_types';
const fetchSettings = ({ settingsPath }) => {
return axios.get(settingsPath).then((res) => mapApprovalSettingsResponse(res.data));
};
const fetchExternalApprovalRules = ({ externalApprovalRulesPath }) => {
return axios.get(externalApprovalRulesPath).then((res) => mapExternalApprovalResponse(res.data));
};
export const requestRules = ({ commit }) => {
commit(types.SET_LOADING, true);
};
......@@ -37,14 +26,11 @@ export const receiveRulesError = () => {
export const fetchRules = ({ rootState, dispatch }) => {
dispatch('requestRules');
const requests = [fetchSettings(rootState.settings)];
if (gon?.features?.ffComplianceApprovalGates) {
requests.push(fetchExternalApprovalRules(rootState.settings));
}
const { settingsPath } = rootState.settings;
return Promise.all(requests)
.then((responses) => dispatch('receiveRulesSuccess', joinRuleResponses(responses)))
return axios
.get(settingsPath)
.then((response) => dispatch('receiveRulesSuccess', mapApprovalSettingsResponse(response.data)))
.catch(() => dispatch('receiveRulesError'));
};
......@@ -53,31 +39,6 @@ export const postRuleSuccess = ({ dispatch }) => {
dispatch('fetchRules');
};
export const putExternalApprovalRule = ({ rootState, dispatch }, { id, ...newRule }) => {
const { externalApprovalRulesPath } = rootState.settings;
return axios
.put(`${externalApprovalRulesPath}/${id}`, mapExternalApprovalRuleRequest(newRule))
.then(() => dispatch('postRuleSuccess'));
};
export const deleteExternalApprovalRule = ({ rootState, dispatch }, id) => {
const { externalApprovalRulesPath } = rootState.settings;
return axios
.delete(`${externalApprovalRulesPath}/${id}`)
.then(() => dispatch('deleteRuleSuccess'))
.catch(() => dispatch('deleteRuleError'));
};
export const postExternalApprovalRule = ({ rootState, dispatch }, rule) => {
const { externalApprovalRulesPath } = rootState.settings;
return axios
.post(externalApprovalRulesPath, mapExternalApprovalRuleRequest(rule))
.then(() => dispatch('postRuleSuccess'));
};
export const postRule = ({ rootState, dispatch }, rule) => {
const { rulesPath } = rootState.settings;
......
import { flatten } from 'lodash';
export const joinRuleResponses = (responsesArray) =>
Object.assign({}, ...responsesArray, {
rules: flatten(responsesArray.map(({ rules }) => rules)),
});
......@@ -152,12 +152,7 @@ export default {
:invalid-feedback="invalidNameMessage"
data-testid="name-group"
>
<gl-form-input
v-model="name"
:state="nameState"
data-qa-selector="rule_name_field"
data-testid="name"
/>
<gl-form-input v-model="name" :state="nameState" data-testid="name" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.form.addStatusChecks"
......@@ -171,7 +166,6 @@ export default {
:state="urlState"
type="url"
:placeholder="`https://api.gitlab.com/`"
data-qa-selector="external_url_field"
data-testid="url"
/>
</gl-form-group>
......
......@@ -82,6 +82,7 @@ export default {
:empty-text="$options.i18n.emptyTableText"
show-empty
stacked="md"
data-testid="status-checks-table"
>
<template #cell(protectedBranches)="{ item }">
<branch :branches="item.protectedBranches" />
......
......@@ -10,10 +10,6 @@ module EE
before_action :log_archive_audit_event, only: [:archive]
before_action :log_unarchive_audit_event, only: [:unarchive]
before_action only: :edit do
push_frontend_feature_flag(:ff_compliance_approval_gates, project, default_enabled: :yaml)
end
before_action only: :show do
push_frontend_feature_flag(:cve_id_request_button, project)
end
......
......@@ -68,23 +68,21 @@ module EE
end
def approvals_app_data(project = @project)
data = { 'project_id': project.id,
{
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'),
'security_approvals_help_page_path': help_page_path('user/application_security/index', anchor: 'security-approvals-in-merge-requests'),
'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') }
if ::Feature.enabled?(:ff_compliance_approval_gates, project, default_enabled: :yaml)
data[:external_approval_rules_path] = expose_path(api_v4_projects_external_status_checks_path(id: project.id))
end
{ data: data }
'license_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-license-approvals-within-a-project')
}
}
end
def status_checks_app_data(project)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project settings > [EE] Merge Request Approvals', :js do
include GitlabRoutingHelper
include FeatureApprovalHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:group_member) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:config_selector) { '.js-approval-rules' }
let_it_be(:modal_selector) { '#project-settings-approvals-create-modal' }
before do
sign_in(user)
project.add_maintainer(user)
group.add_developer(user)
group.add_developer(group_member)
end
it 'adds approver' do
visit edit_project_path(project)
open_modal(text: 'Add approval rule', expand: false)
open_approver_select
expect(find('.select2-results')).to have_content(user.name)
expect(find('.select2-results')).not_to have_content(non_member.name)
find('.user-result', text: user.name).click
close_approver_select
expect(find('.content-list')).to have_content(user.name)
open_approver_select
expect(find('.select2-results')).not_to have_content(user.name)
close_approver_select
within('.modal-content') do
click_button 'Add approval rule'
end
wait_for_requests
expect_avatar(find('.js-members'), user)
end
it 'adds approver group' do
visit edit_project_path(project)
open_modal(text: 'Add approval rule', expand: false)
open_approver_select
expect(find('.select2-results')).to have_content(group.name)
find('.user-result', text: group.name).click
close_approver_select
expect(find('.content-list')).to have_content(group.name)
within('.modal-content') do
click_button 'Add approval rule'
end
wait_for_requests
expect_avatar(find('.js-members'), group.users)
end
context 'with an approver group' do
let_it_be(:non_group_approver) { create(:user) }
let_it_be(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) }
before do
project.add_developer(non_group_approver)
end
it 'removes approver group' do
visit edit_project_path(project)
expect_avatar(find('.js-members'), rule.approvers)
open_modal(text: 'Edit', expand: false)
remove_approver(group.name)
click_button "Update approval rule"
wait_for_requests
expect_avatar(find('.js-members'), [non_group_approver])
end
end
end
......@@ -3,112 +3,50 @@ require 'spec_helper'
RSpec.describe 'Project settings > [EE] Merge Requests', :js do
include GitlabRoutingHelper
include FeatureApprovalHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:group_member) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:config_selector) { '.js-approval-rules' }
let_it_be(:modal_selector) { '#project-settings-approvals-create-modal' }
before do
stub_licensed_features(compliance_approval_gates: true)
sign_in(user)
project.add_maintainer(user)
group.add_developer(user)
group.add_developer(group_member)
end
it 'adds approver' do
visit edit_project_path(project)
open_modal(text: 'Add approval rule', expand: false)
open_approver_select
expect(find('.select2-results')).to have_content(user.name)
expect(find('.select2-results')).not_to have_content(non_member.name)
find('.user-result', text: user.name).click
close_approver_select
expect(find('.content-list')).to have_content(user.name)
open_approver_select
expect(find('.select2-results')).not_to have_content(user.name)
close_approver_select
within('.modal-content') do
click_button 'Add approval rule'
context 'Status checks' do
context 'Feature is not available' do
before do
stub_licensed_features(compliance_approval_gates: false)
end
wait_for_requests
expect_avatar(find('.js-members'), user)
it 'does not render the status checks area' do
expect(page).not_to have_selector('[data-testid="status-checks-table"]')
end
it 'adds approver group' do
visit edit_project_path(project)
open_modal(text: 'Add approval rule', expand: false)
open_approver_select
expect(find('.select2-results')).to have_content(group.name)
find('.user-result', text: group.name).click
close_approver_select
expect(find('.content-list')).to have_content(group.name)
within('.modal-content') do
click_button 'Add approval rule'
end
wait_for_requests
expect_avatar(find('.js-members'), group.users)
end
context 'with an approver group' do
let_it_be(:non_group_approver) { create(:user) }
let_it_be(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) }
context 'Feature is available' do
before do
project.add_developer(non_group_approver)
end
it 'removes approver group' do
visit edit_project_path(project)
expect_avatar(find('.js-members'), rule.approvers)
open_modal(text: 'Edit', expand: false)
remove_approver(group.name)
click_button "Update approval rule"
wait_for_requests
expect_avatar(find('.js-members'), [non_group_approver])
end
stub_licensed_features(compliance_approval_gates: true)
end
it 'adds a status check' do
visit edit_project_path(project)
open_modal(text: 'Add approval rule', expand: false)
click_button 'Add status check'
within('.modal-content') do
find('button', text: "Users or groups").click
find('button', text: "Status check").click
find('[data-testid="name"]').set('My new check')
find('[data-testid="url"]').set('https://api.gitlab.com')
find('[data-qa-selector="rule_name_field"]').set('My new rule')
find('[data-qa-selector="external_url_field"]').set('https://api.gitlab.com')
click_button 'Add approval rule'
click_button 'Add status check'
end
wait_for_requests
expect(first('.js-name')).to have_content('My new rule')
expect(find('[data-testid="status-checks-table"]')).to have_content('My new check')
end
context 'with a status check' do
......@@ -117,27 +55,31 @@ RSpec.describe 'Project settings > [EE] Merge Requests', :js do
it 'updates the status check' do
visit edit_project_path(project)
expect(first('.js-name')).to have_content(rule.name)
expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
open_modal(text: 'Edit', expand: false)
within('[data-testid="status-checks-table"]') do
click_button 'Edit'
end
within('.modal-content') do
find('[data-qa-selector="rule_name_field"]').set('Something new')
find('[data-testid="name"]').set('Something new')
click_button 'Update approval rule'
click_button 'Update status check'
end
wait_for_requests
expect(first('.js-name')).to have_content('Something new')
expect(find('[data-testid="status-checks-table"]')).to have_content('Something new')
end
it 'removes the status check' do
visit edit_project_path(project)
expect(first('.js-name')).to have_content(rule.name)
expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
first('.js-controls').find('[data-testid="remove-icon"]').click
within('[data-testid="status-checks-table"]') do
click_button 'Remove...'
end
within('.modal-content') do
click_button 'Remove status check'
......@@ -145,11 +87,14 @@ RSpec.describe 'Project settings > [EE] Merge Requests', :js do
wait_for_requests
expect(first('.js-name')).not_to have_content(rule.name)
expect(find('[data-testid="status-checks-table"]')).not_to have_content(rule.name)
end
end
end
end
context 'issuable default templates feature not available' do
context 'Issuable default templates' do
context 'Feature is not available' do
before do
stub_licensed_features(issuable_default_templates: false)
end
......@@ -167,7 +112,7 @@ RSpec.describe 'Project settings > [EE] Merge Requests', :js do
end
end
context 'issuable default templates feature is available' do
context 'Feature is available' do
before do
stub_licensed_features(issuable_default_templates: true)
end
......@@ -184,4 +129,5 @@ RSpec.describe 'Project settings > [EE] Merge Requests', :js do
expect(page).to have_content('Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests.')
end
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Approvals ModalRuleRemove matches the snapshot for external approval 1`] = `
<div
title="Remove status check?"
>
<p>
You are about to remove the
<strong>
API Gate
</strong>
status check.
</p>
</div>
`;
exports[`Approvals ModalRuleRemove matches the snapshot for multiple approvers 1`] = `
<div
title="Remove approvers?"
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import ApprovalTypeSelect from 'ee/approvals/components/approver_type_select.vue';
jest.mock('lodash/uniqueId', () => (id) => `${id}mock`);
const OPTIONS = [
{ type: 'x', text: 'foo' },
{ type: 'y', text: 'bar' },
];
describe('ApprovalTypeSelect', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const createComponent = () => {
return shallowMount(ApprovalTypeSelect, {
propsData: {
approverTypeOptions: OPTIONS,
},
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
wrapper = createComponent();
});
it('should select the first option by default', () => {
expect(findDropdownItems().at(0).props('isChecked')).toBe(true);
});
it('renders the dropdown with the selected text', () => {
expect(findDropdown().props('text')).toBe(OPTIONS[0].text);
});
it('renders a dropdown item for each option', () => {
OPTIONS.forEach((option, idx) => {
expect(findDropdownItems().at(idx).text()).toBe(option.text);
});
});
it('should select an item when clicked', async () => {
const item = findDropdownItems().at(1);
expect(item.props('isChecked')).toBe(false);
item.vm.$emit('click');
await nextTick();
expect(item.props('isChecked')).toBe(true);
});
});
......@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import ModalRuleRemove from 'ee/approvals/components/modal_rule_remove.vue';
import { stubComponent } from 'helpers/stub_component';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import { createExternalRule } from '../mocks';
const MODAL_MODULE = 'deleteModal';
const TEST_MODAL_ID = 'test-delete-modal-id';
......@@ -19,7 +18,6 @@ const SINGLE_APPROVER = {
...TEST_RULE,
approvers: [{ id: 1 }],
};
const EXTERNAL_RULE = createExternalRule();
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -67,7 +65,6 @@ describe('Approvals ModalRuleRemove', () => {
};
actions = {
deleteRule: jest.fn(),
deleteExternalApprovalRule: jest.fn(),
};
});
......@@ -94,7 +91,6 @@ describe('Approvals ModalRuleRemove', () => {
type | rule
${'multiple approvers'} | ${TEST_RULE}
${'singular approver'} | ${SINGLE_APPROVER}
${'external approval'} | ${EXTERNAL_RULE}
`('matches the snapshot for $type', ({ rule }) => {
deleteModalState.data = rule;
factory();
......@@ -102,19 +98,15 @@ describe('Approvals ModalRuleRemove', () => {
expect(findModal().element).toMatchSnapshot();
});
it.each`
typeType | action | rule
${'regular'} | ${'deleteRule'} | ${TEST_RULE}
${'external'} | ${'deleteExternalApprovalRule'} | ${EXTERNAL_RULE}
`('calls $action when the modal is submitted for a $typeType rule', ({ action, rule }) => {
deleteModalState.data = rule;
it('calls deleteRule when the modal is submitted', () => {
deleteModalState.data = TEST_RULE;
factory();
expect(actions[action]).not.toHaveBeenCalled();
expect(actions.deleteRule).not.toHaveBeenCalled();
const modal = findModal();
modal.vm.$emit('ok', new Event('submit'));
expect(actions[action]).toHaveBeenCalledWith(expect.anything(), rule.id);
expect(actions.deleteRule).toHaveBeenCalledWith(expect.anything(), TEST_RULE.id);
});
});
......@@ -6,11 +6,10 @@ import ProjectRules from 'ee/approvals/components/project_settings/project_rules
import RuleName from 'ee/approvals/components/rule_name.vue';
import Rules from 'ee/approvals/components/rules.vue';
import UnconfiguredSecurityRules from 'ee/approvals/components/security_configuration/unconfigured_security_rules.vue';
import StatusChecksIcon from 'ee/approvals/components/status_checks_icon.vue';
import { createStoreOptions } from 'ee/approvals/stores';
import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import { createProjectRules, createExternalRule } from '../../mocks';
import { createProjectRules } from '../../mocks';
const TEST_RULES = createProjectRules();
......@@ -149,26 +148,4 @@ describe('Approvals ProjectRules', () => {
expect(wrapper.find(UnconfiguredSecurityRules).exists()).toBe(true);
});
});
describe('when the rule is external', () => {
const rule = createExternalRule();
beforeEach(() => {
store.modules.approvals.state.rules = [rule];
factory();
});
it('renders the status check component with URL', () => {
expect(wrapper.findComponent(StatusChecksIcon).props('url')).toBe(rule.externalUrl);
});
it('does not render a user avatar component', () => {
expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false);
});
it('does not render the approvals required input', () => {
expect(wrapper.findComponent(RuleInput).exists()).toBe(false);
});
});
});
......@@ -2,23 +2,16 @@ import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ApproverTypeSelect from 'ee/approvals/components/approver_type_select.vue';
import ApproversList from 'ee/approvals/components/approvers_list.vue';
import ApproversSelect from 'ee/approvals/components/approvers_select.vue';
import RuleForm from 'ee/approvals/components/rule_form.vue';
import {
TYPE_USER,
TYPE_GROUP,
TYPE_HIDDEN_GROUPS,
RULE_TYPE_EXTERNAL_APPROVAL,
} from 'ee/approvals/constants';
import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from 'ee/approvals/constants';
import { createStoreOptions } from 'ee/approvals/stores';
import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createExternalRule } from '../mocks';
const TEST_PROJECT_ID = '7';
const TEST_RULE = {
......@@ -39,10 +32,6 @@ const TEST_FALLBACK_RULE = {
approvalsRequired: 1,
isFallback: true,
};
const TEST_EXTERNAL_APPROVAL_RULE = {
...createExternalRule(),
protectedBranches: TEST_PROTECTED_BRANCHES,
};
const TEST_LOCKED_RULE_NAME = 'LOCKED_RULE';
const nameTakenError = {
response: {
......@@ -53,13 +42,6 @@ const nameTakenError = {
},
},
};
const urlTakenError = {
response: {
data: {
message: ['External url has already been taken'],
},
},
};
Vue.use(Vuex);
......@@ -70,19 +52,11 @@ describe('EE Approvals RuleForm', () => {
let store;
let actions;
const createComponent = (props = {}, features = {}) => {
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(RuleForm, {
propsData: props,
store: new Vuex.Store(store),
provide: {
glFeatures: {
ffComplianceApprovalGates: true,
scopedApprovalRules: true,
...features,
},
},
stubs: {
GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback'],
......@@ -106,9 +80,6 @@ describe('EE Approvals RuleForm', () => {
const findApproversValidation = () => wrapper.findByTestId('approvers-group');
const findApproversList = () => wrapper.findComponent(ApproversList);
const findProtectedBranchesSelector = () => wrapper.findComponent(ProtectedBranchesSelector);
const findApproverTypeSelect = () => wrapper.findComponent(ApproverTypeSelect);
const findExternalUrlInput = () => wrapper.findByTestId('status-checks-url');
const findExternalUrlValidation = () => wrapper.findByTestId('status-checks-url-group');
const findBranchesValidation = () => wrapper.findByTestId('branches-group');
const inputsAreValid = (inputs) => inputs.every((x) => x.props('state'));
......@@ -126,20 +97,12 @@ describe('EE Approvals RuleForm', () => {
findBranchesValidation(),
];
const findValidationForExternal = () => [
findNameValidation(),
findExternalUrlValidation(),
findBranchesValidation(),
];
beforeEach(() => {
store = createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID });
['postRule', 'putRule', 'deleteRule', 'putFallbackRule', 'postExternalApprovalRule'].forEach(
(actionName) => {
['postRule', 'putRule', 'deleteRule', 'putFallbackRule'].forEach((actionName) => {
jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {});
},
);
});
({ actions } = store.modules.approvals);
});
......@@ -231,112 +194,6 @@ describe('EE Approvals RuleForm', () => {
});
});
describe('when the rule is an external rule', () => {
describe('with initial rule', () => {
beforeEach(() => {
createComponent({
isMrEdit: false,
initRule: TEST_EXTERNAL_APPROVAL_RULE,
});
});
it('does not render the approver type select input', () => {
expect(findApproverTypeSelect().exists()).toBe(false);
});
it('on load, it populates the external URL', () => {
expect(findExternalUrlInput().props('value')).toBe(
TEST_EXTERNAL_APPROVAL_RULE.externalUrl,
);
});
});
describe('without an initial rule', () => {
beforeEach(() => {
createComponent({
isMrEdit: false,
});
findApproverTypeSelect().vm.$emit('input', RULE_TYPE_EXTERNAL_APPROVAL);
});
it('renders the approver type select input', () => {
expect(findApproverTypeSelect().exists()).toBe(true);
});
it('renders the inputs for external rules', () => {
expect(findNameInput().exists()).toBe(true);
expect(findExternalUrlInput().exists()).toBe(true);
expect(findProtectedBranchesSelector().exists()).toBe(true);
});
it('does not render the user and group input fields', () => {
expect(findApprovalsRequiredInput().exists()).toBe(false);
expect(findApproversList().exists()).toBe(false);
expect(findApproversSelect().exists()).toBe(false);
});
it('at first, shows no validation', () => {
expect(inputsAreValid(findValidationForExternal())).toBe(true);
});
it('on submit, does not dispatch action', async () => {
await findForm().trigger('submit');
expect(actions.postExternalApprovalRule).not.toHaveBeenCalled();
});
it('on submit, shows external URL validation', async () => {
findNameInput().setValue('');
await findForm().trigger('submit');
await nextTick();
const externalUrlGroup = findExternalUrlValidation();
expect(externalUrlGroup.props('state')).toBe(false);
expect(externalUrlGroup.props('invalidFeedback')).toBe('Please provide a valid URL');
});
describe('with valid data', () => {
const branches = [TEST_PROTECTED_BRANCHES[0]];
const expected = {
id: null,
name: 'Lorem',
externalUrl: 'https://gitlab.com/',
protectedBranchIds: branches.map((x) => x.id),
};
beforeEach(async () => {
await findNameInput().vm.$emit('input', expected.name);
await findExternalUrlInput().vm.$emit('input', expected.externalUrl);
await findProtectedBranchesSelector().vm.$emit('input', branches[0]);
});
it('on submit, posts external approval rule', async () => {
await findForm().trigger('submit');
expect(actions.postExternalApprovalRule).toHaveBeenCalledWith(
expect.anything(),
expected,
);
});
it('when submitted with a duplicate external URL, shows the "url already taken" validation', async () => {
store.state.settings.prefix = 'project-settings';
actions.postExternalApprovalRule.mockRejectedValueOnce(urlTakenError);
await findForm().trigger('submit');
await waitForPromises();
const externalUrlGroup = findExternalUrlValidation();
expect(externalUrlGroup.props('state')).toBe(false);
expect(externalUrlGroup.props('invalidFeedback')).toBe(
'External url has already been taken',
);
});
});
});
});
describe('without initRule', () => {
beforeEach(() => {
createComponent({ isMrEdit: false });
......@@ -655,13 +512,13 @@ describe('EE Approvals RuleForm', () => {
describe('with approval suggestions', () => {
describe.each`
defaultRuleName | expectedDisabledAttribute | approverTypeSelect
${'Vulnerability-Check'} | ${true} | ${false}
${'License-Check'} | ${true} | ${false}
${'Foo Bar Baz'} | ${false} | ${true}
defaultRuleName | expectedDisabledAttribute
${'Vulnerability-Check'} | ${true}
${'License-Check'} | ${true}
${'Foo Bar Baz'} | ${false}
`(
'with defaultRuleName set to $defaultRuleName',
({ defaultRuleName, expectedDisabledAttribute, approverTypeSelect }) => {
({ defaultRuleName, expectedDisabledAttribute }) => {
beforeEach(() => {
createComponent({
initRule: null,
......@@ -675,12 +532,6 @@ describe('EE Approvals RuleForm', () => {
} the name text field`, () => {
expect(findNameInput().props('disabled')).toBe(expectedDisabledAttribute);
});
it(`${
approverTypeSelect ? 'renders' : 'does not render'
} the approver type select`, () => {
expect(findApproverTypeSelect().exists()).toBe(approverTypeSelect);
});
},
);
});
......@@ -848,14 +699,4 @@ describe('EE Approvals RuleForm', () => {
});
});
});
describe('when the status check feature is disabled', () => {
it('does not render the approver type select input', async () => {
createComponent({ isMrEdit: false }, { ffComplianceApprovalGates: false });
await nextTick();
expect(findApproverTypeSelect().exists()).toBe(false);
});
});
});
import { GlPopover, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusChecksIcon from 'ee/approvals/components/status_checks_icon.vue';
jest.mock('lodash/uniqueId', () => (id) => `${id}mock`);
describe('StatusChecksIcon', () => {
let wrapper;
const findPopover = () => wrapper.findComponent(GlPopover);
const findIcon = () => wrapper.findComponent(GlIcon);
const createComponent = () => {
return shallowMount(StatusChecksIcon, {
propsData: {
url: 'https://gitlab.com/',
},
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
wrapper = createComponent();
});
it('renders the icon', () => {
expect(findIcon().props('name')).toBe('api');
expect(findIcon().attributes('id')).toBe('status-checks-icon-mock');
});
it('renders the popover with the URL for the icon', () => {
expect(findPopover().exists()).toBe(true);
expect(findPopover().attributes()).toMatchObject({
content: 'https://gitlab.com/',
title: 'Status to check',
target: 'status-checks-icon-mock',
});
});
});
export const createExternalRule = () => ({
id: 9,
name: 'API Gate',
externalUrl: 'https://gitlab.com',
ruleType: 'external_approval',
});
export const createProjectRules = () => [
{
id: 1,
......
import MockAdapter from 'axios-mock-adapter';
import {
mapApprovalRuleRequest,
mapApprovalSettingsResponse,
mapExternalApprovalResponse,
} from 'ee/approvals/mappers';
import { mapApprovalRuleRequest, mapApprovalSettingsResponse } from 'ee/approvals/mappers';
import * as types from 'ee/approvals/stores/modules/base/mutation_types';
import * as actions from 'ee/approvals/stores/modules/project_settings/actions';
import { joinRuleResponses } from 'ee/approvals/utils';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
......@@ -22,11 +17,6 @@ const TEST_RULE_REQUEST = {
groups: [7],
users: [8, 9],
};
const TEST_EXTERNAL_RULE_REQUEST = {
name: 'Lorem',
protected_branch_ids: [],
external_url: 'https://www.gitlab.com',
};
const TEST_RULE_RESPONSE = {
id: 7,
name: 'Ipsum',
......@@ -37,19 +27,14 @@ const TEST_RULE_RESPONSE = {
};
const TEST_SETTINGS_PATH = 'projects/9/approval_settings';
const TEST_RULES_PATH = 'projects/9/approval_settings/rules';
const TEST_EXTERNAL_RULES_PATH = 'projects/9/external_status_checks';
describe('EE approvals project settings module actions', () => {
let state;
let mock;
let originalGon;
beforeEach(() => {
originalGon = { ...window.gon };
window.gon = { features: { ffComplianceApprovalGates: true } };
state = {
settings: {
externalApprovalRulesPath: TEST_EXTERNAL_RULES_PATH,
projectId: TEST_PROJECT_ID,
settingsPath: TEST_SETTINGS_PATH,
rulesPath: TEST_RULES_PATH,
......@@ -60,7 +45,6 @@ describe('EE approvals project settings module actions', () => {
afterEach(() => {
mock.restore();
window.gon = originalGon;
});
describe('requestRules', () => {
......@@ -106,33 +90,23 @@ describe('EE approvals project settings module actions', () => {
});
describe('fetchRules', () => {
const testFetchRuleAction = (payload, history) => {
it('dispatches request/receive', () => {
const data = { rules: [TEST_RULE_RESPONSE] };
mock.onGet(TEST_SETTINGS_PATH).replyOnce(httpStatus.OK, data);
return testAction(
actions.fetchRules,
null,
state,
[],
[{ type: 'requestRules' }, { type: 'receiveRulesSuccess', payload }],
[
{ type: 'requestRules' },
{ type: 'receiveRulesSuccess', payload: mapApprovalSettingsResponse(data) },
],
() => {
expect(mock.history.get.map((x) => x.url)).toEqual(history);
expect(mock.history.get.map((x) => x.url)).toEqual([TEST_SETTINGS_PATH]);
},
);
};
it('dispatches request/receive', () => {
const data = { rules: [TEST_RULE_RESPONSE] };
mock.onGet(TEST_SETTINGS_PATH).replyOnce(httpStatus.OK, data);
const externalRuleData = [TEST_RULE_RESPONSE];
mock.onGet(TEST_EXTERNAL_RULES_PATH).replyOnce(httpStatus.OK, externalRuleData);
return testFetchRuleAction(
joinRuleResponses([
mapApprovalSettingsResponse(data),
mapExternalApprovalResponse(externalRuleData),
]),
[TEST_SETTINGS_PATH, TEST_EXTERNAL_RULES_PATH],
);
});
it('dispatches request/receive on error', () => {
......@@ -146,21 +120,6 @@ describe('EE approvals project settings module actions', () => {
[{ type: 'requestRules' }, { type: 'receiveRulesError' }],
);
});
describe('when the ffComplianceApprovalGates feature flag is disabled', () => {
beforeEach(() => {
window.gon = { features: { ffComplianceApprovalGates: false } };
});
it('dispatches request/receive for a single request', () => {
const data = { rules: [TEST_RULE_RESPONSE] };
mock.onGet(TEST_SETTINGS_PATH).replyOnce(httpStatus.OK, data);
return testFetchRuleAction(joinRuleResponses([mapApprovalSettingsResponse(data)]), [
TEST_SETTINGS_PATH,
]);
});
});
});
describe('postRuleSuccess', () => {
......@@ -175,44 +134,43 @@ describe('EE approvals project settings module actions', () => {
});
});
describe('POST', () => {
it.each`
action | path | request
${'postRule'} | ${TEST_RULES_PATH} | ${TEST_RULE_REQUEST}
${'postExternalApprovalRule'} | ${TEST_EXTERNAL_RULES_PATH} | ${TEST_EXTERNAL_RULE_REQUEST}
`('dispatches success on success for $action', ({ action, path, request }) => {
mock.onPost(path).replyOnce(httpStatus.OK);
describe('postRule', () => {
it('dispatches success on success', () => {
mock.onPost(TEST_RULES_PATH).replyOnce(httpStatus.OK);
return testAction(actions[action], request, state, [], [{ type: 'postRuleSuccess' }], () => {
return testAction(
actions.postRule,
TEST_RULE_REQUEST,
state,
[],
[{ type: 'postRuleSuccess' }],
() => {
expect(mock.history.post).toEqual([
expect.objectContaining({
url: path,
data: JSON.stringify(mapApprovalRuleRequest(request)),
url: TEST_RULES_PATH,
data: JSON.stringify(mapApprovalRuleRequest(TEST_RULE_REQUEST)),
}),
]);
});
},
);
});
});
describe('PUT', () => {
it.each`
action | path | request
${'putRule'} | ${TEST_RULES_PATH} | ${TEST_RULE_REQUEST}
${'putExternalApprovalRule'} | ${TEST_EXTERNAL_RULES_PATH} | ${TEST_EXTERNAL_RULE_REQUEST}
`('dispatches success on success for $action', ({ action, path, request }) => {
mock.onPut(`${path}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
describe('putRule', () => {
it('dispatches success on success', () => {
mock.onPut(`${TEST_RULES_PATH}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
return testAction(
actions[action],
{ id: TEST_RULE_ID, ...request },
actions.putRule,
{ id: TEST_RULE_ID, ...TEST_RULE_REQUEST },
state,
[],
[{ type: 'postRuleSuccess' }],
() => {
expect(mock.history.put).toEqual([
expect.objectContaining({
url: `${path}/${TEST_RULE_ID}`,
data: JSON.stringify(mapApprovalRuleRequest(request)),
url: `${TEST_RULES_PATH}/${TEST_RULE_ID}`,
data: JSON.stringify(mapApprovalRuleRequest(TEST_RULE_REQUEST)),
}),
]);
},
......@@ -244,16 +202,12 @@ describe('EE approvals project settings module actions', () => {
});
});
describe('DELETE', () => {
it.each`
action | path
${'deleteRule'} | ${TEST_RULES_PATH}
${'deleteExternalApprovalRule'} | ${TEST_EXTERNAL_RULES_PATH}
`('dispatches success on success for $action', ({ action, path }) => {
mock.onDelete(`${path}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
describe('deleteRule', () => {
it('dispatches success on success', () => {
mock.onDelete(`${TEST_RULES_PATH}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
return testAction(
actions[action],
actions.deleteRule,
TEST_RULE_ID,
state,
[],
......@@ -261,7 +215,7 @@ describe('EE approvals project settings module actions', () => {
() => {
expect(mock.history.delete).toEqual([
expect.objectContaining({
url: `${path}/${TEST_RULE_ID}`,
url: `${TEST_RULES_PATH}/${TEST_RULE_ID}`,
}),
]);
},
......
import * as Utils from 'ee/approvals/utils';
describe('Utils', () => {
describe('joinRuleResponses', () => {
it('should join multiple response objects and concatenate the rules array of all objects', () => {
const resX = { foo: 'bar', rules: [1, 2, 3] };
const resY = { foo: 'something', rules: [4, 5] };
expect(Utils.joinRuleResponses([resX, resY])).toStrictEqual({
foo: 'something',
rules: [1, 2, 3, 4, 5],
});
});
});
});
......@@ -367,17 +367,20 @@ RSpec.describe ProjectsHelper do
allow(helper).to receive(:can?).and_return(true)
end
context 'with the status check feature flag' do
where(feature_flag_enabled: [true, false])
with_them do
before do
stub_feature_flags(ff_compliance_approval_gates: feature_flag_enabled)
end
it 'includes external_status_checks_path only when enabled' do
expect(subject[:data].key?(:external_approval_rules_path)).to eq(feature_flag_enabled)
end
end
it 'returns the correct data' do
expect(subject[:data]).to eq({
project_id: project.id,
can_edit: 'true',
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', anchor: 'security-approvals-in-merge-requests'),
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
end
......
......@@ -4142,15 +4142,9 @@ msgstr ""
msgid "ApprovalRule|Rule name"
msgstr ""
msgid "ApprovalRule|Status check"
msgstr ""
msgid "ApprovalRule|Target branch"
msgstr ""
msgid "ApprovalRule|Users or groups"
msgstr ""
msgid "ApprovalStatusTooltip|Adheres to separation of duties"
msgstr ""
......@@ -13431,9 +13425,6 @@ msgstr ""
msgid "External storage authentication token"
msgstr ""
msgid "External url has already been taken"
msgstr ""
msgid "ExternalAuthorizationService|Classification label"
msgstr ""
......@@ -31122,9 +31113,6 @@ msgstr ""
msgid "StatusCheck|External API is already in use by another status check."
msgstr ""
msgid "StatusCheck|Invoke an external API as part of the approvals"
msgstr ""
msgid "StatusCheck|Invoke an external API as part of the pipeline process."
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