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 c8823ad
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 4 deletions.
60 changes: 59 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,29 @@ module ActiveRecord
# with_default(:closed)
# end
#
# ##### validating
#
# Use `validating` to test that the enum is being validated.
# Can take a boolean value and defaults to true:
#
# 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
#
# @return [DefineEnumForMatcher]
#
def define_enum_for(attribute_name)
Expand Down Expand Up @@ -247,6 +270,11 @@ def description
description
end

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

def with_values(expected_enum_values)
options[:expected_enum_values] = expected_enum_values
self
Expand Down Expand Up @@ -285,7 +313,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 +337,19 @@ def failure_message_when_negated

private

def validating_matches?
return true if options[:validating].nil? || expected_validating? == actual_validating?

@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

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

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

if expected_validating?
expectation << ', and being validated'
end

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

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

def actual_validating?
record.class.validators.any? 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,130 @@ def self.statuses
end
end

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, but it did.
MESSAGE

expect(&matcher).to match_against(record).or_fail_with(message)
end
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. 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

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 +1110,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 +1123,7 @@ def build_record_with_array_values(
attribute_alias: attribute_alias,
scopes: scopes,
default: default,
validate: validate,
)
end

Expand Down Expand Up @@ -1030,7 +1156,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 +1175,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 c8823ad

Please sign in to comment.