mirror of
https://github.com/thoughtbot/shoulda-matchers.git
synced 2022-11-09 12:01:38 -05:00
4ad15205a5
Secondary author: Elliot Winkler <elliot.winkler@gmail.com> This commit fixes `validate_uniqueness_of` when used with `scoped_to` so that when one of the scope attributes is a polymorphic *_type attribute and the model has another validation on the same attribute, the matcher does not fail with an error. As a part of the matching process, `validate_uniqueness_of` tries to create two records that have the same values for each of the attributes passed to `scoped_to`, except one of the attributes has a different value. This way, the second record should be valid because it shouldn't clash with the first one. It does this one attribute at a time. Let's say the attribute in question is a polymorphic *_type attribute. The value of this attribute is intended to be the name of a model as a string. Let's assume the first record has a meaningful value for this attribute already and we're trying to find a value for the second record. In order to produce such a value, `validate_uniqueness_of` generally calls #next (a common method in Ruby to generate a succeeding version of an object sequentially) on the first object's corresponding value. So in the case of a *_type attribute, since it's a string, it would call #next on that string. For instance, "User" would become "Uses". You might have noticed a problem with this, which is "what if Uses is not a valid model?" This is okay as long as there's nothing that is trying to access the polymorphic association. Because as soon as this happens, Rails will attempt to find a record using the polymorphic type -- in other words, it will try to find the model that the *_type attribute corresponds to. One of the ways this can happen is if the *_type attribute in question has a validation on it itself. Let's look at an example. Given these models: ``` ruby class User < ActiveRecord::Base end class Favorite < ActiveRecord::Base belongs_to :favoriteable, polymorphic: true validates :favoriteable, presence: true validates :favoriteable_id, uniqueness: { scope: [:favoriteable_type] } end FactoryGirl.define do factory :user factory :favorite do association :favoriteable, factory: :user end end ``` and the following test: ``` ruby require 'rails_helper' describe Favorite do context 'validations' do before { FactoryGirl.create(:favorite) } it do should validate_uniqueness_of(:favoriteable_id). scoped_to(:favoriteable_type) end end end ``` prior to this commit, the test would have failed with: ``` Failures: 1) Favorite validations should require case sensitive unique value for favoriteable_id scoped to favoriteable_type Failure/Error: should validate_uniqueness_of(:favoriteable_id). NameError: uninitialized constant Uses # ./spec/models/favorite_spec.rb:6:in `block (3 levels) in <top (required)>' ``` Here, a Favorite record is created where `favoriteable_type` is set to "Uses", and then validations are run on that record. The presence validation on `favoriteable_type` is run which tries to access a "Uses" model. But that model doesn't exist, so the test raises an error. Now `validates_uniqueness_of` will set the *_type attribute to a meaningful value. It still does this by calling #next on the first record's value, but then it makes a new model that is simply an alias for the original model. Hence, in our example, Uses would become a model that is aliased to User.
76 lines
1.8 KiB
Ruby
76 lines
1.8 KiB
Ruby
module ClassBuilder
|
|
def self.included(example_group)
|
|
example_group.class_eval do
|
|
after do
|
|
teardown_defined_constants
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.parse_constant_name(name)
|
|
namespace = Shoulda::Matchers::Util.deconstantize(name)
|
|
qualified_namespace = (namespace.presence || 'Object').constantize
|
|
name_without_namespace = name.to_s.demodulize
|
|
[qualified_namespace, name_without_namespace]
|
|
end
|
|
|
|
def define_module(module_name, &block)
|
|
module_name = module_name.to_s.camelize
|
|
|
|
namespace, name_without_namespace =
|
|
ClassBuilder.parse_constant_name(module_name)
|
|
|
|
if namespace.const_defined?(name_without_namespace, false)
|
|
namespace.__send__(:remove_const, name_without_namespace)
|
|
end
|
|
|
|
eval <<-RUBY
|
|
module #{namespace}::#{name_without_namespace}
|
|
end
|
|
RUBY
|
|
|
|
namespace.const_get(name_without_namespace).tap do |constant|
|
|
constant.unloadable
|
|
|
|
if block
|
|
constant.module_eval(&block)
|
|
end
|
|
end
|
|
end
|
|
|
|
def define_class(class_name, parent_class = Object, &block)
|
|
class_name = class_name.to_s.camelize
|
|
|
|
namespace, name_without_namespace =
|
|
ClassBuilder.parse_constant_name(class_name)
|
|
|
|
if namespace.const_defined?(name_without_namespace, false)
|
|
namespace.__send__(:remove_const, name_without_namespace)
|
|
end
|
|
|
|
eval <<-RUBY
|
|
class #{namespace}::#{name_without_namespace} < #{parent_class}
|
|
end
|
|
RUBY
|
|
|
|
namespace.const_get(name_without_namespace).tap do |constant|
|
|
constant.unloadable
|
|
|
|
if block_given?
|
|
constant.class_eval(&block)
|
|
end
|
|
|
|
if constant.respond_to?(:reset_column_information)
|
|
constant.reset_column_information
|
|
end
|
|
end
|
|
end
|
|
|
|
def teardown_defined_constants
|
|
ActiveSupport::Dependencies.clear
|
|
end
|
|
end
|
|
|
|
RSpec.configure do |config|
|
|
config.include ClassBuilder
|
|
end
|