Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add validating qualifier to enum matcher #1630

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion lib/shoulda/matchers/active_record/define_enum_for_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,47 @@ module ActiveRecord
# with_default(:closed)
# end
#
# ##### validating
#
# Use `validating` to test that the enum is being validated.
# Can take a boolean value and an allowing_nil keyword argument:
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], validate: true
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# validating
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# validating
# end
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], validate: { allow_nil: true }
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# validating(allowing_nil: true)
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# validating(allowing_nil: true)
# end
#
# @return [DefineEnumForMatcher]
#
def define_enum_for(attribute_name)
Expand Down Expand Up @@ -247,6 +288,12 @@ def description
description
end

def validating(value = true, allowing_nil: false)
options[:validating] = value
options[:allowing_nil] = allowing_nil
self
end

def with_values(expected_enum_values)
options[:expected_enum_values] = expected_enum_values
self
Expand Down Expand Up @@ -285,7 +332,8 @@ def matches?(subject)
column_type_matches? &&
enum_value_methods_exist? &&
scope_presence_matches? &&
default_value_matches?
default_value_matches? &&
validating_matches?
end

def failure_message
Expand All @@ -308,6 +356,30 @@ def failure_message_when_negated

private

def validating_matches?
return true if options[:validating].nil?

validator = find_enum_validator

if expected_validating? == !!validator
if validator&.options&.dig(:allow_nil).present? == expected_allowing_nil?
true
else
@failure_message_continuation =
"However, #{attribute_name.inspect} is allowing nil values"
false
end
else
@failure_message_continuation =
if expected_validating?
"However, #{attribute_name.inspect} is not being validated"
else
"However, #{attribute_name.inspect} is being validated"
end
false
end
end

attr_reader :attribute_name, :options, :record,
:failure_message_continuation

Expand All @@ -328,6 +400,16 @@ def expectation # rubocop:disable Metrics/MethodLength
expectation << Shoulda::Matchers::Util.inspect_value(expected_default_value)
end

if expected_validating?
expectation << ', and being validated '
expectation <<
if expected_allowing_nil?
'allowing nil values'
else
'not allowing nil values'
end
end

if expected_prefix
expectation <<
if expected_suffix
Expand Down Expand Up @@ -602,6 +684,22 @@ def expected_suffix
end
end

def expected_validating?
options[:validating].present?
end

def expected_allowing_nil?
options[:allowing_nil].present?
end

def find_enum_validator
record.class.validators.detect do |validator|
validator.kind == :inclusion &&
validator.attributes.include?(attribute_name.to_s) &&
validator.options[:in] == expected_enum_values
end
end

def exclude_scopes?
!options[:scopes]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,217 @@ def self.statuses
end
end

if rails_version >= 7.1
describe 'qualified with #validating' do
context 'if enum is being validated' do
context 'but validating qualifier is not used' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published', validate: true)

matcher = lambda do
define_enum_for(:attr).with_values(['published', 'unpublished', 'draft'])
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
end

context 'and validating qualifier is used as false' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published', validate: true)

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating(false)
end

message = format_message(<<-MESSAGE)
Expected Example to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›. However, :attr is being validated.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'and validating qualifier is used' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, validate: true)

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated not allowing nil values, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
end

context 'using allow_nil' do
context 'when allowing nil on qualifier' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, validate: { allow_nil: true })

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating(allowing_nil: true)
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated allowing nil values, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
end

context 'when not allowing nil on qualifier' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, validate: { allow_nil: true })

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating
end

message = format_message(<<-MESSAGE)
Expected Example to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated not allowing nil values. However, :attr is
allowing nil values.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end
end
end

context 'when not allowing nil values' do
it 'matches if qualifier does not allow' do
record = build_record_with_array_values(attribute_name: :attr, validate: { allow_nil: false })

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating(allowing_nil: false)
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated not allowing nil values, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end

it 'rejects with an appropriate failure message if qualifier allows' do
record = build_record_with_array_values(attribute_name: :attr, validate: { allow_nil: false })

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating(allowing_nil: true)
end

message = format_message(<<-MESSAGE)
Expected Example to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated allowing nil values. However, :attr is allowing
nil values.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'if enum is not being validated' do
context 'but validating qualifier is used' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating
end

message = format_message(<<-MESSAGE)
Expected Example to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, and being validated not allowing nil values. However, :attr
is not being validated.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'and validating qualifier is used as false' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
validating(false)
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
end

context 'and validating qualifier is not used' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

matcher = lambda do
define_enum_for(:attr).with_values(['published', 'unpublished', 'draft'])
end

message = format_message(<<-MESSAGE)
Expected Example not to define :attr as an enum backed by an integer,
mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"›
to ‹2›, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
end
end
end
end

if rails_version =~ '~> 6.0'
context 'qualified with #without_scopes' do
context 'if scopes are set to false on the enum but without_scopes is not used' do
Expand Down Expand Up @@ -986,7 +1197,8 @@ def build_record_with_array_values(
suffix: false,
attribute_alias: nil,
scopes: true,
default: nil
default: nil,
validate: false
)
build_record_with_enum_attribute(
model_name: model_name,
Expand All @@ -998,6 +1210,7 @@ def build_record_with_array_values(
attribute_alias: attribute_alias,
scopes: scopes,
default: default,
validate: validate,
)
end

Expand Down Expand Up @@ -1030,7 +1243,8 @@ def build_record_with_enum_attribute(
scopes: true,
prefix: false,
suffix: false,
default: nil
default: nil,
validate: false
)
enum_name = attribute_alias || attribute_name
model = define_model(
Expand All @@ -1048,7 +1262,7 @@ def build_record_with_enum_attribute(
}

if rails_version >= 7.0
model.enum(enum_name, values, prefix: prefix, suffix: suffix, default: default)
model.enum(enum_name, values, prefix: prefix, suffix: suffix, validate: validate, default: default)
else
params.merge!(_scopes: scopes)
model.enum(params)
Expand Down
Loading