diff --git a/config/bounded_contexts.yml b/config/bounded_contexts.yml index 3a1d223210f3584e2030ef64d4a58c275264e212..96b3668905f001c8cf1fe72ff1c187ff50a12b49 100644 --- a/config/bounded_contexts.yml +++ b/config/bounded_contexts.yml @@ -99,7 +99,7 @@ domains: feature_categories: - code_suggestions - Compliance: + ComplianceManagement: description: feature_categories: - compliance_management diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6814542cecc0de803fe3216e1968f94ebf559885..70263531bc49d2e9c31816d440c0f73971fe73db 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7927,6 +7927,28 @@ Input type: `projectTextReplaceInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.projectUpdateComplianceFrameworks` + +Update compliance frameworks for a project. + +Input type: `ProjectUpdateComplianceFrameworksInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `complianceFrameworkIds` | [`[ComplianceManagementFrameworkID!]!`](#compliancemanagementframeworkid) | IDs of the compliance framework to update for the project. | +| `projectId` | [`ProjectID!`](#projectid) | ID of the project to change the compliance framework of. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `project` | [`Project`](#project) | Project after mutation. | + ### `Mutation.prometheusIntegrationCreate` Input type: `PrometheusIntegrationCreateInput` diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 0489154fe619f3a98bdf4870c02a2cf67fb5a1e3..11593e806a634e70102829ed06c1a4c27c53888a 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -134,8 +134,10 @@ Audit event types belong to the following product categories. | [`allow_committer_approval_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102256) | Event triggered on updating prevent merge request approval from committers from group merge request setting| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.6](https://gitlab.com/gitlab-org/gitlab/-/issues/373949) | Group | | [`allow_overrides_to_approver_list_per_merge_request_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102256) | Event triggered on updating prevent users from modifying MR approval rules in merge requests from group merge request setting| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.6](https://gitlab.com/gitlab-org/gitlab/-/issues/373949) | Group | | [`audit_events_streaming_headers_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) | Group | +| [`compliance_framework_added`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157893) | Triggered when a framework label is added to a project.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.2](https://gitlab.com/gitlab-org/gitlab/-/issues/464160) | Project | | [`compliance_framework_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65343) | Triggered when a framework gets removed from a project| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/329362) | Project | | [`compliance_framework_id_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94711) | audit when compliance framework ID is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369310) | Project | +| [`compliance_framework_removed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157893) | Triggered when a framework label is removed from a project.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.2](https://gitlab.com/gitlab-org/gitlab/-/issues/464160) | Project | | [`create_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | Triggered on successful compliance framework creation| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group | | [`create_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | Event triggered when an external status check is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | | [`delete_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | Event triggered when an external status check is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 4974f9a66642c2f5fb4fbb29ce973a368391f784..0a41d135908924b484d836634d30825488cb50a5 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -215,6 +215,7 @@ def self.authorization_scopes mount_mutation ::Mutations::Ai::SelfHostedModels::Delete, alpha: { milestone: '17.2' } mount_mutation ::Mutations::MergeTrains::Cars::Delete, alpha: { milestone: '17.2' } + mount_mutation ::Mutations::Projects::UpdateComplianceFrameworks prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/projects/update_compliance_frameworks.rb b/ee/app/graphql/mutations/projects/update_compliance_frameworks.rb new file mode 100644 index 0000000000000000000000000000000000000000..76207d31ea94ddf2c925abfdca7272e43ccc2d61 --- /dev/null +++ b/ee/app/graphql/mutations/projects/update_compliance_frameworks.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Mutations + module Projects + class UpdateComplianceFrameworks < BaseMutation + graphql_name 'ProjectUpdateComplianceFrameworks' + description 'Update compliance frameworks for a project.' + + MAX_FRAMEWORKS = 10 + + authorize :admin_compliance_framework + + argument :project_id, Types::GlobalIDType[::Project], + required: true, + description: 'ID of the project to change the compliance framework of.' + + argument :compliance_framework_ids, [Types::GlobalIDType[::ComplianceManagement::Framework]], + required: true, + description: 'IDs of the compliance framework to update for the project.' + + field :project, + Types::ProjectType, + null: true, + description: "Project after mutation." + + def ready?(**args) + if args[:compliance_framework_ids].size > MAX_FRAMEWORKS + raise Gitlab::Graphql::Errors::ArgumentError, + format( + _('No more than %{max_frameworks} compliance frameworks can be updated at the same time.'), + max_frameworks: MAX_FRAMEWORKS + ) + end + + super + end + + def resolve(project_id:, compliance_framework_ids:) + project = GitlabSchema.find_by_gid(project_id).sync + + authorize!(project) + + compliance_frameworks = compliance_frameworks(compliance_framework_ids) + + service_response = ::ComplianceManagement::Frameworks::UpdateProjectService + .new(project, current_user, compliance_frameworks) + .execute + + { project: project, errors: errors_on_object(project) + service_response.errors } + end + + private + + def compliance_frameworks(compliance_framework_ids) + ids = GitlabSchema.parse_gids(compliance_framework_ids).map(&:model_id).map(&:to_i).uniq + frameworks = ::ComplianceManagement::Framework.id_in(ids) + + if frameworks.length != ids.length + raise Gitlab::Graphql::Errors::ArgumentError, format(_("Framework id(s) %{missing_ids} are invalid."), + missing_ids: (ids - frameworks.pluck(:id))) # rubocop: disable CodeReuse/ActiveRecord -- Using pluck only + end + + frameworks + end + end + end +end diff --git a/ee/app/models/compliance_management/compliance_framework/project_settings.rb b/ee/app/models/compliance_management/compliance_framework/project_settings.rb index d0e8f1937c3bfd505c7aeedd5eea61ad293c4624..8919facd6d9f55dd3f66ef79ffcec6052ca04958 100644 --- a/ee/app/models/compliance_management/compliance_framework/project_settings.rb +++ b/ee/app/models/compliance_management/compliance_framework/project_settings.rb @@ -4,7 +4,6 @@ module ComplianceManagement module ComplianceFramework class ProjectSettings < ApplicationRecord self.table_name = 'project_compliance_framework_settings' - self.primary_key = :project_id belongs_to :project belongs_to :compliance_management_framework, class_name: "ComplianceManagement::Framework", foreign_key: :framework_id @@ -13,6 +12,8 @@ class ProjectSettings < ApplicationRecord delegate :full_path, to: :project + scope :by_framework_and_project, ->(project_id, framework_id) { where(project_id: project_id, framework_id: framework_id) } + def self.find_or_create_by_project(project, framework) find_or_initialize_by(project: project).tap do |setting| setting.framework_id = framework.id diff --git a/ee/app/services/compliance_management/frameworks/update_project_service.rb b/ee/app/services/compliance_management/frameworks/update_project_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..9a8afcaef0ed62d2388c43eea401657326dc08b8 --- /dev/null +++ b/ee/app/services/compliance_management/frameworks/update_project_service.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module ComplianceManagement + module Frameworks + class UpdateProjectService < BaseService + def initialize(project, current_user, frameworks) + @project = project + @current_user = current_user + @frameworks = frameworks + end + + def execute + return error unless permitted? + + old_frameworks = project.compliance_management_frameworks + + frameworks_to_be_added = frameworks - old_frameworks + frameworks_to_be_removed = old_frameworks - frameworks + + frameworks_to_be_added.each do |framework| + response = add_framework_setting(framework) + return response if response.respond_to?(:error?) && response.error? + end + + frameworks_to_be_removed.each do |framework| + response = remove_framework_setting(framework) + return response if response.respond_to?(:error?) && response.error? + end + + success + end + + private + + attr_reader :project, :current_user, :frameworks + + def add_framework_setting(framework) + return unless project.root_namespace.self_and_descendants_ids.include?(framework.namespace_id) + + framework_project_setting = ComplianceManagement::ComplianceFramework::ProjectSettings.new(project: project, + compliance_management_framework: framework) + + unless framework_project_setting.save + error_message = "Error while adding framework #{framework.name}. Errors: " \ + "#{framework_project_setting.errors.full_messages.to_sentence}" + + return error(error_message) + end + + track_event(::Projects::ComplianceFrameworkChangedEvent::EVENT_TYPES[:added], framework) + end + + def remove_framework_setting(framework) + framework_project_setting = ComplianceManagement::ComplianceFramework::ProjectSettings + .by_framework_and_project(project.id, framework.id).first + + unless framework_project_setting.destroy + error_message = "Error while removing framework #{framework.name}. Errors: " \ + "#{framework_project_setting.errors.full_messages.to_sentence}" + return error(error_message) + end + + track_event(::Projects::ComplianceFrameworkChangedEvent::EVENT_TYPES[:removed], framework) + end + + def permitted? + can?(current_user, :admin_compliance_framework, project) + end + + def error(message = "Failed to assign the framework to the project") + ServiceResponse.error(message: _(message)) + end + + def success + ServiceResponse.success + end + + def track_event(event_type, framework) + publish_event(event_type, framework) + audit_event(event_type, framework) + end + + def audit_event(event_type, framework) + audit_context = { + name: "compliance_framework_#{event_type}", + author: current_user, + scope: project, + target: framework, + message: "#{event_type.capitalize} framework label #{framework.name}", + additional_details: { + framework: { + id: framework.id, + name: framework.name + } + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + + def publish_event(event_type, framework) + event = ::Projects::ComplianceFrameworkChangedEvent.new(data: { + project_id: project.id, + compliance_framework_id: framework.id, + event_type: event_type + }) + + ::Gitlab::EventStore.publish(event) + end + end + end +end diff --git a/ee/config/audit_events/types/compliance_framework_added.yml b/ee/config/audit_events/types/compliance_framework_added.yml new file mode 100644 index 0000000000000000000000000000000000000000..ec18d4f6d49a269b27bde2a887b4038ccb4f8d75 --- /dev/null +++ b/ee/config/audit_events/types/compliance_framework_added.yml @@ -0,0 +1,10 @@ +--- +name: compliance_framework_added +description: Triggered when a framework label is added to a project. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/464160 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157893 +milestone: '17.2' +feature_category: compliance_management +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/config/audit_events/types/compliance_framework_removed.yml b/ee/config/audit_events/types/compliance_framework_removed.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc1a168c75c0f06a4e811c9d78229c591bbad315 --- /dev/null +++ b/ee/config/audit_events/types/compliance_framework_removed.yml @@ -0,0 +1,10 @@ +--- +name: compliance_framework_removed +description: Triggered when a framework label is removed from a project. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/464160 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157893 +milestone: '17.2' +feature_category: compliance_management +saved_to_database: true +streamed: true +scope: [Project] diff --git a/ee/spec/graphql/mutations/projects/update_compliance_frameworks_spec.rb b/ee/spec/graphql/mutations/projects/update_compliance_frameworks_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..75b06bc7ebb0caa05a67c0eabe805951dfa45415 --- /dev/null +++ b/ee/spec/graphql/mutations/projects/update_compliance_frameworks_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Projects::UpdateComplianceFrameworks, feature_category: :compliance_management do + let_it_be(:group) { create(:group) } + let_it_be(:framework) { create(:compliance_framework, :sox, namespace: group) } + let_it_be(:project) { create(:project, :repository, :with_compliance_framework, group: group) } + let_it_be(:current_user) { create(:user) } + let_it_be(:existing_framework) { project.compliance_management_frameworks.first } + + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + + subject do + mutation.resolve(project_id: GitlabSchema.id_from_object(project), + compliance_framework_ids: [GitlabSchema.id_from_object(existing_framework), + GitlabSchema.id_from_object(framework)]) + end + + shared_examples "the user cannot update the project's compliance framework" do + it 'raises an exception' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + shared_examples "the user can update compliance frameworks of the project" do + it 'updates the compliance frameworks to the project' do + expect { subject }.to change { project.reload.compliance_management_frameworks } + .from([existing_framework]).to([framework, existing_framework]) + end + + it 'returns the project that was updated' do + expect(subject).to include(project: project) + end + end + + describe '#resolve' do + context 'when feature is licensed' do + before do + stub_licensed_features(compliance_framework: true) + end + + context 'when current_user is a project maintainer' do + before_all do + project.add_maintainer(current_user) + end + + it_behaves_like "the user cannot update the project's compliance framework" + end + + context 'when current_user is a project owner' do + before_all do + group.add_owner(current_user) + project.add_owner(current_user) + end + + it_behaves_like "the user can update compliance frameworks of the project" + + context 'when framework id is invalid' do + subject(:resolve) do + mutation.resolve(project_id: GitlabSchema.id_from_object(project), + compliance_framework_ids: ["gid://gitlab/ComplianceManagement::Framework/#{non_existing_record_id}"]) + end + + it 'returns Argument error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, + format(_("Framework id(s) [%{record_id}] are invalid."), record_id: non_existing_record_id)) + end + end + end + end + + context 'when feature is unlicensed' do + before do + stub_licensed_features(compliance_framework: false) + end + + it_behaves_like "the user cannot update the project's compliance framework" + end + end +end diff --git a/ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb b/ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb index b8eb96d66c71635fe51493be5c492bfcb841f1cd..799189636e47afefce515fc9942c9404d2fbc2f9 100644 --- a/ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb +++ b/ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb @@ -40,6 +40,21 @@ end end + describe '.by_framework_and_project' do + let_it_be(:framework1) do + create(:compliance_framework, namespace: project.group.root_ancestor, name: 'framework1') + end + + let_it_be(:setting) do + create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework1) + end + + it 'returns the setting' do + expect(described_class.by_framework_and_project(project.id, framework1.id)) + .to eq([setting]) + end + end + describe '.find_or_create_by_project' do let_it_be(:framework) { create(:compliance_framework, namespace: project.group.root_ancestor) } diff --git a/ee/spec/requests/api/graphql/mutations/projects/update_compliance_frameworks_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/update_compliance_frameworks_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a5caef75b7e1cdf51e5019e99999d98376773489 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/projects/update_compliance_frameworks_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update project compliance framework', feature_category: :compliance_management do + include GraphqlHelpers + + let_it_be(:namespace) { create(:group) } + let_it_be(:project) { create(:project, namespace: namespace) } + let_it_be(:framework1) { create(:compliance_framework, namespace: namespace) } + let_it_be(:framework2) { create(:compliance_framework, :sox, namespace: namespace) } + let_it_be(:current_user) { create(:user, owner_of: namespace) } + + let(:variables) do + { + project_id: GitlabSchema.id_from_object(project).to_s, + compliance_framework_ids: [ + GitlabSchema.id_from_object(framework1).to_s, + GitlabSchema.id_from_object(framework2).to_s, + GitlabSchema.id_from_object(framework1).to_s # Adding same framework twice for checking it only gets added once + ] + } + end + + let(:mutation) do + graphql_mutation(:project_update_compliance_frameworks, variables) do + <<~QL + errors + project { + complianceFrameworks { + nodes { + name + } + } + } + QL + end + end + + def mutation_response + graphql_mutation_response(:project_update_compliance_frameworks) + end + + describe '#resolve' do + context 'when feature is not available' do + before do + stub_licensed_features(compliance_framework: false) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist ' \ + 'or you don\'t have permission to perform this action'] + end + + context 'when feature is available' do + before do + stub_licensed_features(compliance_framework: true) + end + + context 'when there is no framework associated with the project' do + it_behaves_like 'a working GraphQL mutation' + + it 'adds the frameworks' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { + project.reload.compliance_management_frameworks + }.from([]).to([framework1, framework2]) + end + end + + context 'when there is a framework associated with the project' do + let_it_be(:existing_framework) { create(:compliance_framework, namespace: namespace, name: 'framework3') } + + before do + create(:compliance_framework_project_setting, project: project, + compliance_management_framework: existing_framework) + end + + it 'adds the new frameworks' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { + project.reload.compliance_management_frameworks + }.from([existing_framework]).to([framework1, framework2]) + end + end + + context 'when there are more than 20 frameworks' do + let(:framework_ids) { (1..21).map { |num| "gid://gitlab/ComplianceManagement::Framework/#{num}" } } + + let(:variables) do + { + project_id: GitlabSchema.id_from_object(project).to_s, + compliance_framework_ids: framework_ids + } + end + + it 'return error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('No more than 10 compliance frameworks can be updated at the same time.') + end + end + end + end +end diff --git a/ee/spec/services/compliance_management/frameworks/update_project_service_spec.rb b/ee/spec/services/compliance_management/frameworks/update_project_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a2a58d10db11000a1dc1196c7b198cc2e292eb7 --- /dev/null +++ b/ee/spec/services/compliance_management/frameworks/update_project_service_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ComplianceManagement::Frameworks::UpdateProjectService, feature_category: :compliance_management do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be(:framework1) { create(:compliance_framework, name: 'framework1', namespace: group) } + let_it_be(:framework2) { create(:compliance_framework, name: 'framework2', namespace: group) } + let_it_be(:framework3) { create(:compliance_framework, name: 'framework3', namespace: group) } + let_it_be(:framework4) { create(:compliance_framework, name: 'framework4', namespace: group) } + + let(:frameworks) { [framework1, framework2] } + + let(:service) { described_class.new(project, user, frameworks) } + + subject(:update_framework) { service.execute } + + context 'when compliance framework feature is available' do + before do + allow(service).to receive(:can?).with(user, :admin_compliance_framework, project).and_return(true) + end + + context 'when the input parameters are correct' do + context 'when project has no framework associated with it' do + it 'adds the framework association' do + expect { update_framework }.to change { + project.reload.compliance_management_frameworks + }.from([]).to([framework1, framework2]) + end + + it 'logs audit events' do + expect { update_framework }.to change { + AuditEvent.where("details LIKE ?", "%compliance_framework_added%").count + }.by(2) + end + + it 'publishes Projects::ComplianceFrameworkChangedEvent' do + expect(::Gitlab::EventStore).to receive(:publish) + .with(an_instance_of(::Projects::ComplianceFrameworkChangedEvent)).twice + + update_framework + end + end + + context 'when project already has some frameworks associated with it' do + before do + create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework2) + create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework3) + create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework4) + end + + it 'adds and removes framework associations' do + expect { update_framework }.to change { + project.reload.compliance_management_frameworks + }.from([framework2, framework3, framework4]).to([framework1, framework2]) + end + + it 'logs audit events' do + expect { update_framework }.to change { + AuditEvent.where("details LIKE ?", "%compliance_framework_added%").count + }.by(1).and change { + AuditEvent.where("details LIKE ?", "%compliance_framework_removed%").count + }.by(2) + end + + it 'publishes Projects::ComplianceFrameworkChangedEvent' do + expect(::Gitlab::EventStore).to receive(:publish) + .with(an_instance_of(::Projects::ComplianceFrameworkChangedEvent)).exactly(3).times + + update_framework + end + end + + context 'when there is an error while saving framework project setting' do + it 'returns error' do + save_error_message = 'Not able to save project settings for compliance framework' + error_message = "Error while adding framework #{frameworks.first.name}. Errors: #{save_error_message}" + + allow_next_instance_of(ComplianceManagement::ComplianceFramework::ProjectSettings) do |instance| + allow(instance).to receive(:save).and_return(false) + + errors = ActiveModel::Errors.new(instance).tap { |e| e.add(:base, save_error_message) } + allow(instance).to receive(:errors).and_return(errors) + end + + expect(update_framework.errors).to eq([error_message]) + end + end + + context 'when there is an error while deleting a framework project setting' do + before do + create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework3) + end + + let(:frameworks) { [] } + + it 'returns error' do + save_error_message = 'Not able to delete project settings for compliance framework' + error_message = "Error while removing framework framework3. Errors: #{save_error_message}" + + allow_next_found_instance_of(ComplianceManagement::ComplianceFramework::ProjectSettings) do |instance| + allow(instance).to receive(:destroy).and_return(false) + + errors = ActiveModel::Errors.new(instance).tap { |e| e.add(:base, save_error_message) } + allow(instance).to receive(:errors).and_return(errors) + end + + expect(update_framework.errors).to eq([error_message]) + end + end + end + end + + context 'when compliance framework feature is unavailable' do + before do + stub_licensed_features(compliance_framework: false) + end + + before_all do + group.add_owner(user) + end + + it 'returns an error response' do + response = update_framework + + expect(response).to be_error + expect(response.message).to eq('Failed to assign the framework to the project') + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8655c2c9c327a67e5911d5b215e668a36e32606c..ca15b383fd59cea44dd0d15e0c75b70e687f6c91 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22887,6 +22887,9 @@ msgstr "" msgid "ForksDivergence|View merge request" msgstr "" +msgid "Framework id(s) %{missing_ids} are invalid." +msgstr "" + msgid "Framework successfully deleted" msgstr "" @@ -34972,6 +34975,9 @@ msgstr "" msgid "No more seats in subscription" msgstr "" +msgid "No more than %{max_frameworks} compliance frameworks can be updated at the same time." +msgstr "" + msgid "No more than %{max_issues} issues can be updated at the same time" msgstr ""