Skip to content
This repository

Support :strict option for validations #151

Merged
merged 7 commits into from over 1 year ago

2 participants

Joe Ferris Dan Croak
Joe Ferris
Owner

This adds support to all validation matchers for validates! and :strict => true. Strict validations raise an exception instead of adding messages to be displayed to the user.

Dan Croak
Owner

Implementation looks great. Looks like it could use some documentation.

Joe Ferris
Owner

Good call. I'll add some docs today.

Joe Ferris jferris merged commit fde078d into from
Joe Ferris jferris closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
13  lib/shoulda/matchers/active_model.rb
... ...
@@ -1,5 +1,7 @@
1 1
 require 'shoulda/matchers/active_model/helpers'
2 2
 require 'shoulda/matchers/active_model/validation_matcher'
  3
+require 'shoulda/matchers/active_model/validation_message_finder'
  4
+require 'shoulda/matchers/active_model/exception_message_finder'
3 5
 require 'shoulda/matchers/active_model/allow_value_matcher'
4 6
 require 'shoulda/matchers/active_model/ensure_length_of_matcher'
5 7
 require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher'
@@ -27,8 +29,19 @@ module Matchers
27 29
     #     end
28 30
     #     it { should allow_value("(123) 456-7890").for(:phone_number) }
29 31
     #     it { should_not allow_mass_assignment_of(:password) }
  32
+    #     it { should allow_value('Activated', 'Pending').for(:status).strict }
  33
+    #     it { should_not allow_value('Amazing').for(:status).strict }
30 34
     #   end
31 35
     #
  36
+    # These tests work with the following model:
  37
+    #
  38
+    # class User < ActiveRecord::Base
  39
+    #   validates_presence_of :name
  40
+    #   validates_presence_of :phone_number
  41
+    #   validates_format_of :phone_number, :with => /\\(\\d{3}\\) \\d{3}\\-\\d{4}/
  42
+    #   validates_inclusion_of :status, :in => %w(Activated Pending), :strict => true
  43
+    #   attr_accessible :name, :phone_number
  44
+    # end
32 45
     module ActiveModel
33 46
     end
34 47
   end
66  lib/shoulda/matchers/active_model/allow_value_matcher.rb
@@ -11,6 +11,9 @@ module ActiveModel # :nodoc:
11 11
       # * <tt>with_message</tt> - value the test expects to find in
12 12
       #   <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
13 13
       #   the test looks for any errors in <tt>errors.on(:attribute)</tt>.
  14
+      # * <tt>strict</tt> - expects the model to raise an exception when the
  15
+      #   validation fails rather than adding to the errors collection. Used for
  16
+      #   testing `validates!` and the `:strict => true` validation options.
14 17
       #
15 18
       # Example:
16 19
       #   it { should_not allow_value('bad').for(:isbn) }
@@ -29,6 +32,7 @@ class AllowValueMatcher # :nodoc:
29 32
 
30 33
         def initialize(*values)
31 34
           @values_to_match = values
  35
+          @message_finder_factory = ValidationMessageFinder
32 36
           @options = {}
33 37
         end
34 38
 
@@ -42,6 +46,11 @@ def with_message(message)
42 46
           self
43 47
         end
44 48
 
  49
+        def strict
  50
+          @message_finder_factory = ExceptionMessageFinder
  51
+          self
  52
+        end
  53
+
45 54
         def matches?(instance)
46 55
           @instance = instance
47 56
           @values_to_match.none? do |value|
@@ -60,30 +69,29 @@ def negative_failure_message
60 69
         end
61 70
 
62 71
         def description
63  
-          "allow #{@attribute} to be set to #{allowed_values}"
  72
+          message_finder.allow_description(allowed_values)
64 73
         end
65 74
 
66 75
         private
67 76
 
68 77
         def errors_match?
69  
-          if @instance.valid?
70  
-            false
  78
+          has_messages? && errors_for_attribute_match?
  79
+        end
  80
+
  81
+        def has_messages?
  82
+          message_finder.has_messages?
  83
+        end
  84
+
  85
+        def errors_for_attribute_match?
  86
+          if expected_message
  87
+            @matched_error = errors_match_regexp? || errors_match_string?
71 88
           else
72  
-            if expected_message
73  
-              @matched_error = errors_match_regexp? || errors_match_string?
74  
-            else
75  
-              errors_for_attribute.compact.any?
76  
-            end
  89
+            errors_for_attribute.compact.any?
77 90
           end
78 91
         end
79 92
 
80 93
         def errors_for_attribute
81  
-          if @instance.errors.respond_to?(:[])
82  
-            errors = @instance.errors[@attribute]
83  
-          else
84  
-            errors = @instance.errors.on(@attribute)
85  
-          end
86  
-          Array.wrap(errors)
  94
+          message_finder.messages
87 95
         end
88 96
 
89 97
         def errors_match_regexp?
@@ -100,15 +108,15 @@ def errors_match_string?
100 108
 
101 109
         def expectation
102 110
           includes_expected_message = expected_message ? "to include #{expected_message.inspect}" : ''
103  
-          ["errors", includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
  111
+          [error_source, includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
  112
+        end
  113
+
  114
+        def error_source
  115
+          message_finder.source_description
104 116
         end
105 117
 
106 118
         def error_description
107  
-          if @instance.errors.empty?
108  
-            "no errors"
109  
-          else
110  
-            "errors: #{pretty_error_messages(@instance)}"
111  
-          end
  119
+          message_finder.messages_description
112 120
         end
113 121
 
114 122
         def allowed_values
@@ -122,16 +130,32 @@ def allowed_values
122 130
         def expected_message
123 131
           if @options.key?(:expected_message)
124 132
             if Symbol === @options[:expected_message]
125  
-              default_error_message(@options[:expected_message], :model_name => model_name, :attribute => @attribute)
  133
+              default_expected_message
126 134
             else
127 135
               @options[:expected_message]
128 136
             end
129 137
           end
130 138
         end
131 139
 
  140
+        def default_expected_message
  141
+          message_finder.expected_message_from(default_attribute_message)
  142
+        end
  143
+
  144
+        def default_attribute_message
  145
+          default_error_message(
  146
+            @options[:expected_message],
  147
+            :model_name => model_name,
  148
+            :attribute => @attribute
  149
+          )
  150
+        end
  151
+
132 152
         def model_name
133 153
           @instance.class.to_s.underscore
134 154
         end
  155
+
  156
+        def message_finder
  157
+          @message_finder ||= @message_finder_factory.new(@instance, @attribute)
  158
+        end
135 159
       end
136 160
     end
137 161
   end
58  lib/shoulda/matchers/active_model/exception_message_finder.rb
... ...
@@ -0,0 +1,58 @@
  1
+module Shoulda
  2
+  module Matchers
  3
+    module ActiveModel
  4
+
  5
+      # Finds message information from exceptions thrown by #valid?
  6
+      class ExceptionMessageFinder
  7
+        def initialize(instance, attribute)
  8
+          @instance = instance
  9
+          @attribute = attribute
  10
+        end
  11
+
  12
+        def allow_description(allowed_values)
  13
+          "doesn't raise when #{@attribute} is set to #{allowed_values}"
  14
+        end
  15
+
  16
+        def messages_description
  17
+          if has_messages?
  18
+            messages.join
  19
+          else
  20
+            'no exception'
  21
+          end
  22
+        end
  23
+
  24
+        def has_messages?
  25
+          messages.any?
  26
+        end
  27
+
  28
+        def messages
  29
+          @messages ||= validate_and_rescue
  30
+        end
  31
+
  32
+        def source_description
  33
+          'exception'
  34
+        end
  35
+
  36
+        def expected_message_from(attribute_message)
  37
+          "#{human_attribute_name} #{attribute_message}"
  38
+        end
  39
+
  40
+        private
  41
+
  42
+        def validate_and_rescue
  43
+          @instance.valid?
  44
+          []
  45
+        rescue ::ActiveModel::StrictValidationFailed => exception
  46
+          [exception.message]
  47
+        end
  48
+
  49
+        def human_attribute_name
  50
+          @instance.class.human_attribute_name(@attribute)
  51
+        end
  52
+      end
  53
+
  54
+    end
  55
+  end
  56
+end
  57
+
  58
+
35  lib/shoulda/matchers/active_model/validation_matcher.rb
@@ -6,6 +6,12 @@ class ValidationMatcher # :nodoc:
6 6
 
7 7
         def initialize(attribute)
8 8
           @attribute = attribute
  9
+          @strict = false
  10
+        end
  11
+
  12
+        def strict
  13
+          @strict = true
  14
+          self
9 15
         end
10 16
 
11 17
         def negative_failure_message
@@ -20,10 +26,8 @@ def matches?(subject)
20 26
         private
21 27
 
22 28
         def allows_value_of(value, message = nil)
23  
-          allow = AllowValueMatcher.
24  
-            new(value).
25  
-            for(@attribute).
26  
-            with_message(message)
  29
+          allow = allow_value_matcher(value, message)
  30
+
27 31
           if allow.matches?(@subject)
28 32
             @negative_failure_message = allow.failure_message
29 33
             true
@@ -34,10 +38,8 @@ def allows_value_of(value, message = nil)
34 38
         end
35 39
 
36 40
         def disallows_value_of(value, message = nil)
37  
-          disallow = AllowValueMatcher.
38  
-            new(value).
39  
-            for(@attribute).
40  
-            with_message(message)
  41
+          disallow = allow_value_matcher(value, message)
  42
+
41 43
           if disallow.matches?(@subject)
42 44
             @failure_message = disallow.negative_failure_message
43 45
             false
@@ -46,6 +48,23 @@ def disallows_value_of(value, message = nil)
46 48
             true
47 49
           end
48 50
         end
  51
+
  52
+        def allow_value_matcher(value, message)
  53
+          matcher = AllowValueMatcher.
  54
+            new(value).
  55
+            for(@attribute).
  56
+            with_message(message)
  57
+
  58
+          if strict?
  59
+            matcher.strict
  60
+          else
  61
+            matcher
  62
+          end
  63
+        end
  64
+
  65
+        def strict?
  66
+          @strict
  67
+        end
49 68
       end
50 69
     end
51 70
   end
69  lib/shoulda/matchers/active_model/validation_message_finder.rb
... ...
@@ -0,0 +1,69 @@
  1
+module Shoulda
  2
+  module Matchers
  3
+    module ActiveModel
  4
+
  5
+      # Finds message information from a model's #errors method.
  6
+      class ValidationMessageFinder
  7
+        include Helpers
  8
+
  9
+        def initialize(instance, attribute)
  10
+          @instance = instance
  11
+          @attribute = attribute
  12
+        end
  13
+
  14
+        def allow_description(allowed_values)
  15
+          "allow #{@attribute} to be set to #{allowed_values}"
  16
+        end
  17
+
  18
+        def expected_message_from(attribute_message)
  19
+          attribute_message
  20
+        end
  21
+
  22
+        def has_messages?
  23
+          errors.present?
  24
+        end
  25
+
  26
+        def source_description
  27
+          'errors'
  28
+        end
  29
+
  30
+        def messages_description
  31
+          if errors.empty?
  32
+            "no errors"
  33
+          else
  34
+            "errors: #{pretty_error_messages(validated_instance)}"
  35
+          end
  36
+        end
  37
+
  38
+        def messages
  39
+          Array.wrap(messages_for_attribute)
  40
+        end
  41
+
  42
+        private
  43
+
  44
+        def messages_for_attribute
  45
+          if errors.respond_to?(:[])
  46
+            errors[@attribute]
  47
+          else
  48
+            errors.on(@attribute)
  49
+          end
  50
+        end
  51
+
  52
+        def errors
  53
+          validated_instance.errors
  54
+        end
  55
+
  56
+        def validated_instance
  57
+          @validated_instance ||= validate_instance
  58
+        end
  59
+
  60
+        def validate_instance
  61
+          @instance.valid?
  62
+          @instance
  63
+        end
  64
+      end
  65
+
  66
+    end
  67
+  end
  68
+end
  69
+
32  spec/shoulda/active_model/allow_value_matcher_spec.rb
@@ -88,4 +88,36 @@
88 88
       end.should raise_error(ArgumentError, /at least one argument/)
89 89
     end
90 90
   end
  91
+
  92
+  if Rails::VERSION::STRING.to_f >= 3.2
  93
+    context "an attribute with a strict format validation" do
  94
+      let(:model) do
  95
+        define_model :example, :attr => :string do
  96
+          validates_format_of :attr, :with => /abc/, :strict => true
  97
+        end.new
  98
+      end
  99
+
  100
+      it "strictly rejects a bad value" do
  101
+        model.should_not allow_value("xyz").for(:attr).strict
  102
+      end
  103
+
  104
+      it "strictly allows a bad value with a different message" do
  105
+        model.should allow_value("xyz").for(:attr).with_message(/abc/).strict
  106
+      end
  107
+
  108
+      it "describes itself" do
  109
+        allow_value("xyz").for(:attr).strict.description.
  110
+          should == %{doesn't raise when attr is set to "xyz"}
  111
+      end
  112
+
  113
+      it "provides a useful negative failure message" do
  114
+        matcher = allow_value("xyz").for(:attr).strict.with_message(/abc/)
  115
+        matcher.matches?(model)
  116
+        matcher.negative_failure_message.
  117
+          should == 'Expected exception to include /abc/ ' +
  118
+            'when attr is set to "xyz", got Attr is invalid'
  119
+      end
  120
+    end
  121
+  end
  122
+
91 123
 end
18  spec/shoulda/active_model/ensure_inclusion_of_matcher_spec.rb
@@ -120,4 +120,22 @@ def custom_validation
120 120
       @model.should_not ensure_inclusion_of(:attr).in_array(['one', 'two']).allow_nil(false)
121 121
     end
122 122
   end
  123
+
  124
+  if Rails::VERSION::STRING.to_f >= 3.2
  125
+    context "a strict attribute which must be included in a range" do
  126
+      before do
  127
+        @model = define_model(:example, :attr => :integer) do
  128
+          validates_inclusion_of :attr, :in => 2..5, :strict => true
  129
+        end.new
  130
+      end
  131
+
  132
+      it "should accept ensuring the correct range" do
  133
+        @model.should ensure_inclusion_of(:attr).in_range(2..5).strict
  134
+      end
  135
+
  136
+      it "should not accept ensuring another range" do
  137
+        @model.should_not ensure_inclusion_of(:attr).in_range(2..6).strict
  138
+      end
  139
+    end
  140
+  end
123 141
 end
112  spec/shoulda/active_model/exception_message_finder_spec.rb
... ...
@@ -0,0 +1,112 @@
  1
+require 'spec_helper'
  2
+
  3
+describe Shoulda::Matchers::ActiveModel::ExceptionMessageFinder do
  4
+  if Rails::VERSION::STRING.to_f >= 3.2
  5
+    context '#allow_description' do
  6
+      it 'describes its attribute' do
  7
+        finder = build_finder(:attribute => :attr)
  8
+
  9
+        description = finder.allow_description('allowed values')
  10
+
  11
+        description.should == "doesn't raise when attr is set to allowed values"
  12
+      end
  13
+    end
  14
+
  15
+    context '#expected_message_from' do
  16
+      it 'returns the message with the attribute name prefixed' do
  17
+        finder = build_finder(:attribute => :attr)
  18
+
  19
+        message = finder.expected_message_from('some message')
  20
+
  21
+        message.should == 'Attr some message'
  22
+      end
  23
+    end
  24
+
  25
+    context '#has_messages?' do
  26
+      it 'has messages when some validations fail' do
  27
+        finder = build_finder(:format => /abc/, :value => 'xyz')
  28
+
  29
+        result = finder.has_messages?
  30
+
  31
+        result.should be_true
  32
+      end
  33
+
  34
+      it 'has no messages when all validations pass' do
  35
+        finder = build_finder(:format => /abc/, :value => 'abc')
  36
+
  37
+        result = finder.has_messages?
  38
+
  39
+        result.should be_false
  40
+      end
  41
+    end
  42
+
  43
+    context '#messages' do
  44
+      it 'returns errors for the given attribute' do
  45
+        finder = build_finder(
  46
+          :attribute => :attr,
  47
+          :format => /abc/,
  48
+          :value => 'xyz'
  49
+        )
  50
+
  51
+        messages = finder.messages
  52
+
  53
+        messages.should == ['Attr is invalid']
  54
+      end
  55
+    end
  56
+
  57
+    context '#messages_description' do
  58
+      it 'describes errors for the given attribute' do
  59
+        finder = build_finder(
  60
+          :attribute => :attr,
  61
+          :format => /abc/,
  62
+          :value => 'xyz'
  63
+        )
  64
+
  65
+        description = finder.messages_description
  66
+
  67
+        description.should == 'Attr is invalid'
  68
+      end
  69
+
  70
+      it 'describes errors when there are none' do
  71
+        finder = build_finder(:format => /abc/, :value => 'abc')
  72
+
  73
+        description = finder.messages_description
  74
+
  75
+        description.should == 'no exception'
  76
+      end
  77
+    end
  78
+
  79
+    context '#source_description' do
  80
+      it 'describes the source of its messages' do
  81
+        finder = build_finder
  82
+
  83
+        description = finder.source_description
  84
+
  85
+        description.should == 'exception'
  86
+      end
  87
+    end
  88
+  end
  89
+
  90
+  def build_finder(arguments = {})
  91
+    arguments[:attribute] ||= :attr
  92
+    instance = build_instance_validating(
  93
+      arguments[:attribute],
  94
+      arguments[:format] || /abc/,
  95
+      arguments[:value] || 'abc'
  96
+    )
  97
+    Shoulda::Matchers::ActiveModel::ExceptionMessageFinder.new(
  98
+      instance,
  99
+      arguments[:attribute]
  100
+    )
  101
+  end
  102
+
  103
+  def build_instance_validating(attribute, format, value)
  104
+    model_class = define_model(:example, attribute => :string) do
  105
+      attr_accessible attribute
  106
+      validates_format_of attribute, :with => format, :strict => true
  107
+    end
  108
+
  109
+    model_class.new(attribute => value)
  110
+  end
  111
+end
  112
+
15  spec/shoulda/active_model/validate_presence_of_matcher_spec.rb
@@ -117,4 +117,19 @@
117 117
     end
118 118
   end
119 119
 
  120
+  if Rails::VERSION::STRING.to_f >= 3.2
  121
+    context "a strictly required attribute" do
  122
+      before do
  123
+        define_model :example, :attr => :string do
  124
+          validates_presence_of :attr, :strict => true
  125
+        end
  126
+        @model = Example.new
  127
+      end
  128
+
  129
+      it "should require a value" do
  130
+        @model.should validate_presence_of(:attr).strict
  131
+      end
  132
+    end
  133
+  end
  134
+
120 135
 end
107  spec/shoulda/active_model/validation_message_finder_spec.rb
... ...
@@ -0,0 +1,107 @@
  1
+require 'spec_helper'
  2
+
  3
+describe Shoulda::Matchers::ActiveModel::ValidationMessageFinder do
  4
+  context '#allow_description' do
  5
+    it 'describes its attribute' do
  6
+      finder = build_finder(:attribute => :attr)
  7
+
  8
+      description = finder.allow_description('allowed values')
  9
+
  10
+      description.should == 'allow attr to be set to allowed values'
  11
+    end
  12
+  end
  13
+
  14
+  context '#expected_message_from' do
  15
+    it 'returns the message as-is' do
  16
+      finder = build_finder
  17
+
  18
+      message = finder.expected_message_from('some message')
  19
+
  20
+      message.should == 'some message'
  21
+    end
  22
+  end
  23
+
  24
+  context '#has_messages?' do
  25
+    it 'has messages when some validations fail' do
  26
+      finder = build_finder(:format => /abc/, :value => 'xyz')
  27
+
  28
+      result = finder.has_messages?
  29
+
  30
+      result.should be_true
  31
+    end
  32
+
  33
+    it 'has no messages when all validations pass' do
  34
+      finder = build_finder(:format => /abc/, :value => 'abc')
  35
+
  36
+      result = finder.has_messages?
  37
+
  38
+      result.should be_false
  39
+    end
  40
+  end
  41
+
  42
+  context '#messages' do
  43
+    it 'returns errors for the given attribute' do
  44
+      finder = build_finder(:format => /abc/, :value => 'xyz')
  45
+
  46
+      messages = finder.messages
  47
+
  48
+      messages.should == ['is invalid']
  49
+    end
  50
+  end
  51
+
  52
+  context '#messages_description' do
  53
+    it 'describes errors for the given attribute' do
  54
+      value = 'xyz'
  55
+      finder = build_finder(
  56
+        :attribute => :attr,
  57
+        :format => /abc/,
  58
+        :value => 'xyz'
  59
+      )
  60
+
  61
+      description = finder.messages_description
  62
+
  63
+      expected_messages = ['attr is invalid ("xyz")']
  64
+      description.should == "errors: #{expected_messages}"
  65
+    end
  66
+
  67
+    it 'describes errors when there are none' do
  68
+      finder = build_finder(:format => /abc/, :value => 'abc')
  69
+
  70
+      description = finder.messages_description
  71
+
  72
+      description.should == 'no errors'
  73
+    end
  74
+  end
  75
+
  76
+  context '#source_description' do
  77
+    it 'describes the source of its messages' do
  78
+      finder = build_finder
  79
+
  80
+      description = finder.source_description
  81
+
  82
+      description.should == 'errors'
  83
+    end
  84
+  end
  85
+
  86
+  def build_finder(arguments = {})
  87
+    arguments[:attribute] ||= :attr
  88
+    instance = build_instance_validating(
  89
+      arguments[:attribute],
  90
+      arguments[:format] || /abc/,
  91
+      arguments[:value] || 'abc'
  92
+    )
  93
+    Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(
  94
+      instance,
  95
+      arguments[:attribute]
  96
+    )
  97
+  end
  98
+
  99
+  def build_instance_validating(attribute, format, value)
  100
+    model_class = define_model(:example, attribute => :string) do
  101
+      attr_accessible attribute
  102
+      validates_format_of attribute, :with => format
  103
+    end
  104
+
  105
+    model_class.new(attribute => value)
  106
+  end
  107
+end
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.