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
end
def association_relation
def association_relation(related_instance)
relation = associated_class.all
if reflection.scope
# Source: AR::Associations::AssociationScope#eval_scope
relation.instance_exec(subject, &reflection.scope)
relation.instance_exec(related_instance, &reflection.scope)
else
relation
end

View File

@ -7,7 +7,6 @@ module Shoulda
delegate(
:associated_class,
:association_foreign_key,
:association_relation,
:foreign_key,
:has_and_belongs_to_many_name,
:join_table_name,
@ -27,6 +26,10 @@ module Shoulda
@name = name
end
def association_relation
reflection.association_relation(subject)
end
def reflection
@reflection ||= reflect_on_association(name)
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/)
end
it 'accepts an association with a valid :conditions option' do
define_model :child, parent_id: :integer, adopted: :boolean
define_model(:parent).tap do |model|
define_association_with_conditions(model, :has_many, :children, adopted: true)
context 'if the association has a scope block' do
context 'and the block does not take an argument' do
context 'and the matcher is given conditions that match the conditions used in the scope' do
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
expect(Parent.new).to have_many(:children).conditions(adopted: true)
end
context 'and the block takes an argument' do
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
define_model :child, parent_id: :integer, adopted: :boolean
define_model :parent do
has_many :children
expect(Bike.new(default_tire_id: 42)).
to have_many(:wheels).conditions(tire_id: 42)
end
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
expect(Parent.new).not_to have_many(:children).conditions(adopted: true)
end
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') }
end
delegate_reflection = person_model.reflect_on_association(:country)
person = person_model.new
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
expect(actual_sql).to eq expected_sql
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
context 'when the scope is nil' do
@ -110,9 +126,10 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
belongs_to :country
end
delegate_reflection = person_model.reflect_on_association(:country)
person = person_model.new
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
expect(actual_sql).to eq expected_sql
end