diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
index 79586bc42dfedc153c10141f2570e593f17b104e..41681e373b0a785bda219f2e68e36c8837740237 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
@@ -13,15 +13,18 @@ import createPackagesProtectionRuleMutation from '~/packages_and_registries/sett
import updatePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_protection_rule.mutation.graphql';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import {
- MinimumAccessLevelOptions,
- GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
-} from '~/packages_and_registries/settings/project/constants';
+import { GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER } from '~/packages_and_registries/settings/project/constants';
const PACKAGES_PROTECTION_RULES_SAVED_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while saving the package protection rule.',
);
+// Needs to be an empty string instead of `null` for @vue/compat. The value
+// should be transformed back to `null` as an input to the GraphQL query.
+const GRAPHQL_ACCESS_LEVEL_VALUE_NULL = '';
+const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
+const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
+
export default {
components: {
GlButton,
@@ -53,8 +56,12 @@ export default {
packageProtectionRuleFormData: {
packageNamePattern: this.rule?.packageNamePattern ?? '',
packageType: this.rule?.packageType ?? 'NPM',
- minimumAccessLevelForPush:
- this.rule?.minimumAccessLevelForPush ?? GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
+ minimumAccessLevelForPush: this.isExistingRule()
+ ? this.rule.minimumAccessLevelForPush ?? GRAPHQL_ACCESS_LEVEL_VALUE_NULL
+ : GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
+ minimumAccessLevelForDelete: this.isExistingRule()
+ ? this.rule.minimumAccessLevelForDelete ?? GRAPHQL_ACCESS_LEVEL_VALUE_NULL
+ : GRAPHQL_ACCESS_LEVEL_VALUE_OWNER,
},
updateInProgress: false,
alertErrorMessage: '',
@@ -62,18 +69,20 @@ export default {
},
computed: {
mutation() {
- return this.rule
+ return this.isExistingRule()
? updatePackagesProtectionRuleMutation
: createPackagesProtectionRuleMutation;
},
mutationKey() {
- return this.rule ? 'updatePackagesProtectionRule' : 'createPackagesProtectionRule';
+ return this.isExistingRule()
+ ? 'updatePackagesProtectionRule'
+ : 'createPackagesProtectionRule';
},
showLoadingIcon() {
return this.updateInProgress;
},
submitButtonText() {
- return this.rule ? __('Save changes') : s__('PackageRegistry|Add rule');
+ return this.isExistingRule() ? __('Save changes') : s__('PackageRegistry|Add rule');
},
isEmptyPackageName() {
return !this.packageProtectionRuleFormData.packageNamePattern;
@@ -88,12 +97,20 @@ export default {
return {
projectPath: this.projectPath,
...this.packageProtectionRuleFormData,
+ minimumAccessLevelForPush:
+ this.packageProtectionRuleFormData.minimumAccessLevelForPush || null,
+ minimumAccessLevelForDelete:
+ this.packageProtectionRuleFormData.minimumAccessLevelForDelete || null,
};
},
updatePackagesProtectionRuleMutationInput() {
return {
id: this.rule?.id,
...this.packageProtectionRuleFormData,
+ minimumAccessLevelForPush:
+ this.packageProtectionRuleFormData.minimumAccessLevelForPush || null,
+ minimumAccessLevelForDelete:
+ this.packageProtectionRuleFormData.minimumAccessLevelForDelete || null,
};
},
packageTypeOptions() {
@@ -112,13 +129,37 @@ export default {
return packageTypeOptions.sort((a, b) => a.text.localeCompare(b.text));
},
+ minimumAccessLevelForPushOptions() {
+ return [
+ ...(this.glFeatures.packagesProtectedPackagesDelete
+ ? [{ value: GRAPHQL_ACCESS_LEVEL_VALUE_NULL, text: __('Developer (default)') }]
+ : []),
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: s__('AdminUsers|Administrator') },
+ ];
+ },
+ minimumAccessLevelForDeleteOptions() {
+ return [
+ {
+ value: GRAPHQL_ACCESS_LEVEL_VALUE_NULL,
+ text: s__('PackageRegistry|Maintainer (default)'),
+ },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: s__('AdminUsers|Administrator') },
+ ];
+ },
},
methods: {
+ isExistingRule() {
+ return Boolean(this.rule);
+ },
submit() {
this.clearAlertErrorMessage();
this.updateInProgress = true;
- const input = this.rule
+
+ const input = this.isExistingRule()
? this.updatePackagesProtectionRuleMutationInput
: this.createPackagesProtectionRuleMutationInput;
@@ -153,7 +194,6 @@ export default {
this.$emit('cancel');
},
},
- minimumAccessLevelForPushOptions: MinimumAccessLevelOptions,
};
@@ -214,9 +254,22 @@ export default {
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
index 488ec241d9c19939f341c2796d9b15c50b9b78f9..858eab0ee9641aafcb35651dcbd7bdbbf616907b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -17,11 +17,17 @@ import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/
import deletePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql';
import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
import { getAccessLevelLabel } from '~/packages_and_registries/settings/project/utils';
+import {
+ PackagesMinimumAccessForPushLevelText,
+ PackagesMinimumAccessForDeleteLevelText,
+} from '~/packages_and_registries/settings/project/constants';
import { s__, __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const PAGINATION_DEFAULT_PER_PAGE = 10;
const I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH = s__('PackageRegistry|Minimum access level for push');
+const I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE = s__('PackageRegistry|Minimum access level for delete');
export default {
components: {
@@ -40,6 +46,7 @@ export default {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectPath'],
i18n: {
delete: __('Delete'),
@@ -59,6 +66,7 @@ export default {
),
},
minimumAccessLevelForPush: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
+ minimumAccessLevelForDelete: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
},
data() {
return {
@@ -80,10 +88,16 @@ export default {
? s__('PackageRegistry|Edit protection rule')
: s__('PackageRegistry|Add protection rule');
},
+ fields() {
+ return this.glFeatures.packagesProtectedPackagesDelete
+ ? this.$options.fields
+ : this.$options.fields.filter((field) => field.key !== 'minimumAccessLevelForDelete');
+ },
tableItems() {
return this.packageProtectionRulesQueryResult.map((packagesProtectionRule) => {
return {
id: packagesProtectionRule.id,
+ minimumAccessLevelForDelete: packagesProtectionRule.minimumAccessLevelForDelete,
minimumAccessLevelForPush: packagesProtectionRule.minimumAccessLevelForPush,
packageNamePattern: packagesProtectionRule.packageNamePattern,
packageType: packagesProtectionRule.packageType,
@@ -219,6 +233,11 @@ export default {
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
tdClass: '!gl-align-middle',
},
+ {
+ key: 'minimumAccessLevelForDelete',
+ label: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
+ tdClass: '!gl-align-middle',
+ },
{
key: 'rowActions',
label: __('Actions'),
@@ -228,6 +247,8 @@ export default {
],
getAccessLevelLabel,
getPackageTypeLabel,
+ minimumAccessForPushLevelText: PackagesMinimumAccessForPushLevelText,
+ minimumAccessForDeleteLevelText: PackagesMinimumAccessForDeleteLevelText,
modal: { id: 'delete-package-protection-rule-confirmation-modal' },
modalActionPrimary: {
text: s__('PackageRegistry|Delete package protection rule'),
@@ -272,7 +293,7 @@ export default {
v-else-if="containsTableItems"
class="gl-border-t-1 gl-border-t-gray-100 gl-border-t-solid"
:items="tableItems"
- :fields="$options.fields"
+ :fields="fields"
stacked="md"
:aria-label="$options.i18n.settingBlockTitle"
:busy="isLoadingPackageProtectionRules"
@@ -289,7 +310,16 @@ export default {
- {{ $options.getAccessLevelLabel(item.minimumAccessLevelForPush) }}
+ {{ $options.minimumAccessForPushLevelText[item.minimumAccessLevelForPush] }}
+
+
+
+
+
+ {{ $options.minimumAccessForDeleteLevelText[item.minimumAccessLevelForDelete] }}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index d8764bba6f7373048713819b76fe0d55d3598639..c5d27c5108bf6cd3719fe3ee404aa07093668450 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -122,9 +122,9 @@ export const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
export const MinimumAccessLevelText = {
- [GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN]: s__('AdminUsers|Administrator'),
[GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER]: __('Maintainer'),
[GRAPHQL_ACCESS_LEVEL_VALUE_OWNER]: __('Owner'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN]: s__('AdminUsers|Administrator'),
};
export const MinimumAccessLevelOptions = [
@@ -133,6 +133,32 @@ export const MinimumAccessLevelOptions = [
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: s__('AdminUsers|Administrator') },
];
+export const PackagesMinimumAccessForPushLevelText = {
+ null: s__('PackageRegistry|Developer (default)'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER]: __('Maintainer'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_OWNER]: __('Owner'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN]: s__('AdminUsers|Administrator'),
+};
+
+export const PackagesMinimumAccessForDeleteLevelText = {
+ null: s__('PackageRegistry|Maintainer (default)'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_OWNER]: __('Owner'),
+ [GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN]: s__('AdminUsers|Administrator'),
+};
+
+export const PackagesMinimumAccessLevelOptions = [
+ { value: null, text: __('Developer (default)') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: s__('AdminUsers|Administrator') },
+];
+
+export const PackagesMinimumAccessLevelForDeleteOptions = [
+ { value: null, text: __('Developer (default)') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
+ { value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: s__('AdminUsers|Administrator') },
+];
+
export const FETCH_SETTINGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the cleanup policy.',
);
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql
index 5286c70129d66c10530140604ea2276a72c92cd0..372a5f2743e5e38dc7b967ddc7ad7bbf757f4d6b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql
@@ -4,6 +4,7 @@ mutation createPackagesProtectionRule($input: CreatePackagesProtectionRuleInput!
id
packageNamePattern
packageType
+ minimumAccessLevelForDelete
minimumAccessLevelForPush
}
errors
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
index dd9c8187f9255996ac93a1b7f8cfc6324969adde..3abdeaec5a71e2c33bc32248e74b0956fe7e45db 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
@@ -15,6 +15,7 @@ query getProjectPackageProtectionRules(
packageNamePattern
packageType
minimumAccessLevelForPush
+ minimumAccessLevelForDelete
}
pageInfo {
...PageInfo
diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb
index 8bf28f43be8948d380b189aeb9e9eaff9fb94db0..d2e04c538f52a67cd44501ae2f0f98084c49d4e6 100644
--- a/app/controllers/projects/settings/packages_and_registries_controller.rb
+++ b/app/controllers/projects/settings/packages_and_registries_controller.rb
@@ -35,6 +35,7 @@ def registry_settings_enabled!
def set_feature_flag_packages_protected_packages
push_frontend_feature_flag(:packages_protected_packages_conan, project)
push_frontend_feature_flag(:packages_protected_packages_maven, project)
+ push_frontend_feature_flag(:packages_protected_packages_delete, project)
end
def set_feature_flag_container_registry_protected_tags
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 253b92d27526b953c2777615d1110464fda0200b..777319a8bc413a8e33c9e9416df6970b359ec652 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21325,6 +21325,9 @@ msgstr ""
msgid "Developer"
msgstr ""
+msgid "Developer (default)"
+msgstr ""
+
msgid "Development widget is not enabled for this work item type"
msgstr ""
@@ -42015,6 +42018,9 @@ msgstr ""
msgid "PackageRegistry|Deleting this package while request forwarding is enabled for the project can pose a security risk. Do you want to delete %{name} version %{version} anyway? %{docLinkStart}What are the risks?%{docLinkEnd}"
msgstr ""
+msgid "PackageRegistry|Developer (default)"
+msgstr ""
+
msgid "PackageRegistry|Duplicate packages"
msgstr ""
@@ -42123,6 +42129,9 @@ msgstr ""
msgid "PackageRegistry|Machine learning model"
msgstr ""
+msgid "PackageRegistry|Maintainer (default)"
+msgstr ""
+
msgid "PackageRegistry|Manage storage used by package assets"
msgstr ""
@@ -42138,6 +42147,9 @@ msgstr ""
msgid "PackageRegistry|Maven XML"
msgstr ""
+msgid "PackageRegistry|Minimum access level for delete"
+msgstr ""
+
msgid "PackageRegistry|Minimum access level for push"
msgstr ""
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
index 7c7e1a3b69131cc3c3cf7f8bcd83bf6ab575e357..497aa407aca23dcec5f873a38a433cb12662a6a2 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
@@ -26,6 +26,7 @@ describe('Packages Protection Rule Form', () => {
glFeatures: {
packagesProtectedPackagesConan: true,
packagesProtectedPackagesMaven: true,
+ packagesProtectedPackagesDelete: true,
},
};
@@ -34,10 +35,20 @@ describe('Packages Protection Rule Form', () => {
const findPackageTypeSelect = () => wrapper.findByRole('combobox', { name: /type/i });
const findMinimumAccessLevelForPushSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
+ const findMinimumAccessLevelForDeleteSelect = () =>
+ wrapper.findByRole('combobox', { name: /minimum access level for delete/i });
const findCancelButton = () => wrapper.findByRole('button', { name: /cancel/i });
const findSubmitButton = () => wrapper.findByTestId('submit-btn');
const findForm = () => wrapper.findComponent(GlForm);
+ const setSelectValue = async (selectWrapper, value) => {
+ await selectWrapper.setValue(value);
+ // Work around compat flag which prevents change event from being triggered by setValue.
+ // TODO: Disable WRAPPER_SET_VALUE_DOES_NOT_TRIGGER_CHANGE globally:
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/526008
+ await selectWrapper.trigger('change');
+ };
+
const mountComponent = ({ data, config, props, provide = defaultProvidedValues } = {}) => {
wrapper = mountExtended(PackagesProtectionRuleForm, {
provide,
@@ -123,14 +134,93 @@ describe('Packages Protection Rule Form', () => {
});
describe('form field "minimumAccessLevelForPushSelect"', () => {
+ const findMinimumAccessLevelForPushSelectOptionValues = () =>
+ findMinimumAccessLevelForPushSelect()
+ .findAll('option')
+ .wrappers.map((option) => option.element.value);
+
it('contains only the options for maintainer and owner', () => {
mountComponent();
expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
- const minimumAccessLevelForPushSelectOptions = findMinimumAccessLevelForPushSelect()
+ expect(findMinimumAccessLevelForPushSelectOptionValues()).toEqual([
+ '',
+ 'MAINTAINER',
+ 'OWNER',
+ 'ADMIN',
+ ]);
+ });
+
+ it('sets correct option for "null" value', () => {
+ mountComponent({
+ props: {
+ rule: { ...packagesProtectionRulesData[0], minimumAccessLevelForPush: null },
+ },
+ });
+
+ expect(findMinimumAccessLevelForPushSelect().element.value).toBe('');
+ });
+
+ describe('when feature flag packagesProtectedPackagesDelete is disabled', () => {
+ it('does not show option "Developer (default)"', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvidedValues,
+ glFeatures: {
+ ...defaultProvidedValues.glFeatures,
+ packagesProtectedPackagesDelete: false,
+ },
+ },
+ });
+
+ expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
+ expect(findMinimumAccessLevelForPushSelectOptionValues()).toEqual([
+ 'MAINTAINER',
+ 'OWNER',
+ 'ADMIN',
+ ]);
+ });
+ });
+ });
+
+ describe('form field "minimumAccessLevelForDeleteSelect"', () => {
+ const findMinimumAccessLevelForDeleteSelectOptionValues = () =>
+ findMinimumAccessLevelForDeleteSelect()
.findAll('option')
.wrappers.map((option) => option.element.value);
- expect(minimumAccessLevelForPushSelectOptions).toEqual(['MAINTAINER', 'OWNER', 'ADMIN']);
+
+ it('contains only the options for maintainer and owner', () => {
+ mountComponent();
+
+ expect(findMinimumAccessLevelForDeleteSelect().exists()).toBe(true);
+ expect(findMinimumAccessLevelForDeleteSelectOptionValues()).toEqual(['', 'OWNER', 'ADMIN']);
+ });
+
+ describe('when form has prop "rule"', () => {
+ it('sets correct option for "null" value', () => {
+ mountComponent({
+ props: {
+ rule: { ...packagesProtectionRulesData[0], minimumAccessLevelForDelete: null },
+ },
+ });
+
+ expect(findMinimumAccessLevelForDeleteSelect().element.value).toBe('');
+ });
+ });
+
+ describe('when feature flag packagesProtectedPackagesDelete is disabled', () => {
+ it('does not show form field "minimumAccessLevelForDeleteSelect"', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvidedValues,
+ glFeatures: {
+ ...defaultProvidedValues.glFeatures,
+ packagesProtectedPackagesDelete: false,
+ },
+ },
+ });
+ expect(findMinimumAccessLevelForDeleteSelect().exists()).toBe(false);
+ });
});
});
@@ -146,6 +236,7 @@ describe('Packages Protection Rule Form', () => {
expect(findPackageNamePatternInput().attributes('disabled')).toBe('disabled');
expect(findPackageTypeSelect().attributes('disabled')).toBe('disabled');
expect(findMinimumAccessLevelForPushSelect().attributes('disabled')).toBe('disabled');
+ expect(findMinimumAccessLevelForDeleteSelect().attributes('disabled')).toBe('disabled');
});
it('displays a loading spinner', () => {
@@ -256,11 +347,40 @@ describe('Packages Protection Rule Form', () => {
await submitForm();
expect(mutationResolver).toHaveBeenCalledWith({
- input: { projectPath: 'path', ...createPackagesProtectionRuleMutationInput },
+ input: {
+ projectPath: 'path',
+ ...createPackagesProtectionRuleMutationInput,
+ minimumAccessLevelForDelete: 'OWNER',
+ },
});
expect(updatePackagesProtectionRuleMutationResolver).not.toHaveBeenCalled();
});
+ it('dispatches correct apollo mutation when no minimumAccessLevelForPush is selected', async () => {
+ const mutationResolver = jest
+ .fn()
+ .mockResolvedValue(createPackagesProtectionRuleMutationPayload());
+
+ mountComponentWithApollo({ mutationResolver });
+
+ await findPackageNamePatternInput().setValue(
+ createPackagesProtectionRuleMutationInput.packageNamePattern,
+ );
+ await setSelectValue(findMinimumAccessLevelForPushSelect(), '');
+ await setSelectValue(findMinimumAccessLevelForDeleteSelect(), 'ADMIN');
+
+ await submitForm();
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: {
+ projectPath: 'path',
+ ...createPackagesProtectionRuleMutationInput,
+ minimumAccessLevelForPush: null,
+ minimumAccessLevelForDelete: 'ADMIN',
+ },
+ });
+ });
+
it('emits event "submit" when apollo mutation successful', async () => {
const mutationResolver = jest
.fn()
@@ -316,7 +436,10 @@ describe('Packages Protection Rule Form', () => {
createPackagesProtectionRuleMutationInput.packageNamePattern,
);
await findMinimumAccessLevelForPushSelect().findAll('option').at(0).setSelected();
+ await findMinimumAccessLevelForDeleteSelect().findAll('option').at(2).setSelected();
+
findForm().trigger('submit');
+
await waitForPromises();
};
@@ -343,6 +466,8 @@ describe('Packages Protection Rule Form', () => {
input: {
id: packagesProtectionRulesData[0].id,
...createPackagesProtectionRuleMutationInput,
+ minimumAccessLevelForDelete: 'ADMIN',
+ minimumAccessLevelForPush: null,
},
});
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
index 72b044f0ee8b9a5375a76ba8d967c65041e3c4c1..73671048b85c4c24b4957379e3581b5adf12e46d 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
@@ -24,6 +24,9 @@ describe('Packages protection rules project settings', () => {
const defaultProvidedValues = {
projectPath: 'path',
+ glFeatures: {
+ packagesProtectedPackagesDelete: true,
+ },
};
const $toast = { show: jest.fn() };
@@ -36,6 +39,10 @@ describe('Packages protection rules project settings', () => {
extendedWrapper(wrapper.findByRole('table', { name: /protected packages/i }));
const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1));
const findTableRow = (i) => extendedWrapper(findTableBody().findAllByRole('row').at(i));
+ const findMinimumAccessLevelForPushInTableRow = (i) =>
+ findTableRow(i).findByTestId('minimum-access-level-push-value');
+ const findMinimumAccessLevelForDeleteInTableRow = (i) =>
+ findTableRow(i).findByTestId('minimum-access-level-delete-value');
const findTableRowButtonDelete = (i) =>
extendedWrapper(wrapper.findAllByTestId('delete-rule-btn').at(i));
const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -134,6 +141,7 @@ describe('Packages protection rules project settings', () => {
expect(findTableRow(i).text()).toContain(protectionRule.packageNamePattern);
expect(findTableRow(i).text()).toContain('npm');
expect(findTableRow(i).text()).toContain('Maintainer');
+ expect(findTableRow(i).text()).toContain('Maintainer');
},
);
});
@@ -291,6 +299,76 @@ describe('Packages protection rules project settings', () => {
});
});
+ describe('column "Minimum access level for push"', () => {
+ it('renders correct value for blank value', async () => {
+ const packagesProtectionRuleQueryResolver = jest.fn().mockResolvedValue(
+ packagesProtectionRuleQueryPayload({
+ nodes: [
+ {
+ ...packagesProtectionRulesData[0],
+ minimumAccessLevelForPush: null,
+ minimumAccessLevelForDelete: 'ADMIN',
+ },
+ ],
+ }),
+ );
+
+ createComponent({ packagesProtectionRuleQueryResolver });
+
+ await waitForPromises();
+
+ expect(findMinimumAccessLevelForPushInTableRow(0).text()).toContain('Developer (default)');
+ expect(findMinimumAccessLevelForDeleteInTableRow(0).text()).toContain('Administrator');
+ });
+ });
+
+ describe('column "Minimum access level for delete"', () => {
+ it('renders correct value for blank value', async () => {
+ const packagesProtectionRuleQueryResolver = jest.fn().mockResolvedValue(
+ packagesProtectionRuleQueryPayload({
+ nodes: [
+ {
+ ...packagesProtectionRulesData[0],
+ minimumAccessLevelForPush: 'OWNER',
+ minimumAccessLevelForDelete: null,
+ },
+ ],
+ }),
+ );
+
+ createComponent({ packagesProtectionRuleQueryResolver });
+
+ await waitForPromises();
+
+ expect(findMinimumAccessLevelForPushInTableRow(0).text()).toContain('Owner');
+ expect(findMinimumAccessLevelForDeleteInTableRow(0).text()).toContain(
+ 'Maintainer (default)',
+ );
+ });
+
+ describe('when feature flag packagesProtectedPackagesDelete is disabled', () => {
+ const findTableColumnHeaderMinimumAccessLevelForDelete = () =>
+ wrapper.findByRole('columnheader', { name: /minimum access level for delete/i });
+
+ it('does not show column "Minimum access level for delete"', async () => {
+ createComponent({
+ provide: {
+ ...defaultProvidedValues,
+ glFeatures: {
+ ...defaultProvidedValues.glFeatures,
+ packagesProtectedPackagesDelete: false,
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ expect(findTableColumnHeaderMinimumAccessLevelForDelete().exists()).toBe(false);
+ expect(findMinimumAccessLevelForDeleteInTableRow(0).exists()).toBe(false);
+ });
+ });
+ });
+
describe('column "Actions"', () => {
describe('button "Delete"', () => {
it('exists in table', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
index a49a76103aa2cf5569482e7c2da51f44d22b6887..3fee2fc83faf1e554acdcc970a7de7588da1f9b1 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -98,12 +98,14 @@ export const packagesProtectionRulesData = [
id: `gid://gitlab/Packages::Protection::Rule/${i}`,
packageNamePattern: `@flight/flight-maintainer-${i}-*`,
packageType: 'NPM',
+ minimumAccessLevelForDelete: 'OWNER',
minimumAccessLevelForPush: 'MAINTAINER',
})),
{
id: 'gid://gitlab/Packages::Protection::Rule/16',
packageNamePattern: '@flight/flight-owner-16-*',
packageType: 'NPM',
+ minimumAccessLevelForDelete: 'OWNER',
minimumAccessLevelForPush: 'OWNER',
},
];
@@ -145,6 +147,7 @@ export const createPackagesProtectionRuleMutationPayload = ({ override, errors =
export const createPackagesProtectionRuleMutationInput = {
packageNamePattern: `@flight/flight-developer-14-*`,
packageType: 'NPM',
+ minimumAccessLevelForDelete: 'MAINTAINER',
minimumAccessLevelForPush: 'MAINTAINER',
};
diff --git a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
index b72e8668065c83dbc3cc50ef1a777d105c64dbd5..3a742d92f914b0f3b6efb07db6ef8bc8b5411eb9 100644
--- a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
+++ b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
@@ -29,6 +29,7 @@
it_behaves_like 'pushed feature flag', :packages_protected_packages_conan
it_behaves_like 'pushed feature flag', :packages_protected_packages_maven
+ it_behaves_like 'pushed feature flag', :packages_protected_packages_delete
it_behaves_like 'pushed feature flag', :container_registry_protected_tags
end
end