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

Support :strict option for validations #151

Merged
merged 7 commits into from
Sep 14, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/shoulda/matchers/active_model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/validation_matcher'
require 'shoulda/matchers/active_model/validation_message_finder'
require 'shoulda/matchers/active_model/exception_message_finder'
require 'shoulda/matchers/active_model/allow_value_matcher'
require 'shoulda/matchers/active_model/ensure_length_of_matcher'
require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher'
Expand Down Expand Up @@ -27,8 +29,19 @@ module Matchers
# end
# it { should allow_value("(123) 456-7890").for(:phone_number) }
# it { should_not allow_mass_assignment_of(:password) }
# it { should allow_value('Activated', 'Pending').for(:status).strict }
# it { should_not allow_value('Amazing').for(:status).strict }
# end
#
# These tests work with the following model:
#
# class User < ActiveRecord::Base
# validates_presence_of :name
# validates_presence_of :phone_number
# validates_format_of :phone_number, :with => /\\(\\d{3}\\) \\d{3}\\-\\d{4}/
# validates_inclusion_of :status, :in => %w(Activated Pending), :strict => true
# attr_accessible :name, :phone_number
# end
module ActiveModel
end
end
Expand Down
66 changes: 45 additions & 21 deletions lib/shoulda/matchers/active_model/allow_value_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ module ActiveModel # :nodoc:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
# the test looks for any errors in <tt>errors.on(:attribute)</tt>.
# * <tt>strict</tt> - expects the model to raise an exception when the
# validation fails rather than adding to the errors collection. Used for
# testing `validates!` and the `:strict => true` validation options.
#
# Example:
# it { should_not allow_value('bad').for(:isbn) }
Expand All @@ -29,6 +32,7 @@ class AllowValueMatcher # :nodoc:

def initialize(*values)
@values_to_match = values
@message_finder_factory = ValidationMessageFinder
@options = {}
end

Expand All @@ -42,6 +46,11 @@ def with_message(message)
self
end

def strict
@message_finder_factory = ExceptionMessageFinder
self
end

def matches?(instance)
@instance = instance
@values_to_match.none? do |value|
Expand All @@ -60,30 +69,29 @@ def negative_failure_message
end

def description
"allow #{@attribute} to be set to #{allowed_values}"
message_finder.allow_description(allowed_values)
end

private

def errors_match?
if @instance.valid?
false
has_messages? && errors_for_attribute_match?
end

def has_messages?
message_finder.has_messages?
end

def errors_for_attribute_match?
if expected_message
@matched_error = errors_match_regexp? || errors_match_string?
else
if expected_message
@matched_error = errors_match_regexp? || errors_match_string?
else
errors_for_attribute.compact.any?
end
errors_for_attribute.compact.any?
end
end

def errors_for_attribute
if @instance.errors.respond_to?(:[])
errors = @instance.errors[@attribute]
else
errors = @instance.errors.on(@attribute)
end
Array.wrap(errors)
message_finder.messages
end

def errors_match_regexp?
Expand All @@ -100,15 +108,15 @@ def errors_match_string?

def expectation
includes_expected_message = expected_message ? "to include #{expected_message.inspect}" : ''
["errors", includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
[error_source, includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
end

def error_source
message_finder.source_description
end

def error_description
if @instance.errors.empty?
"no errors"
else
"errors: #{pretty_error_messages(@instance)}"
end
message_finder.messages_description
end

def allowed_values
Expand All @@ -122,16 +130,32 @@ def allowed_values
def expected_message
if @options.key?(:expected_message)
if Symbol === @options[:expected_message]
default_error_message(@options[:expected_message], :model_name => model_name, :attribute => @attribute)
default_expected_message
else
@options[:expected_message]
end
end
end

def default_expected_message
message_finder.expected_message_from(default_attribute_message)
end

def default_attribute_message
default_error_message(
@options[:expected_message],
:model_name => model_name,
:attribute => @attribute
)
end

def model_name
@instance.class.to_s.underscore
end

def message_finder
@message_finder ||= @message_finder_factory.new(@instance, @attribute)
end
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions lib/shoulda/matchers/active_model/exception_message_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Shoulda
module Matchers
module ActiveModel

# Finds message information from exceptions thrown by #valid?
class ExceptionMessageFinder
def initialize(instance, attribute)
@instance = instance
@attribute = attribute
end

def allow_description(allowed_values)
"doesn't raise when #{@attribute} is set to #{allowed_values}"
end

def messages_description
if has_messages?
messages.join
else
'no exception'
end
end

def has_messages?
messages.any?
end

def messages
@messages ||= validate_and_rescue
end

def source_description
'exception'
end

def expected_message_from(attribute_message)
"#{human_attribute_name} #{attribute_message}"
end

private

def validate_and_rescue
@instance.valid?
[]
rescue ::ActiveModel::StrictValidationFailed => exception
[exception.message]
end

def human_attribute_name
@instance.class.human_attribute_name(@attribute)
end
end

end
end
end


35 changes: 27 additions & 8 deletions lib/shoulda/matchers/active_model/validation_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ class ValidationMatcher # :nodoc:

def initialize(attribute)
@attribute = attribute
@strict = false
end

def strict
@strict = true
self
end

def negative_failure_message
Expand All @@ -20,10 +26,8 @@ def matches?(subject)
private

def allows_value_of(value, message = nil)
allow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
allow = allow_value_matcher(value, message)

if allow.matches?(@subject)
@negative_failure_message = allow.failure_message
true
Expand All @@ -34,10 +38,8 @@ def allows_value_of(value, message = nil)
end

def disallows_value_of(value, message = nil)
disallow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
disallow = allow_value_matcher(value, message)

if disallow.matches?(@subject)
@failure_message = disallow.negative_failure_message
false
Expand All @@ -46,6 +48,23 @@ def disallows_value_of(value, message = nil)
true
end
end

def allow_value_matcher(value, message)
matcher = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)

if strict?
matcher.strict
else
matcher
end
end

def strict?
@strict
end
end
end
end
Expand Down
69 changes: 69 additions & 0 deletions lib/shoulda/matchers/active_model/validation_message_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Shoulda
module Matchers
module ActiveModel

# Finds message information from a model's #errors method.
class ValidationMessageFinder
include Helpers

def initialize(instance, attribute)
@instance = instance
@attribute = attribute
end

def allow_description(allowed_values)
"allow #{@attribute} to be set to #{allowed_values}"
end

def expected_message_from(attribute_message)
attribute_message
end

def has_messages?
errors.present?
end

def source_description
'errors'
end

def messages_description
if errors.empty?
"no errors"
else
"errors: #{pretty_error_messages(validated_instance)}"
end
end

def messages
Array.wrap(messages_for_attribute)
end

private

def messages_for_attribute
if errors.respond_to?(:[])
errors[@attribute]
else
errors.on(@attribute)
end
end

def errors
validated_instance.errors
end

def validated_instance
@validated_instance ||= validate_instance
end

def validate_instance
@instance.valid?
@instance
end
end

end
end
end

32 changes: 32 additions & 0 deletions spec/shoulda/active_model/allow_value_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,36 @@
end.should raise_error(ArgumentError, /at least one argument/)
end
end

if Rails::VERSION::STRING.to_f >= 3.2
context "an attribute with a strict format validation" do
let(:model) do
define_model :example, :attr => :string do
validates_format_of :attr, :with => /abc/, :strict => true
end.new
end

it "strictly rejects a bad value" do
model.should_not allow_value("xyz").for(:attr).strict
end

it "strictly allows a bad value with a different message" do
model.should allow_value("xyz").for(:attr).with_message(/abc/).strict
end

it "describes itself" do
allow_value("xyz").for(:attr).strict.description.
should == %{doesn't raise when attr is set to "xyz"}
end

it "provides a useful negative failure message" do
matcher = allow_value("xyz").for(:attr).strict.with_message(/abc/)
matcher.matches?(model)
matcher.negative_failure_message.
should == 'Expected exception to include /abc/ ' +
'when attr is set to "xyz", got Attr is invalid'
end
end
end

end