Skip to content

Commit

Permalink
feat: Add validating qualifier to enum matcher
Browse files Browse the repository at this point in the history
On this commit we add a new qualifier to the `define_enum_for` matcher
called `validating`. This qualifier is used to test if the enum is being
validated or not.

```ruby
class Issue < ActiveRecord::Base
  enum status: [:open, :closed], validate: true
end

RSpec.describe Issue, type: :model do
  it do
    should define_enum_for(:status).
      validating
  end
end
```
  • Loading branch information
matsales28 committed May 10, 2024
1 parent c9d234a commit 9ed56e4
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 4 deletions.
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

0 comments on commit 9ed56e4

Please sign in to comment.