diff --git a/config/authz/permissions/ai_catalog_item/delete.yml b/config/authz/permissions/ai_catalog_item/delete.yml new file mode 100644 index 0000000000000000000000000000000000000000..062b850ab23073d7f1af4a34ecbc74cc808febdb --- /dev/null +++ b/config/authz/permissions/ai_catalog_item/delete.yml @@ -0,0 +1,4 @@ +--- +name: delete_ai_catalog_item +description: Delete an AI Catalog item +feature_category: workflow_catalog diff --git a/config/authz/permissions/hard_delete_ai_catalog_item/force.yml b/config/authz/permissions/hard_delete_ai_catalog_item/force.yml new file mode 100644 index 0000000000000000000000000000000000000000..f5e6cb6d999552796047fb81cedd09e1f8a52e5a --- /dev/null +++ b/config/authz/permissions/hard_delete_ai_catalog_item/force.yml @@ -0,0 +1,4 @@ +--- +name: force_hard_delete_ai_catalog_item +description: Force hard delete an AI Catalog item +feature_category: workflow_catalog diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 0798b9f78063d3da58a542136043700897a7084f..d081843768c8c58ec4e2805f89958851bf77e487 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2561,6 +2561,7 @@ Input type: `AiCatalogAgentDeleteInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `forceHardDelete` | [`Boolean`](#boolean) | When true, the flow will always be hard deleted and never soft deleted. Can only be used by instance admins. | | `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog Agent to delete. | #### Fields @@ -2674,6 +2675,7 @@ Input type: `AiCatalogFlowDeleteInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `forceHardDelete` | [`Boolean`](#boolean) | When true, the flow will always be hard deleted and never soft deleted. Can only be used by instance admins. | | `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog flow to delete. | #### Fields @@ -2858,6 +2860,7 @@ Input type: `AiCatalogThirdPartyFlowDeleteInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `forceHardDelete` | [`Boolean`](#boolean) | When true, the Third Party Flow will always be hard deleted and never soft deleted. Can only be used by instance admins. | | `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog Third Party Flow to delete. | #### Fields diff --git a/ee/app/graphql/mutations/ai/catalog/agent/delete.rb b/ee/app/graphql/mutations/ai/catalog/agent/delete.rb index a3c7bab6813744c458b7e3419197bc298d0499ec..d930731f6d30f5c2b481d64836bd3ab5dc9eb237 100644 --- a/ee/app/graphql/mutations/ai/catalog/agent/delete.rb +++ b/ee/app/graphql/mutations/ai/catalog/agent/delete.rb @@ -15,15 +15,28 @@ class Delete < BaseMutation required: true, description: 'Global ID of the catalog Agent to delete.' - authorize :admin_ai_catalog_item + argument :force_hard_delete, GraphQL::Types::Boolean, + required: false, + description: 'When true, the flow will always be hard deleted and never soft deleted. ' \ + 'Can only be used by instance admins' + + authorize :delete_ai_catalog_item def resolve(args) - agent = authorized_find!(id: args[:id]) + id = args.delete(:id) + + item = authorized_find!(id: id) + + if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item) + raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete') + end + + service_args = args.merge(item: item) result = ::Ai::Catalog::Agents::DestroyService.new( - project: agent.project, + project: service_args[:item].project, current_user: current_user, - params: { item: agent }).execute + params: service_args).execute { success: result.success?, diff --git a/ee/app/graphql/mutations/ai/catalog/flow/delete.rb b/ee/app/graphql/mutations/ai/catalog/flow/delete.rb index 6fa4c6193f3aea3b0c1c4e901cfe4fa4222d1123..8c65c32dff635b3a5d2de4462ea2228259f6a660 100644 --- a/ee/app/graphql/mutations/ai/catalog/flow/delete.rb +++ b/ee/app/graphql/mutations/ai/catalog/flow/delete.rb @@ -15,15 +15,28 @@ class Delete < BaseMutation required: true, description: 'Global ID of the catalog flow to delete.' - authorize :admin_ai_catalog_item + argument :force_hard_delete, GraphQL::Types::Boolean, + required: false, + description: 'When true, the flow will always be hard deleted and never soft deleted. ' \ + 'Can only be used by instance admins' + + authorize :delete_ai_catalog_item def resolve(args) - flow = authorized_find!(id: args[:id]) + id = args.delete(:id) + + item = authorized_find!(id: id) + + if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item) + raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete') + end + + service_args = args.merge(item: item) result = ::Ai::Catalog::Flows::DestroyService.new( - project: flow.project, + project: service_args[:item].project, current_user: current_user, - params: { item: flow }).execute + params: service_args).execute { success: result.success?, diff --git a/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb b/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb index f9717cda260fb9a7e8335900e0fd0f701f649e62..c97b8f4c67288a1dc1c6389b132b5424e70b98cf 100644 --- a/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb +++ b/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb @@ -15,15 +15,28 @@ class Delete < BaseMutation required: true, description: 'Global ID of the catalog Third Party Flow to delete.' - authorize :admin_ai_catalog_item + argument :force_hard_delete, GraphQL::Types::Boolean, + required: false, + description: 'When true, the Third Party Flow will always be hard deleted and never soft deleted. ' \ + 'Can only be used by instance admins' + + authorize :delete_ai_catalog_item def resolve(args) - third_party_flow = authorized_find!(id: args[:id]) + id = args.delete(:id) + + item = authorized_find!(id: id) + + if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item) + raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete') + end + + service_args = args.merge(item: item) result = ::Ai::Catalog::ThirdPartyFlows::DestroyService.new( - project: third_party_flow.project, + project: service_args[:item].project, current_user: current_user, - params: { item: third_party_flow }).execute + params: service_args).execute { success: result.success?, diff --git a/ee/app/policies/ai/catalog/item_policy.rb b/ee/app/policies/ai/catalog/item_policy.rb index 74a193441dd1c115bdab161378bbea6331f3228a..efff7627f2b94809d6ffd9be1b9d6e60244cea18 100644 --- a/ee/app/policies/ai/catalog/item_policy.rb +++ b/ee/app/policies/ai/catalog/item_policy.rb @@ -48,34 +48,44 @@ class ItemPolicy < ::BasePolicy end rule { maintainer_access }.policy do - enable :admin_ai_catalog_item + enable :admin_ai_catalog_item # (Create and update) + enable :delete_ai_catalog_item end - rule { ~ai_catalog_enabled }.policy do + rule { admin }.policy do + enable :force_hard_delete_ai_catalog_item + end + + rule { ~ai_catalog_enabled & ~admin }.policy do prevent :read_ai_catalog_item prevent :admin_ai_catalog_item + prevent :delete_ai_catalog_item end - rule { deleted_item }.policy do + rule { deleted_item & ~admin }.policy do prevent :admin_ai_catalog_item + prevent :delete_ai_catalog_item end - rule { ~public_item & ~project_ai_catalog_available }.policy do + rule { ~public_item & ~project_ai_catalog_available & ~admin }.policy do prevent :read_ai_catalog_item end - rule { ~project_ai_catalog_available }.policy do + rule { ~project_ai_catalog_available & ~admin }.policy do prevent :admin_ai_catalog_item + prevent :delete_ai_catalog_item end - rule { flow & ~flows_enabled }.policy do + rule { flow & ~flows_enabled & ~admin }.policy do prevent :read_ai_catalog_item prevent :admin_ai_catalog_item + prevent :delete_ai_catalog_item end - rule { third_party_flow & ~third_party_flows_enabled }.policy do + rule { third_party_flow & ~third_party_flows_enabled & ~admin }.policy do prevent :read_ai_catalog_item prevent :admin_ai_catalog_item + prevent :delete_ai_catalog_item end end end diff --git a/ee/app/services/ai/catalog/items/base_destroy_service.rb b/ee/app/services/ai/catalog/items/base_destroy_service.rb index 510d7196e2d958232c235445058c70aee0a55ecd..2ccc61ca37e0830c8c1421e6986eea8eb06abde5 100644 --- a/ee/app/services/ai/catalog/items/base_destroy_service.rb +++ b/ee/app/services/ai/catalog/items/base_destroy_service.rb @@ -13,15 +13,11 @@ def execute return error_no_permissions unless allowed? return error_no_item unless valid? - result = delete_item_consumer - return error(result.errors) if result.error? + result = force_hard_delete? ? perform_hard_delete : perform_soft_delete - if delete_item - track_ai_item_events('delete_ai_catalog_item', { label: item.item_type }) - return success - end + track_deletion_event if result.success? - error_response + result end private @@ -29,7 +25,13 @@ def execute attr_reader :item def allowed? - super && Ability.allowed?(current_user, :admin_ai_catalog_item, item) + allowed = super && Ability.allowed?(current_user, :delete_ai_catalog_item, item) + + if force_hard_delete? # rubocop:disable Style/IfUnlessModifier -- Improves readability + allowed &= Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item) + end + + allowed end def valid? @@ -44,11 +46,25 @@ def error_no_item error('Item not found') end - def error_response - error(item.errors.full_messages) + def perform_hard_delete + item.class.transaction do + item.consumers.each_batch do |batch| + batch.delete_all + end + item.destroy + end + + ServiceResponse.success + end + + def perform_soft_delete + result = destroy_item_consumer + return result if result.error? + + destroy_item_with_strategy end - def delete_item_consumer + def destroy_item_consumer consumer = project.configured_ai_catalog_items.for_item(item).first return ServiceResponse.success unless consumer @@ -56,10 +72,22 @@ def delete_item_consumer Ai::Catalog::ItemConsumers::DestroyService.new(consumer, current_user).execute end - def delete_item - return item.soft_delete if item.consumers.any? || item.dependents.any? + def destroy_item_with_strategy + success = if item.consumers.any? || item.dependents.any? + item.soft_delete + else + item.destroy + end + + success ? ServiceResponse.success : error(item.errors.full_messages) + end + + def force_hard_delete? + params[:force_hard_delete] == true + end - item.destroy + def track_deletion_event + track_ai_item_events('delete_ai_catalog_item', { label: item.item_type }) end end end diff --git a/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb index e7349b88035ab4625a77a2a7b47ebfcb15ea074f..87218fb491c33f493bfc4f34de2feda1d5ddda0e 100644 --- a/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb +++ b/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb @@ -9,9 +9,9 @@ it { is_expected.to have_graphql_name('AiCatalogAgentDelete') } - it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) } + it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) } it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) } - it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) } + it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) } end diff --git a/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb index 197187f594232c9a744c8dfc86d80734018fcfe0..339c83be2f9e33237d4476952284994186125a6f 100644 --- a/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb +++ b/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb @@ -9,9 +9,9 @@ it { is_expected.to have_graphql_name('AiCatalogFlowDelete') } - it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) } + it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) } it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) } - it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) } + it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) } end diff --git a/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb index 1a95108b26edbd3ede12c9ce0fc65c0efea5caf8..6a7c4760b66cfbbf58d93ea9376881ec2c5e82b7 100644 --- a/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb +++ b/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb @@ -9,9 +9,9 @@ it { is_expected.to have_graphql_name('AiCatalogThirdPartyFlowDelete') } - it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) } + it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) } it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) } - it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) } + it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) } end diff --git a/ee/spec/policies/ai/catalog/item_policy_spec.rb b/ee/spec/policies/ai/catalog/item_policy_spec.rb index ae1b43c4c0469baf1257fe33e6c3ca6b1bbf1897..5fdb2f02546ec6874054e0513df448f104a7294c 100644 --- a/ee/spec/policies/ai/catalog/item_policy_spec.rb +++ b/ee/spec/policies/ai/catalog/item_policy_spec.rb @@ -9,6 +9,7 @@ let_it_be(:maintainer) { create(:user) } let_it_be(:reporter) { create(:user) } let_it_be(:guest) { create(:user) } + let_it_be(:admin) { create(:admin) } let_it_be_with_reload(:private_project) do create(:project, :private, guests: guest, reporters: reporter, developers: developer, maintainers: maintainer) end @@ -36,13 +37,21 @@ shared_examples 'no permissions' do it { is_expected.to be_disallowed(:admin_ai_catalog_item) } + it { is_expected.to be_disallowed(:delete_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } it { is_expected.to be_disallowed(:read_ai_catalog_item) } + + include_examples 'all permissions when admin' end shared_examples 'read-only permissions' do it { is_expected.to be_disallowed(:admin_ai_catalog_item) } + it { is_expected.to be_disallowed(:delete_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } it { is_expected.to be_allowed(:read_ai_catalog_item) } + include_examples 'all permissions when admin' + it_behaves_like 'no permissions with global_ai_catalog feature flag disabled' it_behaves_like 'no permissions with project stage check false, unless item is public' it_behaves_like 'no permissions when project Duo features disabled, unless item is public' @@ -51,7 +60,11 @@ shared_examples 'read-write permissions' do it { is_expected.to be_allowed(:admin_ai_catalog_item) } + it { is_expected.to be_allowed(:delete_ai_catalog_item) } it { is_expected.to be_allowed(:read_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } + + include_examples 'all permissions when admin' it_behaves_like 'no permissions with global_ai_catalog feature flag disabled' it_behaves_like 'no permissions with project stage check false, unless item is public' @@ -59,6 +72,17 @@ it_behaves_like 'read-only permissions with deleted item' end + shared_examples 'all permissions when admin' do + context 'when admin', :enable_admin_mode do + let(:current_user) { admin } + + it { is_expected.to be_allowed(:admin_ai_catalog_item) } + it { is_expected.to be_allowed(:delete_ai_catalog_item) } + it { is_expected.to be_allowed(:force_hard_delete_ai_catalog_item) } + it { is_expected.to be_allowed(:read_ai_catalog_item) } + end + end + shared_examples 'no permissions with global_ai_catalog feature flag disabled' do before do stub_feature_flags(global_ai_catalog: false) @@ -73,31 +97,43 @@ end it { is_expected.to be_disallowed(:admin_ai_catalog_item) } + it { is_expected.to be_disallowed(:delete_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } it { is_expected.to be_allowed(:read_ai_catalog_item) } + + include_examples 'all permissions when admin' end shared_examples 'no permissions with project stage check false, unless item is public' do let(:stage_check) { false } it { is_expected.to be_disallowed(:admin_ai_catalog_item) } + it { is_expected.to be_disallowed(:delete_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } it 'is expected not to allow read_ai_catalog_item, unless item is public' do allowed = item.public? expect(policy.allowed?(:read_ai_catalog_item)).to eq(allowed) end + + include_examples 'all permissions when admin' end shared_examples 'no permissions when project Duo features disabled, unless item is public' do let(:duo_features_enabled) { false } it { is_expected.to be_disallowed(:admin_ai_catalog_item) } + it { is_expected.to be_disallowed(:delete_ai_catalog_item) } + it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) } it 'is expected not to allow read_ai_catalog_item, unless item is public' do allowed = item.public? expect(policy.allowed?(:read_ai_catalog_item)).to eq(allowed) end + + include_examples 'all permissions when admin' end context 'when maintainer' do diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb index 425095ccfb29de3b4d9579cbf2103b869a351ebd..f811da6a5b5ae528cabfb65bef9b1054592ac3ae 100644 --- a/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb @@ -82,5 +82,25 @@ it 'destroy the agent versions' do expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1) end + + context 'with `forceHardDelete` argument', :enable_admin_mode do + let(:params) { super().merge(force_hard_delete: true) } + + it_behaves_like 'a mutation that returns top-level errors', errors: + ['You must be an instance admin to use forceHardDelete'] + + it 'does not destroy the agent' do + expect { execute }.not_to change { Ai::Catalog::Item.count } + end + + context 'when user is an admin' do + let(:current_user) { create(:admin) } + + it 'destroys the agent and returns a success response' do + expect { execute }.to change { Ai::Catalog::Item.count }.by(-1) + expect(graphql_data_at(:ai_catalog_agent_delete, :success)).to be(true) + end + end + end end end diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb index 2ba25d003d3d2582cb89ff8dda25d4e32adde371..e9be32510162ef5b9b4566fbba6806a6628124ff 100644 --- a/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb @@ -74,13 +74,33 @@ end context 'when destroy service succeeds' do - it 'destroy the agent and returns a success response' do + it 'destroy the flow and returns a success response' do expect { execute }.to change { Ai::Catalog::Item.count }.by(-1) expect(graphql_data_at(:ai_catalog_flow_delete, :success)).to be(true) end - it 'destroy the agent versions' do + it 'destroy the flow versions' do expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1) end + + context 'with `forceHardDelete` argument', :enable_admin_mode do + let(:params) { super().merge(force_hard_delete: true) } + + it_behaves_like 'a mutation that returns top-level errors', errors: + ['You must be an instance admin to use forceHardDelete'] + + it 'does not destroy the flow' do + expect { execute }.not_to change { Ai::Catalog::Item.count } + end + + context 'when user is an admin' do + let(:current_user) { create(:admin) } + + it 'destroys the flow and returns a success response' do + expect { execute }.to change { Ai::Catalog::Item.count }.by(-1) + expect(graphql_data_at(:ai_catalog_flow_delete, :success)).to be(true) + end + end + end end end diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb index 47b9e0751359655ae7e2405af503f4222bdeb794..5e4b208d99d4b9c44df5ba85aa1ee240133f6e8b 100644 --- a/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb @@ -82,5 +82,25 @@ it 'destroy the third_party_flow versions' do expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1) end + + context 'with `forceHardDelete` argument', :enable_admin_mode do + let(:params) { super().merge(force_hard_delete: true) } + + it_behaves_like 'a mutation that returns top-level errors', errors: + ['You must be an instance admin to use forceHardDelete'] + + it 'does not delete the third_party_flow' do + expect { execute }.not_to change { Ai::Catalog::Item.count } + end + + context 'when user is an admin' do + let(:current_user) { create(:admin) } + + it 'destroys the third_party_flow and returns a success response' do + expect { execute }.to change { Ai::Catalog::Item.count }.by(-1) + expect(graphql_data_at(:ai_catalog_third_party_flow_delete, :success)).to be(true) + end + end + end end end diff --git a/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb b/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb index 756f0a94f6da99fc4b827b52e9306c13865cf0ba..69bb5576e9c048dfd7371d834f87283f86e573ae 100644 --- a/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb +++ b/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples Ai::Catalog::Items::BaseDestroyService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } let(:params) { { item: item } } let(:service) { described_class.new(project: project, current_user: user, params: params) } @@ -62,6 +63,7 @@ expect { execute_service } .to trigger_internal_events('delete_ai_catalog_item') .with(user: user, project: project, additional_properties: { label: item.item_type }) + .and increment_usage_metrics('counts.count_total_delete_ai_catalog_item') end it 'returns success response' do @@ -88,6 +90,22 @@ end it_behaves_like 'soft deletes the item' + + context 'with `force_hard_delete` param', :enable_admin_mode do + let(:params) { super().merge(force_hard_delete: true) } + + it_behaves_like 'returns insufficient permissions error' + + context 'when the user is an admin' do + let(:user) { admin } + + it_behaves_like 'hard deletes the item' + + it 'destroys the item consumers' do + expect { execute_service }.to change { Ai::Catalog::ItemConsumer.count }.by(-1) + end + end + end end end @@ -149,6 +167,22 @@ end it_behaves_like 'soft deletes the item' + + context 'with `force_hard_delete` param', :enable_admin_mode do + let(:params) { super().merge(force_hard_delete: true) } + + it_behaves_like 'returns insufficient permissions error' + + context 'when the user is an admin' do + let(:user) { admin } + + it_behaves_like 'hard deletes the item' + + it 'destroys the version dependencies' do + expect { execute_service }.to change { Ai::Catalog::ItemVersionDependency.count }.by(-1) + end + end + end end end