francois-shoulda 2.0.5.4 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +60 -10
- data/Rakefile +7 -7
- data/lib/shoulda.rb +7 -15
- data/lib/shoulda/action_controller.rb +28 -0
- data/lib/shoulda/action_controller/helpers.rb +47 -0
- data/lib/shoulda/action_controller/macros.rb +277 -0
- data/lib/shoulda/action_controller/matchers.rb +37 -0
- data/lib/shoulda/action_controller/matchers/assign_to_matcher.rb +109 -0
- data/lib/shoulda/action_controller/matchers/filter_param_matcher.rb +57 -0
- data/lib/shoulda/action_controller/matchers/render_with_layout_matcher.rb +81 -0
- data/lib/shoulda/action_controller/matchers/respond_with_content_type_matcher.rb +70 -0
- data/lib/shoulda/action_controller/matchers/respond_with_matcher.rb +81 -0
- data/lib/shoulda/action_controller/matchers/route_matcher.rb +93 -0
- data/lib/shoulda/action_controller/matchers/set_session_matcher.rb +87 -0
- data/lib/shoulda/action_controller/matchers/set_the_flash_matcher.rb +85 -0
- data/lib/shoulda/action_mailer.rb +1 -1
- data/lib/shoulda/action_mailer/assertions.rb +32 -33
- data/lib/shoulda/action_view.rb +10 -0
- data/lib/shoulda/action_view/macros.rb +56 -0
- data/lib/shoulda/active_record.rb +6 -2
- data/lib/shoulda/active_record/assertions.rb +62 -89
- data/lib/shoulda/active_record/helpers.rb +40 -0
- data/lib/shoulda/active_record/macros.rb +520 -684
- data/lib/shoulda/active_record/matchers.rb +42 -0
- data/lib/shoulda/active_record/matchers/allow_mass_assignment_of_matcher.rb +83 -0
- data/lib/shoulda/active_record/matchers/allow_value_matcher.rb +102 -0
- data/lib/shoulda/active_record/matchers/association_matcher.rb +226 -0
- data/lib/shoulda/active_record/matchers/ensure_inclusion_of_matcher.rb +87 -0
- data/lib/shoulda/active_record/matchers/ensure_length_of_matcher.rb +141 -0
- data/lib/shoulda/active_record/matchers/have_db_column_matcher.rb +169 -0
- data/lib/shoulda/active_record/matchers/have_index_matcher.rb +105 -0
- data/lib/shoulda/active_record/matchers/have_named_scope_matcher.rb +125 -0
- data/lib/shoulda/active_record/matchers/have_readonly_attribute_matcher.rb +59 -0
- data/lib/shoulda/active_record/matchers/validate_acceptance_of_matcher.rb +41 -0
- data/lib/shoulda/active_record/matchers/validate_numericality_of_matcher.rb +39 -0
- data/lib/shoulda/active_record/matchers/validate_presence_of_matcher.rb +60 -0
- data/lib/shoulda/active_record/matchers/validate_uniqueness_of_matcher.rb +148 -0
- data/lib/shoulda/active_record/matchers/validation_matcher.rb +56 -0
- data/lib/shoulda/assertions.rb +50 -40
- data/lib/shoulda/autoload_macros.rb +46 -0
- data/lib/shoulda/context.rb +124 -126
- data/lib/shoulda/helpers.rb +5 -7
- data/lib/shoulda/macros.rb +63 -64
- data/lib/shoulda/private_helpers.rb +16 -18
- data/lib/shoulda/rails.rb +5 -11
- data/lib/shoulda/rspec.rb +11 -0
- data/lib/shoulda/tasks/list_tests.rake +6 -1
- data/lib/shoulda/test_unit.rb +19 -0
- data/rails/init.rb +7 -1
- data/test/README +2 -2
- data/test/fail_macros.rb +15 -15
- data/test/fixtures/tags.yml +1 -1
- data/test/functional/posts_controller_test.rb +46 -26
- data/test/functional/users_controller_test.rb +0 -19
- data/test/matchers/active_record/allow_mass_assignment_of_matcher_test.rb +68 -0
- data/test/matchers/active_record/allow_value_matcher_test.rb +41 -0
- data/test/matchers/active_record/association_matcher_test.rb +258 -0
- data/test/matchers/active_record/ensure_inclusion_of_matcher_test.rb +80 -0
- data/test/matchers/active_record/ensure_length_of_matcher_test.rb +158 -0
- data/test/matchers/active_record/have_db_column_matcher_test.rb +169 -0
- data/test/matchers/active_record/have_index_matcher_test.rb +74 -0
- data/test/matchers/active_record/have_named_scope_matcher_test.rb +65 -0
- data/test/matchers/active_record/have_readonly_attributes_matcher_test.rb +29 -0
- data/test/matchers/active_record/validate_acceptance_of_matcher_test.rb +44 -0
- data/test/matchers/active_record/validate_numericality_of_matcher_test.rb +52 -0
- data/test/matchers/active_record/validate_presence_of_matcher_test.rb +86 -0
- data/test/matchers/active_record/validate_uniqueness_of_matcher_test.rb +147 -0
- data/test/matchers/controller/assign_to_matcher_test.rb +35 -0
- data/test/matchers/controller/filter_param_matcher_test.rb +32 -0
- data/test/matchers/controller/render_with_layout_matcher_test.rb +33 -0
- data/test/matchers/controller/respond_with_content_type_matcher_test.rb +27 -0
- data/test/matchers/controller/respond_with_matcher_test.rb +106 -0
- data/test/matchers/controller/route_matcher_test.rb +58 -0
- data/test/matchers/controller/set_session_matcher_test.rb +31 -0
- data/test/matchers/controller/set_the_flash_matcher.rb +41 -0
- data/test/model_builder.rb +106 -0
- data/test/other/autoload_macro_test.rb +18 -0
- data/test/other/helpers_test.rb +58 -0
- data/test/other/private_helpers_test.rb +1 -1
- data/test/other/should_test.rb +16 -16
- data/test/rails_root/app/controllers/posts_controller.rb +6 -5
- data/test/rails_root/app/models/pets/dog.rb +10 -0
- data/test/rails_root/app/models/treat.rb +3 -0
- data/test/rails_root/app/models/user.rb +4 -3
- data/test/rails_root/app/views/layouts/posts.rhtml +2 -0
- data/test/rails_root/config/database.yml +1 -1
- data/test/rails_root/config/environment.rb +1 -1
- data/test/rails_root/config/environments/{sqlite3.rb → test.rb} +0 -0
- data/test/rails_root/db/migrate/001_create_users.rb +3 -2
- data/test/rails_root/db/migrate/011_create_treats.rb +12 -0
- data/test/rails_root/test/shoulda_macros/custom_macro.rb +6 -0
- data/test/rails_root/vendor/gems/gem_with_macro-0.0.1/shoulda_macros/gem_macro.rb +6 -0
- data/test/rails_root/vendor/plugins/plugin_with_macro/shoulda_macros/plugin_macro.rb +6 -0
- data/test/rspec_test.rb +207 -0
- data/test/test_helper.rb +3 -1
- data/test/unit/address_test.rb +1 -23
- data/test/unit/dog_test.rb +5 -2
- data/test/unit/post_test.rb +7 -3
- data/test/unit/product_test.rb +2 -2
- data/test/unit/tag_test.rb +2 -1
- data/test/unit/user_test.rb +25 -9
- metadata +84 -23
- data/lib/shoulda/controller.rb +0 -30
- data/lib/shoulda/controller/formats/html.rb +0 -201
- data/lib/shoulda/controller/formats/xml.rb +0 -170
- data/lib/shoulda/controller/helpers.rb +0 -64
- data/lib/shoulda/controller/macros.rb +0 -316
- data/lib/shoulda/controller/resource_options.rb +0 -236
- data/test/rails_root/app/models/dog.rb +0 -5
| @@ -0,0 +1,125 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensures that the model has a method named scope_call that returns a
         | 
| 6 | 
            +
                  # NamedScope object with the proxy options set to the options you supply.
         | 
| 7 | 
            +
                  # scope_call can be either a symbol, or a Ruby expression in a String
         | 
| 8 | 
            +
                  # which will be evaled. The eval'd method call has access to all the same
         | 
| 9 | 
            +
                  # instance variables that an example would.
         | 
| 10 | 
            +
                  #
         | 
| 11 | 
            +
                  # Options:
         | 
| 12 | 
            +
                  #
         | 
| 13 | 
            +
                  #  * <tt>in_context</tt> - Any of the options that the named scope would
         | 
| 14 | 
            +
                  #    pass on to find.
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # Example:
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  #   it { should have_named_scope(:visible).
         | 
| 19 | 
            +
                  #                 finding(:conditions => {:visible => true}) }
         | 
| 20 | 
            +
                  #
         | 
| 21 | 
            +
                  # Passes for
         | 
| 22 | 
            +
                  #
         | 
| 23 | 
            +
                  #   named_scope :visible, :conditions => {:visible => true}
         | 
| 24 | 
            +
                  #
         | 
| 25 | 
            +
                  # Or for
         | 
| 26 | 
            +
                  #
         | 
| 27 | 
            +
                  #   def self.visible
         | 
| 28 | 
            +
                  #     scoped(:conditions => {:visible => true})
         | 
| 29 | 
            +
                  #   end
         | 
| 30 | 
            +
                  #
         | 
| 31 | 
            +
                  # You can test lambdas or methods that return ActiveRecord#scoped calls:
         | 
| 32 | 
            +
                  #
         | 
| 33 | 
            +
                  #   it { should have_named_scope('recent(5)').finding(:limit => 5) }
         | 
| 34 | 
            +
                  #   it { should have_named_scope('recent(1)').finding(:limit => 1) }
         | 
| 35 | 
            +
                  #
         | 
| 36 | 
            +
                  # Passes for
         | 
| 37 | 
            +
                  #   named_scope :recent, lambda {|c| {:limit => c}}
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # Or for
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  #   def self.recent(c)
         | 
| 42 | 
            +
                  #     scoped(:limit => c)
         | 
| 43 | 
            +
                  #   end
         | 
| 44 | 
            +
                  #
         | 
| 45 | 
            +
                  def have_named_scope(scope_call)
         | 
| 46 | 
            +
                    HaveNamedScopeMatcher.new(scope_call).in_context(self)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  class HaveNamedScopeMatcher # :nodoc:
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    def initialize(scope_call)
         | 
| 52 | 
            +
                      @scope_call = scope_call.to_s
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    def finding(finding)
         | 
| 56 | 
            +
                      @finding = finding
         | 
| 57 | 
            +
                      self
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    def in_context(context)
         | 
| 61 | 
            +
                      @context = context
         | 
| 62 | 
            +
                      self
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    def matches?(subject)
         | 
| 66 | 
            +
                      @subject = subject
         | 
| 67 | 
            +
                      call_succeeds? && returns_scope? && finds_correct_scope?
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    def failure_message
         | 
| 71 | 
            +
                      "Expected #{@missing_expectation}"
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    def negative_failure_message
         | 
| 75 | 
            +
                      "Didn't expect a named scope for #{@scope_call}"
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    def description
         | 
| 79 | 
            +
                      result = "have a named scope for #{@scope_call}"
         | 
| 80 | 
            +
                      result << " finding #{@finding.inspect}" unless @finding.nil?
         | 
| 81 | 
            +
                      result
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    private
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                    def call_succeeds?
         | 
| 87 | 
            +
                      scope
         | 
| 88 | 
            +
                      true
         | 
| 89 | 
            +
                    rescue Exception => exception
         | 
| 90 | 
            +
                      @missing_expectation = "#{@subject.class.name} " <<
         | 
| 91 | 
            +
                        "to respond to #{@scope_call} " <<
         | 
| 92 | 
            +
                        "but raised error: #{exception.inspect}"
         | 
| 93 | 
            +
                      false
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    def scope
         | 
| 97 | 
            +
                      @scope ||= @context.instance_eval("#{@subject.class.name}.#{@scope_call}")
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    def returns_scope?
         | 
| 101 | 
            +
                      if ::ActiveRecord::NamedScope::Scope === scope
         | 
| 102 | 
            +
                        true
         | 
| 103 | 
            +
                      else
         | 
| 104 | 
            +
                        @missing_expectation = "#{@scope_call} to return a scope"
         | 
| 105 | 
            +
                        false
         | 
| 106 | 
            +
                      end
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def finds_correct_scope?
         | 
| 110 | 
            +
                      return true if @finding.nil?
         | 
| 111 | 
            +
                      if @finding == scope.proxy_options
         | 
| 112 | 
            +
                        true
         | 
| 113 | 
            +
                      else
         | 
| 114 | 
            +
                        @missing_expectation = "#{@scope_call} to return results scoped to "
         | 
| 115 | 
            +
                        @missing_expectation << "#{@finding.inspect} but was scoped to "
         | 
| 116 | 
            +
                        @missing_expectation << scope.proxy_options.inspect
         | 
| 117 | 
            +
                        false
         | 
| 118 | 
            +
                      end
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensures that the attribute cannot be changed once the record has been
         | 
| 6 | 
            +
                  # created.
         | 
| 7 | 
            +
                  #
         | 
| 8 | 
            +
                  #   it { should have_readonly_attributes(:password) }
         | 
| 9 | 
            +
                  #
         | 
| 10 | 
            +
                  def have_readonly_attribute(value)
         | 
| 11 | 
            +
                    HaveReadonlyAttributeMatcher.new(value)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  class HaveReadonlyAttributeMatcher # :nodoc:
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    def initialize(attribute)
         | 
| 17 | 
            +
                      @attribute = attribute.to_s
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    def matches?(subject)
         | 
| 21 | 
            +
                      @subject = subject
         | 
| 22 | 
            +
                      if readonly_attributes.include?(@attribute)
         | 
| 23 | 
            +
                        @negative_failure_message =
         | 
| 24 | 
            +
                          "Did not expect #{@attribute} to be read-only"
         | 
| 25 | 
            +
                        true
         | 
| 26 | 
            +
                      else
         | 
| 27 | 
            +
                        if readonly_attributes.empty?
         | 
| 28 | 
            +
                          @failure_message = "#{class_name} attribute #{@attribute} " <<
         | 
| 29 | 
            +
                            "is not read-only"
         | 
| 30 | 
            +
                        else
         | 
| 31 | 
            +
                          @failure_message = "#{class_name} is making " <<
         | 
| 32 | 
            +
                            "#{readonly_attributes.to_sentence} " <<
         | 
| 33 | 
            +
                            "read-only, but not #{@attribute}."
         | 
| 34 | 
            +
                        end
         | 
| 35 | 
            +
                        false
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    attr_reader :failure_message, :negative_failure_message
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def description
         | 
| 42 | 
            +
                      "make #{@attribute} read-only"
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def readonly_attributes
         | 
| 48 | 
            +
                      @readonly_attributes ||= (@subject.class.readonly_attributes || [])
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    def class_name
         | 
| 52 | 
            +
                      @subject.class.name
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensures that the model cannot be saved the given attribute is not
         | 
| 6 | 
            +
                  # accepted.
         | 
| 7 | 
            +
                  #
         | 
| 8 | 
            +
                  # Options:
         | 
| 9 | 
            +
                  # * <tt>with_message</tt> - value the test expects to find in
         | 
| 10 | 
            +
                  #   <tt>errors.on(:attribute)</tt>. Regexp or string.  Defaults to the
         | 
| 11 | 
            +
                  #   translation for <tt>:accepted</tt>.
         | 
| 12 | 
            +
                  #
         | 
| 13 | 
            +
                  # Example:
         | 
| 14 | 
            +
                  #   it { should validate_acceptance_of(:eula) }
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  def validate_acceptance_of(attr)
         | 
| 17 | 
            +
                    ValidateAcceptanceOfMatcher.new(attr)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  class ValidateAcceptanceOfMatcher < ValidationMatcher # :nodoc:
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def with_message(message)
         | 
| 23 | 
            +
                      @expected_message = message if message
         | 
| 24 | 
            +
                      self
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    def matches?(subject)
         | 
| 28 | 
            +
                      super(subject)
         | 
| 29 | 
            +
                      @expected_message ||= :accepted
         | 
| 30 | 
            +
                      disallows_value_of(false, @expected_message)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    def description
         | 
| 34 | 
            +
                      "require #{@attribute} to be accepted"
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensure that the attribute is numeric
         | 
| 6 | 
            +
                  #
         | 
| 7 | 
            +
                  # Options:
         | 
| 8 | 
            +
                  # * <tt>with_message</tt> - value the test expects to find in
         | 
| 9 | 
            +
                  #   <tt>errors.on(:attribute)</tt>. Regexp or string.  Defaults to the
         | 
| 10 | 
            +
                  #   translation for <tt>:not_a_number</tt>.
         | 
| 11 | 
            +
                  #
         | 
| 12 | 
            +
                  # Example:
         | 
| 13 | 
            +
                  #   it { should validate_numericality_of(:age) }
         | 
| 14 | 
            +
                  #
         | 
| 15 | 
            +
                  def validate_numericality_of(attr)
         | 
| 16 | 
            +
                    ValidateNumericalityOfMatcher.new(attr)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  class ValidateNumericalityOfMatcher < ValidationMatcher # :nodoc:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def with_message(message)
         | 
| 22 | 
            +
                      @expected_message = message if message
         | 
| 23 | 
            +
                      self
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def matches?(subject)
         | 
| 27 | 
            +
                      super(subject)
         | 
| 28 | 
            +
                      @expected_message ||= :not_a_number
         | 
| 29 | 
            +
                      disallows_value_of('abcd', @expected_message)
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    def description
         | 
| 33 | 
            +
                      "only allow numeric values for #{@attribute}"
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensures that the model is not valid if the given attribute is not
         | 
| 6 | 
            +
                  # present.
         | 
| 7 | 
            +
                  #
         | 
| 8 | 
            +
                  # Options:
         | 
| 9 | 
            +
                  # * <tt>with_message</tt> - value the test expects to find in
         | 
| 10 | 
            +
                  #   <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
         | 
| 11 | 
            +
                  #   Defaults to the translation for <tt>:blank</tt>.
         | 
| 12 | 
            +
                  #
         | 
| 13 | 
            +
                  # Examples:
         | 
| 14 | 
            +
                  #   it { should validate_presence_of(:name) }
         | 
| 15 | 
            +
                  #   it { should validate_presence_of(:name).
         | 
| 16 | 
            +
                  #                 with_message(/is not optional/) }
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  def validate_presence_of(attr)
         | 
| 19 | 
            +
                    ValidatePresenceOfMatcher.new(attr)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  class ValidatePresenceOfMatcher < ValidationMatcher # :nodoc:
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    def with_message(message)
         | 
| 25 | 
            +
                      @expected_message = message if message
         | 
| 26 | 
            +
                      self
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    def matches?(subject)
         | 
| 30 | 
            +
                      super(subject)
         | 
| 31 | 
            +
                      @expected_message ||= :blank
         | 
| 32 | 
            +
                      disallows_value_of(blank_value, @expected_message)
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    def description
         | 
| 36 | 
            +
                      "require #{@attribute} to be set"
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def blank_value
         | 
| 42 | 
            +
                      if collection?
         | 
| 43 | 
            +
                        []
         | 
| 44 | 
            +
                      else
         | 
| 45 | 
            +
                        nil
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    def collection?
         | 
| 50 | 
            +
                      if reflection = @subject.class.reflect_on_association(@attribute)
         | 
| 51 | 
            +
                        [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
         | 
| 52 | 
            +
                      else
         | 
| 53 | 
            +
                        false
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| @@ -0,0 +1,148 @@ | |
| 1 | 
            +
            module Shoulda # :nodoc:
         | 
| 2 | 
            +
              module ActiveRecord # :nodoc:
         | 
| 3 | 
            +
                module Matchers
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Ensures that the model is invalid if the given attribute is not unique.
         | 
| 6 | 
            +
                  #
         | 
| 7 | 
            +
                  # Internally, this uses values from existing records to test validations,
         | 
| 8 | 
            +
                  # so this will always fail if you have not saved at least one record for
         | 
| 9 | 
            +
                  # the model being tested, like so:
         | 
| 10 | 
            +
                  #
         | 
| 11 | 
            +
                  #   describe User do
         | 
| 12 | 
            +
                  #     before(:each) { User.create!(:email => 'address@example.com') }
         | 
| 13 | 
            +
                  #     it { should validate_uniqueness_of(:email) }
         | 
| 14 | 
            +
                  #   end
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # Options:
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  # * <tt>with_message</tt> - value the test expects to find in
         | 
| 19 | 
            +
                  #   <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
         | 
| 20 | 
            +
                  #   Defaults to the translation for <tt>:taken</tt>.
         | 
| 21 | 
            +
                  # * <tt>scoped_to</tt> - field(s) to scope the uniqueness to.
         | 
| 22 | 
            +
                  # * <tt>case_insensitive</tt> - ensures that the validation does not
         | 
| 23 | 
            +
                  #   check case. Off by default. Ignored by non-text attributes.
         | 
| 24 | 
            +
                  #
         | 
| 25 | 
            +
                  # Examples:
         | 
| 26 | 
            +
                  #   it { should validate_uniqueness_of(:keyword) }
         | 
| 27 | 
            +
                  #   it { should validate_uniqueness_of(:keyword).with_message(/dup/) }
         | 
| 28 | 
            +
                  #   it { should validate_uniqueness_of(:email).scoped_to(:name) }
         | 
| 29 | 
            +
                  #   it { should validate_uniqueness_of(:email).
         | 
| 30 | 
            +
                  #                 scoped_to(:first_name, :last_name) }
         | 
| 31 | 
            +
                  #   it { should validate_uniqueness_of(:keyword).case_insensitive }
         | 
| 32 | 
            +
                  #
         | 
| 33 | 
            +
                  def validate_uniqueness_of(attr)
         | 
| 34 | 
            +
                    ValidateUniquenessOfMatcher.new(attr)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  class ValidateUniquenessOfMatcher < ValidationMatcher # :nodoc:
         | 
| 38 | 
            +
                    include Helpers
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    def initialize(attribute)
         | 
| 41 | 
            +
                      @attribute = attribute
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    def scoped_to(*scopes)
         | 
| 45 | 
            +
                      @scopes = [*scopes].flatten
         | 
| 46 | 
            +
                      self
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    def with_message(message)
         | 
| 50 | 
            +
                      @expected_message = message
         | 
| 51 | 
            +
                      self
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    def case_insensitive
         | 
| 55 | 
            +
                      @case_insensitive = true
         | 
| 56 | 
            +
                      self
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    def description
         | 
| 60 | 
            +
                      result = "require "
         | 
| 61 | 
            +
                      result << "case sensitive " unless @case_insensitive
         | 
| 62 | 
            +
                      result << "unique value for #{@attribute}"
         | 
| 63 | 
            +
                      result << " scoped to #{@scopes.join(', ')}" unless @scopes.blank?
         | 
| 64 | 
            +
                      result
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    def matches?(subject)
         | 
| 68 | 
            +
                      @subject = subject.class.new
         | 
| 69 | 
            +
                      @expected_message ||= :taken
         | 
| 70 | 
            +
                      find_existing && 
         | 
| 71 | 
            +
                        set_scoped_attributes && 
         | 
| 72 | 
            +
                        validate_attribute &&
         | 
| 73 | 
            +
                        validate_after_scope_change
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    private
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    def find_existing
         | 
| 79 | 
            +
                      if @existing = @subject.class.find(:first)
         | 
| 80 | 
            +
                        true
         | 
| 81 | 
            +
                      else
         | 
| 82 | 
            +
                        @failure_message = "Can't find first #{class_name}"
         | 
| 83 | 
            +
                        false
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    def set_scoped_attributes
         | 
| 88 | 
            +
                      unless @scopes.blank?
         | 
| 89 | 
            +
                        @scopes.each do |scope|
         | 
| 90 | 
            +
                          setter = :"#{scope}="
         | 
| 91 | 
            +
                          unless @subject.respond_to?(setter)
         | 
| 92 | 
            +
                            @failure_message =
         | 
| 93 | 
            +
                              "#{class_name} doesn't seem to have a #{scope} attribute."
         | 
| 94 | 
            +
                            return false
         | 
| 95 | 
            +
                          end
         | 
| 96 | 
            +
                          @subject.send("#{scope}=", @existing.send(scope))
         | 
| 97 | 
            +
                        end
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
                      true
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    def validate_attribute
         | 
| 103 | 
            +
                      disallows_value_of(existing_value, @expected_message)
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                    # TODO:  There is a chance that we could change the scoped field
         | 
| 107 | 
            +
                    # to a value that's already taken.  An alternative implementation
         | 
| 108 | 
            +
                    # could actually find all values for scope and create a unique
         | 
| 109 | 
            +
                    def validate_after_scope_change
         | 
| 110 | 
            +
                      if @scopes.blank?
         | 
| 111 | 
            +
                        true
         | 
| 112 | 
            +
                      else
         | 
| 113 | 
            +
                        @scopes.all? do |scope|
         | 
| 114 | 
            +
                          previous_value = @existing.send(scope)
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                          # Assume the scope is a foreign key if the field is nil
         | 
| 117 | 
            +
                          previous_value ||= 0
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                          next_value = previous_value.next
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                          @subject.send("#{scope}=", next_value)
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                          if allows_value_of(existing_value, @expected_message)
         | 
| 124 | 
            +
                            @negative_failure_message << 
         | 
| 125 | 
            +
                              " (with different value of #{scope})"
         | 
| 126 | 
            +
                            true
         | 
| 127 | 
            +
                          else
         | 
| 128 | 
            +
                            @failure_message << " (with different value of #{scope})"
         | 
| 129 | 
            +
                            false
         | 
| 130 | 
            +
                          end
         | 
| 131 | 
            +
                        end
         | 
| 132 | 
            +
                      end
         | 
| 133 | 
            +
                    end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    def class_name
         | 
| 136 | 
            +
                      @subject.class.name
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    def existing_value
         | 
| 140 | 
            +
                      value = @existing.send(@attribute)
         | 
| 141 | 
            +
                      value.swapcase! if @case_insensitive && value.respond_to?(:swapcase!)
         | 
| 142 | 
            +
                      value
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
              end
         | 
| 148 | 
            +
            end
         |