From eab36fad8a2b24312f6648b37df6e2f92b348dbd Mon Sep 17 00:00:00 2001 From: Marcello Rocha Date: Tue, 17 Jun 2025 17:37:00 +0200 Subject: [PATCH] update patterns contract and changes to the set attributes service --- .../update_subject_pattern_contract.rb | 57 ++++++++++++ .../set_attributes_service.rb | 33 ++++++- .../update_subject_pattern_contract_spec.rb | 86 +++++++++++++++++ spec/factories/type_factory.rb | 4 + .../set_attributes_service_spec.rb | 93 +++++++++++++++++++ 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 app/contracts/work_package_types/update_subject_pattern_contract.rb create mode 100644 spec/contracts/work_package_types/update_subject_pattern_contract_spec.rb create mode 100644 spec/services/work_package_types/set_attributes_service_spec.rb diff --git a/app/contracts/work_package_types/update_subject_pattern_contract.rb b/app/contracts/work_package_types/update_subject_pattern_contract.rb new file mode 100644 index 00000000000..521fc240957 --- /dev/null +++ b/app/contracts/work_package_types/update_subject_pattern_contract.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module WorkPackageTypes + class UpdateSubjectPatternContract < BaseContract + attribute :patterns + + validate :validate_subject_generation_pattern + + private + + def validate_subject_generation_pattern + blueprint = model.patterns.subject&.blueprint + return if blueprint.nil? + + valid_tokens = flat_valid_token_list + invalid_tokens = blueprint.scan(Types::PatternResolver::TOKEN_REGEX) + .reduce([]) do |acc, match| + token = Types::Patterns::PatternToken.build(match).key + valid_tokens.include?(token) ? acc : acc << token + end + + if invalid_tokens.any? + errors.add(:patterns, :invalid_tokens) + end + end + + def flat_valid_token_list = Types::Patterns::TokenPropertyMapper.new.tokens_for_type(model).map(&:key) + end +end diff --git a/app/services/work_package_types/set_attributes_service.rb b/app/services/work_package_types/set_attributes_service.rb index 0251bf61689..fa585fd8565 100644 --- a/app/services/work_package_types/set_attributes_service.rb +++ b/app/services/work_package_types/set_attributes_service.rb @@ -30,10 +30,41 @@ module WorkPackageTypes class SetAttributesService < ::BaseServices::SetAttributes + def initialize(user:, model:, contract_class:, contract_options: nil) + super + @valid_pattern = true + end + private def set_attributes(params) - super(params.except(:copy_workflow_from)) + permitted = params.except(:copy_workflow_from) + @valid_pattern = check_patterns(permitted) + + if @valid_pattern + super(permitted) + else + super(permitted.except(:patterns)) + end + end + + def validate_and_result + success, errors = validate(model, user, options: {}) + + if @valid_pattern + ServiceResult.new(success:, errors:, result: model) + else + errors.add(:patterns, :is_invalid) + ServiceResult.failure(errors:, result: model) + end + end + + def check_patterns(params) + return true if params.key?(:patterns) && params[:patterns].blank? + + Types::Patterns::CollectionContract.new.call(params[:patterns]).success? + rescue ArgumentError + false end end end diff --git a/spec/contracts/work_package_types/update_subject_pattern_contract_spec.rb b/spec/contracts/work_package_types/update_subject_pattern_contract_spec.rb new file mode 100644 index 00000000000..9da14e37127 --- /dev/null +++ b/spec/contracts/work_package_types/update_subject_pattern_contract_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +module WorkPackageTypes + RSpec.describe UpdateSubjectPatternContract do + let(:model) { create(:type, :with_subject_pattern) } + let(:user) { create(:admin) } + + subject(:contract) { described_class.new(model, user) } + + context "when the user isn't admin" do + let(:user) { create(:user) } + + it "the contract is invalid" do + expect(contract.validate).to be_falsey + end + + it "adds and error to the contract" do + contract.validate + expect(contract.errors.details).to eq(base: [{ error: :error_unauthorized }]) + end + end + + describe "subject_pattern validation" do + let(:valid_pattern) { { subject: { blueprint: "{{author}}", enabled: true } } } + let(:invalid_pattern) { { subject: { blueprint: "{{vader_s_rubber_duck}}", enabled: true } } } + + context "with no previous subject patterns" do + let(:model) { create(:type) } + + it "is valid with a valid pattern" do + model.patterns = valid_pattern + expect(contract.validate).to be_truthy + end + + it "is invalid if the pattern has bad tokens" do + model.patterns = invalid_pattern + expect(contract.validate).to be_falsey + end + + it "adds an error if the pattern has bad tokens" do + model.patterns = invalid_pattern + contract.validate + + expect(contract.errors.details).to eq(patterns: [{ error: :invalid_tokens }]) + end + end + + context "with a valid subject pattern" do + it "succeeds" do + model.patterns = valid_pattern + expect(model.validate).to be_truthy + end + end + end + end +end diff --git a/spec/factories/type_factory.rb b/spec/factories/type_factory.rb index 878755a291f..e41bf547f69 100644 --- a/spec/factories/type_factory.rb +++ b/spec/factories/type_factory.rb @@ -58,6 +58,10 @@ FactoryBot.define do end end + trait :with_subject_pattern do + patterns { { subject: { blueprint: "{{author}} - {{status}}/{{type}} - {{id}}", enabled: true } } } + end + trait :default do is_default { true } end diff --git a/spec/services/work_package_types/set_attributes_service_spec.rb b/spec/services/work_package_types/set_attributes_service_spec.rb new file mode 100644 index 00000000000..3335b9cdf79 --- /dev/null +++ b/spec/services/work_package_types/set_attributes_service_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +module WorkPackageTypes + RSpec.describe SetAttributesService do + let(:user) { create(:admin) } + let(:model) { create(:type, :with_subject_pattern) } + let(:params) { Hash.new } + + subject(:service) { described_class.new(user:, model:, contract_class: UpdateSubjectPatternContract) } + + context "when the pattern is malformed rubbish" do + let(:params) { { patterns: "vader_s_rubber_duck" } } + + it "fails" do + result = service.perform(params) + + expect(result).to be_failure + end + + it "adds an error on the patterns atrribute" do + result = service.perform(params) + expect(result.errors.details).to eq(patterns: [{ error: :is_invalid }]) + end + + it "does not override the already existing value on the model" do + service.perform(params) + expect(model).not_to be_changed + end + end + + context "when the pattern is invalid" do + let(:params) { { patterns: { subject: { blueprint: "{{author}}" } } } } + + it "fails" do + result = service.perform(params) + expect(result).to be_failure + end + + it "adds an error on the patterns attribute" do + result = service.perform(params) + expect(result.errors.details).to eq(patterns: [{ error: :is_invalid }]) + end + + it "does not override the already existing value on the model" do + service.perform(params) + expect(model).not_to be_changed + end + end + + context "when the pattern is blank" do + let(:params) { { patterns: nil } } + + it "succeeds" do + expect(service.perform(params)).to be_success + end + + it "sets the patterns to an empty collection" do + service.perform(params) + expect(model.patterns).to eq(Types::Patterns::Collection.empty) + end + end + end +end