Clear up confusion w/ presence around belongs_to associations
Now that `belongs_to` associations add a presence validation automatically, if you have a test such as the following: class StudentBook < ApplicationRecord belongs_to :student end RSpec.describe StudentBook do it { is_expected.not_to be_valid } it { is_expected.to validate_presence_of(:student) } end then the test for presence of :student will fail, because the validation message on the automatic presence validation is different than the usual message. The solution here, of course, is to just use `belong_to`, but that is not very obvious. So this commit updates the presence matcher to remind users about using `belong_to` in a case such as this.
This commit is contained in:
parent
32f6a35e39
commit
7cad5feaa7
|
@ -181,6 +181,27 @@ module Shoulda
|
||||||
"validate that :#{@attribute} cannot be empty/falsy"
|
"validate that :#{@attribute} cannot be empty/falsy"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
message = super
|
||||||
|
|
||||||
|
if should_add_footnote_about_belongs_to?
|
||||||
|
message << "\n\n"
|
||||||
|
message << Shoulda::Matchers.word_wrap(<<-MESSAGE.strip, indent: 2)
|
||||||
|
You're getting this error because #{reason_for_existing_presence_validation}.
|
||||||
|
*This* presence validation doesn't use "can't be blank", the usual validation
|
||||||
|
message, but "must exist" instead.
|
||||||
|
|
||||||
|
With that said, did you know that the `belong_to` matcher can test this
|
||||||
|
validation for you? Instead of using `validate_presence_of`, try the following
|
||||||
|
instead:
|
||||||
|
|
||||||
|
it { should #{representation_of_belongs_to} }
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
|
message
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def secure_password_being_validated?
|
def secure_password_being_validated?
|
||||||
|
@ -197,7 +218,7 @@ module Shoulda
|
||||||
def allows_and_double_checks_value_of!(value)
|
def allows_and_double_checks_value_of!(value)
|
||||||
allows_value_of(value, @expected_message)
|
allows_value_of(value, @expected_message)
|
||||||
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
|
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
|
||||||
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
|
raise ActiveModel::CouldNotSetPasswordError.create(model)
|
||||||
end
|
end
|
||||||
|
|
||||||
def allows_original_or_typecast_value?(value)
|
def allows_original_or_typecast_value?(value)
|
||||||
|
@ -207,7 +228,7 @@ module Shoulda
|
||||||
def disallows_and_double_checks_value_of!(value)
|
def disallows_and_double_checks_value_of!(value)
|
||||||
disallows_value_of(value, @expected_message)
|
disallows_value_of(value, @expected_message)
|
||||||
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
|
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
|
||||||
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
|
raise ActiveModel::CouldNotSetPasswordError.create(model)
|
||||||
end
|
end
|
||||||
|
|
||||||
def disallows_original_or_typecast_value?(value)
|
def disallows_original_or_typecast_value?(value)
|
||||||
|
@ -218,25 +239,79 @@ module Shoulda
|
||||||
if collection?
|
if collection?
|
||||||
[Array.new]
|
[Array.new]
|
||||||
else
|
else
|
||||||
[''].tap do |disallowed|
|
values = []
|
||||||
if !expects_to_allow_nil?
|
|
||||||
disallowed << nil
|
if !association_being_validated?
|
||||||
end
|
values << ''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if !expects_to_allow_nil?
|
||||||
|
values << nil
|
||||||
|
end
|
||||||
|
|
||||||
|
values
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection?
|
def collection?
|
||||||
if reflection
|
if association_reflection
|
||||||
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
[:has_many, :has_and_belongs_to_many].include?(
|
||||||
|
association_reflection.macro,
|
||||||
|
)
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reflection
|
def should_add_footnote_about_belongs_to?
|
||||||
@subject.class.respond_to?(:reflect_on_association) &&
|
belongs_to_association_being_validated? &&
|
||||||
@subject.class.reflect_on_association(@attribute)
|
presence_validation_exists_on_attribute?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reason_for_existing_presence_validation
|
||||||
|
if belongs_to_association_configured_to_be_required?
|
||||||
|
"you've instructed your `belongs_to` association to add a " +
|
||||||
|
'presence validation to the attribute'
|
||||||
|
else
|
||||||
|
# assume ::ActiveRecord::Base.belongs_to_required_by_default == true
|
||||||
|
'ActiveRecord is configured to add a presence validation to all ' +
|
||||||
|
'`belongs_to` associations, and this includes yours'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def representation_of_belongs_to
|
||||||
|
'belong_to(:parent)'.tap do |str|
|
||||||
|
if association_reflection.options.include?(:optional)
|
||||||
|
str << ".optional(#{association_reflection.options[:optional]})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def belongs_to_association_configured_to_be_required?
|
||||||
|
association_reflection.options[:optional] == false ||
|
||||||
|
association_reflection.options[:required] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
def belongs_to_association_being_validated?
|
||||||
|
association_being_validated? &&
|
||||||
|
association_reflection.macro == :belongs_to
|
||||||
|
end
|
||||||
|
|
||||||
|
def association_being_validated?
|
||||||
|
!!association_reflection
|
||||||
|
end
|
||||||
|
|
||||||
|
def association_reflection
|
||||||
|
model.respond_to?(:reflect_on_association) &&
|
||||||
|
model.reflect_on_association(@attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def presence_validation_exists_on_attribute?
|
||||||
|
model._validators.include?(@attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def model
|
||||||
|
@subject.class
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@ module Shoulda
|
||||||
module Matchers
|
module Matchers
|
||||||
# @private
|
# @private
|
||||||
module WordWrap
|
module WordWrap
|
||||||
|
TERMINAL_WIDTH = 72
|
||||||
|
|
||||||
def word_wrap(document, options = {})
|
def word_wrap(document, options = {})
|
||||||
Document.new(document, options).wrap
|
Document.new(document, options).wrap
|
||||||
end
|
end
|
||||||
|
@ -112,7 +114,6 @@ module Shoulda
|
||||||
|
|
||||||
# @private
|
# @private
|
||||||
class Line
|
class Line
|
||||||
TERMINAL_WIDTH = 72
|
|
||||||
OFFSETS = { left: -1, right: +1 }
|
OFFSETS = { left: -1, right: +1 }
|
||||||
|
|
||||||
def initialize(line, indent: 0)
|
def initialize(line, indent: 0)
|
||||||
|
@ -171,7 +172,7 @@ module Shoulda
|
||||||
def wrap_line(line, direction: :left)
|
def wrap_line(line, direction: :left)
|
||||||
index = nil
|
index = nil
|
||||||
|
|
||||||
if line.length > TERMINAL_WIDTH
|
if line.length > Shoulda::Matchers::WordWrap::TERMINAL_WIDTH
|
||||||
index = determine_where_to_break_line(line, direction: :left)
|
index = determine_where_to_break_line(line, direction: :left)
|
||||||
|
|
||||||
if index == -1
|
if index == -1
|
||||||
|
@ -192,7 +193,7 @@ module Shoulda
|
||||||
|
|
||||||
def determine_where_to_break_line(line, args)
|
def determine_where_to_break_line(line, args)
|
||||||
direction = args.fetch(:direction)
|
direction = args.fetch(:direction)
|
||||||
index = TERMINAL_WIDTH
|
index = Shoulda::Matchers::WordWrap::TERMINAL_WIDTH
|
||||||
offset = OFFSETS.fetch(direction)
|
offset = OFFSETS.fetch(direction)
|
||||||
|
|
||||||
while line[index] !~ /\s/ && (0...line.length).cover?(index)
|
while line[index] !~ /\s/ && (0...line.length).cover?(index)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
module UnitTests
|
||||||
|
module ApplicationConfigurationHelpers
|
||||||
|
def with_belongs_to_as_required_by_default(&block)
|
||||||
|
configuring_application(
|
||||||
|
::ActiveRecord::Base,
|
||||||
|
:belongs_to_required_by_default,
|
||||||
|
true,
|
||||||
|
&block
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_belongs_to_as_optional_by_default(&block)
|
||||||
|
configuring_application(
|
||||||
|
::ActiveRecord::Base,
|
||||||
|
:belongs_to_required_by_default,
|
||||||
|
false,
|
||||||
|
&block
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def configuring_application(config, name, value)
|
||||||
|
previous_value = config.send(name)
|
||||||
|
config.send("#{name}=", value)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
config.send("#{name}=", previous_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,151 @@
|
||||||
|
module UnitTests
|
||||||
|
module Matchers
|
||||||
|
def match_against(object)
|
||||||
|
MatchAgainstMatcher.new(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
class MatchAgainstMatcher
|
||||||
|
DIVIDER = ('-' * Shoulda::Matchers::WordWrap::TERMINAL_WIDTH).freeze
|
||||||
|
|
||||||
|
attr_reader :failure_message, :failure_message_when_negated
|
||||||
|
|
||||||
|
def initialize(object)
|
||||||
|
@object = object
|
||||||
|
@failure_message = nil
|
||||||
|
@failure_message_when_negated = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_fail_with(message)
|
||||||
|
@message = message.strip
|
||||||
|
self
|
||||||
|
end
|
||||||
|
alias_method :or_fail_with, :and_fail_with
|
||||||
|
|
||||||
|
def matches?(generate_matcher)
|
||||||
|
@positive_matcher = generate_matcher.call
|
||||||
|
@negative_matcher = generate_matcher.call
|
||||||
|
|
||||||
|
if positive_matcher.matches?(object)
|
||||||
|
!message || matcher_fails_in_negative?
|
||||||
|
else
|
||||||
|
@failure_message = <<-MESSAGE
|
||||||
|
Expected the matcher to match in the positive, but it failed with this message:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{positive_matcher.failure_message}
|
||||||
|
#{DIVIDER}
|
||||||
|
MESSAGE
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def does_not_match?(generate_matcher)
|
||||||
|
@positive_matcher = generate_matcher.call
|
||||||
|
@negative_matcher = generate_matcher.call
|
||||||
|
|
||||||
|
if negative_matcher.does_not_match?(object)
|
||||||
|
!message || matcher_fails_in_positive?
|
||||||
|
else
|
||||||
|
@failure_message_when_negated = <<-MESSAGE
|
||||||
|
Expected the matcher to match in the negative, but it failed with this message:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{negative_matcher.failure_message_when_negated}
|
||||||
|
#{DIVIDER}
|
||||||
|
MESSAGE
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def supports_block_expectations?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :object, :message, :positive_matcher, :negative_matcher
|
||||||
|
|
||||||
|
def matcher_fails_in_negative?
|
||||||
|
if !negative_matcher.does_not_match?(object)
|
||||||
|
if message == negative_matcher.failure_message_when_negated.strip
|
||||||
|
true
|
||||||
|
else
|
||||||
|
diff_result = diff(
|
||||||
|
message,
|
||||||
|
negative_matcher.failure_message_when_negated.strip,
|
||||||
|
)
|
||||||
|
@failure_message = <<-MESSAGE
|
||||||
|
Expected the negative version of the matcher not to match and for the failure
|
||||||
|
message to be:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{message.chomp}
|
||||||
|
#{DIVIDER}
|
||||||
|
|
||||||
|
However, it was:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{negative_matcher.failure_message_when_negated}
|
||||||
|
#{DIVIDER}
|
||||||
|
|
||||||
|
Diff:
|
||||||
|
|
||||||
|
#{Shoulda::Matchers::Util.indent(diff_result, 2)}
|
||||||
|
MESSAGE
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@failure_message =
|
||||||
|
'Expected the negative version of the matcher not to match, ' +
|
||||||
|
'but it did.'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def matcher_fails_in_positive?
|
||||||
|
if !positive_matcher.matches?(object)
|
||||||
|
if message == positive_matcher.failure_message.strip
|
||||||
|
true
|
||||||
|
else
|
||||||
|
diff_result = diff(
|
||||||
|
message,
|
||||||
|
positive_matcher.failure_message.strip,
|
||||||
|
)
|
||||||
|
@failure_message_when_negated = <<-MESSAGE
|
||||||
|
Expected the positive version of the matcher not to match and for the failure
|
||||||
|
message to be:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{message.chomp}
|
||||||
|
#{DIVIDER}
|
||||||
|
|
||||||
|
However, it was:
|
||||||
|
|
||||||
|
#{DIVIDER}
|
||||||
|
#{positive_matcher.failure_message}
|
||||||
|
#{DIVIDER}
|
||||||
|
|
||||||
|
Diff:
|
||||||
|
|
||||||
|
#{Shoulda::Matchers::Util.indent(diff_result, 2)}
|
||||||
|
MESSAGE
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@failure_message_when_negated =
|
||||||
|
'Expected the positive version of the matcher not to match, ' +
|
||||||
|
'but it did.'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff(expected, actual)
|
||||||
|
differ.diff(expected, actual)[1..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def differ
|
||||||
|
@_differ ||= RSpec::Support::Differ.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
require 'unit_spec_helper'
|
require 'unit_spec_helper'
|
||||||
|
|
||||||
describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher, type: :model do
|
describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher, type: :model do
|
||||||
|
include UnitTests::ApplicationConfigurationHelpers
|
||||||
|
|
||||||
context 'a model with a presence validation' do
|
context 'a model with a presence validation' do
|
||||||
it 'accepts' do
|
it 'accepts' do
|
||||||
expect(validating_presence).to matcher
|
expect(validating_presence).to matcher
|
||||||
|
@ -176,7 +178,7 @@ could not be proved.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'a required has_and_belongs_to_many association' do
|
context 'a has_and_belongs_to_many association with a presence validation on it' do
|
||||||
it 'accepts' do
|
it 'accepts' do
|
||||||
expect(build_record_having_and_belonging_to_many).
|
expect(build_record_having_and_belonging_to_many).
|
||||||
to validate_presence_of(:children)
|
to validate_presence_of(:children)
|
||||||
|
@ -228,7 +230,7 @@ could not be proved.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'an optional has_and_belongs_to_many association' do
|
context 'a has_and_belongs_to_many association without a presence validation on it' do
|
||||||
before do
|
before do
|
||||||
define_model :child
|
define_model :child
|
||||||
@model = define_model :parent do
|
@model = define_model :parent do
|
||||||
|
@ -256,6 +258,300 @@ this could not be proved.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'against a belongs_to association' do
|
||||||
|
if active_record_supports_optional_for_associations?
|
||||||
|
context 'declared with optional: true' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
optional: true,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
optional: true,
|
||||||
|
validate_presence: false,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid, but it was valid instead.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'declared with optional: false' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
optional: false,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match, instructing the user to use belong_to instead' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
optional: false,
|
||||||
|
validate_presence: false,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid and to produce the validation error "can't be blank" on
|
||||||
|
:parent. The record was indeed invalid, but it produced these
|
||||||
|
validation errors instead:
|
||||||
|
|
||||||
|
* parent: ["must exist"]
|
||||||
|
|
||||||
|
You're getting this error because you've instructed your `belongs_to`
|
||||||
|
association to add a presence validation to the attribute. *This*
|
||||||
|
presence validation doesn't use "can't be blank", the usual validation
|
||||||
|
message, but "must exist" instead.
|
||||||
|
|
||||||
|
With that said, did you know that the `belong_to` matcher can test
|
||||||
|
this validation for you? Instead of using `validate_presence_of`, try
|
||||||
|
the following instead:
|
||||||
|
|
||||||
|
it { should belong_to(:parent).optional(false) }
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not declared with an optional or required option' do
|
||||||
|
context 'when belongs_to is configured to be required by default' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
with_belongs_to_as_required_by_default do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match, instructing the user to use belong_to instead' do
|
||||||
|
with_belongs_to_as_required_by_default do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
validate_presence: false,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid and to produce the validation error "can't be blank" on
|
||||||
|
:parent. The record was indeed invalid, but it produced these
|
||||||
|
validation errors instead:
|
||||||
|
|
||||||
|
* parent: ["must exist"]
|
||||||
|
|
||||||
|
You're getting this error because ActiveRecord is configured to add a
|
||||||
|
presence validation to all `belongs_to` associations, and this
|
||||||
|
includes yours. *This* presence validation doesn't use "can't be
|
||||||
|
blank", the usual validation message, but "must exist" instead.
|
||||||
|
|
||||||
|
With that said, did you know that the `belong_to` matcher can test
|
||||||
|
this validation for you? Instead of using `validate_presence_of`, try
|
||||||
|
the following instead:
|
||||||
|
|
||||||
|
it { should belong_to(:parent) }
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when belongs_to is configured to be optional by default' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
with_belongs_to_as_optional_by_default do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match' do
|
||||||
|
with_belongs_to_as_optional_by_default do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
validate_presence: false,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid, but it was valid instead.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
context 'declared with required: true' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
required: true,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'still matches' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
required: true,
|
||||||
|
validate_presence: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'declared with required: false' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
required: false,
|
||||||
|
validate_presence: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match' do
|
||||||
|
record = record_belonging_to(
|
||||||
|
:parent,
|
||||||
|
required: false,
|
||||||
|
validate_presence: false,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid, but it was valid instead.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not declared with a required option' do
|
||||||
|
context 'and an explicit presence validation is on the association' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_belonging_to(:parent, validate_presence: true)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.to match_against(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and an explicit presence validation is not on the association' do
|
||||||
|
it 'does not match' do
|
||||||
|
record = record_belonging_to(:parent, validate_presence: false)
|
||||||
|
|
||||||
|
expect { validate_presence_of(:parent) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected Child to validate that :parent cannot be empty/falsy, but this
|
||||||
|
could not be proved.
|
||||||
|
After setting :parent to ‹nil›, the matcher expected the Child to be
|
||||||
|
invalid, but it was valid instead.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_belonging_to(
|
||||||
|
attribute_name,
|
||||||
|
model_name: 'Child',
|
||||||
|
parent_model_name: 'Parent',
|
||||||
|
column_name: "#{attribute_name}_id",
|
||||||
|
validate_presence: false,
|
||||||
|
**association_options,
|
||||||
|
&block
|
||||||
|
)
|
||||||
|
define_model(parent_model_name)
|
||||||
|
|
||||||
|
child_model = define_model(model_name, column_name => :integer) do
|
||||||
|
belongs_to(attribute_name, **association_options)
|
||||||
|
|
||||||
|
if validate_presence
|
||||||
|
validates_presence_of(attribute_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if block
|
||||||
|
instance_eval(&block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
child_model.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "an i18n translation containing %{attribute} and %{model}" do
|
context "an i18n translation containing %{attribute} and %{model}" do
|
||||||
before do
|
before do
|
||||||
stub_translation(
|
stub_translation(
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'unit_spec_helper'
|
require 'unit_spec_helper'
|
||||||
|
|
||||||
describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
include UnitTests::ApplicationConfigurationHelpers
|
||||||
|
|
||||||
context 'belong_to' do
|
context 'belong_to' do
|
||||||
it 'accepts a good association with the default foreign key' do
|
it 'accepts a good association with the default foreign key' do
|
||||||
expect(belonging_to_parent).to belong_to(:parent)
|
expect(belonging_to_parent).to belong_to(:parent)
|
||||||
|
@ -284,7 +286,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
if active_record_supports_optional_for_associations?
|
if active_record_supports_optional_for_associations?
|
||||||
context 'when belongs_to is configured to be required by default' do
|
context 'when belongs_to is configured to be required by default' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(belonging_to_parent).to belong_to(:parent).required(true)
|
expect(belonging_to_parent).to belong_to(:parent).required(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -292,7 +294,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
|
||||||
context 'when belongs_to is not configured to be required by default' do
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
it 'fails with an appropriate message' do
|
it 'fails with an appropriate message' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
assertion = lambda do
|
assertion = lambda do
|
||||||
expect(belonging_to_parent).
|
expect(belonging_to_parent).
|
||||||
to belong_to(:parent).required(true)
|
to belong_to(:parent).required(true)
|
||||||
|
@ -333,7 +335,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
if active_record_supports_optional_for_associations?
|
if active_record_supports_optional_for_associations?
|
||||||
context 'when belongs_to is configured to be required by default' do
|
context 'when belongs_to is configured to be required by default' do
|
||||||
it 'fails with an appropriate message' do
|
it 'fails with an appropriate message' do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
assertion = lambda do
|
assertion = lambda do
|
||||||
expect(belonging_to_parent).
|
expect(belonging_to_parent).
|
||||||
to belong_to(:parent).required(false)
|
to belong_to(:parent).required(false)
|
||||||
|
@ -354,7 +356,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
|
||||||
context 'when belongs_to is not configured to be required by default' do
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
expect(belonging_to_parent).to belong_to(:parent).required(false)
|
expect(belonging_to_parent).to belong_to(:parent).required(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -370,7 +372,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
if active_record_supports_optional_for_associations?
|
if active_record_supports_optional_for_associations?
|
||||||
context 'when belongs_to is configured to be required by default' do
|
context 'when belongs_to is configured to be required by default' do
|
||||||
it 'fails with an appropriate message' do
|
it 'fails with an appropriate message' do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
assertion = lambda do
|
assertion = lambda do
|
||||||
expect(belonging_to_parent).
|
expect(belonging_to_parent).
|
||||||
to belong_to(:parent).optional
|
to belong_to(:parent).optional
|
||||||
|
@ -391,7 +393,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
|
||||||
context 'when belongs_to is not configured to be required by default' do
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
expect(belonging_to_parent).to belong_to(:parent).optional
|
expect(belonging_to_parent).to belong_to(:parent).optional
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -407,7 +409,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
if active_record_supports_optional_for_associations?
|
if active_record_supports_optional_for_associations?
|
||||||
context 'when belongs_to is configured to be required by default' do
|
context 'when belongs_to is configured to be required by default' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(belonging_to_parent).to belong_to(:parent)
|
expect(belonging_to_parent).to belong_to(:parent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -415,14 +417,14 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
|
||||||
context 'when belongs_to is not configured to be required by default' do
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
expect(belonging_to_parent).to belong_to(:parent)
|
expect(belonging_to_parent).to belong_to(:parent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'and a presence validation is on the attribute instead of using required: true' do
|
context 'and a presence validation is on the attribute instead of using required: true' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
record = belonging_to_parent do
|
record = belonging_to_parent do
|
||||||
validates_presence_of :parent
|
validates_presence_of :parent
|
||||||
end
|
end
|
||||||
|
@ -435,7 +437,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
context 'and a presence validation is on the attribute with a condition' do
|
context 'and a presence validation is on the attribute with a condition' do
|
||||||
context 'and the condition is true' do
|
context 'and the condition is true' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
child_model = create_child_model_belonging_to_parent do
|
child_model = create_child_model_belonging_to_parent do
|
||||||
attr_accessor :condition
|
attr_accessor :condition
|
||||||
validates_presence_of :parent, if: :condition
|
validates_presence_of :parent, if: :condition
|
||||||
|
@ -450,7 +452,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
|
|
||||||
context 'and the condition is false' do
|
context 'and the condition is false' do
|
||||||
it 'passes' do
|
it 'passes' do
|
||||||
configuring_default_belongs_to_requiredness(false) do
|
with_belongs_to_as_optional_by_default do
|
||||||
child_model = create_child_model_belonging_to_parent do
|
child_model = create_child_model_belonging_to_parent do
|
||||||
attr_accessor :condition
|
attr_accessor :condition
|
||||||
validates_presence_of :parent, if: :condition
|
validates_presence_of :parent, if: :condition
|
||||||
|
@ -630,7 +632,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
assertion = lambda do
|
assertion = lambda do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(model.new).to belong_to(:parent)
|
expect(model.new).to belong_to(:parent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -656,7 +658,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(model.new).
|
expect(model.new).
|
||||||
to belong_to(:parent).
|
to belong_to(:parent).
|
||||||
without_validating_presence
|
without_validating_presence
|
||||||
|
@ -677,7 +679,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
assertion = lambda do
|
assertion = lambda do
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(model.new).to belong_to(:parent).required
|
expect(model.new).to belong_to(:parent).required
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -703,7 +705,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
configuring_default_belongs_to_requiredness(true) do
|
with_belongs_to_as_required_by_default do
|
||||||
expect(model.new).
|
expect(model.new).
|
||||||
to belong_to(:parent).
|
to belong_to(:parent).
|
||||||
required.
|
required.
|
||||||
|
@ -1765,21 +1767,4 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
[:destroy, :delete, :nullify, :restrict]
|
[:destroy, :delete, :nullify, :restrict]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def configuring_default_belongs_to_requiredness(value, &block)
|
|
||||||
configuring_application(
|
|
||||||
ActiveRecord::Base,
|
|
||||||
:belongs_to_required_by_default,
|
|
||||||
value,
|
|
||||||
&block
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def configuring_application(config, name, value)
|
|
||||||
previous_value = config.send(name)
|
|
||||||
config.send("#{name}=", value)
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
config.send("#{name}=", previous_value)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue