diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index d285688c7..8948aae9c 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -217,6 +217,36 @@ module ActiveRecord # should belong_to(:organization).touch(true) # end # + # ##### strict_loading + # + # Use `strict_loading` to assert that the `:strict_loading` option was specified. + # + # class Organization < ActiveRecord::Base + # has_many :people, strict_loading: true + # end + # + # # RSpec + # RSpec.describe Organization, type: :model do + # it { should have_many(:people).strict_loading(true) } + # end + # + # # Minitest (Shoulda) + # class OrganizationTest < ActiveSupport::TestCase + # should have_many(:people).strict_loading(true) + # end + # + # Default value is true when no argument is specified + # + # # RSpec + # RSpec.describe Organization, type: :model do + # it { should have_many(:people).strict_loading } + # end + # + # # Minitest (Shoulda) + # class OrganizationTest < ActiveSupport::TestCase + # should have_many(:people).strict_loading + # end + # # ##### autosave # # Use `autosave` to assert that the `:autosave` option was specified. @@ -1128,6 +1158,11 @@ def touch(touch = true) self end + def strict_loading(strict_loading = true) + @options[:strict_loading] = strict_loading + self + end + def join_table(join_table_name) @options[:join_table_name] = join_table_name self @@ -1170,6 +1205,7 @@ def matches?(subject) conditions_correct? && validate_correct? && touch_correct? && + strict_loading_correct? && submatchers_match? end @@ -1414,6 +1450,21 @@ def touch_correct? end end + def strict_loading_correct? + return true unless options.key?(:strict_loading) + + if option_verifier.correct_for_boolean?(:strict_loading, options[:strict_loading]) + return true + end + + @missing = [ + "#{name} should have strict_loading set to ", + options[:strict_loading].to_s, + ].join + + false + end + def class_has_foreign_key?(klass) @missing = validate_foreign_key(klass) diff --git a/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb b/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb index 13e41e55f..a415d84cf 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb @@ -70,6 +70,10 @@ def has_and_belongs_to_many_name reflection.options[:through] end + def strict_loading? + reflection.options.fetch(:strict_loading, subject.strict_loading_by_default) + end + protected attr_reader :reflection, :subject 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 ecf7818b1..15cf0032d 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb @@ -122,6 +122,10 @@ def actual_value_for_class_name reflector.associated_class end + def actual_value_for_strict_loading + reflection.strict_loading? + end + def actual_value_for_option(name) option_value = reflection.options[name] diff --git a/spec/support/unit/helpers/application_configuration_helpers.rb b/spec/support/unit/helpers/application_configuration_helpers.rb index 87d013bc1..f7f10342e 100644 --- a/spec/support/unit/helpers/application_configuration_helpers.rb +++ b/spec/support/unit/helpers/application_configuration_helpers.rb @@ -18,6 +18,24 @@ def with_belongs_to_as_optional_by_default(&block) ) end + def with_strict_loading_by_default_enabled(&block) + configuring_application( + ::ActiveRecord::Base, + :strict_loading_by_default, + true, + &block + ) + end + + def with_strict_loading_by_default_disabled(&block) + configuring_application( + ::ActiveRecord::Base, + :strict_loading_by_default, + false, + &block + ) + end + private def configuring_application(config, name, value) 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 c1b7d4af1..2a7e96499 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -1199,6 +1199,411 @@ def belonging_to_non_existent_class(model_name, assoc_name, options = {}) expect(Parent.new).to have_many(:children) end + describe 'strict_loading' do + context 'when the application is configured with strict_loading disabled by default' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_disabled do + expect(having_many_children). + to have_many(:children).strict_loading(false) + end + end + + it 'rejects an association with a non-matching :strict_loading option without explicit value with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children). + to have_many(:children).strict_loading + }.to fail_with_message(message) + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children). + to have_many(:children).strict_loading(true) + }.to fail_with_message(message) + end + end + + context 'when the association is configured with a strict_loading constraint' do + context 'when qualified with strict_loading(true)' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_disabled do + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading(true) + end + end + + it 'accepts an association with a matching :strict_loading option without explicit value' do + with_strict_loading_by_default_disabled do + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to false)', + ].join + + expect { + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading(false) + }.to fail_with_message(message) + end + end + end + + context 'when qualified with strict_loading(false)' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_disabled do + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading(false) + end + end + + it 'rejects an association with a non-matching :strict_loading option without explicit value with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading + }.to fail_with_message(message) + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading(true) + }.to fail_with_message(message) + end + end + end + end + + context 'when strict_loading is defined on the model level' do + context 'when it is set to true' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_disabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading(true) + end + end + + it 'accepts an association with a matching :strict_loading option without explicit value' do + with_strict_loading_by_default_disabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to false)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading(false) + }.to fail_with_message(message) + end + end + end + + context 'when it is set to false' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_disabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading(false) + end + end + + it 'rejects an association with a non-matching :strict_loading option without explicit value with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading + }.to fail_with_message(message) + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_disabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading(true) + }.to fail_with_message(message) + end + end + end + end + end + + context 'when the application is configured with strict_loading enabled by default' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_enabled do + expect(having_many_children). + to have_many(:children).strict_loading(true) + end + end + + it 'accepts an association with a matching :strict_loading option without explicit value' do + with_strict_loading_by_default_enabled do + expect(having_many_children). + to have_many(:children).strict_loading + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to false)', + ].join + + expect { + expect(having_many_children). + to have_many(:children).strict_loading(false) + }.to fail_with_message(message) + end + end + + context 'when the association is configured with a strict_loading constraint' do + context 'when qualified with strict_loading(true)' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_enabled do + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading(true) + end + end + + it 'accepts an association with a matching :strict_loading option without explicit value' do + with_strict_loading_by_default_enabled do + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to false)', + ].join + + expect { + expect(having_many_children(strict_loading: true)). + to have_many(:children).strict_loading(false) + }.to fail_with_message(message) + end + end + end + + context 'when qualified with strict_loading(false)' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_enabled do + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading(false) + end + end + + it 'rejects an association with a non-matching :strict_loading option without explicit value with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading + }.to fail_with_message(message) + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + expect { + expect(having_many_children(strict_loading: false)). + to have_many(:children).strict_loading(true) + }.to fail_with_message(message) + end + end + end + end + + context 'when strict_loading is defined on the model level' do + context 'when it is set to true' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_enabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading(true) + end + end + + it 'accepts an association with a matching :strict_loading option without explicit value' do + with_strict_loading_by_default_enabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to false)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = true + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading(false) + }.to fail_with_message(message) + end + end + end + + context 'when it is set to false' do + it 'accepts an association with a matching :strict_loading option' do + with_strict_loading_by_default_enabled do + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect(parent).to have_many(:children).strict_loading(false) + end + end + + it 'rejects an association with a non-matching :strict_loading option without explicit value with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading + }.to fail_with_message(message) + end + end + + it 'rejects an association with a non-matching :strict_loading option with the correct message' do + with_strict_loading_by_default_enabled do + message = [ + 'Expected Parent to have a has_many association called children ', + '(children should have strict_loading set to true)', + ].join + + define_model :child, parent_id: :integer + parent = define_model(:parent) do |model| + model.strict_loading_by_default = false + model.has_many :children + end.new + + expect { + expect(parent).to have_many(:children).strict_loading(true) + }.to fail_with_message(message) + end + end + end + end + end + end + def having_many_children(options = {}) define_model :child, parent_id: :integer define_model(:parent).tap do |model|