belongs_to: Change default req/opt to match global default
In Rails 5, belongs_to associations default to `required: true`. This is configurable by setting `ActiveRecord::Base.belongs_to_required_by_default` to true. In new Rails 5 apps, this is set to true, because the Rails generator will add an initializer with the following line in it: config.active_record.belongs_to_required_by_default = true However, for Rails apps that have been upgraded from 4 to 5, this initializer may not be present, and in that case, that setting will not be set, and `belong_to` associations will not default to `required: true`. This means that under Rails 5, our `belong_to` matcher cannot always default to applying the `required` qualifier; it must abide by the `belongs_to_required_by_default` setting in doing so.
This commit is contained in:
parent
e8330fd448
commit
cd96089a56
|
@ -253,8 +253,6 @@ module Shoulda
|
||||||
# should belong_to(:organization).inverse_of(:employees)
|
# should belong_to(:organization).inverse_of(:employees)
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# @return [AssociationMatcher]
|
|
||||||
#
|
|
||||||
# ##### required
|
# ##### required
|
||||||
#
|
#
|
||||||
# Use `required` to assert that the association is not allowed to be nil.
|
# Use `required` to assert that the association is not allowed to be nil.
|
||||||
|
@ -274,8 +272,6 @@ module Shoulda
|
||||||
# should belong_to(:organization).required
|
# should belong_to(:organization).required
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# @return [AssociationMatcher]
|
|
||||||
#
|
|
||||||
# ##### optional
|
# ##### optional
|
||||||
#
|
#
|
||||||
# Use `optional` to assert that the association is allowed to be nil.
|
# Use `optional` to assert that the association is allowed to be nil.
|
||||||
|
@ -954,7 +950,7 @@ module Shoulda
|
||||||
@missing = ''
|
@missing = ''
|
||||||
|
|
||||||
if macro == :belongs_to
|
if macro == :belongs_to
|
||||||
if RailsShim.active_record_gte_5?
|
if RailsShim.active_record_gte_5? && belongs_to_required_by_default?
|
||||||
required
|
required
|
||||||
else
|
else
|
||||||
optional
|
optional
|
||||||
|
@ -1051,12 +1047,12 @@ module Shoulda
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def optional(optional = true)
|
def optional
|
||||||
remove_submatcher(AssociationMatchers::RequiredMatcher)
|
remove_submatcher(AssociationMatchers::RequiredMatcher)
|
||||||
add_submatcher(
|
add_submatcher(
|
||||||
AssociationMatchers::OptionalMatcher,
|
AssociationMatchers::OptionalMatcher,
|
||||||
name,
|
name,
|
||||||
optional,
|
true,
|
||||||
)
|
)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
@ -1156,8 +1152,8 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
def failing_submatchers
|
def failing_submatchers
|
||||||
@failing_submatchers ||= submatchers.reject do |matcher|
|
@failing_submatchers ||= submatchers.select do |matcher|
|
||||||
matcher.matches?(subject)
|
!matcher.matches?(subject)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1339,6 +1335,10 @@ module Shoulda
|
||||||
def submatchers_match?
|
def submatchers_match?
|
||||||
failing_submatchers.empty?
|
failing_submatchers.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def belongs_to_required_by_default?
|
||||||
|
::ActiveRecord::Base.belongs_to_required_by_default
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,37 +7,36 @@ module Shoulda
|
||||||
attr_reader :missing_option
|
attr_reader :missing_option
|
||||||
|
|
||||||
def initialize(attribute_name, optional)
|
def initialize(attribute_name, optional)
|
||||||
@attribute_name = attribute_name
|
@optional = optional
|
||||||
|
@submatcher = ActiveModel::AllowValueMatcher.new(nil).
|
||||||
|
for(attribute_name)
|
||||||
@missing_option = ''
|
@missing_option = ''
|
||||||
@submatcher = submatcher_class_for(optional).new(nil).
|
|
||||||
for(attribute_name).
|
|
||||||
with_message(:required)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def description
|
||||||
'required: true'
|
"optional: #{optional}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches?(subject)
|
def matches?(subject)
|
||||||
if submatcher.matches?(subject)
|
if submatcher_passes?(subject)
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
@missing_option =
|
@missing_option =
|
||||||
'the association should have been defined ' +
|
'the association should have been defined ' +
|
||||||
'with `optional: true`, but was not'
|
"with `optional: #{optional}`, but was not"
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :subject, :submatcher
|
attr_reader :optional, :submatcher
|
||||||
|
|
||||||
def submatcher_class_for(optional)
|
def submatcher_passes?(subject)
|
||||||
if optional
|
if optional
|
||||||
ActiveModel::AllowValueMatcher
|
submatcher.matches?(subject)
|
||||||
else
|
else
|
||||||
ActiveModel::DisallowValueMatcher
|
submatcher.does_not_match?(subject)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,36 +7,37 @@ module Shoulda
|
||||||
attr_reader :missing_option
|
attr_reader :missing_option
|
||||||
|
|
||||||
def initialize(attribute_name, required)
|
def initialize(attribute_name, required)
|
||||||
@missing_option = ''
|
@required = required
|
||||||
@submatcher = submatcher_class_for(required).new(nil).
|
@submatcher = ActiveModel::DisallowValueMatcher.new(nil).
|
||||||
for(attribute_name).
|
for(attribute_name).
|
||||||
with_message(validation_message_key)
|
with_message(validation_message_key)
|
||||||
|
@missing_option = ''
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def description
|
||||||
'required: true'
|
"required: #{required}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches?(subject)
|
def matches?(subject)
|
||||||
if submatcher.matches?(subject)
|
if submatcher_passes?(subject)
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
@missing_option =
|
@missing_option =
|
||||||
'the association should have been defined ' +
|
'the association should have been defined ' +
|
||||||
'with `required: true`, but was not'
|
"with `required: #{required}`, but was not"
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :subject, :submatcher
|
attr_reader :required, :submatcher
|
||||||
|
|
||||||
def submatcher_class_for(required)
|
def submatcher_passes?(subject)
|
||||||
if required
|
if required
|
||||||
ActiveModel::DisallowValueMatcher
|
submatcher.matches?(subject)
|
||||||
else
|
else
|
||||||
ActiveModel::AllowValueMatcher
|
submatcher.does_not_match?(subject)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,14 @@ module UnitTests
|
||||||
example_group.include(self)
|
example_group.include(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_message(message)
|
def format_message(message, one_line: false)
|
||||||
word_wrap(message.strip_heredoc.strip)
|
stripped_message = message.strip_heredoc.strip
|
||||||
|
|
||||||
|
if one_line
|
||||||
|
stripped_message.tr("\n", " ").squeeze(" ")
|
||||||
|
else
|
||||||
|
word_wrap(stripped_message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -279,62 +279,258 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'given an association with neither :required nor :optional specified' do
|
context 'given the association is neither configured to be required nor optional' do
|
||||||
|
context 'when qualified with required(true)' do
|
||||||
|
if active_record_supports_required_for_associations?
|
||||||
|
context 'when belongs_to is configured to be required by default' do
|
||||||
|
it 'passes' do
|
||||||
|
configuring_default_belongs_to_requiredness(true) do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).required(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
configuring_default_belongs_to_requiredness(false) do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent).
|
||||||
|
to belong_to(:parent).required(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent
|
||||||
|
(the association should have been defined with `required:
|
||||||
|
true`, but was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).required(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `required: true`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with required(false)' do
|
||||||
|
if active_record_supports_required_for_associations?
|
||||||
|
context 'when belongs_to is configured to be required by default' do
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
configuring_default_belongs_to_requiredness(true) do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent).
|
||||||
|
to belong_to(:parent).required(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent
|
||||||
|
(the association should have been defined with `required:
|
||||||
|
false`, but was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
|
it 'passes' do
|
||||||
|
configuring_default_belongs_to_requiredness(false) do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).required(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
it 'passes' do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).required(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with optional' do
|
||||||
|
if active_record_supports_required_for_associations?
|
||||||
|
context 'when belongs_to is configured to be required by default' do
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
configuring_default_belongs_to_requiredness(true) do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent).
|
||||||
|
to belong_to(:parent).optional
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `optional: true`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
|
it 'passes' do
|
||||||
|
configuring_default_belongs_to_requiredness(false) do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).optional
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
it 'passes' do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent).optional
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if active_record_supports_required_for_associations?
|
if active_record_supports_required_for_associations?
|
||||||
it 'assumes it is required' do
|
context 'when qualified with nothing' do
|
||||||
expect(belonging_to_parent).to belong_to(:parent).required
|
context 'when belongs_to is configured to be required by default' do
|
||||||
end
|
it 'passes' do
|
||||||
else
|
configuring_default_belongs_to_requiredness(true) do
|
||||||
it 'assumes it is optional' do
|
expect(belonging_to_parent).to belong_to(:parent)
|
||||||
expect(belonging_to_parent).to belong_to(:parent).optional
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when belongs_to is not configured to be required by default' do
|
||||||
|
it 'passes' do
|
||||||
|
configuring_default_belongs_to_requiredness(false) do
|
||||||
|
expect(belonging_to_parent).to belong_to(:parent)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'given an association with a matching :required option' do
|
context 'given the association is configured with required: true' do
|
||||||
it 'passes' do
|
context 'when qualified with required(true)' do
|
||||||
expect(belonging_to_parent(required: true)).
|
it 'passes' do
|
||||||
to belong_to(:parent).required
|
expect(belonging_to_parent(required: true)).
|
||||||
end
|
to belong_to(:parent).required(true)
|
||||||
end
|
|
||||||
|
|
||||||
context 'given an association with a non-matching :required option' do
|
|
||||||
it 'fails with an appropriate message' do
|
|
||||||
assertion = lambda do
|
|
||||||
expect(belonging_to_parent(required: false)).
|
|
||||||
to belong_to(:parent).required
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
message =
|
context 'when qualified with required(false)' do
|
||||||
'Expected Child to have a belongs_to association called parent ' +
|
it 'passes' do
|
||||||
'(the association should have been defined with `required: true`, ' +
|
assertion = lambda do
|
||||||
'but was not)'
|
expect(belonging_to_parent(required: true)).
|
||||||
|
to belong_to(:parent).required(false)
|
||||||
|
end
|
||||||
|
|
||||||
expect(&assertion).to fail_with_message(message)
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `required: false`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with optional' do
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent(required: true)).
|
||||||
|
to belong_to(:parent).optional
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `optional: true`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with nothing' do
|
||||||
|
if active_record_supports_required_for_associations?
|
||||||
|
it 'passes' do
|
||||||
|
expect(belonging_to_parent(required: true)).
|
||||||
|
to belong_to(:parent)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
it 'fails with an appropriate message' do
|
||||||
|
assertion = lambda do
|
||||||
|
expect(belonging_to_parent(required: true)).
|
||||||
|
to belong_to(:parent)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `optional: true`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if active_record_supports_required_for_associations?
|
if active_record_supports_required_for_associations?
|
||||||
context 'given an association with a matching :optional option' do
|
context 'given the association is configured as optional: true' do
|
||||||
it 'passes' do
|
context 'when qualified with required(true)' do
|
||||||
expect(belonging_to_parent(optional: true)).
|
it 'fails with an appropriate message' do
|
||||||
to belong_to(:parent).optional
|
assertion = lambda do
|
||||||
end
|
expect(belonging_to_parent(optional: true)).
|
||||||
end
|
to belong_to(:parent).required(true)
|
||||||
|
end
|
||||||
|
|
||||||
context 'given an association with a non-matching :optional option' do
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
it 'fails with an appropriate message' do
|
Expected Child to have a belongs_to association called parent (the
|
||||||
assertion = lambda do
|
association should have been defined with `required: true`, but
|
||||||
expect(belonging_to_parent(optional: false)).
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with required(false)' do
|
||||||
|
it 'passes' do
|
||||||
|
expect(belonging_to_parent(optional: true)).
|
||||||
|
to belong_to(:parent).required(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when qualified with optional' do
|
||||||
|
it 'passes' do
|
||||||
|
expect(belonging_to_parent(optional: true)).
|
||||||
to belong_to(:parent).optional
|
to belong_to(:parent).optional
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
message =
|
context 'when qualified with nothing' do
|
||||||
'Expected Child to have a belongs_to association called parent ' +
|
it 'fails with an appropriate message' do
|
||||||
'(the association should have been defined with `optional: ' +
|
assertion = lambda do
|
||||||
'true`, but was not)'
|
expect(belonging_to_parent(optional: true)).
|
||||||
|
to belong_to(:parent)
|
||||||
|
end
|
||||||
|
|
||||||
expect(&assertion).to fail_with_message(message)
|
message = format_message(<<-MESSAGE, one_line: true)
|
||||||
|
Expected Child to have a belongs_to association called parent (the
|
||||||
|
association should have been defined with `required: true`, but
|
||||||
|
was not)
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1342,4 +1538,21 @@ 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