diff --git a/NEWS.md b/NEWS.md index 68e8ed47..4b1f30ae 100644 --- a/NEWS.md +++ b/NEWS.md @@ -41,6 +41,8 @@ ### Features * Add ability to test `:primary_key` option on associations. ([#597]) +* Add `allow_blank` qualifier to `validate_uniqueness_of` to complement + the `allow_blank` option. ### Improvements @@ -125,8 +127,6 @@ from `validate_uniqueness_of`, your best bet continues to be creating a record manually and calling `validate_uniqueness_of` on that instead. -### Improvements - * The majority of warnings that the gem produced have been removed. The gem still produces warnings under Ruby 1.9.3; we will address this in a future release. diff --git a/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb b/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb index 2f4b8cb5..6a77d311 100644 --- a/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +++ b/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb @@ -171,6 +171,26 @@ module Shoulda # # @return [ValidateUniquenessOfMatcher] # + # ##### allow_blank + # + # Use `allow_blank` to assert that the attribute allows a blank value. + # + # class Post < ActiveRecord::Base + # validates_uniqueness_of :author_id, allow_blank: true + # end + # + # # RSpec + # describe Post do + # it { should validate_uniqueness_of(:author_id).allow_blank } + # end + # + # # Test::Unit + # class PostTest < ActiveSupport::TestCase + # should validate_uniqueness_of(:author_id).allow_blank + # end + # + # @return [ValidateUniquenessOfMatcher] + # def validate_uniqueness_of(attr) ValidateUniquenessOfMatcher.new(attr) end @@ -204,6 +224,11 @@ module Shoulda self end + def allow_blank + @options[:allow_blank] = true + self + end + def description result = "require " result << "case sensitive " unless @options[:case_insensitive] @@ -218,9 +243,10 @@ module Shoulda @expected_message ||= :taken set_scoped_attributes && - validate_everything_except_duplicate_nils? && + validate_everything_except_duplicate_nils_or_blanks? && validate_after_scope_change? && - allows_nil? + allows_nil? && + allows_blank? ensure Uniqueness::TestModels.remove_all end @@ -236,6 +262,15 @@ module Shoulda end end + def allows_blank? + if @options[:allow_blank] + ensure_blank_record_in_database + allows_value_of('', @expected_message) + else + true + end + end + def existing_record @existing_record ||= first_instance end @@ -250,19 +285,23 @@ module Shoulda end end + def ensure_blank_record_in_database + unless existing_record_is_blank? + create_record_in_database(blank_value: true) + end + end + def existing_record_is_nil? @existing_record.present? && existing_value.nil? end - def create_record_in_database(options = {}) - if options[:nil_value] - value = nil - else - value = 'a' - end + def existing_record_is_blank? + @existing_record.present? && existing_value.strip == '' + end + def create_record_in_database(options = {}) @original_subject.tap do |instance| - instance.__send__("#{@attribute}=", value) + instance.__send__("#{@attribute}=", value_for_new_record(options)) ensure_secure_password_set(instance) instance.save(validate: false) @created_record = instance @@ -276,6 +315,14 @@ module Shoulda end end + def value_for_new_record(options = {}) + case + when options[:nil_value] then nil + when options[:blank_value] then '' + else 'a' + end + end + def has_secure_password? @subject.class.ancestors.map(&:to_s).include?('ActiveModel::SecurePassword::InstanceMethodsOnActivation') end @@ -297,15 +344,16 @@ module Shoulda end end - def validate_everything_except_duplicate_nils? - if @options[:allow_nil] && existing_value.nil? - create_record_without_nil + def validate_everything_except_duplicate_nils_or_blanks? + if (@options[:allow_nil] && existing_value.nil?) || + (@options[:allow_blank] && existing_value.blank?) + create_record_with_value end disallows_value_of(existing_value, @expected_message) end - def create_record_without_nil + def create_record_with_value @existing_record = create_record_in_database end diff --git a/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb index b0dac4d8..78223a88 100644 --- a/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb @@ -418,6 +418,81 @@ describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do end end + context 'when the validation allows blank' do + context 'when there is an existing record with a blank value' do + it 'accepts' do + model = model_allowing_blank + model.create!(attribute_name => '') + expect(model.new).to matcher.allow_blank + end + end + + context 'when there is not an an existing record with a blank value' do + it 'still accepts' do + expect(record_allowing_blank).to matcher.allow_blank + end + + it 'automatically creates a record' do + model = model_allowing_blank + matcher.allow_blank.matches?(model.new) + + record_created = model.all.any? do |instance| + instance.__send__(attribute_name).blank? + end + + expect(record_created).to be true + end + end + + def attribute_name + :attr + end + + def model_allowing_blank + _attribute_name = attribute_name + + define_model(:example, attribute_name => :string) do + attr_accessible _attribute_name + validates_uniqueness_of _attribute_name, allow_blank: true + end + end + + def record_allowing_blank + model_allowing_blank.new + end + end + + context 'when the validation does not allow blank' do + context 'when there is an existing entry with a blank value' do + it 'rejects' do + model = model_disallowing_blank + model.create!(attribute_name => '') + expect(model.new).not_to matcher.allow_blank + end + end + + it 'should not allow_blank' do + expect(record_disallowing_blank).not_to matcher.allow_blank + end + + def attribute_name + :attr + end + + def model_disallowing_blank + _attribute_name = attribute_name + + define_model(:example, attribute_name => :string) do + attr_accessible _attribute_name + validates_uniqueness_of _attribute_name, allow_blank: false + end + end + + def record_disallowing_blank + model_disallowing_blank.new + end + end + context "when testing that a polymorphic *_type column is one of the validation scopes" do it "sets that column to a meaningful value that works with other validations on the same column" do user_model = define_model :user