Commit 4b869c11 authored by Robert Hunt's avatar Robert Hunt Committed by Thong Kuah

Remove status checks from approval rules

This strips out all the status checks code from the approval rules,
updates the controller to stop passing the feature flag, updates the
helper to not pass status checks data, and updates the feature specs
to test the new status checks area
parent 49e51d0c
<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) =>
n__(
'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{strongStart}%{count} member%{strongEnd}. Approvals from this member are not revoked.',
'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.'),
},
primaryButtonText: __('Remove approvers'),
modalTitle: __('Remove approvers?'),
removeWarningText: (i) =>
n__(
'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{strongStart}%{count} member%{strongEnd}. Approvals from this member are not revoked.',
'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{strongStart}%{count} members%{strongEnd}. Approvals from these members are not revoked.',
i,
),
};
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);
}
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 || {},
);
}
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,62 +365,38 @@ 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
:label="$options.i18n.form.approvalsRequiredLabel"
:state="isValidApprovalsRequired"
:invalid-feedback="invalidApprovalsRequired"
data-testid="approvals-required-group"
>
<gl-form-input
v-model.number="approvalsRequired"
:state="isValidApprovalsRequired"
:min="minApprovalsRequired"
class="mw-6em"
type="number"
data-testid="approvals-required"
data-qa-selector="approvals_required_field"
/>
</gl-form-group>
<template v-if="!isExternalApprovalRule">
<gl-form-group
:label="$options.i18n.form.approvalsRequiredLabel"
:state="isValidApprovalsRequired"
:invalid-feedback="invalidApprovalsRequired"
data-testid="approvals-required-group"
>
<gl-form-input
v-model.number="approvalsRequired"
:state="isValidApprovalsRequired"
:min="minApprovalsRequired"
class="mw-6em"
type="number"
data-testid="approvals-required"
data-qa-selector="approvals_required_field"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.form.approversLabel"
:state="isValidApprovers"
:invalid-feedback="invalidApprovers"
data-testid="approvers-group"
>
<approvers-select
v-model="approversToAdd"
:project-id="settings.projectId"
:skip-user-ids="userIds"
:skip-group-ids="groupIds"
:is-invalid="!isValidApprovers"
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"
:label="$options.i18n.form.approversLabel"
:state="isValidApprovers"
:invalid-feedback="invalidApprovers"
data-testid="approvers-group"
>
<gl-form-input
v-model="externalUrl"
:state="isValidStatusChecksUrl"
type="url"
data-qa-selector="external_url_field"
data-testid="status-checks-url"
<approvers-select
v-model="approversToAdd"
:project-id="settings.projectId"
:skip-user-ids="userIds"
:skip-group-ids="groupIds"
:is-invalid="!isValidApprovers"
data-qa-selector="member_select_field"
/>
</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,
'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_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 }
{
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', 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
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,185 +3,131 @@ 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'
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]) }
context 'Status checks' do
context 'Feature is not available' do
before do
stub_licensed_features(compliance_approval_gates: false)
end
before do
project.add_developer(non_group_approver)
it 'does not render the status checks area' do
expect(page).not_to have_selector('[data-testid="status-checks-table"]')
end
end
it 'removes approver group' do
visit edit_project_path(project)
expect_avatar(find('.js-members'), rule.approvers)
context 'Feature is available' do
before do
stub_licensed_features(compliance_approval_gates: true)
end
open_modal(text: 'Edit', expand: false)
remove_approver(group.name)
click_button "Update approval rule"
wait_for_requests
it 'adds a status check' do
visit edit_project_path(project)
expect_avatar(find('.js-members'), [non_group_approver])
end
end
click_button 'Add status check'
it 'adds a status check' do
visit edit_project_path(project)
within('.modal-content') do
find('[data-testid="name"]').set('My new check')
find('[data-testid="url"]').set('https://api.gitlab.com')
open_modal(text: 'Add approval rule', expand: false)
click_button 'Add status check'
end
within('.modal-content') do
find('button', text: "Users or groups").click
find('button', text: "Status check").click
wait_for_requests
find('[data-qa-selector="rule_name_field"]').set('My new rule')
find('[data-qa-selector="external_url_field"]').set('https://api.gitlab.com')
expect(find('[data-testid="status-checks-table"]')).to have_content('My new check')
end
click_button 'Add approval rule'
end
context 'with a status check' do
let_it_be(:rule) { create(:external_status_check, project: project) }
wait_for_requests
it 'updates the status check' do
visit edit_project_path(project)
expect(first('.js-name')).to have_content('My new rule')
end
expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
context 'with a status check' do
let_it_be(:rule) { create(:external_status_check, project: project) }
within('[data-testid="status-checks-table"]') do
click_button 'Edit'
end
it 'updates the status check' do
visit edit_project_path(project)
within('.modal-content') do
find('[data-testid="name"]').set('Something new')
expect(first('.js-name')).to have_content(rule.name)
click_button 'Update status check'
end
open_modal(text: 'Edit', expand: false)
wait_for_requests
within('.modal-content') do
find('[data-qa-selector="rule_name_field"]').set('Something new')
expect(find('[data-testid="status-checks-table"]')).to have_content('Something new')
end
click_button 'Update approval rule'
end
wait_for_requests
it 'removes the status check' do
visit edit_project_path(project)
expect(first('.js-name')).to have_content('Something new')
end
expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
it 'removes the status check' do
visit edit_project_path(project)
within('[data-testid="status-checks-table"]') do
click_button 'Remove...'
end
expect(first('.js-name')).to have_content(rule.name)
within('.modal-content') do
click_button 'Remove status check'
end
first('.js-controls').find('[data-testid="remove-icon"]').click
wait_for_requests
within('.modal-content') do
click_button 'Remove status check'
expect(find('[data-testid="status-checks-table"]')).not_to have_content(rule.name)
end
end
wait_for_requests
expect(first('.js-name')).not_to have_content(rule.name)
end
end
context 'issuable default templates feature not available' do
before do
stub_licensed_features(issuable_default_templates: false)
end
context 'Issuable default templates' do
context 'Feature is not available' do
before do
stub_licensed_features(issuable_default_templates: false)
end
it 'input to configure merge request template is not shown' do
visit edit_project_path(project)
it 'input to configure merge request template is not shown' do
visit edit_project_path(project)
expect(page).not_to have_selector('#project_merge_requests_template')
end
expect(page).not_to have_selector('#project_merge_requests_template')
end
it "does not mention the merge request template in the section's description text" do
visit edit_project_path(project)
it "does not mention the merge request template in the section's description text" do
visit edit_project_path(project)
expect(page).to have_content('Choose your merge method, merge options, merge checks, and merge suggestions.')
expect(page).to have_content('Choose your merge method, merge options, merge checks, and merge suggestions.')
end
end
end
context 'issuable default templates feature is available' do
before do
stub_licensed_features(issuable_default_templates: true)
end
context 'Feature is available' do
before do
stub_licensed_features(issuable_default_templates: true)
end
it 'input to configure merge request template is shown' do
visit edit_project_path(project)
it 'input to configure merge request template is shown' do
visit edit_project_path(project)
expect(page).to have_selector('#project_merge_requests_template')
end
expect(page).to have_selector('#project_merge_requests_template')
end
it "mentions the merge request template in the section's description text" do
visit edit_project_path(project)
it "mentions the merge request template in the section's description text" do
visit edit_project_path(project)
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.')
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) => {
jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {});
},
);
['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' }], () => {
expect(mock.history.post).toEqual([
expect.objectContaining({
url: path,
data: JSON.stringify(mapApprovalRuleRequest(request)),
}),
]);
});
return testAction(
actions.postRule,
TEST_RULE_REQUEST,
state,
[],
[{ type: 'postRuleSuccess' }],
() => {
expect(mock.history.post).toEqual([
expect.objectContaining({
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