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

validate_uniqueness_of validation fails when having more than one validates to the same field #830

Closed
luismaia opened this issue Oct 30, 2015 · 16 comments

Comments

@luismaia
Copy link

I have the following validations in one of my models:

class CalibrationConstantVersion < ActiveRecord::Base
  (...)
  validates :calibration_constant,
            presence: true,
            uniqueness: { scope: [:name], case_sensitive: false }

  validates :calibration_constant,
            presence: true,
            uniqueness: { scope: [:begin_at, :physical_device_id] }
  (...)
end

And my RSpec is:

RSpec.describe CalibrationConstantVersion do
  (...)
  it { should validate_uniqueness_of(:calibration_constant).scoped_to(:name).case_insensitive }
  it { should validate_uniqueness_of(:calibration_constant).scoped_to(:begin_at, :physical_device_id)}
  (...)
end

When upgrading from version 2.8.0 to 3.0.1 I start having this error:

Failures:

  1) CalibrationConstantVersion should require unique value for calibration_constant scoped to begin_at, physical_device_id
     Failure/Error: it { expect(create(:calibration_constant_version)).to validate_uniqueness_of(:calibration_constant).scoped_to(:begin_at, :physical_device_id)}
       Expected validation to be scoped to [:begin_at, :physical_device_id], but it was scoped to [:name].

If I exchange the validates order, in the model I got this error:

Failures:

  1) CalibrationConstantVersion should require unique value for calibration_constant scoped to name
     Failure/Error: it { should validate_uniqueness_of(:calibration_constant).scoped_to(:name).case_insensitive }
       Expected validation to be scoped to [:name], but it was scoped to [:begin_at, :physical_device_id].

My conclusion is that shoulda-matchers (in version 3) is only executing the first validates definition per field. Is this conclusion correct?

I tested against master and I get the same error.
Is this an issue or am I doing something wrong?

@mcmire
Copy link
Collaborator

mcmire commented Oct 30, 2015

Yes, starting in version 3, the matcher will look at the existing uniqueness validation that is on the attribute in question and will compare the scopes defined there with the scopes you are passing to the
matcher. To be specific, it looks for the first existing uniqueness validation on the attribute. So this obviously assumes that there is only one.

So you've definitely found a bug 🪲

@durhamka
Copy link

durhamka commented Nov 2, 2015

+1 to this issue

@durhamka
Copy link

durhamka commented Nov 2, 2015

@luismaia did you find a work around by chance?

@luismaia
Copy link
Author

luismaia commented Nov 3, 2015

@durhamka I decide to stay (for now) with version 2.8.0. Anyway I guess that if you create a custom validator (containing the logic of your multiple validators) it will work.

@durhamka
Copy link

durhamka commented Nov 3, 2015

Thanks! @luismaia

@lolgear
Copy link

lolgear commented Jan 18, 2016

the same, but for my cases 3.0.1 works pretty well

@mcmire
Copy link
Collaborator

mcmire commented Jan 18, 2016

@lolgear Can you elaborate how 3.0.1 was an improvement?

@mcmire mcmire added this to the 3.1.1 milestone Jan 18, 2016
@lolgear
Copy link

lolgear commented Jan 19, 2016

class Device < ActiveRecord::Base
  validates :title, presence: true, uniqueness: true
end

# Rspec
describe Device, type: :model do
  # fail on 3.1.0, but 3.0.1 passed
  it { is_expected.to validate_uniqueness_of(:title) }
end

Error:
       Device did not properly validate that :title is case-sensitively unique.
         The record you provided could not be created, as it failed with the
         following validation errors:

         * title: ["can't be blank"]
         * width: ["can't be blank"]
         * height: ["can't be blank"]


class AppTargeting < ActiveRecord::Base
  # join model
  validates :app_id, presence: true, uniqueness: { scope: :campaign_id }
  validates :campaign_id, presence: true
end

# Rspec
describe AppTargeting, type: :model do
  # fail on 3.1.0, but 3.0.1 passed
  it { is_expected.to validate_uniqueness_of(:app_id).scoped_to(:campaign_id) }
end

Error:
       AppTargeting did not properly validate that :app_id is case-sensitively
       unique within the scope of :campaign_id.
         The record you provided could not be created, as it failed with the
         following validation errors:

         * app_id: ["can't be blank"]
         * campaign_id: ["can't be blank"]

@mcmire
Copy link
Collaborator

mcmire commented Jan 19, 2016

@lolgear Oh okay. That's actually a different bug than the one presented here -- see #880 -- but it will be fixed in the next release along with this one.

@mcmire
Copy link
Collaborator

mcmire commented Jan 28, 2016

@luismaia

I've got a partial fix for the original issue mentioned at the very top here: 28bd9a1.

The reason why it's a partial fix is because the matcher now knows about multiple uniqueness validations that are on the same attribute, so you won't get the exact failure you got again.

But there's still an issue lurking here, and this happens to be one of those issues that is actually a little complicated to fix. (I'm surprised it even worked in 2.8.0.)

Remember that the uniqueness matcher works like this: the matcher will take (or create) an existing record, then make a duplicate of it (this becomes the "new" record). This record is invalid out of the gate, so the matcher will then set certain attributes on the new record that are different than corresponding attributes in the existing record, then assert that the record is valid with the changes (because it is now unique). The attributes it chooses to set are based on the list you provide to scoped_to.

So when you say .scoped_to(:name), the matcher doesn't know that begin_at and physical_device_id need to be unique values in order for the new record to be valid, because it doesn't care about those attributes. Similarly, when you say .scoped_to(:begin_at, :physical_device_id), it doesn't care about name.

So the new record that it is working with won't be completely valid, because it won't be completely unique.

I know that wasn't the best explanation, but the point is, I might need to add another qualifier to properly handle this case, and I'm not sure what that looks like yet. So I'll have to think about it.

But the failure message you saw is at least fixed.

@mcmire mcmire modified the milestones: 3.x, 3.1.1 Jan 28, 2016
@s2t2
Copy link

s2t2 commented Jun 9, 2017

FYI I'm getting a slightly different error:

it { should validate_uniqueness_of(:first_attribute).scoped_to([:poly_type, :poly_id]) }

 NoMethodError:
    undefined method `superclass' for nil:NilClass

@mcmire
Copy link
Collaborator

mcmire commented Jun 11, 2017

@s2t2 Can you provide a backtrace?

@s2t2
Copy link

s2t2 commented Jun 11, 2017

Sure. See below. Hope this is helpful.

--->> bundle exec rspec spec/models/notification_spec.rb --backtrace 
.......F

Failures:

  1) Notification validations uniqueness should validate that :other_attribute is case-sensitively unique within the scope of :poly_type and :poly_id
     Failure/Error: it { should validate_uniqueness_of(:other_attribute).scoped_to(:poly_type, :poly_id) }
     
     NoMethodError:
       undefined method `superclass' for nil:NilClass
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/uniqueness.rb:47:in `find_finder_class_for'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/uniqueness.rb:14:in `validate_each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:151:in `block in validate'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:148:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:148:in `validate'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:405:in `public_send'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:405:in `block in make_lambda'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:169:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:169:in `block (2 levels) in halting'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:547:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:547:in `block (2 levels) in default_terminator'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:546:in `catch'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:546:in `block in default_terminator'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:170:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:170:in `block in halting'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `block in call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:750:in `_run_validate_callbacks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations.rb:408:in `run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations/callbacks.rb:113:in `block in run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:126:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:126:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:506:in `block (2 levels) in compile'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:455:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:455:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:750:in `_run_validation_callbacks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations/callbacks.rb:113:in `run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations.rb:338:in `valid?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations.rb:65:in `valid?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/associated.rb:13:in `valid_object?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/associated.rb:5:in `block in validate_each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/associated.rb:5:in `reject'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations/associated.rb:5:in `validate_each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:151:in `block in validate'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:148:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validator.rb:148:in `validate'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:405:in `public_send'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:405:in `block in make_lambda'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:169:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:169:in `block (2 levels) in halting'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:547:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:547:in `block (2 levels) in default_terminator'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:546:in `catch'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:546:in `block in default_terminator'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:170:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:170:in `block in halting'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `block in call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:454:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:750:in `_run_validate_callbacks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations.rb:408:in `run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations/callbacks.rb:113:in `block in run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:126:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:126:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:506:in `block (2 levels) in compile'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:455:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:455:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-5.0.3/lib/active_support/callbacks.rb:750:in `_run_validation_callbacks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations/callbacks.rb:113:in `run_validations!'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activemodel-5.0.3/lib/active_model/validations.rb:338:in `valid?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activerecord-5.0.3/lib/active_record/validations.rb:65:in `valid?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:96:in `perform_validation'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:89:in `validation_result'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:85:in `validation_error_messages'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:64:in `messages'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:25:in `has_messages?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:55:in `messages_match?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validator.rb:21:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:38:in `matches?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:42:in `does_not_match?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:28:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:28:in `detect'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb:28:in `first_failing'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher.rb:533:in `public_send'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher.rb:533:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/allow_value_matcher.rb:394:in `matches?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validation_matcher.rb:155:in `run_allow_or_disallow_matcher'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_model/validation_matcher.rb:88:in `allows_value_of'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:660:in `block in validate_after_scope_change?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:648:in `each'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:648:in `all?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:648:in `validate_after_scope_change?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/shoulda-matchers-3.1.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb:332:in `matches?'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-expectations-3.5.0/lib/rspec/expectations/handler.rb:50:in `block in handle_matcher'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-expectations-3.5.0/lib/rspec/expectations/handler.rb:27:in `with_matcher'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-expectations-3.5.0/lib/rspec/expectations/handler.rb:48:in `handle_matcher'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/memoized_helpers.rb:81:in `should'
     # ./spec/models/notification_spec.rb:24:in `block (4 levels) in <top (required)>'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:254:in `instance_exec'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:254:in `block in run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:496:in `block in with_around_and_singleton_context_hooks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:453:in `block in with_around_example_hooks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:464:in `block in run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:604:in `block in run_around_example_hooks_for'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:338:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:338:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-rails-3.5.2/lib/rspec/rails/adapters.rb:127:in `block (2 levels) in <module:MinitestLifecycleAdapter>'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:443:in `instance_exec'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:443:in `instance_exec'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:375:in `execute_with'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:606:in `block (2 levels) in run_around_example_hooks_for'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:338:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:338:in `call'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:607:in `run_around_example_hooks_for'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/hooks.rb:464:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:453:in `with_around_example_hooks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:496:in `with_around_and_singleton_context_hooks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example.rb:251:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:627:in `block in run_examples'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:623:in `map'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:623:in `run_examples'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:589:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `block in run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `map'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `block in run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `map'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/example_group.rb:590:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:113:in `block (3 levels) in run_specs'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:113:in `map'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:113:in `block (2 levels) in run_specs'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1835:in `with_suite_hooks'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:112:in `block in run_specs'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/reporter.rb:77:in `report'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:111:in `run_specs'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:87:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:71:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:45:in `invoke'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rspec-core-3.5.4/exe/rspec:4:in `<top (required)>'
     # /usr/local/var/rbenv/versions/2.2.3/bin/rspec:23:in `load'
     # /usr/local/var/rbenv/versions/2.2.3/bin/rspec:23:in `<top (required)>'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli/exec.rb:74:in `load'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli/exec.rb:74:in `kernel_load'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli/exec.rb:27:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli.rb:332:in `exec'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/vendor/thor/lib/thor.rb:359:in `dispatch'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli.rb:20:in `dispatch'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/vendor/thor/lib/thor/base.rb:440:in `start'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/cli.rb:11:in `start'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/exe/bundle:34:in `block in <top (required)>'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/lib/bundler/friendly_errors.rb:100:in `with_friendly_errors'
     # /usr/local/var/rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/bundler-1.13.6/exe/bundle:26:in `<top (required)>'
     # /usr/local/var/rbenv/versions/2.2.3/bin/bundle:23:in `load'
     # /usr/local/var/rbenv/versions/2.2.3/bin/bundle:23:in `<main>'

Finished in 0.27109 seconds (files took 1.75 seconds to load)
8 examples, 1 failure

Failed examples:

rspec ./spec/models/notification_spec.rb:24 # Notification validations uniqueness should validate that :other_attribute is case-sensitively unique within the scope of :poly_type and :poly_id

 --->> 

@mcmire
Copy link
Collaborator

mcmire commented Jun 19, 2017

@s2t2 I think you're getting that error because the matcher is trying to set poly_type on the record in question to a value that doesn't represent a real class.

I'm curious -- how would you write a manual test for this?

@s2t2
Copy link

s2t2 commented Jun 21, 2017

@mcmire something like this (model spec for Thing):

describe "uniqueness of composite key which includes polymorphic association" do
  context "when there is an existing other attribute for a given polymorphic resource" do
    let(:poly){ create(:other_model) }
    let!(:thing){ create(:thing, poly: poly, other_attribute: "MyVal")}
    let(:duplicate){ create(:thing, poly: poly, other_attribute: "MyVal")}
    let(:different){ create(:thing, poly: poly, other_attribute: "OtherVal")}

    it "should not allow duplicate combinations of the other attribute and the polymorphic resource" do
      expect{ duplicate }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Other attribute has already been taken")
    end

    it "should allow different other attributes to be associated with the same polymorphic resource" do
      expect{ different }.to change{ Thing.count }.by(1)
    end
  end
end

@mcmire
Copy link
Collaborator

mcmire commented Feb 5, 2018

I'm going to go ahead and close this issue since I think the original issue has been fixed and I can't quite understand the edge case I posted above. If there any more issues with the uniqueness matcher in the future I think it's better to start a new issue so I can keep track of it better.

@mcmire mcmire closed this as completed Feb 5, 2018
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this issue Sep 23, 2018
# 3.1.2

### Deprecations

* This is the **last version** that supports Rails 4.0 and 4.1 and Ruby 2.0 and 2.1.

### Bug fixes

* When the `permit` matcher was used without `#on`, the controller did not use
  `params#require`, the params object was duplicated, and the matcher did not
  recognize the `#permit` call inside the controller. This behavior happened
  because the matcher overwrote double registries with the same parameter hash
  whenever ActionController::Parameters was instantiated.

  * *Commit: [44c019]*
  * *Issue: [#899]*
  * *Pull request: [#902]*

# 3.1.1

### Bug fixes

* Some matchers make use of ActiveSupport's `in?` method, but do not include the
  file where this is defined in ActiveSupport. This causes problems with
  projects using shoulda-matchers that do not include all of ActiveSupport by
  default. To fix this, replace `in?` with Ruby's builtin `include?`.

  * *Pull request: [#879]*

* `validate_uniqueness_of` works by creating a record if it doesn't exist, and
  then testing against a new record with various attributes set that are equal
  to (or different than) corresponding attributes in the existing record. In
  3.1.0 a change was made whereby when the uniqueness matcher is given a new
  record and creates an existing record out of it, it ensures that the record is
  valid before continuing on. This created a problem because if the subject,
  before it was saved, was empty and therefore in an invalid state, it could not
  effectively be saved. While ideally this should be enforced, doing so would be
  a backward-incompatible change, so this behavior has been rolled back.
  ([#880], [#884], [#885])

  * *Commit: [45de869]*
  * *Issues: [#880], [#884], [#885]*

* Fix an issue with `validate_uniqueness_of` + `scoped_to` when used against a
  model where the attribute has multiple uniqueness validations and each
  validation has a different set of scopes. In this case, a test written for the
  first validation (and its scopes) would pass, but tests for the other
  validations (and their scopes) would not, as the matcher only considered the
  first set of scopes as the *actual* set of scopes.

  * *Commit: [28bd9a1]*
  * *Issues: [#830]*

### Improvements

* Update `validate_uniqueness_of` so that if an existing record fails to be
  created because a column is non-nullable and was not filled in, raise an
  ExistingRecordInvalid exception with details on how to fix the test.

  * *Commit: [78ccfc5]*

[#879]: thoughtbot/shoulda-matchers#879
[45de869]: thoughtbot/shoulda-matchers@45de869
[#880]: thoughtbot/shoulda-matchers#880
[#884]: thoughtbot/shoulda-matchers#884
[#885]: thoughtbot/shoulda-matchers#885
[78ccfc5]: thoughtbot/shoulda-matchers@78ccfc5
[28bd9a1]: thoughtbot/shoulda-matchers@28bd9a1
[#830]: thoughtbot/shoulda-matchers#830

# 3.1.0

### Bug fixes

* Update `validate_numericality_of` so that submatchers are applied lazily
  instead of immediately. Previously, qualifiers were order-dependent, meaning
  that if you used `strict` before you used, say, `odd`, then `strict` wouldn't
  actually apply to `odd`. Now the order that you specify qualifiers doesn't
  matter.

  * *Source: [6c67a5e]*

* Fix `allow_value` so that it does not raise an AttributeChangedValueError
  (formerly CouldNotSetAttributeError) when used against an attribute that is an
  enum in an ActiveRecord model.

  * *Source: [9e8603e]*

* Add a `ignoring_interference_by_writer` qualifier to all matchers, not just
  `allow_value`. *This is enabled by default, which means that you should never
  get a CouldNotSetAttributeError again.* (You may get some more information if
  a test fails, however.)

  * *Source: [1189934], [5532f43]*
  * *Fixes: [#786], [#799], [#801], [#804], [#817], [#841], [#849], [#872],
    [#873], and [#874]*

* Fix `validate_numericality_of` so that it does not blow up when used against
  a virtual attribute defined in an ActiveRecord model (that is, an attribute
  that is not present in the database but is defined using `attr_accessor`).

  * *Source: [#822]*

* Update `validate_numericality_of` so that it no longer raises an
  IneffectiveTestError if used against a numeric column.

  * *Source: [5ed0362]*
  * *Fixes: [#832]*

[6c67a5e]: thoughtbot/shoulda-matchers@6c67a5e
[9e8603e]: thoughtbot/shoulda-matchers@9e8603e
[1189934]: thoughtbot/shoulda-matchers@1189934
[5532f43]: thoughtbot/shoulda-matchers@5532f43
[#786]: thoughtbot/shoulda-matchers#786
[#799]: thoughtbot/shoulda-matchers#799
[#801]: thoughtbot/shoulda-matchers#801
[#804]: thoughtbot/shoulda-matchers#804
[#817]: thoughtbot/shoulda-matchers#817
[#841]: thoughtbot/shoulda-matchers#841
[#849]: thoughtbot/shoulda-matchers#849
[#872]: thoughtbot/shoulda-matchers#872
[#873]: thoughtbot/shoulda-matchers#873
[#874]: thoughtbot/shoulda-matchers#874
[#822]: thoughtbot/shoulda-matchers#822
[5ed0362]: thoughtbot/shoulda-matchers@5ed0362
[#832]: thoughtbot/shoulda-matchers#832

### Features

* Add a new qualifier, `ignoring_case_sensitivity`, to `validate_uniqueness_of`.
  This provides a way to test uniqueness of an attribute whose case is
  normalized, either in a custom writer method for that attribute, or in a
  custom `before_validation` callback.

  * *Source: [#840]*
  * *Fixes: [#836]*

[#840]: thoughtbot/shoulda-matchers#840
[#836]: thoughtbot/shoulda-matchers#836

### Improvements

* Improve failure messages and descriptions of all matchers across the board so
  that it is easier to understand what the matcher was doing when it failed.
  (You'll see a huge difference in the output of the numericality and uniqueness
  matchers in particular.)

* Matchers now raise an error if any attributes that the matcher is attempting
  to set do not exist on the model.

  * *Source: [2962112]*

* Update `validate_numericality_of` so that it doesn't always run all of the
  submatchers, but stops on the first one that fails. Since failure messages
  now contain information as to what value the matcher set on the attribute when
  it failed, this change guarantees that the correct value will be shown.

  * *Source: [8e24a6e]*

* Continue to detect if attributes change incoming values, but now instead of
  immediately seeing a CouldNotSetAttributeError, you will only be informed
  about it if the test you've written fails.

  * *Source: [1189934]*

* Add an additional check to `define_enum_for` to ensure that the column that
  underlies the enum attribute you're testing is an integer column.

  * *Source: [68dd70a]*

* Add a test for `validate_numericality_of` so that it officially supports money
  columns.

  * *Source: [a559713]*
  * *Refs: [#841]*

[2962112]: thoughtbot/shoulda-matchers@2962112
[8e24a6e]: thoughtbot/shoulda-matchers@8e24a6e
[68dd70a]: thoughtbot/shoulda-matchers@68dd70a
[a559713]: thoughtbot/shoulda-matchers@a559713

# 3.0.1

### Bug fixes

* Fix `validate_inclusion_of` + `in_array` when used against a date or datetime
  column/attribute so that it does not raise a CouldNotSetAttributeError.
  ([#783], [8fa97b4])

* Fix `validate_numericality_of` when used against a numeric column so that it
  no longer raises a CouldNotSetAttributeError if the matcher has been qualified
  in any way (`only_integer`, `greater_than`, `odd`, etc.). ([#784], [#812])

### Improvements

* `validate_uniqueness_of` now raises a NonCaseSwappableValueError if the value
  the matcher is using to test uniqueness cannot be case-swapped -- in other
  words, if it doesn't contain any alpha characters. When this is the case, the
  matcher cannot work effectively. ([#789], [ada9bd3])

[#783]: thoughtbot/shoulda-matchers#783
[8fa97b4]: thoughtbot/shoulda-matchers@8fa97b4
[#784]: thoughtbot/shoulda-matchers#784
[#789]: thoughtbot/shoulda-matchers#789
[ada9bd3]: thoughtbot/shoulda-matchers@ada9bd3
[#812]: thoughtbot/shoulda-matchers#812

# 3.0.0

### Backward-incompatible changes

* We've dropped support for Rails 3.x, Ruby 1.9.2, and Ruby 1.9.3, and RSpec 2.
  All of these have been end-of-lifed. ([a4045a1], [b7fe87a], [32c0e62])

* The gem no longer detects the test framework you're using or mixes itself into
  that framework automatically. [History][no-auto-integration-1] has
  [shown][no-auto-integration-2] that performing any kind of detection is prone
  to bugs and more complicated than it should be.

  Here are the updated instructions:

  * You no longer need to say `require: false` in your Gemfile; you can
    include the gem as normal.
  * You'll need to add the following somewhere in your `rails_helper` (for
    RSpec) or `test_helper` (for Minitest / Test::Unit):

    ``` ruby
    Shoulda::Matchers.configure do |config|
      config.integrate do |with|
        # Choose a test framework:
        with.test_framework :rspec
        with.test_framework :minitest
        with.test_framework :minitest_4
        with.test_framework :test_unit

        # Choose one or more libraries:
        with.library :active_record
        with.library :active_model
        with.library :action_controller
        # Or, choose the following (which implies all of the above):
        with.library :rails
      end
    end
    ```

  ([1900071])

* Previously, under RSpec, all of the matchers were mixed into all of the
  example groups. This created a problem because some gems, such as
  [active_model_serializers-matchers], provide matchers that share the same
  name as some of our own matchers. Now, matchers are only mixed into whichever
  example group they belong to:

    * ActiveModel and ActiveRecord matchers are available only in model example
      groups.
    * ActionController matchers are available only in controller example groups.
    * The `route` matcher is available only in routing example groups.

  ([af98a23], [8cf449b])

* There are two changes to `allow_value`:

  * The negative form of `allow_value` has been changed so that instead of
    asserting that any of the given values is an invalid value (allowing good
    values to pass through), assert that *all* values are invalid values
    (allowing good values not to pass through). This means that this test which
    formerly passed will now fail:

    ``` ruby
    expect(record).not_to allow_value('good value', *bad_values)
    ```

    ([19ce8a6])

  * `allow_value` now raises a CouldNotSetAttributeError if in setting the
    attribute, the value of the attribute from reading the attribute back is
    different from the one used to set it.

    This would happen if the writer method for that attribute has custom logic
    to ignore certain incoming values or change them in any way. Here are three
    examples we've seen:

    * You're attempting to assert that an attribute should not allow nil, yet
      the attribute's writer method contains a conditional to do nothing if
      the attribute is set to nil:

      ``` ruby
      class Foo
        include ActiveModel::Model

        attr_reader :bar

        def bar=(value)
          return if value.nil?
          @bar = value
        end
      end

      describe Foo do
        it do
          foo = Foo.new
          foo.bar = "baz"
          # This will raise a CouldNotSetAttributeError since `foo.bar` is now "123"
          expect(foo).not_to allow_value(nil).for(:bar)
        end
      end
      ```

    * You're attempting to assert that an numeric attribute should not allow a
      string that contains non-numeric characters, yet the writer method for
      that attribute strips out non-numeric characters:

      ``` ruby
      class Foo
        include ActiveModel::Model

        attr_reader :bar

        def bar=(value)
          @bar = value.gsub(/\D+/, '')
        end
      end

      describe Foo do
        it do
          foo = Foo.new
          # This will raise a CouldNotSetAttributeError since `foo.bar` is now "123"
          expect(foo).not_to allow_value("abc123").for(:bar)
        end
      end
      ```

    * You're passing a value to `allow_value` that the model typecasts into
      another value:

      ``` ruby
      describe Foo do
        # Assume that `attr` is a string
        # This will raise a CouldNotSetAttributeError since `attr` typecasts `[]` to `"[]"`
        it { should_not allow_value([]).for(:attr) }
      end
      ```

    With all of these failing examples, why are we making this change? We want
    to guard you (as the developer) from writing a test that you think acts one
    way but actually acts a different way, as this could lead to a confusing
    false positive or negative.

    If you understand the problem and wish to override this behavior so that
    you do not get a CouldNotSetAttributeError, you can add the
    `ignoring_interference_by_writer` qualifier like so. Note that this will not
    always cause the test to pass.

    ``` ruby
    it { should_not allow_value([]).for(:attr).ignoring_interference_by_writer }
    ```

    ([9d9dc4e])

* `validate_uniqueness_of` is now properly case-sensitive by default, to match
  the default behavior of the validation itself. This is a backward-incompatible
  change because this test which incorrectly passed before will now fail:

    ``` ruby
    class Product < ActiveRecord::Base
      validates_uniqueness_of :name, case_sensitive: false
    end

    describe Product do
      it { is_expected.to validate_uniqueness_of(:name) }
    end
    ```

    ([57a1922])

* `ensure_inclusion_of`, `ensure_exclusion_of`, and `ensure_length_of` have been
  removed in favor of their `validate_*` counterparts. ([55c8d09])

* `set_the_flash` and `set_session` have been changed to more closely align with
  each other:
  * `set_the_flash` has been removed in favor of `set_flash`. ([801f2c7])
  * `set_session('foo')` is no longer valid syntax, please use
    `set_session['foo']` instead. ([535fe05])
  * `set_session['key'].to(nil)` will no longer pass when the key in question
    has not been set yet. ([535fe05])

* Change `set_flash` so that `set_flash[:foo].now` is no longer valid syntax.
  You'll want to use `set_flash.now[:foo]` instead. This was changed in order to
  more closely align with how `flash.now` works when used in a controller.
  ([#755], [#752])

* Change behavior of `validate_uniqueness_of` when the matcher is not
  qualified with any scopes, but your validation is. Previously the following
  test would pass when it now fails:

  ``` ruby
  class Post < ActiveRecord::Base
    validate :slug, uniqueness: { scope: :user_id }
  end

  describe Post do
    it { should validate_uniqueness_of(:slug) }
  end
  ```

  ([6ac7b81])

[active_model_serializers-matchers]: https://github.com/adambarber/active_model_serializers-matchers
[no-auto-integration-1]: freerange/mocha@049080c
[no-auto-integration-2]: rr/rr#29
[1900071]: thoughtbot/shoulda-matchers@1900071
[b7fe87a]: thoughtbot/shoulda-matchers@b7fe87a
[a4045a1]: thoughtbot/shoulda-matchers@a4045a1
[57a1922]: thoughtbot/shoulda-matchers@57a1922
[19ce8a6]: thoughtbot/shoulda-matchers@19c38a6
[eaaa2d8]: thoughtbot/shoulda-matchers@eaaa2d8
[55c8d09]: thoughtbot/shoulda-matchers@55c8d09
[801f2c7]: thoughtbot/shoulda-matchers@801f2c7
[535fe05]: thoughtbot/shoulda-matchers@535fe05
[6ac7b81]: thoughtbot/shoulda-matchers@6ac7b81
[#755]: thoughtbot/shoulda-matchers#755
[#752]: thoughtbot/shoulda-matchers#752
[9d9dc4e]: thoughtbot/shoulda-matchers@9d9dc4e
[32c0e62]: thoughtbot/shoulda-matchers@32c0e62
[af98a23]: thoughtbot/shoulda-matchers@af98a23
[8cf449b]: thoughtbot/shoulda-matchers@8cf449b

### Bug fixes

* So far the tests for the gem have been running against only SQLite. Now they
  run against PostgreSQL, too. As a result we were able to fix some
  Postgres-related bugs, specifically around `validate_uniqueness_of`:

  * When scoped to a UUID column that ends in an "f", the matcher is able to
    generate a proper "next" value without erroring. ([#402], [#587], [#662])

  * Support scopes that are PostgreSQL array columns. Please note that this is
    only supported for Rails 4.2 and greater, as versions before this cannot
    handle array columns correctly, particularly in conjunction with the
    uniqueness validator. ([#554])

  * Fix so that when scoped to a text column and the scope is set to nil before
    running it through the matcher, the matcher does not fail. ([#521], [#607])

* Fix `define_enum_for` so that it actually tests that the attribute is present
  in the list of defined enums, as you could fool it by merely defining a class
  method that was the pluralized version of the attribute name. In the same
  vein, passing a pluralized version of the attribute name to `define_enum_for`
  would erroneously pass, and now it fails. ([#641])

* Fix `permit` so that it does not break the functionality of
  ActionController::Parameters#require. ([#648], [#675])

* Fix `validate_uniqueness_of` + `scoped_to` so that it does not raise an error
  if a record exists where the scoped attribute is nil. ([#677])

* Fix `route` matcher so if your route includes a default `format`, you can
  specify this as a symbol or string. ([#693])

* Fix `validate_uniqueness_of` so that it allows you to test against scoped
  attributes that are boolean columns. ([#457], [#694])

* Fix failure message for `validate_numericality_of` as it sometimes didn't
  provide the reason for failure. ([#699])

* Fix `shoulda/matchers/independent` so that it can be required
  independently, without having to require all of the gem. ([#746], [e0a0200])

### Features

* Add `on` qualifier to `permit`. This allows you to make an assertion that
  a restriction was placed on a slice of the `params` hash and not the entire
  `params` hash. Although we don't require you to use this qualifier, we do
  recommend it, as it's a more precise check. ([#675])

* Add `strict` qualifier to `validate_numericality_of`. ([#620])

* Add `on` qualifier to `validate_numericality_of`. ([9748869]; h/t [#356],
  [#358])

* Add `join_table` qualifier to `have_and_belong_to_many`. ([#556])

* `allow_values` is now an alias for `allow_value`. This makes more sense when
  checking against multiple values:

  ``` ruby
  it { should allow_values('this', 'and', 'that') }
  ```

  ([#692])

[9748869]: thoughtbot/shoulda-matchers@9748869
[#402]: thoughtbot/shoulda-matchers#402
[#587]: thoughtbot/shoulda-matchers#587
[#662]: thoughtbot/shoulda-matchers#662
[#554]: thoughtbot/shoulda-matchers#554
[#641]: thoughtbot/shoulda-matchers#641
[#521]: thoughtbot/shoulda-matchers#521
[#607]: thoughtbot/shoulda-matchers#607
[#648]: thoughtbot/shoulda-matchers#648
[#675]: thoughtbot/shoulda-matchers#675
[#677]: thoughtbot/shoulda-matchers#677
[#620]: thoughtbot/shoulda-matchers#620
[#693]: thoughtbot/shoulda-matchers#693
[#356]: thoughtbot/shoulda-matchers#356
[#358]: thoughtbot/shoulda-matchers#358
[#556]: thoughtbot/shoulda-matchers#556
[#457]: thoughtbot/shoulda-matchers#457
[#694]: thoughtbot/shoulda-matchers#694
[#692]: thoughtbot/shoulda-matchers#692
[#699]: thoughtbot/shoulda-matchers#699
[#746]: thoughtbot/shoulda-matchers#746
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants