From af85ac1f0e5b9a267efd4c05b30a73fe2f44d84e Mon Sep 17 00:00:00 2001 From: Jacob Morris Date: Wed, 10 Sep 2014 13:38:20 -0600 Subject: [PATCH] Add support for hbtm :join_table option Rails supports declaring a `has_and_belongs_to_many` relationship with a custom `:join_table` option and some developers want to assert that the correct value is being used. Add `AssocationMatcher#join_table` to allow developers to test the following: 1) That the :join_table option is being used for the relationship 2) That the *correct value* is being used 3) That the custom join table exists in the database --- NEWS.md | 3 + .../active_record/association_matcher.rb | 21 ++-- .../join_table_matcher.rb | 20 +++- .../active_record/association_matcher_spec.rb | 109 +++++++++++++----- 4 files changed, 114 insertions(+), 39 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0b27f1b5..d67564ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -72,6 +72,8 @@ * Add `on` qualifier to `validate_numericality_of`. (h/t [#356], [#358]) +* Add `join_table` qualifier to `have_and_belong_to_many`. ([#556]) + [#402]: https://github.com/thoughtbot/shoulda-matchers/pull/402 [#587]: https://github.com/thoughtbot/shoulda-matchers/pull/587 [#662]: https://github.com/thoughtbot/shoulda-matchers/pull/662 @@ -86,6 +88,7 @@ [#693]: https://github.com/thoughtbot/shoulda-matchers/pull/693 [#356]: https://github.com/thoughtbot/shoulda-matchers/pull/356 [#358]: https://github.com/thoughtbot/shoulda-matchers/pull/358 +[#556]: https://github.com/thoughtbot/shoulda-matchers/pull/556 # 2.8.0 diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index 3e742b6f..440726fb 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -823,7 +823,9 @@ module Shoulda # @private class AssociationMatcher delegate :reflection, :model_class, :associated_class, :through?, - :join_table_name, :polymorphic?, to: :reflector + :polymorphic?, to: :reflector + + attr_reader :name, :options def initialize(macro, name) @macro = macro @@ -906,6 +908,7 @@ module Shoulda end def join_table(join_table_name) + @options[:join_table_name] = join_table_name self end @@ -941,18 +944,22 @@ module Shoulda submatchers_match? end - protected - - attr_reader :submatchers, :missing, :subject, :macro, :name, :options - - def reflector - @reflector ||= AssociationMatchers::ModelReflector.new(subject, name) + def join_table_name + options[:join_table_name] || reflector.join_table_name end def option_verifier @option_verifier ||= AssociationMatchers::OptionVerifier.new(reflector) end + protected + + attr_reader :submatchers, :missing, :subject, :macro + + def reflector + @reflector ||= AssociationMatchers::ModelReflector.new(subject, name) + end + def add_submatcher(matcher) @submatchers << matcher end diff --git a/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb b/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb index 4dd54710..c46f2d4b 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb @@ -8,8 +8,8 @@ module Shoulda alias :missing_option :failure_message - delegate :model_class, :join_table_name, :associated_class, - to: :association_matcher + delegate :model_class, :join_table_name, :associated_class, :options, + :name, :option_verifier, to: :association_matcher delegate :connection, to: :model_class @@ -19,10 +19,24 @@ module Shoulda end def matches?(subject) - join_table_exists? && + join_table_option_correct? && + join_table_exists? && join_table_has_correct_columns? end + def join_table_option_correct? + if options.key?(:join_table_name) + if option_verifier.correct_for_string?(:join_table, options[:join_table_name]) + true + else + @failure_message = "#{name} should use '#{options[:join_table_name]}' for :join_table option" + false + end + else + true + end + end + def join_table_exists? if connection.tables.include?(join_table_name) true 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 d2998020..b51f9b4e 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -886,46 +886,97 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do end.to fail_with_message_including('missing columns: person_id, relative_id') end - it "rejects an association with a bad :join_table option" do - define_model :relative - join_table_name = 'people_and_their_families' + context 'when the association is declared with a :join_table option' do + it 'accepts when testing with the same :join_table option' do + join_table_name = 'people_and_their_families' - define_model :person do - has_and_belongs_to_many( - :relatives, join_table: join_table_name - ) + define_model :relative + + define_model :person do + has_and_belongs_to_many(:relatives, join_table: join_table_name) + end + + create_table(join_table_name, id: false) do |t| + t.references :person + t.references :relative + end + + expect(Person.new). + to have_and_belong_to_many(:relatives). + join_table(join_table_name) end - create_table("people_relatives", id: false) do |t| - t.references :person - t.references :relative + it 'accepts even when not explicitly testing with a :join_table option' do + join_table_name = 'people_and_their_families' + + define_model :relative + + define_model :person do + has_and_belongs_to_many(:relatives, + join_table: join_table_name + ) + end + + create_table(join_table_name, id: false) do |t| + t.references :person + t.references :relative + end + + expect(Person.new).to have_and_belong_to_many(:relatives) end - expect do - expect(Person.new).to( - have_and_belong_to_many(:relatives).join_table(join_table_name) + it 'rejects when testing with a different :join_table option' do + join_table_name = 'people_and_their_families' + + define_model :relative + + define_model :person do + has_and_belongs_to_many( + :relatives, + join_table: join_table_name + ) + end + + create_table(join_table_name, id: false) do |t| + t.references :person + t.references :relative + end + + assertion = lambda do + expect(Person.new). + to have_and_belong_to_many(:relatives). + join_table('family_tree') + end + + expect(&assertion).to fail_with_message_including( + "relatives should use 'family_tree' for :join_table option" ) - end.to fail_with_message_including("#{join_table_name} doesn't exist") + end end - it "accepts an association with a valid :join_table option" do - define_model :relative - join_table_name = 'people_and_their_families' + context 'when the association is not declared with a :join_table option' do + it 'rejects when testing with a :join_table option' do + define_model :relative - define_model :person do - has_and_belongs_to_many( - :relatives, join_table: join_table_name + define_model :person do + has_and_belongs_to_many(:relatives) + end + + create_table('people_relatives', id: false) do |t| + t.references :person + t.references :relative + end + + assertion = lambda do + expect(Person.new). + to have_and_belong_to_many(:relatives). + join_table('family_tree') + end + + expect(&assertion).to fail_with_message_including( + "relatives should use 'family_tree' for :join_table option" ) end - - create_table(join_table_name, id: false) do |t| - t.references :person - t.references :relative - end - - expect(Person.new).to( - have_and_belong_to_many(:relatives).join_table(join_table_name) - ) end context 'using a custom foreign key' do