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 { + + 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