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> <script>
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex'; 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 GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import { RULE_TYPE_EXTERNAL_APPROVAL } from '../constants';
const i18n = { const i18n = {
cancelButtonText: __('Cancel'), cancelButtonText: __('Cancel'),
regularRule: { primaryButtonText: __('Remove approvers'),
primaryButtonText: __('Remove approvers'), modalTitle: __('Remove approvers?'),
modalTitle: __('Remove approvers?'), removeWarningText: (i) =>
removeWarningText: (i) => n__(
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} 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.',
'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{strongStart}%{count} members%{strongEnd}. Approvals from these members are not revoked.', i,
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 { export default {
...@@ -39,9 +31,6 @@ export default { ...@@ -39,9 +31,6 @@ export default {
...mapState('deleteModal', { ...mapState('deleteModal', {
rule: 'data', rule: 'data',
}), }),
isExternalApprovalRule() {
return this.rule?.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
},
approversCount() { approversCount() {
return this.rule.approvers.length; return this.rule.approvers.length;
}, },
...@@ -52,34 +41,20 @@ export default { ...@@ -52,34 +41,20 @@ export default {
this.rule.approvers.length, this.rule.approvers.length,
); );
}, },
modalTitle() {
return this.isExternalApprovalRule
? i18n.externalRule.modalTitle
: i18n.regularRule.modalTitle;
},
modalText() { modalText() {
return this.isExternalApprovalRule return i18n.removeWarningText(this.approversCount);
? i18n.externalRule.removeWarningText
: i18n.regularRule.removeWarningText(this.approversCount);
}, },
primaryButtonProps() { primaryButtonProps() {
const text = this.isExternalApprovalRule
? i18n.externalRule.primaryButtonText
: i18n.regularRule.primaryButtonText;
return { return {
text, text: i18n.primaryButtonText,
attributes: [{ variant: 'danger' }], attributes: [{ variant: 'danger' }],
}; };
}, },
}, },
methods: { methods: {
...mapActions(['deleteRule', 'deleteExternalApprovalRule']), ...mapActions(['deleteRule']),
submit() { submit() {
if (this.rule.externalUrl) { this.deleteRule(this.rule.id);
this.deleteExternalApprovalRule(this.rule.id);
} else {
this.deleteRule(this.rule.id);
}
}, },
}, },
cancelButtonProps: { cancelButtonProps: {
...@@ -93,7 +68,7 @@ export default { ...@@ -93,7 +68,7 @@ export default {
<gl-modal-vuex <gl-modal-vuex
modal-module="deleteModal" modal-module="deleteModal"
:modal-id="modalId" :modal-id="modalId"
:title="modalTitle" :title="$options.i18n.modalTitle"
:action-primary="primaryButtonProps" :action-primary="primaryButtonProps"
:action-cancel="$options.cancelButtonProps" :action-cancel="$options.cancelButtonProps"
@ok.prevent="submit" @ok.prevent="submit"
......
...@@ -4,11 +4,7 @@ import RuleName from 'ee/approvals/components/rule_name.vue'; ...@@ -4,11 +4,7 @@ import RuleName from 'ee/approvals/components/rule_name.vue';
import { n__, sprintf } from '~/locale'; import { n__, sprintf } from '~/locale';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import { RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR } from '../../constants';
RULE_TYPE_EXTERNAL_APPROVAL,
RULE_TYPE_ANY_APPROVER,
RULE_TYPE_REGULAR,
} from '../../constants';
import EmptyRule from '../empty_rule.vue'; import EmptyRule from '../empty_rule.vue';
import RuleInput from '../mr_edit/rule_input.vue'; import RuleInput from '../mr_edit/rule_input.vue';
...@@ -16,11 +12,9 @@ import RuleBranches from '../rule_branches.vue'; ...@@ -16,11 +12,9 @@ import RuleBranches from '../rule_branches.vue';
import RuleControls from '../rule_controls.vue'; import RuleControls from '../rule_controls.vue';
import Rules from '../rules.vue'; import Rules from '../rules.vue';
import UnconfiguredSecurityRules from '../security_configuration/unconfigured_security_rules.vue'; import UnconfiguredSecurityRules from '../security_configuration/unconfigured_security_rules.vue';
import StatusChecksIcon from '../status_checks_icon.vue';
export default { export default {
components: { components: {
StatusChecksIcon,
RuleControls, RuleControls,
Rules, Rules,
UserAvatarList, UserAvatarList,
...@@ -101,9 +95,6 @@ export default { ...@@ -101,9 +95,6 @@ export default {
return canEdit && (!allowMultiRule || !rule.hasSource); return canEdit && (!allowMultiRule || !rule.hasSource);
}, },
isExternalApprovalRule({ ruleType }) {
return ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
},
}, },
}; };
</script> </script>
...@@ -141,14 +132,13 @@ export default { ...@@ -141,14 +132,13 @@ export default {
class="js-members" class="js-members"
:class="settings.allowMultiRule ? 'd-none d-sm-table-cell' : null" :class="settings.allowMultiRule ? 'd-none d-sm-table-cell' : null"
> >
<status-checks-icon v-if="isExternalApprovalRule(rule)" :url="rule.externalUrl" /> <user-avatar-list :items="rule.approvers" :img-size="24" empty-text="" />
<user-avatar-list v-else :items="rule.approvers" :img-size="24" empty-text="" />
</td> </td>
<td v-if="settings.allowMultiRule" class="js-branches"> <td v-if="settings.allowMultiRule" class="js-branches">
<rule-branches :rule="rule" /> <rule-branches :rule="rule" />
</td> </td>
<td class="js-approvals-required"> <td class="js-approvals-required">
<rule-input v-if="!isExternalApprovalRule(rule)" :rule="rule" /> <rule-input :rule="rule" />
</td> </td>
<td class="text-nowrap px-2 w-0 js-controls"> <td class="text-nowrap px-2 w-0 js-controls">
<rule-controls v-if="canEdit(rule)" :rule="rule" /> <rule-controls v-if="canEdit(rule)" :rule="rule" />
......
<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'; ...@@ -15,9 +15,7 @@ export const RULE_TYPE_REGULAR = 'regular';
export const RULE_TYPE_REPORT_APPROVER = 'report_approver'; export const RULE_TYPE_REPORT_APPROVER = 'report_approver';
export const RULE_TYPE_CODE_OWNER = 'code_owner'; export const RULE_TYPE_CODE_OWNER = 'code_owner';
export const RULE_TYPE_ANY_APPROVER = 'any_approver'; 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_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 VULNERABILITY_CHECK_NAME = 'Vulnerability-Check';
export const LICENSE_CHECK_NAME = 'License-Check'; export const LICENSE_CHECK_NAME = 'License-Check';
......
import { import { RULE_TYPE_REGULAR, RULE_TYPE_ANY_APPROVER } from './constants';
RULE_TYPE_REGULAR,
RULE_TYPE_ANY_APPROVER,
RULE_TYPE_EXTERNAL_APPROVAL,
} from './constants';
const visibleTypes = new Set([RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR]); const visibleTypes = new Set([RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR]);
...@@ -24,17 +20,10 @@ function withDefaultEmptyRule(rules = []) { ...@@ -24,17 +20,10 @@ function withDefaultEmptyRule(rules = []) {
ruleType: RULE_TYPE_ANY_APPROVER, ruleType: RULE_TYPE_ANY_APPROVER,
protectedBranches: [], protectedBranches: [],
overridden: false, 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) => ({ export const mapApprovalRuleRequest = (req) => ({
name: req.name, name: req.name,
approvals_required: req.approvalsRequired, approvals_required: req.approvalsRequired,
...@@ -61,16 +50,6 @@ export const mapApprovalRuleResponse = (res) => ({ ...@@ -61,16 +50,6 @@ export const mapApprovalRuleResponse = (res) => ({
ruleType: res.rule_type, ruleType: res.rule_type,
protectedBranches: res.protected_branches, protectedBranches: res.protected_branches,
overridden: res.overridden, 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) => ({ export const mapApprovalSettingsResponse = (res) => ({
......
import { import {
mapExternalApprovalRuleRequest,
mapApprovalRuleRequest, mapApprovalRuleRequest,
mapApprovalSettingsResponse, mapApprovalSettingsResponse,
mapApprovalFallbackRuleRequest, mapApprovalFallbackRuleRequest,
mapExternalApprovalResponse,
} from 'ee/approvals/mappers'; } from 'ee/approvals/mappers';
import { joinRuleResponses } from 'ee/approvals/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from '../base/mutation_types'; 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 }) => { export const requestRules = ({ commit }) => {
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
}; };
...@@ -37,14 +26,11 @@ export const receiveRulesError = () => { ...@@ -37,14 +26,11 @@ export const receiveRulesError = () => {
export const fetchRules = ({ rootState, dispatch }) => { export const fetchRules = ({ rootState, dispatch }) => {
dispatch('requestRules'); dispatch('requestRules');
const requests = [fetchSettings(rootState.settings)]; const { settingsPath } = rootState.settings;
if (gon?.features?.ffComplianceApprovalGates) {
requests.push(fetchExternalApprovalRules(rootState.settings));
}
return Promise.all(requests) return axios
.then((responses) => dispatch('receiveRulesSuccess', joinRuleResponses(responses))) .get(settingsPath)
.then((response) => dispatch('receiveRulesSuccess', mapApprovalSettingsResponse(response.data)))
.catch(() => dispatch('receiveRulesError')); .catch(() => dispatch('receiveRulesError'));
}; };
...@@ -53,31 +39,6 @@ export const postRuleSuccess = ({ dispatch }) => { ...@@ -53,31 +39,6 @@ export const postRuleSuccess = ({ dispatch }) => {
dispatch('fetchRules'); 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) => { export const postRule = ({ rootState, dispatch }, rule) => {
const { rulesPath } = rootState.settings; 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 { ...@@ -152,12 +152,7 @@ export default {
:invalid-feedback="invalidNameMessage" :invalid-feedback="invalidNameMessage"
data-testid="name-group" data-testid="name-group"
> >
<gl-form-input <gl-form-input v-model="name" :state="nameState" data-testid="name" />
v-model="name"
:state="nameState"
data-qa-selector="rule_name_field"
data-testid="name"
/>
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
:label="$options.i18n.form.addStatusChecks" :label="$options.i18n.form.addStatusChecks"
...@@ -171,7 +166,6 @@ export default { ...@@ -171,7 +166,6 @@ export default {
:state="urlState" :state="urlState"
type="url" type="url"
:placeholder="`https://api.gitlab.com/`" :placeholder="`https://api.gitlab.com/`"
data-qa-selector="external_url_field"
data-testid="url" data-testid="url"
/> />
</gl-form-group> </gl-form-group>
......
...@@ -82,6 +82,7 @@ export default { ...@@ -82,6 +82,7 @@ export default {
:empty-text="$options.i18n.emptyTableText" :empty-text="$options.i18n.emptyTableText"
show-empty show-empty
stacked="md" stacked="md"
data-testid="status-checks-table"
> >
<template #cell(protectedBranches)="{ item }"> <template #cell(protectedBranches)="{ item }">
<branch :branches="item.protectedBranches" /> <branch :branches="item.protectedBranches" />
......
...@@ -10,10 +10,6 @@ module EE ...@@ -10,10 +10,6 @@ module EE
before_action :log_archive_audit_event, only: [:archive] before_action :log_archive_audit_event, only: [:archive]
before_action :log_unarchive_audit_event, only: [:unarchive] 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 before_action only: :show do
push_frontend_feature_flag(:cve_id_request_button, project) push_frontend_feature_flag(:cve_id_request_button, project)
end end
......
...@@ -68,23 +68,21 @@ module EE ...@@ -68,23 +68,21 @@ module EE
end end
def approvals_app_data(project = @project) def approvals_app_data(project = @project)
data = { 'project_id': project.id, {
'can_edit': can_modify_approvers.to_s, data: {
'project_path': expose_path(api_v4_projects_path(id: project.id)), 'project_id': project.id,
'settings_path': expose_path(api_v4_projects_approval_settings_path(id: project.id)), 'can_edit': can_modify_approvers.to_s,
'rules_path': expose_path(api_v4_projects_approval_settings_rules_path(id: project.id)), 'project_path': expose_path(api_v4_projects_path(id: project.id)),
'allow_multi_rule': project.multiple_approval_rules_available?.to_s, 'settings_path': expose_path(api_v4_projects_approval_settings_path(id: project.id)),
'eligible_approvers_docs_path': help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'), 'rules_path': expose_path(api_v4_projects_approval_settings_rules_path(id: project.id)),
'security_approvals_help_page_path': help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests'), 'allow_multi_rule': project.multiple_approval_rules_available?.to_s,
'security_configuration_path': project_security_configuration_path(project), 'eligible_approvers_docs_path': help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'),
'vulnerability_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-security-approvals-within-a-project'), 'security_approvals_help_page_path': help_page_path('user/application_security/index', anchor: 'security-approvals-in-merge-requests'),
'license_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-license-approvals-within-a-project') } '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'),
if ::Feature.enabled?(:ff_compliance_approval_gates, project, default_enabled: :yaml) 'license_check_help_page_path': help_page_path('user/application_security/index', anchor: 'enabling-license-approvals-within-a-project')
data[:external_approval_rules_path] = expose_path(api_v4_projects_external_status_checks_path(id: project.id)) }
end }
{ data: data }
end end
def status_checks_app_data(project) 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' ...@@ -3,185 +3,131 @@ require 'spec_helper'
RSpec.describe 'Project settings > [EE] Merge Requests', :js do RSpec.describe 'Project settings > [EE] Merge Requests', :js do
include GitlabRoutingHelper include GitlabRoutingHelper
include FeatureApprovalHelper
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:group_member) { create(:user) } 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 before do
stub_licensed_features(compliance_approval_gates: true)
sign_in(user) sign_in(user)
project.add_maintainer(user) project.add_maintainer(user)
group.add_developer(user) group.add_developer(user)
group.add_developer(group_member) group.add_developer(group_member)
end end
it 'adds approver' do context 'Status checks' do
visit edit_project_path(project) context 'Feature is not available' do
before do
open_modal(text: 'Add approval rule', expand: false) stub_licensed_features(compliance_approval_gates: false)
open_approver_select end
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 it 'does not render the status checks area' do
project.add_developer(non_group_approver) expect(page).not_to have_selector('[data-testid="status-checks-table"]')
end
end end
it 'removes approver group' do context 'Feature is available' do
visit edit_project_path(project) before do
stub_licensed_features(compliance_approval_gates: true)
expect_avatar(find('.js-members'), rule.approvers) end
open_modal(text: 'Edit', expand: false) it 'adds a status check' do
remove_approver(group.name) visit edit_project_path(project)
click_button "Update approval rule"
wait_for_requests
expect_avatar(find('.js-members'), [non_group_approver]) click_button 'Add status check'
end
end
it 'adds a status check' do within('.modal-content') do
visit edit_project_path(project) 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 wait_for_requests
find('button', text: "Users or groups").click
find('button', text: "Status check").click
find('[data-qa-selector="rule_name_field"]').set('My new rule') expect(find('[data-testid="status-checks-table"]')).to have_content('My new check')
find('[data-qa-selector="external_url_field"]').set('https://api.gitlab.com') end
click_button 'Add approval rule' context 'with a status check' do
end 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') expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
end
context 'with a status check' do within('[data-testid="status-checks-table"]') do
let_it_be(:rule) { create(:external_status_check, project: project) } click_button 'Edit'
end
it 'updates the status check' do within('.modal-content') do
visit edit_project_path(project) 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 expect(find('[data-testid="status-checks-table"]')).to have_content('Something new')
find('[data-qa-selector="rule_name_field"]').set('Something new') end
click_button 'Update approval rule' it 'removes the status check' do
end visit edit_project_path(project)
wait_for_requests
expect(first('.js-name')).to have_content('Something new') expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name)
end
it 'removes the status check' do within('[data-testid="status-checks-table"]') do
visit edit_project_path(project) 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 expect(find('[data-testid="status-checks-table"]')).not_to have_content(rule.name)
click_button 'Remove status check' end
end end
wait_for_requests
expect(first('.js-name')).not_to have_content(rule.name)
end end
end end
context 'issuable default templates feature not available' do context 'Issuable default templates' do
before do context 'Feature is not available' do
stub_licensed_features(issuable_default_templates: false) before do
end stub_licensed_features(issuable_default_templates: false)
end
it 'input to configure merge request template is not shown' do it 'input to configure merge request template is not shown' do
visit edit_project_path(project) visit edit_project_path(project)
expect(page).not_to have_selector('#project_merge_requests_template') expect(page).not_to have_selector('#project_merge_requests_template')
end end
it "does not mention the merge request template in the section's description text" do it "does not mention the merge request template in the section's description text" do
visit edit_project_path(project) 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
end
context 'issuable default templates feature is available' do context 'Feature is available' do
before do before do
stub_licensed_features(issuable_default_templates: true) stub_licensed_features(issuable_default_templates: true)
end end
it 'input to configure merge request template is shown' do it 'input to configure merge request template is shown' do
visit edit_project_path(project) visit edit_project_path(project)
expect(page).to have_selector('#project_merge_requests_template') expect(page).to have_selector('#project_merge_requests_template')
end end
it "mentions the merge request template in the section's description text" do it "mentions the merge request template in the section's description text" do
visit edit_project_path(project) 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 end
end end
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`Approvals ModalRuleRemove matches the snapshot for multiple approvers 1`] = `
<div <div
title="Remove approvers?" 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'; ...@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import ModalRuleRemove from 'ee/approvals/components/modal_rule_remove.vue'; import ModalRuleRemove from 'ee/approvals/components/modal_rule_remove.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import { createExternalRule } from '../mocks';
const MODAL_MODULE = 'deleteModal'; const MODAL_MODULE = 'deleteModal';
const TEST_MODAL_ID = 'test-delete-modal-id'; const TEST_MODAL_ID = 'test-delete-modal-id';
...@@ -19,7 +18,6 @@ const SINGLE_APPROVER = { ...@@ -19,7 +18,6 @@ const SINGLE_APPROVER = {
...TEST_RULE, ...TEST_RULE,
approvers: [{ id: 1 }], approvers: [{ id: 1 }],
}; };
const EXTERNAL_RULE = createExternalRule();
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -67,7 +65,6 @@ describe('Approvals ModalRuleRemove', () => { ...@@ -67,7 +65,6 @@ describe('Approvals ModalRuleRemove', () => {
}; };
actions = { actions = {
deleteRule: jest.fn(), deleteRule: jest.fn(),
deleteExternalApprovalRule: jest.fn(),
}; };
}); });
...@@ -94,7 +91,6 @@ describe('Approvals ModalRuleRemove', () => { ...@@ -94,7 +91,6 @@ describe('Approvals ModalRuleRemove', () => {
type | rule type | rule
${'multiple approvers'} | ${TEST_RULE} ${'multiple approvers'} | ${TEST_RULE}
${'singular approver'} | ${SINGLE_APPROVER} ${'singular approver'} | ${SINGLE_APPROVER}
${'external approval'} | ${EXTERNAL_RULE}
`('matches the snapshot for $type', ({ rule }) => { `('matches the snapshot for $type', ({ rule }) => {
deleteModalState.data = rule; deleteModalState.data = rule;
factory(); factory();
...@@ -102,19 +98,15 @@ describe('Approvals ModalRuleRemove', () => { ...@@ -102,19 +98,15 @@ describe('Approvals ModalRuleRemove', () => {
expect(findModal().element).toMatchSnapshot(); expect(findModal().element).toMatchSnapshot();
}); });
it.each` it('calls deleteRule when the modal is submitted', () => {
typeType | action | rule deleteModalState.data = TEST_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;
factory(); factory();
expect(actions[action]).not.toHaveBeenCalled(); expect(actions.deleteRule).not.toHaveBeenCalled();
const modal = findModal(); const modal = findModal();
modal.vm.$emit('ok', new Event('submit')); 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 ...@@ -6,11 +6,10 @@ import ProjectRules from 'ee/approvals/components/project_settings/project_rules
import RuleName from 'ee/approvals/components/rule_name.vue'; import RuleName from 'ee/approvals/components/rule_name.vue';
import Rules from 'ee/approvals/components/rules.vue'; import Rules from 'ee/approvals/components/rules.vue';
import UnconfiguredSecurityRules from 'ee/approvals/components/security_configuration/unconfigured_security_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 { createStoreOptions } from 'ee/approvals/stores';
import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; 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(); const TEST_RULES = createProjectRules();
...@@ -149,26 +148,4 @@ describe('Approvals ProjectRules', () => { ...@@ -149,26 +148,4 @@ describe('Approvals ProjectRules', () => {
expect(wrapper.find(UnconfiguredSecurityRules).exists()).toBe(true); 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'; ...@@ -2,23 +2,16 @@ import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import ApproverTypeSelect from 'ee/approvals/components/approver_type_select.vue';
import ApproversList from 'ee/approvals/components/approvers_list.vue'; import ApproversList from 'ee/approvals/components/approvers_list.vue';
import ApproversSelect from 'ee/approvals/components/approvers_select.vue'; import ApproversSelect from 'ee/approvals/components/approvers_select.vue';
import RuleForm from 'ee/approvals/components/rule_form.vue'; import RuleForm from 'ee/approvals/components/rule_form.vue';
import { import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from 'ee/approvals/constants';
TYPE_USER,
TYPE_GROUP,
TYPE_HIDDEN_GROUPS,
RULE_TYPE_EXTERNAL_APPROVAL,
} from 'ee/approvals/constants';
import { createStoreOptions } from 'ee/approvals/stores'; import { createStoreOptions } from 'ee/approvals/stores';
import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue'; import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { createExternalRule } from '../mocks';
const TEST_PROJECT_ID = '7'; const TEST_PROJECT_ID = '7';
const TEST_RULE = { const TEST_RULE = {
...@@ -39,10 +32,6 @@ const TEST_FALLBACK_RULE = { ...@@ -39,10 +32,6 @@ const TEST_FALLBACK_RULE = {
approvalsRequired: 1, approvalsRequired: 1,
isFallback: true, isFallback: true,
}; };
const TEST_EXTERNAL_APPROVAL_RULE = {
...createExternalRule(),
protectedBranches: TEST_PROTECTED_BRANCHES,
};
const TEST_LOCKED_RULE_NAME = 'LOCKED_RULE'; const TEST_LOCKED_RULE_NAME = 'LOCKED_RULE';
const nameTakenError = { const nameTakenError = {
response: { response: {
...@@ -53,13 +42,6 @@ const nameTakenError = { ...@@ -53,13 +42,6 @@ const nameTakenError = {
}, },
}, },
}; };
const urlTakenError = {
response: {
data: {
message: ['External url has already been taken'],
},
},
};
Vue.use(Vuex); Vue.use(Vuex);
...@@ -70,19 +52,11 @@ describe('EE Approvals RuleForm', () => { ...@@ -70,19 +52,11 @@ describe('EE Approvals RuleForm', () => {
let store; let store;
let actions; let actions;
const createComponent = (props = {}, features = {}) => { const createComponent = (props = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(RuleForm, { shallowMount(RuleForm, {
propsData: props, propsData: props,
store: new Vuex.Store(store), store: new Vuex.Store(store),
provide: {
glFeatures: {
ffComplianceApprovalGates: true,
scopedApprovalRules: true,
...features,
},
},
stubs: { stubs: {
GlFormGroup: stubComponent(GlFormGroup, { GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback'], props: ['state', 'invalidFeedback'],
...@@ -106,9 +80,6 @@ describe('EE Approvals RuleForm', () => { ...@@ -106,9 +80,6 @@ describe('EE Approvals RuleForm', () => {
const findApproversValidation = () => wrapper.findByTestId('approvers-group'); const findApproversValidation = () => wrapper.findByTestId('approvers-group');
const findApproversList = () => wrapper.findComponent(ApproversList); const findApproversList = () => wrapper.findComponent(ApproversList);
const findProtectedBranchesSelector = () => wrapper.findComponent(ProtectedBranchesSelector); 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 findBranchesValidation = () => wrapper.findByTestId('branches-group');
const inputsAreValid = (inputs) => inputs.every((x) => x.props('state')); const inputsAreValid = (inputs) => inputs.every((x) => x.props('state'));
...@@ -126,20 +97,12 @@ describe('EE Approvals RuleForm', () => { ...@@ -126,20 +97,12 @@ describe('EE Approvals RuleForm', () => {
findBranchesValidation(), findBranchesValidation(),
]; ];
const findValidationForExternal = () => [
findNameValidation(),
findExternalUrlValidation(),
findBranchesValidation(),
];
beforeEach(() => { beforeEach(() => {
store = createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID }); store = createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID });
['postRule', 'putRule', 'deleteRule', 'putFallbackRule', 'postExternalApprovalRule'].forEach( ['postRule', 'putRule', 'deleteRule', 'putFallbackRule'].forEach((actionName) => {
(actionName) => { jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {});
jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {}); });
},
);
({ actions } = store.modules.approvals); ({ actions } = store.modules.approvals);
}); });
...@@ -231,112 +194,6 @@ describe('EE Approvals RuleForm', () => { ...@@ -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', () => { describe('without initRule', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ isMrEdit: false }); createComponent({ isMrEdit: false });
...@@ -655,13 +512,13 @@ describe('EE Approvals RuleForm', () => { ...@@ -655,13 +512,13 @@ describe('EE Approvals RuleForm', () => {
describe('with approval suggestions', () => { describe('with approval suggestions', () => {
describe.each` describe.each`
defaultRuleName | expectedDisabledAttribute | approverTypeSelect defaultRuleName | expectedDisabledAttribute
${'Vulnerability-Check'} | ${true} | ${false} ${'Vulnerability-Check'} | ${true}
${'License-Check'} | ${true} | ${false} ${'License-Check'} | ${true}
${'Foo Bar Baz'} | ${false} | ${true} ${'Foo Bar Baz'} | ${false}
`( `(
'with defaultRuleName set to $defaultRuleName', 'with defaultRuleName set to $defaultRuleName',
({ defaultRuleName, expectedDisabledAttribute, approverTypeSelect }) => { ({ defaultRuleName, expectedDisabledAttribute }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
initRule: null, initRule: null,
...@@ -675,12 +532,6 @@ describe('EE Approvals RuleForm', () => { ...@@ -675,12 +532,6 @@ describe('EE Approvals RuleForm', () => {
} the name text field`, () => { } the name text field`, () => {
expect(findNameInput().props('disabled')).toBe(expectedDisabledAttribute); 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', () => { ...@@ -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 = () => [ export const createProjectRules = () => [
{ {
id: 1, id: 1,
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { import { mapApprovalRuleRequest, mapApprovalSettingsResponse } from 'ee/approvals/mappers';
mapApprovalRuleRequest,
mapApprovalSettingsResponse,
mapExternalApprovalResponse,
} from 'ee/approvals/mappers';
import * as types from 'ee/approvals/stores/modules/base/mutation_types'; import * as types from 'ee/approvals/stores/modules/base/mutation_types';
import * as actions from 'ee/approvals/stores/modules/project_settings/actions'; 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 testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -22,11 +17,6 @@ const TEST_RULE_REQUEST = { ...@@ -22,11 +17,6 @@ const TEST_RULE_REQUEST = {
groups: [7], groups: [7],
users: [8, 9], users: [8, 9],
}; };
const TEST_EXTERNAL_RULE_REQUEST = {
name: 'Lorem',
protected_branch_ids: [],
external_url: 'https://www.gitlab.com',
};
const TEST_RULE_RESPONSE = { const TEST_RULE_RESPONSE = {
id: 7, id: 7,
name: 'Ipsum', name: 'Ipsum',
...@@ -37,19 +27,14 @@ const TEST_RULE_RESPONSE = { ...@@ -37,19 +27,14 @@ const TEST_RULE_RESPONSE = {
}; };
const TEST_SETTINGS_PATH = 'projects/9/approval_settings'; const TEST_SETTINGS_PATH = 'projects/9/approval_settings';
const TEST_RULES_PATH = 'projects/9/approval_settings/rules'; 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', () => { describe('EE approvals project settings module actions', () => {
let state; let state;
let mock; let mock;
let originalGon;
beforeEach(() => { beforeEach(() => {
originalGon = { ...window.gon };
window.gon = { features: { ffComplianceApprovalGates: true } };
state = { state = {
settings: { settings: {
externalApprovalRulesPath: TEST_EXTERNAL_RULES_PATH,
projectId: TEST_PROJECT_ID, projectId: TEST_PROJECT_ID,
settingsPath: TEST_SETTINGS_PATH, settingsPath: TEST_SETTINGS_PATH,
rulesPath: TEST_RULES_PATH, rulesPath: TEST_RULES_PATH,
...@@ -60,7 +45,6 @@ describe('EE approvals project settings module actions', () => { ...@@ -60,7 +45,6 @@ describe('EE approvals project settings module actions', () => {
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
window.gon = originalGon;
}); });
describe('requestRules', () => { describe('requestRules', () => {
...@@ -106,33 +90,23 @@ describe('EE approvals project settings module actions', () => { ...@@ -106,33 +90,23 @@ describe('EE approvals project settings module actions', () => {
}); });
describe('fetchRules', () => { 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( return testAction(
actions.fetchRules, actions.fetchRules,
null, null,
state, 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', () => { it('dispatches request/receive on error', () => {
...@@ -146,21 +120,6 @@ describe('EE approvals project settings module actions', () => { ...@@ -146,21 +120,6 @@ describe('EE approvals project settings module actions', () => {
[{ type: 'requestRules' }, { type: 'receiveRulesError' }], [{ 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', () => { describe('postRuleSuccess', () => {
...@@ -175,44 +134,43 @@ describe('EE approvals project settings module actions', () => { ...@@ -175,44 +134,43 @@ describe('EE approvals project settings module actions', () => {
}); });
}); });
describe('POST', () => { describe('postRule', () => {
it.each` it('dispatches success on success', () => {
action | path | request mock.onPost(TEST_RULES_PATH).replyOnce(httpStatus.OK);
${'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);
return testAction(actions[action], request, state, [], [{ type: 'postRuleSuccess' }], () => { return testAction(
expect(mock.history.post).toEqual([ actions.postRule,
expect.objectContaining({ TEST_RULE_REQUEST,
url: path, state,
data: JSON.stringify(mapApprovalRuleRequest(request)), [],
}), [{ type: 'postRuleSuccess' }],
]); () => {
}); expect(mock.history.post).toEqual([
expect.objectContaining({
url: TEST_RULES_PATH,
data: JSON.stringify(mapApprovalRuleRequest(TEST_RULE_REQUEST)),
}),
]);
},
);
}); });
}); });
describe('PUT', () => { describe('putRule', () => {
it.each` it('dispatches success on success', () => {
action | path | request mock.onPut(`${TEST_RULES_PATH}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
${'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);
return testAction( return testAction(
actions[action], actions.putRule,
{ id: TEST_RULE_ID, ...request }, { id: TEST_RULE_ID, ...TEST_RULE_REQUEST },
state, state,
[], [],
[{ type: 'postRuleSuccess' }], [{ type: 'postRuleSuccess' }],
() => { () => {
expect(mock.history.put).toEqual([ expect(mock.history.put).toEqual([
expect.objectContaining({ expect.objectContaining({
url: `${path}/${TEST_RULE_ID}`, url: `${TEST_RULES_PATH}/${TEST_RULE_ID}`,
data: JSON.stringify(mapApprovalRuleRequest(request)), data: JSON.stringify(mapApprovalRuleRequest(TEST_RULE_REQUEST)),
}), }),
]); ]);
}, },
...@@ -244,16 +202,12 @@ describe('EE approvals project settings module actions', () => { ...@@ -244,16 +202,12 @@ describe('EE approvals project settings module actions', () => {
}); });
}); });
describe('DELETE', () => { describe('deleteRule', () => {
it.each` it('dispatches success on success', () => {
action | path mock.onDelete(`${TEST_RULES_PATH}/${TEST_RULE_ID}`).replyOnce(httpStatus.OK);
${'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);
return testAction( return testAction(
actions[action], actions.deleteRule,
TEST_RULE_ID, TEST_RULE_ID,
state, state,
[], [],
...@@ -261,7 +215,7 @@ describe('EE approvals project settings module actions', () => { ...@@ -261,7 +215,7 @@ describe('EE approvals project settings module actions', () => {
() => { () => {
expect(mock.history.delete).toEqual([ expect(mock.history.delete).toEqual([
expect.objectContaining({ 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 ...@@ -367,17 +367,20 @@ RSpec.describe ProjectsHelper do
allow(helper).to receive(:can?).and_return(true) allow(helper).to receive(:can?).and_return(true)
end end
context 'with the status check feature flag' do it 'returns the correct data' do
where(feature_flag_enabled: [true, false]) expect(subject[:data]).to eq({
with_them do project_id: project.id,
before do can_edit: 'true',
stub_feature_flags(ff_compliance_approval_gates: feature_flag_enabled) project_path: expose_path(api_v4_projects_path(id: project.id)),
end 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)),
it 'includes external_status_checks_path only when enabled' do allow_multi_rule: project.multiple_approval_rules_available?.to_s,
expect(subject[:data].key?(:external_approval_rules_path)).to eq(feature_flag_enabled) eligible_approvers_docs_path: help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers'),
end security_approvals_help_page_path: help_page_path('user/application_security/index', anchor: 'security-approvals-in-merge-requests'),
end 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
end end
......
...@@ -4142,15 +4142,9 @@ msgstr "" ...@@ -4142,15 +4142,9 @@ msgstr ""
msgid "ApprovalRule|Rule name" msgid "ApprovalRule|Rule name"
msgstr "" msgstr ""
msgid "ApprovalRule|Status check"
msgstr ""
msgid "ApprovalRule|Target branch" msgid "ApprovalRule|Target branch"
msgstr "" msgstr ""
msgid "ApprovalRule|Users or groups"
msgstr ""
msgid "ApprovalStatusTooltip|Adheres to separation of duties" msgid "ApprovalStatusTooltip|Adheres to separation of duties"
msgstr "" msgstr ""
...@@ -13431,9 +13425,6 @@ msgstr "" ...@@ -13431,9 +13425,6 @@ msgstr ""
msgid "External storage authentication token" msgid "External storage authentication token"
msgstr "" msgstr ""
msgid "External url has already been taken"
msgstr ""
msgid "ExternalAuthorizationService|Classification label" msgid "ExternalAuthorizationService|Classification label"
msgstr "" msgstr ""
...@@ -31122,9 +31113,6 @@ msgstr "" ...@@ -31122,9 +31113,6 @@ msgstr ""
msgid "StatusCheck|External API is already in use by another status check." msgid "StatusCheck|External API is already in use by another status check."
msgstr "" 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." msgid "StatusCheck|Invoke an external API as part of the pipeline process."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment