Check correct columns are present in join table

The `have_and_belong_to_many` matcher could give a false positive if the
join table is present but does not contain the correct columns. Check to
see if the columns exist in the join table and provide a meaningful
failure message if one or more of the columns are not present.
This commit is contained in:
Jacob Morris 2014-04-25 23:26:45 -06:00 committed by Elliot Winkler
parent f356561f6e
commit 0695c8647b
4 changed files with 116 additions and 13 deletions

View File

@ -2,6 +2,7 @@ require 'shoulda/matchers/active_record/association_matcher'
require 'shoulda/matchers/active_record/association_matchers'
require 'shoulda/matchers/active_record/association_matchers/counter_cache_matcher'
require 'shoulda/matchers/active_record/association_matchers/inverse_of_matcher'
require 'shoulda/matchers/active_record/association_matchers/join_table_matcher'
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'

View File

@ -847,9 +847,9 @@ module Shoulda
(polymorphic? || class_exists?) &&
foreign_key_exists? &&
class_name_correct? &&
join_table_correct? &&
autosave_correct? &&
conditions_correct? &&
join_table_exists? &&
validate_correct? &&
touch_correct? &&
submatchers_match?
@ -889,7 +889,8 @@ module Shoulda
end
def missing_options
[missing, failing_submatchers.map(&:missing_option)].flatten.join
missing_options = [missing, failing_submatchers.map(&:missing_option)]
missing_options.flatten.compact.join(', ')
end
def failing_submatchers
@ -946,6 +947,19 @@ module Shoulda
end
end
def join_table_correct?
if macro != :has_and_belongs_to_many || join_table_matcher.matches?(@subject)
true
else
@missing = join_table_matcher.failure_message
false
end
end
def join_table_matcher
@join_table_matcher ||= AssociationMatchers::JoinTableMatcher.new(self)
end
def class_exists?
associated_class
true
@ -980,16 +994,6 @@ module Shoulda
end
end
def join_table_exists?
if macro != :has_and_belongs_to_many ||
model_class.connection.tables.include?(join_table)
true
else
@missing = "join table #{join_table} doesn't exist"
false
end
end
def validate_correct?
if option_verifier.correct_for_boolean?(:validate, options[:validate])
true

View File

@ -0,0 +1,81 @@
module Shoulda
module Matchers
module ActiveRecord
module AssociationMatchers
# @private
class JoinTableMatcher
attr_reader :association_matcher, :failure_message
alias :missing_option :failure_message
delegate :model_class, :join_table, :associated_class,
to: :association_matcher
delegate :connection, to: :model_class
def initialize(association_matcher)
@association_matcher = association_matcher
end
def matches?(subject)
join_table_exists? &&
join_table_has_correct_columns?
end
def join_table_exists?
if connection.tables.include?(join_table)
true
else
@failure_message = missing_table_message
false
end
end
def join_table_has_correct_columns?
if missing_columns.empty?
true
else
@failure_message = missing_columns_message
false
end
end
private
def missing_columns
@missing_columns ||= expected_join_table_columns.select do |key|
!actual_join_table_columns.include?(key)
end
end
def expected_join_table_columns
[
"#{model_class.name.underscore}_id",
"#{associated_class.name.underscore}_id"
]
end
def actual_join_table_columns
connection.columns(join_table).map(&:name)
end
def missing_table_message
"join table #{join_table} doesn't exist"
end
def missing_columns_message
missing = missing_columns.join(', ')
"join table #{join_table} missing #{column_label}: #{missing}"
end
def column_label
if missing_columns.count > 1
'columns'
else
'column'
end
end
end
end
end
end
end

View File

@ -712,7 +712,24 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do
has_and_belongs_to_many :relatives
end
expect(Person.new).not_to have_and_belong_to_many(:relatives)
expected_failure_message = "join table people_relatives doesn't exist"
expect do
expect(Person.new).to have_and_belong_to_many(:relatives)
end.to fail_with_message_including(expected_failure_message)
end
it 'rejects an association with a join table with incorrect columns' do
define_model :relative
define_model :person do
has_and_belongs_to_many :relatives
end
define_model :people_relative, id: false, some_crazy_id: :integer
expect do
expect(Person.new).to have_and_belong_to_many(:relatives)
end.to fail_with_message_including('missing columns: person_id, relative_id')
end
it 'rejects an association of the wrong type' do