From 3ad7be7aea5e8c4a8ddc2304caed2cf9c8b3af2d Mon Sep 17 00:00:00 2001 From: Matheus Sales Date: Fri, 12 Jan 2024 16:07:26 -0300 Subject: [PATCH] feat: Add `query_constraints` modifier to `AssociationMatcher` --- .../active_record/association_matcher.rb | 30 +++++++++- .../active_record/association_matcher_spec.rb | 56 +++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index 6ceaaf674..0bd88c718 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -1093,6 +1093,11 @@ def with_primary_key(primary_key) self end + def with_query_constraints(query_constraints) + @options[:query_constraints] = query_constraints + self + end + def required(required = true) remove_submatcher(AssociationMatchers::OptionalMatcher) add_submatcher( @@ -1157,6 +1162,7 @@ def matches?(subject) (polymorphic? || class_exists?) && foreign_key_exists? && primary_key_exists? && + query_constraints_exists? && class_name_correct? && join_table_correct? && autosave_correct? && @@ -1258,7 +1264,7 @@ def validate_inverse_of_through_association false end - def macro_supports_primary_key? + def macro_is_not_through? macro == :belongs_to || ([:has_many, :has_one].include?(macro) && !through?) end @@ -1268,7 +1274,27 @@ def foreign_key_exists? end def primary_key_exists? - !macro_supports_primary_key? || primary_key_correct?(model_class) + !macro_is_not_through? || primary_key_correct?(model_class) + end + + def query_constraints_exists? + !macro_is_not_through? || query_constraints_correct? + end + + def query_constraints_correct? + if options.key?(:query_constraints) + if option_verifier.correct_for_string?( + :query_constraints, + options[:query_constraints], + ) + true + else + @missing = "#{model_class} should have \:query_constraints options set to #{options[:query_constraints]}" + false + end + else + true + end end def belongs_foreign_key_missing? diff --git a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb index 5b8df5646..3743e6f16 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -47,6 +47,23 @@ expect(matcher.failure_message).to match(/Child does not have a custom_primary_key primary key/) end + it 'accepts an association using an valid :query_constraints option' do + define_model :parent, name: :string + define_model :child, parent_id: :integer, parent_name: :string do + belongs_to :parent, query_constraints: [:parent_id, :parent_name] + end + + expect(Child.new).to belong_to(:parent).with_query_constraints([:parent_id, :parent_name]) + end + + it 'rejects an association with a bad :query_constraints option' do + matcher = belong_to(:parent).with_query_constraints([:parent_id, :parent_name]) + + expect(belonging_to_parent).not_to matcher + + expect(matcher.failure_message).to match(/Child should have :query_constraints options set to \[:parent_id, :parent_name\]/) + end + it 'accepts a polymorphic association' do define_model :child, parent_type: :string, parent_id: :integer do belongs_to :parent, polymorphic: true @@ -821,6 +838,28 @@ def belonging_to_non_existent_class(model_name, assoc_name, options = {}) expect(matcher.failure_message).to match(/Parent does not have a custom_primary_key primary key/) end + it 'accepts an association using an valid :query_constraints option' do + define_model :parent, first_name: :string, last_name: :string do + self.primary_key = [:first_name, :last_name] + + has_many :children, query_constraints: [:parent_first_name, :parent_last_name] + end + + define_model :child, parent_first_name: :integer, parent_last_name: :string do + belongs_to :parent, query_constraints: [:parent_first_name, :parent_last_name] + end + + expect(Parent.new).to have_many(:children).with_query_constraints([:parent_first_name, :parent_last_name]) + end + + it 'rejects an association with a bad :query_constraints option' do + matcher = have_many(:children).with_query_constraints([:parent_first_name, :parent_last_name]) + + expect(having_many_children).not_to matcher + + expect(matcher.failure_message).to match(/Parent should have :query_constraints options set to \[:parent_first_name, :parent_last_name\]/) + end + it 'rejects an association with a bad :as option' do define_model( :child, @@ -1223,6 +1262,23 @@ def having_many_non_existent_class(model_name, assoc_name, options = {}) expect(matcher.failure_message).to match(/Person does not have a custom_primary_key primary key/) end + it 'accepts an association using an valid :query_constraints option' do + define_model :detail, person_first_name: :string, person_last_name: :string + define_model :person do + has_one :detail, query_constraints: [:person_first_name, :person_last_name] + end + + expect(Person.new).to have_one(:detail).with_query_constraints([:person_first_name, :person_last_name]) + end + + it 'rejects an association with a bad :query_constraints option' do + matcher = have_one(:detail).with_query_constraints([:person_first_name, :person_last_name]) + + expect(having_one_detail).not_to matcher + + expect(matcher.failure_message).to match(/Person should have :query_constraints options set to \[:person_first_name, :person_last_name\]/) + end + it 'rejects an association with a bad :as option' do define_model :detail, detailable_id: :integer, detailable_type: :string