Pass correct object type to association reflection (#992)

It's currently not possible to test a `has_many` association that is
defined with a scope block where the block takes an instance of the
association source. The `have_many` matcher attempts to call this scope
and when it does so it does not pass that instance. This commit fixes
that.

Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
This commit is contained in:
Rafael Santos 2020-07-11 21:01:29 +12:00 committed by GitHub
parent 2af0958575
commit 421a49d0b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 83 additions and 17 deletions

View File

@ -35,12 +35,12 @@ module Shoulda
join_table_name.to_s join_table_name.to_s
end end
def association_relation def association_relation(related_instance)
relation = associated_class.all relation = associated_class.all
if reflection.scope if reflection.scope
# Source: AR::Associations::AssociationScope#eval_scope # Source: AR::Associations::AssociationScope#eval_scope
relation.instance_exec(subject, &reflection.scope) relation.instance_exec(related_instance, &reflection.scope)
else else
relation relation
end end

View File

@ -7,7 +7,6 @@ module Shoulda
delegate( delegate(
:associated_class, :associated_class,
:association_foreign_key, :association_foreign_key,
:association_relation,
:foreign_key, :foreign_key,
:has_and_belongs_to_many_name, :has_and_belongs_to_many_name,
:join_table_name, :join_table_name,
@ -27,6 +26,10 @@ module Shoulda
@name = name @name = name
end end
def association_relation
reflection.association_relation(subject)
end
def reflection def reflection
@reflection ||= reflect_on_association(name) @reflection ||= reflect_on_association(name)
end end

View File

@ -1004,22 +1004,68 @@ Expected Parent to have a has_many association called children through conceptio
expect(matcher.failure_message).to match(/children should be ordered by id/) expect(matcher.failure_message).to match(/children should be ordered by id/)
end end
it 'accepts an association with a valid :conditions option' do context 'if the association has a scope block' do
define_model :child, parent_id: :integer, adopted: :boolean context 'and the block does not take an argument' do
define_model(:parent).tap do |model| context 'and the matcher is given conditions that match the conditions used in the scope' do
define_association_with_conditions(model, :has_many, :children, adopted: true) it 'matches' do
define_model :Child, parent_id: :integer, adopted: :boolean
define_model(:Parent) do
has_many :children, -> { where(adopted: true) }
end
expect(Parent.new).
to have_many(:children).
conditions(adopted: true)
end
end
context 'and the matcher is given conditions that do not match the conditions used in the scope' do
it 'rejects an association with a bad :conditions option' do
define_model :Child, parent_id: :integer, adopted: :boolean
define_model :Parent do
has_many :children
end
expect(Parent.new).
not_to have_many(:children).
conditions(adopted: true)
end
end
end end
expect(Parent.new).to have_many(:children).conditions(adopted: true) context 'and the block takes an argument' do
end context 'and the matcher is given conditions that match the scope' do
it 'matches' do
define_model :Wheel, bike_id: :integer, tire_id: :integer
define_model :Tire
define_model :Bike, default_tire_id: :integer do
has_many :wheels, -> (bike) do
where(tire_id: bike.default_tire_id)
end
belongs_to :default_tire, class_name: 'Tire'
end
it 'rejects an association with a bad :conditions option' do expect(Bike.new(default_tire_id: 42)).
define_model :child, parent_id: :integer, adopted: :boolean to have_many(:wheels).conditions(tire_id: 42)
define_model :parent do end
has_many :children end
context 'and the matcher is given conditions that do not match the scope' do
it 'matches' do
define_model :Wheel, bike_id: :integer, tire_id: :integer
define_model :Tire
define_model :Bike, default_tire_id: :integer do
has_many :wheels, -> (bike) do
where(tire_id: bike.default_tire_id)
end
belongs_to :default_tire, class_name: 'Tire'
end
expect(Bike.new(default_tire_id: 42)).
not_to have_many(:wheels).conditions(tire_id: 10)
end
end
end end
expect(Parent.new).not_to have_many(:children).conditions(adopted: true)
end end
it 'accepts an association without a :class_name option' do it 'accepts an association without a :class_name option' do

View File

@ -95,12 +95,28 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
belongs_to :country, -> { where(mood: 'nice') } belongs_to :country, -> { where(mood: 'nice') }
end end
delegate_reflection = person_model.reflect_on_association(:country) delegate_reflection = person_model.reflect_on_association(:country)
person = person_model.new
reflection = described_class.new(delegate_reflection) reflection = described_class.new(delegate_reflection)
actual_sql = reflection.association_relation.to_sql actual_sql = reflection.association_relation(person).to_sql
expected_sql = Country.where(mood: 'nice').to_sql expected_sql = Country.where(mood: 'nice').to_sql
expect(actual_sql).to eq expected_sql expect(actual_sql).to eq expected_sql
end end
it 'passes the object that has the association to the block' do
spy = spy('spy')
define_model(:country)
person_model = define_model(:person, country_id: :integer) do
belongs_to :country, -> (person) { spy.receive(person) }
end
delegate_reflection = person_model.reflect_on_association(:country)
person = person_model.new
reflection = described_class.new(delegate_reflection)
reflection.association_relation(person)
expect(spy).to have_received(:receive).with(person)
end
end end
context 'when the scope is nil' do context 'when the scope is nil' do
@ -110,9 +126,10 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
belongs_to :country belongs_to :country
end end
delegate_reflection = person_model.reflect_on_association(:country) delegate_reflection = person_model.reflect_on_association(:country)
person = person_model.new
reflection = described_class.new(delegate_reflection) reflection = described_class.new(delegate_reflection)
actual_sql = reflection.association_relation.to_sql actual_sql = reflection.association_relation(person).to_sql
expected_sql = Country.all.to_sql expected_sql = Country.all.to_sql
expect(actual_sql).to eq expected_sql expect(actual_sql).to eq expected_sql
end end