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 does not account for reliance on other records in other validations #1435

Open
brett-puddick opened this issue Mar 31, 2021 · 2 comments

Comments

@brett-puddick
Copy link

So, let's say we have:

# app/models/product.rb
class Product < ApplicationRecord
    has_many :prices

    validates :code, presence: true, uniqueness: true

    validate :custom_validation
    
    def custom_validation
        prices.length > 1
    end
 end

and then we want to test the uniqueness validation:

# spec/app/models/product.rb
...
describe 'validations' do
    it 'ensures that code is unique' do
        product = Product.new(
            code: '123',
            prices: [Price.first, Price.second]
        )

        expect(product).to validate_uniqueness_of(:code)
    end
end

The observed behaviour of what happens in this test is that the matcher takes the first record in the DB (not necessarily the one you provided), builds a new instance of it (without saving) and then goes about its checks manipulating attribute values and asserting if it's valid.

This approach does not take into concern that other validations may fail, and hence the test, as the model has reliances on other data outside of this record. The matcher should either use the record provided (with an expectation that it passes all other required validations) or also build new instances of any associations when grabbing the first record from the DB.

@mcmire
Copy link
Collaborator

mcmire commented Mar 31, 2021

Hi @brett-puddick,

In general shoulda-matchers does not do anything to fill in attributes on records, so whenever working with validation matchers it works a lot better if you always use a record that satisfies all of the validations. That said, the uniqueness matcher is a weird case, and I agree that it would make more sense to prefer making use of the given record rather than always relying on other data outside of it. I think that particular behavior is a vestige from a long time ago. In fact I remember that someone else raised this issue a while back and I gave some thoughts on it then: #1067. Even though the matcher works in a weird way, I am now not sure whether it now be a good idea to change it fundamentally, as it would technically be a breaking change, and we're more or less feature frozen on the next release. That said, one idea I just thought of would be to change the behavior, but then make it opt-in. That is, add a new configuration option to Shoulda::Matchers.configure, and if it is set, then it will enable the new behavior. This would allow us to deprecate and eventually remove the old behavior at some point in the future in a less surprising manner. What do you think about this?

@myxoh
Copy link

myxoh commented Oct 20, 2022

The workaround fails if your scope includes the model.

For example this fails for me:

model Account
  belongs_to :organization
  validates :email uniqueness: { case_sensitive: false, scope: :organization_id }
  validates :payment_account, presence: true, on: :create, if: -> { organization.requires_payment? }
end
RSpec.describe Account, type: :model do
  subject(:account) { build(:account) }

  describe 'validations' do
    it do
      account.organization = create(:organization)
      is_expected.to validate_uniqueness_of(:email).case_insensitive.scoped_to(:organization_id)
    end
  end
end

as the matcher is setting (on one of the runs) an invalid organization_id and hence the conditional check on payment_account is failing.

Debugging what the matcher was doing - it run perform_validation 5 times - the last time the record had an organization_id but it did not reference a valid organization hence organization evaluated to nil. I also checked there was only one valid organization model at the time of the spec running (which had a different ID than the organization_id on the Account model).

For now I'm just removing the validation / spec alltogether as I couldn't find a useful workaround using shoulda-matchers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants