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 default qualifier to define_enum_for matcher #1627

Merged
merged 1 commit into from
Apr 26, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 74 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 @@ -187,6 +187,31 @@ module ActiveRecord
# without_scopes
# end
#
# ##### with_default
#
# Use `with_default` to test that the enum is defined with a
# default value. A proc can also be passed, and will be called once each
# time a new value is needed. (If using Time or Date, it's recommended to
# freeze time or date to avoid flaky tests):
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], default: :closed
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# with_default(:closed)
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_default(:closed)
# end
#
# @return [DefineEnumForMatcher]
#
def define_enum_for(attribute_name)
Expand Down Expand Up @@ -247,14 +272,20 @@ def without_scopes
self
end

def with_default(default_value)
options[:default] = default_value
self
end

def matches?(subject)
@record = subject

enum_defined? &&
enum_values_match? &&
column_type_matches? &&
enum_value_methods_exist? &&
scope_presence_matches?
scope_presence_matches? &&
default_value_matches?
end

def failure_message
Expand Down Expand Up @@ -292,6 +323,11 @@ def expectation # rubocop:disable Metrics/MethodLength
)
end

if options[:default].present?
expectation << ', with a default value of '
expectation << Shoulda::Matchers::Util.inspect_value(expected_default_value)
end

if expected_prefix
expectation <<
if expected_suffix
Expand Down Expand Up @@ -476,6 +512,43 @@ def missing_methods_message
end
end

def default_value_matches?
return true if options[:default].blank?

if actual_default_value.nil?
@failure_message_continuation = 'However, no default value was set'
return false
end

if actual_default_value == expected_default_value
true
else
String.new.tap do |message|
message << 'However, the default value is '
message << Shoulda::Matchers::Util.inspect_value(actual_default_value)
@failure_message_continuation = message
end
false
end
end

def expected_default_value
options[:default].respond_to?(:call) ? options[:default].call : options[:default]
end

def actual_default_value
attribute_schema = model.attributes_to_define_after_schema_loads[attribute_name.to_s]

value = case attribute_schema
in [_, { default: default_value } ]
default_value
in [_, default_value]
default_value
end

value.respond_to?(:call) ? value.call : value
end

def singleton_methods_exist?
expected_singleton_methods.all? do |method|
model.singleton_methods.include?(method)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,122 @@ def self.statuses
end
end

describe 'qualified with #with_default' do
context 'if default are defined on the enum' do
context 'but with_default is not used' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

expect(record).to define_enum_for(:attr).with_values(['published', 'unpublished', 'draft'])
end
end

context 'with_default is used and default is the same' 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']).
with_default('published')
end

expect(&matcher).
to match_against(record).
or_fail_with(<<-MESSAGE, wrap: true)
Expected Example not to define :attr as an enum backed by an
integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›,
and ‹"draft"› to ‹2›, with a default value of ‹"published"›, but it did.
MESSAGE
end
end

context 'with_default is used but default is different' 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']).
with_default('unpublished')
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›, with a default value of ‹"unpublished"›. However, the default value
is ‹"published"›.
MESSAGE

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

context 'when a Proc is used as the default value' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, default: 'draft')

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default(-> { 'published' })
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›, with a default value of ‹"published"›. However, the default
value is ‹"draft"›.
MESSAGE

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

it 'matches when the default value is the same' do
record = build_record_with_array_values(attribute_name: :attr, default: 'draft')

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default(-> { 'draft' })
end

expect(&matcher).
to match_against(record).
or_fail_with(<<-MESSAGE, wrap: true)
Expected Example not to define :attr as an enum backed by an
integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›,
and ‹"draft"› to ‹2›, with a default value of ‹"draft"›, but it did.
MESSAGE
end
end
end

context 'if default is not defined on the enum' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr)

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default('published')
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›, with a default value of ‹"published"›. However, no default
value was set.
MESSAGE

expect(&assertion).to fail_with_message(message)
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 @@ -869,7 +985,8 @@ def build_record_with_array_values(
prefix: false,
suffix: false,
attribute_alias: nil,
scopes: true
scopes: true,
default: nil
)
build_record_with_enum_attribute(
model_name: model_name,
Expand All @@ -880,6 +997,7 @@ def build_record_with_array_values(
suffix: suffix,
attribute_alias: attribute_alias,
scopes: scopes,
default: default,
)
end

Expand Down Expand Up @@ -911,7 +1029,8 @@ def build_record_with_enum_attribute(
attribute_alias:,
scopes: true,
prefix: false,
suffix: false
suffix: false,
default: nil
)
enum_name = attribute_alias || attribute_name
model = define_model(
Expand All @@ -925,10 +1044,11 @@ def build_record_with_enum_attribute(
enum_name => values,
_prefix: prefix,
_suffix: suffix,
_default: default,
}

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