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:
parent
2af0958575
commit
421a49d0b6
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
end
|
||||||
|
|
||||||
expect(Parent.new).to have_many(:children).conditions(adopted: true)
|
expect(Parent.new).
|
||||||
|
to have_many(:children).
|
||||||
|
conditions(adopted: true)
|
||||||
|
end
|
||||||
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
|
it 'rejects an association with a bad :conditions option' do
|
||||||
define_model :child, parent_id: :integer, adopted: :boolean
|
define_model :Child, parent_id: :integer, adopted: :boolean
|
||||||
define_model :parent do
|
define_model :Parent do
|
||||||
has_many :children
|
has_many :children
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(Parent.new).not_to have_many(:children).conditions(adopted: true)
|
expect(Parent.new).
|
||||||
|
not_to have_many(:children).
|
||||||
|
conditions(adopted: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'accepts an association without a :class_name option' do
|
it 'accepts an association without a :class_name option' do
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue