prevent sprint sharing on contract level

This commit is contained in:
ulferts
2026-05-05 16:31:38 +02:00
parent 584a53292f
commit 02ede99126
3 changed files with 79 additions and 4 deletions
@@ -36,6 +36,7 @@ module Projects
validate :validate_global_sprint_sharer_uniqueness
validates :sprint_sharing, presence: true
validates :sprint_sharing, inclusion: { in: Project::SPRINT_SHARING_MODES }, allow_blank: true
validate :validate_sprint_sharing_in_ee_token
def validate_model? = false
@@ -59,5 +60,19 @@ module Projects
end
end
end
def validate_sprint_sharing_in_ee_token
if !model.not_sharing_sprints? &&
!EnterpriseToken.allows_to?(:sprint_sharing) &&
sprint_sharing_changed?
errors.add :sprint_sharing,
:enterprise_plan_required,
plan_name: I18n.t("ee.upsell.plan_name", plan: OpenProject::Token.lowest_plan_for(:sprint_sharing))
end
end
def sprint_sharing_changed?
model.settings_change&.any? { it.key?("sprint_sharing") }
end
end
end
@@ -3,11 +3,11 @@
require "spec_helper"
require "contracts/shared/model_contract_shared_context"
RSpec.describe Projects::BacklogSettingsContract, type: :model do
RSpec.describe Projects::BacklogSettingsContract, type: :model, with_ee: %i[sprint_sharing] do
include_context "ModelContract shared context"
let(:current_user) { build_stubbed(:user) }
let(:project) { create(:project) }
let(:project) { build_stubbed(:project) }
let(:permissions) { %i(share_sprint) }
subject(:contract) { described_class.new(project, current_user) }
@@ -35,7 +35,7 @@ RSpec.describe Projects::BacklogSettingsContract, type: :model do
# This spec of explicitly setting sprint_sharing to empty is required because the
# simple presence validation spec is not sufficient to catch certain corner cases.
# For example, when the sprint_sharing getter is overriden to provide a default value,
# For example, when the sprint_sharing getter is overridden to provide a default value,
# and the user submits an empty value, the contract should be invalid.
context "when sprint_sharing is empty" do
before { project.sprint_sharing = "" }
@@ -57,6 +57,62 @@ RSpec.describe Projects::BacklogSettingsContract, type: :model do
end
end
context "when the `sprint_sharing` is not part of the current EE token", with_ee: [] do
context "when sprint sharing is set to 'no_sharing'" do
before { project.sprint_sharing = Project::NO_SHARING }
it_behaves_like "contract is valid"
end
context "when sprint sharing is set to 'share_all_projects'" do
before { project.sprint_sharing = Project::SHARE_ALL_PROJECTS }
it_behaves_like "contract is invalid",
sprint_sharing: { error: :enterprise_plan_required, plan_name: "corporate enterprise plan" }
end
context "when sprint sharing is set to 'share_subprojects'" do
before { project.sprint_sharing = Project::SHARE_SUBPROJECTS }
it_behaves_like "contract is invalid",
sprint_sharing: { error: :enterprise_plan_required, plan_name: "corporate enterprise plan" }
end
context "when sprint sharing is set to 'receive_shared'" do
before { project.sprint_sharing = Project::RECEIVE_SHARED }
it_behaves_like "contract is invalid",
sprint_sharing: { error: :enterprise_plan_required, plan_name: "corporate enterprise plan" }
end
context "when sprint sharing remains on 'share_all_projects'" do
before do
project.sprint_sharing = Project::SHARE_ALL_PROJECTS
project.clear_changes_information
end
it_behaves_like "contract is valid"
end
context "when sprint sharing remains on 'share_subprojects'" do
before do
project.sprint_sharing = Project::SHARE_SUBPROJECTS
project.clear_changes_information
end
it_behaves_like "contract is valid"
end
context "when sprint sharing remains on 'receive_shared'" do
before do
project.sprint_sharing = Project::RECEIVE_SHARED
project.clear_changes_information
end
it_behaves_like "contract is valid"
end
end
describe "#validate_global_sprint_sharer_uniqueness" do
before do
project.sprint_sharing = "share_all_projects"
@@ -48,7 +48,11 @@ RSpec.shared_context "ModelContract shared context" do # rubocop:disable RSpec/C
[error_symbols]
end
end
contract_errors = errors.keys.index_with { |key| contract.errors.symbols_for(key) }
contract_errors = errors.keys.index_with do |key|
errors[key].is_a?(Hash) ? contract.errors.details[key] : contract.errors.symbols_for(key)
end
expect(contract_errors).to match(expected_errors)
if RSpec.current_example.metadata[:check_errors_i18n]
# ensure no I18n::MissingTranslationData is raised because of missing attributes and/or errors translations