diff --git a/NEWS.md b/NEWS.md index cce6b545..5a70e4b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,11 +27,14 @@ * Tweak `allow_value` failure message so that it reads a bit nicer when listing existing errors. +* Add ability to test `:primary_key` option on associations. ([#597]) + [#591]: https://github.com/thoughtbot/shoulda-matchers/pull/591 [#592]: https://github.com/thoughtbot/shoulda-matchers/pull/592 [#588]: https://github.com/thoughtbot/shoulda-matchers/pull/588 [#584]: https://github.com/thoughtbot/shoulda-matchers/pull/584 [#593]: https://github.com/thoughtbot/shoulda-matchers/pull/593 +[#597]: https://github.com/thoughtbot/shoulda-matchers/pull/597 # 2.7.0 diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index fae3faca..281fd249 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -100,6 +100,28 @@ module Shoulda # should belong_to(:ancient_city).class_name('City') # end # + # ##### with_primary_key + # + # Use `with_primary_key` to test usage of the `:primary_key` option. + # + # class Person < ActiveRecord::Base + # belongs_to :great_country, primary_key: 'country_id' + # end + # + # # RSpec + # describe Person do + # it do + # should belong_to(:great_country). + # with_primary_key('country_id') + # end + # end + # + # # Test::Unit + # class PersonTest < ActiveSupport::TestCase + # should belong_to(:great_country). + # with_primary_key('country_id') + # end + # # ##### with_foreign_key # # Use `with_foreign_key` to test usage of the `:foreign_key` option. @@ -295,6 +317,24 @@ module Shoulda # should have_many(:hopes).class_name('Dream') # end # + # ##### with_primary_key + # + # Use `with_primary_key` to test usage of the `:primary_key` option. + # + # class Person < ActiveRecord::Base + # has_many :worries, primary_key: 'worrier_id' + # end + # + # # RSpec + # describe Person do + # it { should have_many(:worries).with_primaryu_key('worrier_id') } + # end + # + # # Test::Unit + # class PersonTest < ActiveSupport::TestCase + # should have_many(:worries).with_primary_key('worrier_id') + # end + # # ##### with_foreign_key # # Use `with_foreign_key` to test usage of the `:foreign_key` option. @@ -511,6 +551,24 @@ module Shoulda # should have_one(:contract).dependent(:nullify) # end # + # ##### with_primary_key + # + # Use `with_primary_key` to test usage of the `:primary_key` option. + # + # class Person < ActiveRecord::Base + # has_one :job, primary_key: 'worker_id' + # end + # + # # RSpec + # describe Person do + # it { should have_one(:job).with_primary_key('worker_id') } + # end + # + # # Test::Unit + # class PersonTest < ActiveSupport::TestCase + # should have_one(:job).with_primary_key('worker_id') + # end + # # ##### with_foreign_key # # Use `with_foreign_key` to test usage of the `:foreign_key` option. @@ -814,6 +872,11 @@ module Shoulda self end + def with_primary_key(primary_key) + @options[:primary_key] = primary_key + self + end + def validate(validate = true) @options[:validate] = validate self @@ -846,6 +909,7 @@ module Shoulda macro_correct? && (polymorphic? || class_exists?) && foreign_key_exists? && + primary_key_exists? && class_name_correct? && join_table_correct? && autosave_correct? && @@ -928,10 +992,19 @@ module Shoulda end end + def macro_supports_primary_key? + macro == :belongs_to || + ([:has_many, :has_one].include?(macro) && !through?) + end + def foreign_key_exists? !(belongs_foreign_key_missing? || has_foreign_key_missing?) end + def primary_key_exists? + !macro_supports_primary_key? || primary_key_correct?(model_class) + end + def belongs_foreign_key_missing? macro == :belongs_to && !class_has_foreign_key?(model_class) end @@ -1034,6 +1107,19 @@ module Shoulda end end + def primary_key_correct?(klass) + if options.key?(:primary_key) + if option_verifier.correct_for_string?(:primary_key, options[:primary_key]) + true + else + @missing = "#{klass} does not have a #{options[:primary_key]} primary key" + false + end + else + true + end + end + def foreign_key if foreign_key_reflection if foreign_key_reflection.respond_to?(:foreign_key) diff --git a/spec/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/shoulda/matchers/active_record/association_matcher_spec.rb index e1526e61..07a9401b 100644 --- a/spec/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/shoulda/matchers/active_record/association_matcher_spec.rb @@ -29,6 +29,22 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do expect(Child.new).to belong_to(:parent) end + it 'accepts an association using an existing custom primary key' do + define_model :parent + define_model :child, parent_id: :integer, custom_primary_key: :integer do + belongs_to :parent, primary_key: :custom_primary_key + end + expect(Child.new).to belong_to(:parent).with_primary_key(:custom_primary_key) + end + + it 'rejects an association with a bad :primary_key option' do + matcher = belong_to(:parent).with_primary_key(:custom_primary_key) + + expect(belonging_to_parent).not_to matcher + + expect(matcher.failure_message).to match(/Child does not have a custom_primary_key primary key/) + end + it 'accepts a polymorphic association' do define_model :child, parent_type: :string, parent_id: :integer do belongs_to :parent, polymorphic: true @@ -288,6 +304,22 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do expect(Parent.new).not_to have_many(:children) end + it 'accepts an association using an existing custom primary key' do + define_model :child, parent_id: :integer + define_model :parent, custom_primary_key: :integer do + has_many :children, primary_key: :custom_primary_key + end + expect(Parent.new).to have_many(:children).with_primary_key(:custom_primary_key) + end + + it 'rejects an association with a bad :primary_key option' do + matcher = have_many(:children).with_primary_key(:custom_primary_key) + + expect(having_many_children).not_to matcher + + expect(matcher.failure_message).to match(/Parent does not have a custom_primary_key primary key/) + end + it 'rejects an association with a bad :as option' do define_model :child, caretaker_type: :string, caretaker_id: :integer @@ -537,6 +569,22 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do expect(Person.new).to have_one(:detail).with_foreign_key(:detailed_person_id) end + it 'accepts an association using an existing custom primary key' do + define_model :detail, person_id: :integer + define_model :person, custom_primary_key: :integer do + has_one :detail, primary_key: :custom_primary_key + end + expect(Person.new).to have_one(:detail).with_primary_key(:custom_primary_key) + end + + it 'rejects an association with a bad :primary_key option' do + matcher = have_one(:detail).with_primary_key(:custom_primary_key) + + expect(having_one_detail).not_to matcher + + expect(matcher.failure_message).to match(/Person does not have a custom_primary_key primary key/) + end + it 'rejects an association with a bad :as option' do define_model :detail, detailable_id: :integer, detailable_type: :string