Skip to content

Commit

Permalink
Allow allow_values_for and allow_mass_assignment_of to be used in the…
Browse files Browse the repository at this point in the history
… negative form, checking all values [#85 status:resolved]
  • Loading branch information
josevalim committed Jul 16, 2009
1 parent de72281 commit a1a529c
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 35 deletions.
1 change: 1 addition & 0 deletions remarkable/lib/remarkable.rb
Expand Up @@ -9,6 +9,7 @@
require File.join(dir, 'remarkable', 'base')
require File.join(dir, 'remarkable', 'macros')
require File.join(dir, 'remarkable', 'pending')
require File.join(dir, 'remarkable', 'negative')
require File.join(dir, 'remarkable', 'core_ext', 'array')

if defined?(Spec)
Expand Down
23 changes: 20 additions & 3 deletions remarkable/lib/remarkable/base.rb
Expand Up @@ -11,6 +11,14 @@ def spec(binding)
self
end

def positive?
!negative?
end

def negative?
false
end

protected

# Returns the subject class unless it's a class object.
Expand All @@ -28,11 +36,20 @@ def subject_name

# Iterates over the collection given yielding the block and return false
# if any of them also returns false.
def assert_matcher_for(collection) #:nodoc:
def assert_collection(key, collection, inspect=true) #:nodoc:
collection.each do |item|
return false unless yield(item)
value = yield(item)

if positive? == !value
if key
output = inspect ? item.inspect : item
return negative?, key => output
else
return negative?
end
end
end
true
positive?
end

# Asserts that the given collection contains item x. If x is a regular
Expand Down
42 changes: 33 additions & 9 deletions remarkable/lib/remarkable/dsl/assertions.rb
Expand Up @@ -282,11 +282,13 @@ def matches?(subject)

run_before_assert_callbacks

send_methods_and_generate_message(self.class.matcher_single_assertions) &&
assert_matcher_for(instance_variable_get("@#{self.class.matcher_arguments[:collection]}") || []) do |value|
instance_variable_set("@#{self.class.matcher_arguments[:as]}", value)
send_methods_and_generate_message(self.class.matcher_collection_assertions)
assertions = self.class.matcher_single_assertions
unless assertions.empty?
value = send_methods_and_generate_message(assertions)
return negative? if positive? == !value
end

matches_collection_assertions?
end

protected
Expand Down Expand Up @@ -368,19 +370,41 @@ def send_methods_and_generate_message(methods) #:nodoc:
methods.each do |method|
bool, hash = send(method)

unless bool
if positive? == !bool
parent_scope = matcher_i18n_scope.split('.')
matcher_name = parent_scope.pop
lookup = :"expectations.#{method.to_s.gsub(/(\?|\!)$/, '')}"
method_name = method.to_s.gsub(/(\?|\!)$/, '')

lookup = []
lookup << :"#{matcher_name}.negative_expectations.#{method_name}" if negative?
lookup << :"#{matcher_name}.expectations.#{method_name}"
lookup << :"negative_expectations.#{method_name}" if negative?
lookup << :"expectations.#{method_name}"

hash = { :scope => parent_scope, :default => lookup }.merge(hash || {})
@expectation ||= Remarkable.t "#{matcher_name}.#{lookup}", default_i18n_options.merge(hash)
@expectation ||= Remarkable.t lookup.shift, default_i18n_options.merge(hash)

return false
return negative?
end
end

return true
return positive?
end

def matches_single_assertions? #:nodoc:
assertions = self.class.matcher_single_assertions
send_methods_and_generate_message(assertions)
end

def matches_collection_assertions? #:nodoc:
arguments = self.class.matcher_arguments
assertions = self.class.matcher_collection_assertions
collection = instance_variable_get("@#{self.class.matcher_arguments[:collection]}") || []

assert_collection(nil, collection) do |value|
instance_variable_set("@#{arguments[:as]}", value)
send_methods_and_generate_message(assertions)
end
end


Expand Down
24 changes: 24 additions & 0 deletions remarkable/lib/remarkable/negative.rb
@@ -0,0 +1,24 @@
module Remarkable
# Allows Remarkable matchers to work on the negative way. Your matcher has to
# follow some conventions to allow this to work by default.
#
# In negative cases, expectations can also be found under negative_expectations
# keys, falling back to expectations. This allows to set customized failure
# messages.
#
module Negative
def matches?(subject)
@negative ||= false
super
end

def does_not_match?(subject)
@negative = true
!matches?(subject)
end

def negative?
@negative
end
end
end
5 changes: 5 additions & 0 deletions remarkable/spec/base_spec.rb
Expand Up @@ -28,6 +28,11 @@
matcher.instance_variable_get('@spec').class.ancestors.should include(Spec::Example::ExampleGroup)
end

it 'should be positive' do
contain(1).should be_positive
contain(1).should_not be_negative
end

it { should contain(1) }
it { should_not contain(10) }

Expand Down
2 changes: 1 addition & 1 deletion remarkable/spec/matchers/contain_matcher.rb
Expand Up @@ -9,7 +9,7 @@ def initialize(*values)
def matches?(subject)
@subject = subject

assert_matcher_for(@values) do |value|
assert_collection(nil, @values) do |value|
@value = value
included?
end
Expand Down
3 changes: 3 additions & 0 deletions remarkable_activerecord/CHANGELOG
@@ -1,3 +1,6 @@
* Allow allow_values_for and allow_mass_assignment_of matchers to be executed in
the negative form by including Remarkable::Negative module [#85]

* Ensure quick subject bypass protected attributes [#87]

* Added :token and :separator to deal with :tokenizer in validates_length_of [#77]
Expand Down
16 changes: 6 additions & 10 deletions remarkable_activerecord/lib/remarkable_activerecord/base.rb
Expand Up @@ -25,14 +25,12 @@ def with_options(opts={})
# and an @options key where the message to search for is.
#
def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc:
return true unless @options.key?(key)
return positive? unless @options.key?(key)

if @options[key]
return true if bad?(value, message_key)
return false, :not => not_word
return bad?(value, message_key), :not => not_word
else
return true if good?(value, message_key)
return false, :not => ''
return good?(value, message_key), :not => ''
end
end

Expand All @@ -43,14 +41,12 @@ def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc:
# and an @options key where the message to search for is.
#
def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc:
return true unless @options.key?(key)
return positive? unless @options.key?(key)

if @options[key]
return true if good?(value, message_key)
return false, :not => ''
return good?(value, message_key), :not => ''
else
return true if bad?(value, message_key)
return false, :not => not_word
return bad?(value, message_key), :not => not_word
end
end

Expand Down
Expand Up @@ -2,31 +2,39 @@ module Remarkable
module ActiveRecord
module Matchers
class AllowMassAssignmentOfMatcher < Remarkable::ActiveRecord::Base #:nodoc:
include Remarkable::Negative
arguments :collection => :attributes, :as => :attribute

assertion :allows?
collection_assertions :is_protected?, :is_accessible?
collection_assertions :is_accessible?, :is_protected?

protected

# If no attribute is given, check if no attribute is being protected,
# otherwise it fails.
#
def allows?
!@attributes.empty? || protected_attributes.empty?
return positive? unless @attributes.empty?
protected_attributes.empty?
end

def is_protected?
protected_attributes.empty? || !protected_attributes.include?(@attribute.to_s)
return positive? if protected_attributes.empty?
!protected_attributes.include?(@attribute.to_s)
end

def is_accessible?
accessible_attributes.empty? || accessible_attributes.include?(@attribute.to_s)
return positive? if accessible_attributes.empty?
accessible_attributes.include?(@attribute.to_s)
end

def interpolation_options
if @subject
{ :protected_attributes => array_to_sentence(protected_attributes.to_a, false, '[]') }
if positive?
{ :protected_attributes => array_to_sentence(protected_attributes.to_a, false, '[]') }
else
{ :accessible_attributes => array_to_sentence(accessible_attributes.to_a, false, '[]') }
end
else
{}
end
Expand Down
Expand Up @@ -2,6 +2,7 @@ module Remarkable
module ActiveRecord
module Matchers
class AllowValuesForMatcher < Remarkable::ActiveRecord::Base #:nodoc:
include Remarkable::Negative
arguments :collection => :attributes, :as => :attribute

optional :message
Expand All @@ -28,17 +29,15 @@ class AllowValuesForMatcher < Remarkable::ActiveRecord::Base #:nodoc:
protected

def is_valid?
valid_values.each do |value|
return false, :value => value.inspect unless good?(value)
assert_collection :value, valid_values do |value|
good?(value)
end
true
end

def is_invalid?
invalid_values.each do |value|
return false, :value => value.inspect unless bad?(value)
assert_collection :value, invalid_values do |value|
bad?(value)
end
true
end

def valid_values
Expand Down
Expand Up @@ -4,6 +4,8 @@ module Remarkable
module ActiveRecord
module Matchers
class ValidateExclusionOfMatcher < AllowValuesForMatcher #:nodoc:
# Don't allow it to behave in the negative way.
undef_method :does_not_match?

default_options :message => :exclusion

Expand Down
Expand Up @@ -4,6 +4,8 @@ module Remarkable
module ActiveRecord
module Matchers
class ValidateInclusionOfMatcher < AllowValuesForMatcher #:nodoc:
# Don't allow it to behave in the negative way.
undef_method :does_not_match?

default_options :message => :inclusion

Expand Down
4 changes: 4 additions & 0 deletions remarkable_activerecord/locale/en.yml
Expand Up @@ -45,6 +45,10 @@ en:
allows: "{{subject_name}} to allow mass assignment ({{subject_name}} is protecting {{protected_attributes}})"
is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is protecting {{attribute}})"
is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has not made {{attribute}} accessible)"
negative_expectations:
allows: "{{subject_name}} to allow mass assignment ({{subject_name}} made {{accessible_attributes}} accessible)"
is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is not protecting {{attribute}})"
is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has made {{attribute}} accessible)"

association:
belongs_to: belong to
Expand Down
Expand Up @@ -75,7 +75,24 @@ def define_and_validate(options={})
should_allow_mass_assignment_of :title, :category

should_not_allow_mass_assignment_of :another
should_not_allow_mass_assignment_of :title, :another
end

describe 'failures' do
it "should fail if not all attributes are accessible on should not" do
define_and_validate(:accessible => true)

lambda {
should_not allow_mass_assignment_of :title, :another
}.should raise_error(Spec::Expectations::ExpectationNotMetError, /Product has made title accessible/)
end

it "should fail if accessible when protecting" do
define_and_validate(:accessible => true)

lambda {
should_not allow_mass_assignment_of
}.should raise_error(Spec::Expectations::ExpectationNotMetError, /Product made category and title accessible/)
end
end
end

10 changes: 10 additions & 0 deletions remarkable_activerecord/spec/allow_values_for_matcher_spec.rb
Expand Up @@ -57,5 +57,15 @@ def define_and_validate(options={})
should_not_validate_format_of :title, 'A'
end
end

describe 'failures' do
it "should fail if any of the values are valid on invalid cases" do
define_and_validate(:with => /X|Y|Z/)

lambda {
should_not allow_values_for :title, 'A', 'X', 'B'
}.should raise_error(Spec::Expectations::ExpectationNotMetError, /Did not expect Product to be valid/)
end
end
end

0 comments on commit a1a529c

Please sign in to comment.