diff --git a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb index 7ac46a9cd..09e1da207 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -80,6 +80,55 @@ module ActiveRecord # backed_by_column_of_type(:string) # end # + ## ##### with_prefix + # + # Use `with_prefix` to test that the enum is defined with a `_prefix` + # option (Rails 5 only). Can take either a boolean or a symbol: + # + # class Issue < ActiveRecord::Base + # enum status: [:open, :closed], _prefix: :old + # end + # + # # RSpec + # RSpec.describe Issue, type: :model do + # it do + # should define_enum_for(:status). + # with_values([:open, :closed]). + # with_prefix(:old) + # end + # end + # + # # Minitest (Shoulda) + # class ProcessTest < ActiveSupport::TestCase + # should define_enum_for(:status). + # with_values([:open, :closed]). + # with_prefix(:old) + # end + # + # ##### with_suffix + # + # Use `with_suffix` to test that the enum is defined with a `_suffix` + # option (Rails 5 only). Can take either a boolean or a symbol: + # + # class Issue < ActiveRecord::Base + # enum status: [:open, :closed], _suffix: true + # end + # + # # RSpec + # RSpec.describe Issue, type: :model do + # it do + # should define_enum_for(:status). + # with_values([:open, :closed]). + # with_suffix + # end + # end + # + # # Minitest (Shoulda) + # class ProcessTest < ActiveSupport::TestCase + # should define_enum_for(:status). + # with_values([:open, :closed]). + # with_suffix + # end # # @return [DefineEnumForMatcher] # @@ -98,6 +147,23 @@ def description description = "define :#{attribute_name} as an enum, backed by " description << Shoulda::Matchers::Util.a_or_an(expected_column_type) + if options[:expected_prefix] + description << ', using a prefix of ' + description << "#{options[:expected_prefix].inspect}" + end + + if options[:expected_suffix] + if options[:expected_prefix] + description << ' and' + else + description << ', using' + end + + description << ' a suffix of ' + + description << "#{options[:expected_suffix].inspect}" + end + if presented_expected_enum_values.any? description << ', with possible values ' description << Shoulda::Matchers::Util.inspect_value( @@ -121,6 +187,16 @@ def with(expected_enum_values) with_values(expected_enum_values) end + def with_prefix(expected_prefix = attribute_name) + options[:expected_prefix] = expected_prefix + self + end + + def with_suffix(expected_suffix = attribute_name) + options[:expected_suffix] = expected_suffix + self + end + def backed_by_column_of_type(expected_column_type) options[:expected_column_type] = expected_column_type self @@ -128,7 +204,11 @@ def backed_by_column_of_type(expected_column_type) def matches?(subject) @record = subject - enum_defined? && enum_values_match? && column_type_matches? + + enum_defined? && + enum_values_match? && + column_type_matches? && + enum_value_methods_exist? end def failure_message @@ -168,6 +248,10 @@ def normalized_expected_enum_values to_hash(expected_enum_values) end + def expected_enum_value_names + to_array(expected_enum_values) + end + def expected_enum_values options[:expected_enum_values] end @@ -238,6 +322,38 @@ def model record.class end + def enum_value_methods_exist? + passed = expected_singleton_methods.all? do |method| + model.singleton_methods.include?(method) + end + + if passed + true + else + @failure_reason = + if options[:expected_prefix] + if options[:expected_suffix] + 'it was defined with either a different prefix, a ' + + 'different suffix, or neither one at all' + else + 'it was defined with either a different prefix or none at all' + end + elsif options[:expected_suffix] + 'it was defined with either a different suffix or none at all' + end + false + end + end + + def expected_singleton_methods + expected_enum_value_names.map do |name| + [options[:expected_prefix], name, options[:expected_suffix]]. + select(&:present?). + join('_'). + to_sym + end + end + def to_hash(value) if value.is_a?(Array) value.each_with_index.inject({}) do |hash, (item, index)| diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb index d9d30e3a0..313c7b999 100644 --- a/spec/support/unit/helpers/active_record_versions.rb +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -9,6 +9,10 @@ def active_record_version Tests::Version.new(::ActiveRecord::VERSION::STRING) end + def active_record_enum_supports_prefix_and_suffix? + active_record_version >= 5 + end + def active_record_supports_has_secure_password? active_record_version >= 3.1 end diff --git a/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb index 21bc210ae..a44435ca4 100644 --- a/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb @@ -815,16 +815,14 @@ def name=(value) end end - if active_record_supports_enum? - context 'given an ActiveRecord model' do - context 'where the attribute under test is an enum and the given value is a value in that enum' do - it 'accepts' do - model = define_model('Shipment', status: :integer) do - enum status: { pending: 1, shipped: 2, delivered: 3 } - end - - expect(model.new).to allow_value(1).for(:status) + context 'given an ActiveRecord model' do + context 'where the attribute under test is an enum and the given value is a value in that enum' do + it 'accepts' do + model = define_model('Shipment', status: :integer) do + enum status: { pending: 1, shipped: 2, delivered: 3 } end + + expect(model.new).to allow_value(1).for(:status) end end end diff --git a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb index f7f3229f3..f72bb334a 100644 --- a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb @@ -304,30 +304,361 @@ def self.statuses; end end end + if active_record_enum_supports_prefix_and_suffix? + context 'qualified with #with_prefix' do + context 'when the prefix is explicit' do + context 'if the attribute was not defined with a prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :foo, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a different prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :bar, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same prefix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: :foo, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo) + end + end + end + + context 'when the prefix is implicit' do + context 'if the attribute was not defined with a prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :attr, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a prefix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: true, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix + end + end + end + end + + context 'qualified with #with_suffix' do + context 'when the suffix is explicit' do + context 'if the attribute was not defined with a suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:foo) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :foo, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a different suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + suffix: :foo, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :bar, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + suffix: :foo, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:foo) + end + end + end + + context 'when the suffix is implicit' do + context 'if the attribute was not defined with a suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :attr, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + suffix: true, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix + end + end + end + end + + context 'qualified with both #with_prefix and #with_suffix' do + context 'if the attribute was not defined with a different prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:whatever). + with_suffix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :whatever and a suffix of :bar, with possible + values ‹[:active, :archived]›. However, it was defined with either + a different prefix, a different suffix, or neither one at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + + context 'if the attribute was defined with a different suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo). + with_suffix(:whatever) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :foo and a suffix of :whatever, with possible + values ‹[:active, :archived]›. However, it was defined with + either a different prefix, a different suffix, or neither one at + all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same prefix and suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo). + with_suffix(:bar) + end + end + end + end + end + def build_record_with_array_values( model_name: 'Example', attribute_name: :attr, column_type: :integer, - values: ['published', 'unpublished', 'draft'] + values: ['published', 'unpublished', 'draft'], + prefix: false, + suffix: false ) build_record_with_enum_attribute( model_name: model_name, attribute_name: attribute_name, column_type: column_type, values: values, + prefix: prefix, + suffix: suffix, ) end def build_record_with_hash_values( model_name: 'Example', attribute_name: :attr, - values: { active: 0, archived: 1 } + values: { active: 0, archived: 1 }, + prefix: false, + suffix: false ) build_record_with_enum_attribute( model_name: model_name, attribute_name: attribute_name, column_type: :integer, values: values, + prefix: prefix, + suffix: suffix, ) end @@ -335,13 +666,21 @@ def build_record_with_enum_attribute( model_name:, attribute_name:, column_type:, - values: + values:, + prefix: false, + suffix: false ) model = define_model( model_name, - attribute_name => column_type, + attribute_name => { type: column_type }, ) - model.enum(attribute_name => values) + + if active_record_enum_supports_prefix_and_suffix? + model.enum(attribute_name => values, _prefix: prefix, _suffix: suffix) + else + model.enum(attribute_name => values) + end + model.new end diff --git a/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb index af56f25e0..12225111f 100644 --- a/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb @@ -721,60 +721,58 @@ include_context 'it supports scoped attributes of a certain type', column_type: :integer - if active_record_supports_enum? - context 'when one of the scoped attributes is an enum' do - it 'accepts' do + context 'when one of the scoped attributes is an enum' do + it 'accepts' do + record = build_record_validating_scoped_uniqueness_with_enum( + enum_scope: :scope + ) + expect(record).to validate_uniqueness.scoped_to(:scope) + end + + context 'when too narrow of a scope is specified' do + it 'rejects with an appropriate failure message' do record = build_record_validating_scoped_uniqueness_with_enum( - enum_scope: :scope + enum_scope: :scope1, + additional_scopes: [:scope2], + additional_attributes: [:other] ) - expect(record).to validate_uniqueness.scoped_to(:scope) - end - context 'when too narrow of a scope is specified' do - it 'rejects with an appropriate failure message' do - record = build_record_validating_scoped_uniqueness_with_enum( - enum_scope: :scope1, - additional_scopes: [:scope2], - additional_attributes: [:other] - ) - - assertion = lambda do - expect(record). - to validate_uniqueness. - scoped_to(:scope1, :scope2, :other) - end + assertion = lambda do + expect(record). + to validate_uniqueness. + scoped_to(:scope1, :scope2, :other) + end - message = <<-MESSAGE + message = <<-MESSAGE Example did not properly validate that :attr is case-sensitively unique within the scope of :scope1, :scope2, and :other. Expected the validation to be scoped to :scope1, :scope2, and :other, but it was scoped to :scope1 and :scope2 instead. - MESSAGE + MESSAGE - expect(&assertion).to fail_with_message(message) - end + expect(&assertion).to fail_with_message(message) end + end - context 'when too broad of a scope is specified' do - it 'rejects with an appropriate failure message' do - record = build_record_validating_scoped_uniqueness_with_enum( - enum_scope: :scope1, - additional_scopes: [:scope2] - ) + context 'when too broad of a scope is specified' do + it 'rejects with an appropriate failure message' do + record = build_record_validating_scoped_uniqueness_with_enum( + enum_scope: :scope1, + additional_scopes: [:scope2] + ) - assertion = lambda do - expect(record).to validate_uniqueness.scoped_to(:scope1) - end + assertion = lambda do + expect(record).to validate_uniqueness.scoped_to(:scope1) + end - message = <<-MESSAGE + message = <<-MESSAGE Example did not properly validate that :attr is case-sensitively unique within the scope of :scope1. Expected the validation to be scoped to :scope1, but it was scoped to :scope1 and :scope2 instead. - MESSAGE + MESSAGE - expect(&assertion).to fail_with_message(message) - end + expect(&assertion).to fail_with_message(message) end end end