diff --git a/NEWS.md b/NEWS.md index 781e9f9c..f08a7b35 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,11 +13,14 @@ * The `ensure_inclusion_of` matcher now works with a decimal column. * Fix association matchers on Rails 3 so they work when used in conjunction with - a submatcher such as #order and an association that has :include on it. + a submatcher such as #order and an association that has `:include` on it. * Fix a bug where `validate_uniqueness_of` would fail if the attribute under test had a limit of fewer than 16 characters. +* You can now test that your `has_many :through` or `has_one :through` + associations are defined with a `:source` option. + # v 2.4.0 * Fix a bug with the `validate_numericality_of` matcher that would not allow the diff --git a/README.md b/README.md index 5e5a65de..d5c5b949 100644 --- a/README.md +++ b/README.md @@ -618,6 +618,7 @@ The `have_many` matcher tests your `has_many` and `has_many :through` associatio class Person < ActiveRecord::Base has_many :friends has_many :acquaintances, through: :friends + has_many :job_offers, through: :friends, source: :opportunities has_many :coins, -> { where(condition: 'mint') } has_many :shirts, -> { order('color') } has_many :hopes, class_name: 'Dream' @@ -632,6 +633,7 @@ end describe Person do it { should have_many(:friends) } it { should have_many(:acquaintances).through(:friends) } + it { should have_many(:job_offers).through(:friends).source(:opportunities) } it { should have_many(:coins).conditions(condition: 'mint') } it { should have_many(:shirts).order('color') } it { should have_many(:hopes).class_name('Dream') } @@ -646,6 +648,7 @@ end class PersonTest < ActiveSupport::TestCase should have_many(:friends) should have_many(:acquaintances).through(:friends) + should have_many(:job_offers).through(:friends).source(:opportunities) should have_many(:coins).conditions(condition: 'mint') should have_many(:shirts).order('color') should have_many(:hopes).class_name('Dream') @@ -665,6 +668,7 @@ The `have_one` matcher tests your `has_one` and `has_one :through` associations. class Person < ActiveRecord::Base has_one :partner has_one :life, through: :partner + has_one :car, through: :partner, source: :vehicle has_one :pet, -> { where('weight < 80') } has_one :focus, -> { order('priority desc') } has_one :chance, class_name: 'Opportunity' @@ -677,6 +681,7 @@ end describe Person do it { should have_one(:partner) } it { should have_one(:life).through(:partner) } + it { should have_one(:car).through(:partner).source(:vehicle) } it { should have_one(:pet).conditions('weight < 80') } it { should have_one(:focus).order('priority desc') } it { should have_one(:chance).class_name('Opportunity') } diff --git a/lib/shoulda/matchers/active_record.rb b/lib/shoulda/matchers/active_record.rb index 6cd15d28..6d98735c 100644 --- a/lib/shoulda/matchers/active_record.rb +++ b/lib/shoulda/matchers/active_record.rb @@ -3,6 +3,7 @@ require 'shoulda/matchers/active_record/association_matchers/counter_cache_match require 'shoulda/matchers/active_record/association_matchers/order_matcher' require 'shoulda/matchers/active_record/association_matchers/through_matcher' require 'shoulda/matchers/active_record/association_matchers/dependent_matcher' +require 'shoulda/matchers/active_record/association_matchers/source_matcher' require 'shoulda/matchers/active_record/association_matchers/model_reflector' require 'shoulda/matchers/active_record/association_matchers/model_reflection' require 'shoulda/matchers/active_record/association_matchers/option_verifier' diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index 945b2fab..5db0b4cd 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -109,6 +109,12 @@ module Shoulda # :nodoc: self end + def source(source) + source_matcher = AssociationMatchers::SourceMatcher.new(source, name) + add_submatcher(source_matcher) + self + end + def conditions(conditions) @options[:conditions] = conditions self diff --git a/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb b/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb new file mode 100644 index 00000000..672c76ea --- /dev/null +++ b/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb @@ -0,0 +1,40 @@ +module Shoulda # :nodoc: + module Matchers + module ActiveRecord # :nodoc: + module AssociationMatchers + class SourceMatcher + attr_accessor :missing_option + + def initialize(source, name) + @source = source + @name = name + @missing_option = '' + end + + def description + "source => #{source}" + end + + def matches?(subject) + self.subject = ModelReflector.new(subject, name) + + if option_verifier.correct_for_string?(:source, source) + true + else + self.missing_option = "#{name} should have #{source} as source option" + false + end + end + + private + + attr_accessor :subject, :source, :name + + def option_verifier + @option_verifier ||= OptionVerifier.new(subject) + end + end + end + end + end +end diff --git a/spec/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/shoulda/matchers/active_record/association_matcher_spec.rb index c3f3ad01..75596f11 100644 --- a/spec/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/shoulda/matchers/active_record/association_matcher_spec.rb @@ -282,6 +282,19 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do matcher.failure_message_for_should.should =~ /children should have destroy dependency/ end + it 'accepts an association with a valid :source option' do + having_many_children(:source => :user). + should have_many(:children).source(:user) + end + + it 'rejects an association with a bad :source option' do + matcher = have_many(:children).source(:user) + + having_many_children.should_not matcher + + matcher.failure_message_for_should.should =~ /children should have user as source option/ + end + it 'accepts an association with a valid :order option' do having_many_children(:order => :id). should have_many(:children).order(:id)