0

Given that a User can have_many Addresses, I'm trying to validate that a given user can only have one address for a given address_type. For example, a user can have a primary address and a billing address, but the user cannot have two primary addresses. How do I enforce that rule on my model, and how to I test it? My current best guess is that I need to validate address_type's uniqueness scoped to user_id, but this code is preventing two addresses from existing of the same type. I've seen other people write code very similar to this, but checking on strings instead of on enums.

<!-- language: lang-ruby -->
# user.rb
class User < ApplicationRecord
  has_many :addresses
end

# address.rb
class Address < ApplicationRecord
  belongs_to :user
  enum :address_type => { :primary, :mailing, :billing }
  validates :address_type, :uniqueness => { :scope => :user_id }
end
max
  • 96,212
  • 14
  • 104
  • 165
steamingramen
  • 197
  • 1
  • 1
  • 10
  • This looks correct at first glance. Can you share the logs for the uniqueness SQL query? – Daniel Westendorf May 22 '18 at 17:57
  • 2
    Your Address model contains a syntax error. The enum declaration should read `enum address_type: [ :primary, :mailing, :billing ]`. Using brackets is only allowed if you are declaring a hash of key/values `enum address_type: { 1 => :primary, 2 => :mailing, 3 => :billing }`. – max May 22 '18 at 18:42

1 Answers1

2

The Rails uniqueness validation works perfectly fine with integer columns. However your enum definition is not valid Ruby syntax.

class Address < ApplicationRecord
  belongs_to :user
  enum :address_type => [ :primary, :mailing, :billing ]
  # or preferably with Ruby 2.0+ hash syntax
   enum address_type: [ :primary, :mailing, :billing ]
  # ...
end

You can test validations by calling .valid? on a model instance and checking the errors object:

require 'rails_helper'

RSpec.describe Address, type: :model do
  let(:user) { create(:user) }
  it "should require the user id to be unique" do
    Address.create(user: user, address_type: :primary)
    duplicate = Address.new(user: user, address_type: :primary)
    expect(duplicate.valid?).to be_falsy
    expect(duplicate.errors.full_messages_for(:address_type)).to include "Address type has already been taken"
  end
end

Beware of just testing expect(duplicate.valid?).to be_falsy and expect(duplicate.valid?) as it can lead to false positives/negatives. Instead test for the specific error message or key. Shoulda-matchers is pretty nice for this purpose but not strictly necessary.

require 'rails_helper'

RSpec.describe Address, type: :model do
  # shoulda-matchers takes care of the boilerplate
  it { should validate_uniqueness_of(:address_type).scoped_to(:user_id) }
end

You should also consider adding a compound unique index on addresses.address_type and addresses.user_id as this will prevent race issues.

max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks! I updated my enum to use the proper hash syntax and was able to get the tests to pass. However, I tried using the Shoulda-matchers and I'm getting "ArgumentError: 'an arbitrary value' is not a valid address_type". Am I missing something? – steamingramen May 22 '18 at 20:52
  • You can try setting the subject explicitly like in this question https://stackoverflow.com/questions/39325946/shoulda-matcher-how-to-validate-uniqueness-of-enum-attribute – max May 23 '18 at 17:21