1107 lines
33 KiB
Ruby
1107 lines
33 KiB
Ruby
require 'unit_spec_helper'
|
||
|
||
describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :model do
|
||
shared_context 'for a generic attribute' do
|
||
context 'against an integer attribute' do
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: (1..5).to_a,
|
||
zero: 0,
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_INTEGER
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: 1..5,
|
||
zero: 0
|
||
|
||
context 'when attribute validates a range of values via custom validation' do
|
||
it 'matches ensuring the correct range and messages' do
|
||
expect_to_match_ensuring_range_and_messages(2..5, 2, 5)
|
||
expect_to_match_ensuring_range_and_messages(2...5, 2, 4)
|
||
end
|
||
end
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :integer, value: 1),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :integer, default_value: 1)
|
||
end
|
||
end
|
||
|
||
context 'against an attribute with a specific column limit' do
|
||
it 'does not raise an exception when attempting to use the matcher' do
|
||
possible_values = (1..5).to_a
|
||
builder = build_object_allowing(possible_values)
|
||
assertion = -> { expect_to_match_on_values(builder, possible_values) }
|
||
expect(&assertion).not_to raise_error
|
||
end
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(
|
||
column_type: :integer,
|
||
column_options: { limit: 2 },
|
||
value: 1,
|
||
),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def expect_to_match_on_values(builder, values, &block)
|
||
expect_to_match_in_array(builder, values, &block)
|
||
end
|
||
end
|
||
|
||
context 'against a float attribute' do
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: [1.0, 2.0, 3.0, 4.0, 5.0],
|
||
zero: 0.0,
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_INTEGER
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: 1.0..5.0,
|
||
zero: 0.0
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :float, value: 1.0),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :float, default_value: 1.0)
|
||
end
|
||
end
|
||
|
||
context 'against a decimal attribute' do
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: [1.0, 2.0, 3.0, 4.0, 5.0].map { |number|
|
||
BigDecimal(number.to_s)
|
||
},
|
||
zero: BigDecimal('0.0'),
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DECIMAL
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: BigDecimal('1.0')..BigDecimal('5.0'),
|
||
zero: BigDecimal('0.0')
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :decimal, value: BigDecimal('1.0')),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(
|
||
column_type: :decimal,
|
||
default_value: BigDecimal('1.0'),
|
||
)
|
||
end
|
||
end
|
||
|
||
context 'against a date attribute' do
|
||
today = Date.today
|
||
|
||
define_method(:today) { today }
|
||
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: (1..5).map { |n| today + n },
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATE
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: (today..today + 5)
|
||
|
||
define_method :build_object do |options = {}, &block|
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :date, value: today),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :date, default_value: today)
|
||
end
|
||
end
|
||
|
||
context 'against a datetime attribute' do
|
||
now = DateTime.now
|
||
|
||
define_method(:now) { now }
|
||
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: (1..5).map { |n| now + n },
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATETIME
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: (now..now + 5)
|
||
|
||
define_method :build_object do |options = {}, &block|
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :datetime, value: now),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :datetime, default_value: now)
|
||
end
|
||
end
|
||
|
||
context 'against a time attribute' do
|
||
default_time = Time.zone.local(2000, 1, 1)
|
||
|
||
define_method(:default_time) { default_time }
|
||
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: (1..3).map { |hour| default_time.change(hour: hour) }
|
||
|
||
it_behaves_like 'it supports in_range', possible_values: (
|
||
default_time.change(hour: 1)..default_time.change(hour: 3)
|
||
)
|
||
|
||
define_method :build_object do |options = {}, &block|
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :time, value: default_time),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :time, default_value: default_time)
|
||
end
|
||
end
|
||
|
||
context 'against a string attribute' do
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: %w(foo bar baz),
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_STRING
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :string),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + %w(qux)
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :string)
|
||
end
|
||
end
|
||
end
|
||
|
||
shared_examples_for 'it supports allow_nil' do |args|
|
||
valid_values = args.fetch(:valid_values)
|
||
|
||
it 'matches when the validation specifies allow_nil' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { allow_nil: true },
|
||
)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_nil
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
|
||
it 'allows other qualifiers to be chained afterward' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
using_matcher = -> do
|
||
matcher = validate_inclusion_of(builder.attribute)
|
||
matcher.allow_nil.allow_blank
|
||
end
|
||
|
||
expect(&using_matcher).not_to raise_error
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(validation_options: { allow_nil: true })
|
||
end
|
||
|
||
def configure_validation_matcher(matcher)
|
||
super(matcher).allow_nil
|
||
end
|
||
end
|
||
|
||
shared_examples_for 'it supports allow_blank' do |args|
|
||
valid_values = args.fetch(:valid_values)
|
||
|
||
it 'matches when the validation specifies allow_blank' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { allow_blank: true },
|
||
)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_blank
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(validation_options: { allow_blank: true })
|
||
end
|
||
|
||
def configure_validation_matcher(matcher)
|
||
super(matcher).allow_blank
|
||
end
|
||
end
|
||
|
||
shared_examples_for 'it supports with_message' do |args|
|
||
valid_values = args.fetch(:valid_values)
|
||
|
||
context 'given a string' do
|
||
it 'matches when validation uses given message' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: 'a message' },
|
||
)
|
||
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message('a message')
|
||
end
|
||
end
|
||
|
||
it 'matches when validation uses given message that has an interpolated variable' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: '%{value} is not included' },
|
||
)
|
||
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message(/ is not included\Z/)
|
||
end
|
||
end
|
||
|
||
it 'does not match when validation uses the default message instead of given message' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message('a message')
|
||
end
|
||
end
|
||
|
||
it 'does not match when validation uses a message but it is not same as given' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: 'a different message' },
|
||
)
|
||
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message('a message')
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'given a regex' do
|
||
it 'matches when validation uses a message that matches the regex' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: 'this is a message' },
|
||
)
|
||
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message(/a message/)
|
||
end
|
||
end
|
||
|
||
it 'does not match when validation uses the default message instead of given message' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message(/a message/)
|
||
end
|
||
end
|
||
|
||
it 'does not match when validation uses a message but it does not match regex' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: 'a different message' },
|
||
)
|
||
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message(/a message/)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'given nil' do
|
||
it 'is as if with_message had never been called' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.with_message(nil)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'not having been qualified' do
|
||
it 'matches when the validation uses the default message' do
|
||
builder = build_object_allowing(valid_values)
|
||
expect_to_match_on_values(builder, valid_values)
|
||
end
|
||
|
||
it 'matches when the validation uses the default message and it is overridden in i18n' do
|
||
stubbing_translations(
|
||
'errors.messages.inclusion' => 'some message',
|
||
'errors.models.person.attributes.name.inclusion' => 'a different message',
|
||
) do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
model_name: 'Person',
|
||
attribute_name: :name,
|
||
)
|
||
expect_to_match_on_values(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
it 'does not match when the validation is configured with an overridden message' do
|
||
builder = build_object_allowing(
|
||
valid_values,
|
||
validation_options: { message: 'some message' },
|
||
)
|
||
|
||
expect_not_to_match_on_values(builder, valid_values)
|
||
end
|
||
end
|
||
end
|
||
|
||
shared_examples_for 'it supports in_array' do |args|
|
||
possible_values = args.fetch(:possible_values)
|
||
zero = args[:zero]
|
||
reserved_outside_value = args[:reserved_outside_value]
|
||
|
||
define_method(:valid_values) { args.fetch(:possible_values) }
|
||
|
||
it_behaves_like 'it supports allow_nil', valid_values: possible_values
|
||
it_behaves_like 'it supports allow_blank', valid_values: possible_values
|
||
it_behaves_like 'it supports with_message', valid_values: possible_values
|
||
|
||
it_supports(
|
||
'ignoring_interference_by_writer',
|
||
tests: {
|
||
reject_if_qualified_but_changing_value_interferes: {
|
||
attribute_name: :attr,
|
||
changing_values_with: :next_value,
|
||
expected_message_includes: <<-MESSAGE.strip,
|
||
As indicated in the message above, :attr seems to be changing certain
|
||
values as they are set, and this could have something to do with why
|
||
this test is failing. If you've overridden the writer method for this
|
||
attribute, then you may need to change it to make this test pass, or
|
||
do something else entirely.
|
||
MESSAGE
|
||
},
|
||
},
|
||
)
|
||
|
||
context 'when the record has no validations' do
|
||
it 'passes when used in the negative' do
|
||
builder = build_object
|
||
expect_not_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
it 'fails when used in the positive with an appropriate failure message' do
|
||
builder = build_object
|
||
|
||
assertion = lambda do
|
||
expect_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
expect(&assertion).to fail
|
||
end
|
||
end
|
||
|
||
it 'matches given the same array of valid values' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
it 'matches given a subset of the valid values' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_to_match_on_values(builder, possible_values[1..])
|
||
end
|
||
|
||
if zero
|
||
it 'matches when one of the given values is a zero' do
|
||
valid_values = possible_values + [zero]
|
||
builder = build_object_allowing(valid_values)
|
||
expect_to_match_on_values(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
it 'does not match when one of the given values is invalid' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_not_to_match_on_values(
|
||
builder,
|
||
add_outside_value_to(possible_values),
|
||
)
|
||
end
|
||
|
||
if reserved_outside_value
|
||
it 'raises an error when valid and given value is our test outside value' do
|
||
error_class = Shoulda::Matchers::ActiveModel::CouldNotDetermineValueOutsideOfArray
|
||
builder = build_object_allowing([reserved_outside_value])
|
||
|
||
expect { expect_to_match_on_values(builder, [reserved_outside_value]) }.
|
||
to raise_error(error_class)
|
||
end
|
||
end
|
||
|
||
it 'fails when used in the negative' do
|
||
builder = build_object_allowing(possible_values)
|
||
|
||
assertion = lambda do
|
||
expect_not_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
expect(&assertion).to fail
|
||
end
|
||
|
||
it_behaves_like 'it supports allow_nil', valid_values: possible_values do
|
||
it 'does not match when the validation does not specify allow_nil' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_nil
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
it_behaves_like 'it supports allow_blank', valid_values: possible_values do
|
||
it 'does not match when the validation does not specify allow_blank' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_blank
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
it_behaves_like 'it supports with_message', valid_values: possible_values
|
||
|
||
if active_model_3_2?
|
||
context '+ strict' do
|
||
context 'when the validation specifies strict' do
|
||
it 'matches when the given values match the valid values' do
|
||
builder = build_object_allowing(
|
||
possible_values,
|
||
validation_options: { strict: true },
|
||
)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, possible_values) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
|
||
it 'does not match when the given values do not match the valid values' do
|
||
builder = build_object_allowing(
|
||
possible_values,
|
||
validation_options: { strict: true },
|
||
)
|
||
|
||
values = add_outside_value_to(possible_values)
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, values) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
context 'when the validation does not specify strict' do
|
||
it 'does not match' do
|
||
builder = build_object_allowing(possible_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, possible_values) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def expect_to_match_on_values(builder, values, &block)
|
||
expect_to_match_in_array(builder, values, &block)
|
||
end
|
||
|
||
def expect_not_to_match_on_values(builder, values, &block)
|
||
expect_not_to_match_in_array(builder, values, &block)
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(validation_options: { in: valid_values })
|
||
end
|
||
|
||
def configure_validation_matcher(matcher)
|
||
super(matcher).in_array(valid_values)
|
||
end
|
||
end
|
||
|
||
shared_examples_for 'it supports in_range' do |args|
|
||
possible_values = args[:possible_values]
|
||
|
||
define_method(:valid_values) { args.fetch(:possible_values) }
|
||
|
||
it_behaves_like 'it supports allow_nil', valid_values: possible_values
|
||
it_behaves_like 'it supports allow_blank', valid_values: possible_values
|
||
it_behaves_like 'it supports with_message', valid_values: possible_values
|
||
|
||
it_supports(
|
||
'ignoring_interference_by_writer',
|
||
tests: {
|
||
reject_if_qualified_but_changing_value_interferes: {
|
||
attribute_name: :attr,
|
||
changing_values_with: :next_value,
|
||
expected_message_includes: <<-MESSAGE.strip,
|
||
As indicated in the message above, :attr seems to be changing certain
|
||
values as they are set, and this could have something to do with why
|
||
this test is failing. If you've overridden the writer method for this
|
||
attribute, then you may need to change it to make this test pass, or
|
||
do something else entirely.
|
||
MESSAGE
|
||
},
|
||
},
|
||
)
|
||
|
||
it 'does not match a record with no validations' do
|
||
builder = build_object
|
||
expect_not_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
it 'matches given a range that exactly matches the valid range' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_to_match_on_values(builder, possible_values)
|
||
end
|
||
|
||
it 'does not match given a range whose start value falls outside valid range' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_not_to_match_on_values(
|
||
builder,
|
||
Range.new(possible_values.first - 1, possible_values.last),
|
||
)
|
||
end
|
||
|
||
it 'does not match given a range whose start value falls inside valid range' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_not_to_match_on_values(
|
||
builder,
|
||
Range.new(possible_values.first + 1, possible_values.last),
|
||
)
|
||
end
|
||
|
||
it 'does not match given a range whose end value falls inside valid range' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_not_to_match_on_values(
|
||
builder,
|
||
Range.new(possible_values.first, possible_values.last - 1),
|
||
)
|
||
end
|
||
|
||
it 'does not match given a range whose end value falls outside valid range' do
|
||
builder = build_object_allowing(possible_values)
|
||
expect_not_to_match_on_values(
|
||
builder,
|
||
Range.new(possible_values.first, possible_values.last + 1),
|
||
)
|
||
end
|
||
|
||
it_behaves_like 'it supports allow_nil', valid_values: possible_values do
|
||
it 'matches when the validation does not specify allow_nil' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_nil
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
it_behaves_like 'it supports allow_blank', valid_values: possible_values do
|
||
it 'matches when the validation does not specify allow_blank' do
|
||
builder = build_object_allowing(valid_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, valid_values) do |matcher|
|
||
matcher.allow_blank
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
it_behaves_like 'it supports with_message', valid_values: possible_values
|
||
|
||
if active_model_3_2?
|
||
context '+ strict' do
|
||
context 'when the validation specifies strict' do
|
||
it 'matches when the given range matches the range in the validation' do
|
||
builder = build_object_allowing(
|
||
possible_values,
|
||
validation_options: { strict: true },
|
||
)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_to_match_on_values(builder, possible_values) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
|
||
it 'matches when the given range does not match the range in the validation' do
|
||
builder = build_object_allowing(
|
||
possible_values,
|
||
validation_options: { strict: true },
|
||
)
|
||
|
||
range = Range.new(possible_values.first, possible_values.last + 1)
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, range) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
|
||
context 'when the validation does not specify strict' do
|
||
it 'does not match' do
|
||
builder = build_object_allowing(possible_values)
|
||
|
||
# rubocop:disable Style/SymbolProc
|
||
expect_not_to_match_on_values(builder, possible_values) do |matcher|
|
||
matcher.strict
|
||
end
|
||
# rubocop:enable Style/SymbolProc
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def expect_to_match_on_values(builder, range, &block)
|
||
expect_to_match_in_range(builder, range, &block)
|
||
end
|
||
|
||
def expect_not_to_match_on_values(builder, range, &block)
|
||
expect_not_to_match_in_range(builder, range, &block)
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(validation_options: { in: valid_values })
|
||
end
|
||
|
||
def configure_validation_matcher(matcher)
|
||
super(matcher).in_range(valid_values)
|
||
end
|
||
end
|
||
|
||
shared_context 'against a boolean attribute for true and false' do
|
||
context 'when ensuring inclusion of true' do
|
||
it 'matches' do
|
||
valid_values = [true]
|
||
builder = build_object_allowing(valid_values)
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
context 'when ensuring inclusion of false' do
|
||
it 'matches' do
|
||
valid_values = [false]
|
||
builder = build_object_allowing(valid_values)
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
context 'when ensuring inclusion of true and false' do
|
||
it 'matches' do
|
||
valid_values = [true, false]
|
||
builder = build_object_allowing(valid_values)
|
||
silence_stderr do
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
[[false, true], [true, false]].each do |booleans|
|
||
it 'prints a warning' do
|
||
valid_values = booleans
|
||
builder = build_object_allowing(valid_values)
|
||
message =
|
||
'You are using `validate_inclusion_of` to assert that a boolean '\
|
||
'column allows boolean values and disallows non-boolean ones'
|
||
|
||
stderr = capture(:stderr) do
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
|
||
expect(stderr.gsub(/\n+/, ' ')).to include(message)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'for a database column' do
|
||
include_context 'for a generic attribute'
|
||
|
||
context 'against a timestamp column' do
|
||
now = DateTime.now
|
||
|
||
define_method(:now) { now }
|
||
|
||
it_behaves_like 'it supports in_array',
|
||
possible_values: (1..5).map { |n| now + n },
|
||
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATETIME
|
||
|
||
it_behaves_like 'it supports in_range',
|
||
possible_values: (now..now + 5)
|
||
|
||
define_method :build_object do |options = {}, &block|
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :timestamp, value: now),
|
||
&block
|
||
)
|
||
end
|
||
|
||
def add_outside_value_to(values)
|
||
values + [values.last + 1]
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(column_type: :timestamp, default_value: now)
|
||
end
|
||
end
|
||
|
||
context 'against a boolean attribute' do
|
||
context 'which is nullable' do
|
||
include_context 'against a boolean attribute for true and false'
|
||
|
||
context 'when ensuring inclusion of nil' do
|
||
it 'matches' do
|
||
valid_values = [nil]
|
||
builder = build_object_allowing(valid_values)
|
||
silence_stderr do
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
end
|
||
|
||
it 'prints a warning' do
|
||
valid_values = [nil]
|
||
builder = build_object_allowing(valid_values)
|
||
message =
|
||
'You are using `validate_inclusion_of` to assert that a '\
|
||
'boolean column allows nil'
|
||
|
||
stderr = capture(:stderr) do
|
||
expect_to_match_in_array(builder, valid_values)
|
||
end
|
||
|
||
expect(stderr.gsub(/\n+/, ' ')).to include(message)
|
||
end
|
||
end
|
||
|
||
def build_object(**options, &block)
|
||
super(
|
||
options.merge(column_options: { null: true }, value: true),
|
||
&block
|
||
)
|
||
end
|
||
end
|
||
|
||
context 'which is non-nullable' do
|
||
include_context 'against a boolean attribute for true and false'
|
||
|
||
context 'when ensuring inclusion of nil' do
|
||
it 'raises a specific error' do
|
||
valid_values = [nil]
|
||
builder = build_object_allowing(valid_values)
|
||
error_class = Shoulda::Matchers::ActiveModel::NonNullableBooleanError
|
||
expect {
|
||
expect_to_match_in_array(builder, valid_values)
|
||
}.to raise_error(error_class)
|
||
end
|
||
end
|
||
|
||
def build_object(**options, &block)
|
||
super(options.merge(column_options: { null: false }), &block)
|
||
end
|
||
end
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(
|
||
options.merge(column_type: :boolean),
|
||
&block
|
||
)
|
||
end
|
||
end
|
||
|
||
def define_simple_model(
|
||
model_name: 'Example',
|
||
attribute_name: :attr,
|
||
column_options: {},
|
||
&block
|
||
)
|
||
define_model(model_name, attribute_name => column_options, &block)
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(model_creator: :active_record)
|
||
end
|
||
end
|
||
|
||
context 'for a plain Ruby attribute' do
|
||
include_context 'for a generic attribute'
|
||
|
||
context 'against a boolean attribute (designated by true)' do
|
||
include_context 'against a boolean attribute for true and false'
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(options.merge(value: true), &block)
|
||
end
|
||
end
|
||
|
||
context 'against a boolean attribute (designated by false)' do
|
||
include_context 'against a boolean attribute for true and false'
|
||
|
||
def build_object(**options, &block)
|
||
build_object_with_generic_attribute(options.merge(value: false), &block)
|
||
end
|
||
end
|
||
|
||
def define_simple_model(
|
||
model_name: 'Example',
|
||
attribute_name: :attr,
|
||
**_rest,
|
||
&block
|
||
)
|
||
define_active_model_class(model_name, accessors: [attribute_name], &block)
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(model_creator: :active_model)
|
||
end
|
||
end
|
||
|
||
describe '#description' do
|
||
context 'given an array of values' do
|
||
context 'when there is one value' do
|
||
it 'returns the correct string' do
|
||
matcher = validate_inclusion_of(:attr).in_array([true])
|
||
|
||
expect(matcher.description).to eq(
|
||
'validate that :attr is ‹true›',
|
||
)
|
||
end
|
||
end
|
||
|
||
context 'when there are two values' do
|
||
it 'returns the correct string' do
|
||
matcher = validate_inclusion_of(:attr).in_array([true, 'dog'])
|
||
|
||
expect(matcher.description).to eq(
|
||
'validate that :attr is either ‹true› or ‹"dog"›',
|
||
)
|
||
end
|
||
end
|
||
|
||
context 'when there are three or more values' do
|
||
it 'returns the correct string' do
|
||
matcher = validate_inclusion_of(:attr).in_array([true, 'dog', 'cat'])
|
||
|
||
expect(matcher.description).to eq(
|
||
'validate that :attr is either ‹true›, ‹"dog"›, or ‹"cat"›',
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'given a range of values' do
|
||
it 'returns the correct string' do
|
||
matcher = validate_inclusion_of(:attr).in_range(1..10)
|
||
|
||
expect(matcher.description).to eq(
|
||
'validate that :attr lies inside the range ‹1› to ‹10›',
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
def object_builder_class
|
||
@_object_builder_class ||= Struct.new(
|
||
:attribute,
|
||
:object,
|
||
:validation_options,
|
||
)
|
||
end
|
||
|
||
def build_object_with_generic_attribute(
|
||
model_name: nil,
|
||
attribute_name: :attr,
|
||
validation_options: nil,
|
||
value: nil,
|
||
**other_options
|
||
)
|
||
model = define_model_validating_inclusion(
|
||
model_name: model_name,
|
||
attribute_name: attribute_name,
|
||
validation_options: validation_options,
|
||
**other_options,
|
||
)
|
||
|
||
object = model.new
|
||
object.__send__("#{attribute_name}=", value)
|
||
|
||
object_builder_class.new(attribute_name, object, validation_options)
|
||
end
|
||
|
||
def define_model_validating_inclusion(
|
||
model_name: nil,
|
||
attribute_name: :attr,
|
||
column_type: :string,
|
||
column_options: {},
|
||
validation_options: nil,
|
||
custom_validation: nil,
|
||
customize_model_class: -> (object) { }
|
||
)
|
||
column_options = { type: column_type, options: column_options }
|
||
model_options = {
|
||
model_name: model_name,
|
||
attribute_name: attribute_name,
|
||
column_options: column_options,
|
||
}.compact
|
||
|
||
define_simple_model(model_options) do |model|
|
||
if validation_options
|
||
model.validates_inclusion_of(attribute_name, validation_options)
|
||
end
|
||
|
||
if custom_validation
|
||
model.class_eval do
|
||
define_method :custom_validation do
|
||
custom_validation.call(self, attribute_name)
|
||
end
|
||
|
||
validate :custom_validation
|
||
end
|
||
end
|
||
|
||
if customize_model_class
|
||
model.instance_eval(&customize_model_class)
|
||
end
|
||
end
|
||
end
|
||
|
||
def build_object_allowing(values, validation_options: {}, **other_options)
|
||
build_object(
|
||
validation_options: validation_options.merge(in: values),
|
||
**other_options,
|
||
)
|
||
end
|
||
|
||
def expect_to_match(builder)
|
||
matcher = validate_inclusion_of(builder.attribute)
|
||
yield matcher if block_given?
|
||
expect(builder.object).to(matcher)
|
||
end
|
||
|
||
def expect_not_to_match(builder)
|
||
matcher = validate_inclusion_of(builder.attribute)
|
||
yield matcher if block_given?
|
||
expect(builder.object).not_to(matcher)
|
||
end
|
||
|
||
def expect_to_match_in_array(builder, array)
|
||
expect_to_match(builder) do |matcher|
|
||
matcher.in_array(array)
|
||
yield matcher if block_given?
|
||
end
|
||
end
|
||
|
||
def expect_not_to_match_in_array(builder, array)
|
||
expect_not_to_match(builder) do |matcher|
|
||
matcher.in_array(array)
|
||
yield matcher if block_given?
|
||
end
|
||
end
|
||
|
||
def expect_to_match_in_range(builder, range)
|
||
expect_to_match(builder) do |matcher|
|
||
matcher.in_range(range)
|
||
yield matcher if block_given?
|
||
end
|
||
end
|
||
|
||
def expect_not_to_match_in_range(builder, range)
|
||
expect_not_to_match(builder) do |matcher|
|
||
matcher.in_range(range)
|
||
yield matcher if block_given?
|
||
end
|
||
end
|
||
|
||
def expect_to_match_ensuring_range_and_messages(range, low_value, high_value)
|
||
low_message = 'too low'
|
||
high_message = 'too high'
|
||
|
||
builder = build_object custom_validation: -> (object, attribute) {
|
||
value = object.public_send(attribute)
|
||
|
||
if value < low_value
|
||
object.errors.add(attribute, low_message)
|
||
elsif value > high_value
|
||
object.errors.add(attribute, high_message)
|
||
end
|
||
}
|
||
|
||
expect_to_match(builder) do |matcher|
|
||
matcher.
|
||
in_range(range).
|
||
with_low_message(low_message).
|
||
with_high_message(high_message)
|
||
end
|
||
end
|
||
|
||
def validation_matcher_scenario_args
|
||
super.deep_merge(matcher_name: :validate_inclusion_of)
|
||
end
|
||
end
|