diff --git a/NEWS.md b/NEWS.md index ebc6c220..85de8010 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,15 @@ +# HEAD + +### Features + +* Update `dependent` qualifier on association matchers to support `:destroy`, + `:delete`, `:nullify`, `:restrict`, `:restrict_with_exception`, and + `:restrict_with_error`. You can also pass `true` or `false` to assert that + the association has (or has not) been declared with *any* dependent option. + ([#631]) + +[#631]: https://github.com/thoughtbot/shoulda-matchers/pull/631 + # 2.8.0.rc1 ### Deprecations diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index a336661b..cf596e07 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -162,6 +162,24 @@ module Shoulda # should belong_to(:world).dependent(:destroy) # end # + # To assert that *any* `:dependent` option was specified, use `true`: + # + # # RSpec + # describe Person do + # it { should belong_to(:world).dependent(true) } + # end + # + # To assert that *no* `:dependent` option was specified, use `false`: + # + # class Person < ActiveRecord::Base + # belongs_to :company + # end + # + # # RSpec + # describe Person do + # it { should belong_to(:company).dependent(false) } + # end + # # ##### counter_cache # # Use `counter_cache` to assert that the `:counter_cache` option was @@ -993,7 +1011,7 @@ module Shoulda end def macro_supports_primary_key? - macro == :belongs_to || + macro == :belongs_to || ([:has_many, :has_one].include?(macro) && !through?) end diff --git a/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb b/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb index 4c68364c..973433a0 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb @@ -19,10 +19,10 @@ module Shoulda def matches?(subject) self.subject = ModelReflector.new(subject, name) - if option_verifier.correct_for_string?(:dependent, dependent) + if option_matches? true else - self.missing_option = "#{name} should have #{dependent} dependency" + self.missing_option = generate_missing_option false end end @@ -31,9 +31,30 @@ module Shoulda attr_accessor :subject, :dependent, :name + private + def option_verifier @option_verifier ||= OptionVerifier.new(subject) end + + def option_matches? + option_verifier.correct_for?(option_type, :dependent, dependent) + end + + def option_type + case dependent + when true, false then :boolean + else :string + end + end + + def generate_missing_option + [ + "#{name} should have", + (dependent == true ? 'a' : dependent), + 'dependency' + ].join(' ') + end end end end diff --git a/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb b/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb index 84debf62..999feb7a 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb @@ -32,6 +32,20 @@ module Shoulda correct_for?(:relation_clause, name, expected_value) end + def correct_for?(*args) + expected_value, name, type = args.reverse + if expected_value.nil? + true + else + expected_value = type_cast( + type, + expected_value_for(type, name, expected_value) + ) + actual_value = type_cast(type, actual_value_for(name)) + expected_value == actual_value + end + end + def actual_value_for(name) if RELATION_OPTIONS.include?(name) actual_value_for_relation_clause(name) @@ -49,17 +63,6 @@ module Shoulda attr_reader :reflector - def correct_for?(*args) - expected_value, name, type = args.reverse - if expected_value.nil? - true - else - expected_value = type_cast(type, expected_value_for(type, name, expected_value)) - actual_value = type_cast(type, actual_value_for(name)) - expected_value == actual_value - end - end - def type_cast(type, value) case type when :string, :relation_clause then value.to_s diff --git a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb index b0ab0307..da8f20ba 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -646,8 +646,28 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do end it 'accepts an association with a valid :dependent option' do - expect(having_one_detail(dependent: :destroy)). - to have_one(:detail).dependent(:destroy) + dependent_options.each do |option| + expect(having_one_detail(dependent: option)). + to have_one(:detail).dependent(option) + end + end + + it 'accepts any dependent option if true' do + dependent_options.each do |option| + expect(having_one_detail(dependent: option)). + to have_one(:detail).dependent(true) + end + end + + it 'rejects any dependent options if false' do + dependent_options.each do |option| + expect(having_one_detail(dependent: option)). + to_not have_one(:detail).dependent(false) + end + end + + it 'accepts a nil dependent option if false' do + expect(having_one_detail).to have_one(:detail).dependent(false) end it 'rejects an association with a bad :dependent option' do @@ -1117,4 +1137,13 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do args << options model.__send__(macro, name, *args) end + + def dependent_options + case Rails.version + when /\A3/ + [:destroy, :delete, :nullify, :restrict] + when /\A4/ + [:destroy, :delete, :nullify, :restrict_with_exception, :restrict_with_error] + end + end end