specstar-remarkable 0.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG +147 -0
- data/LICENSE +20 -0
- data/README +103 -0
- data/lib/remarkable/active_record.rb +14 -0
- data/lib/remarkable/active_record/base.rb +6 -0
- data/lib/remarkable/active_record/matchers/accept_nested_attributes_for_matcher.rb +138 -0
- data/lib/remarkable/active_record/matchers/allow_mass_assignment_of_matcher.rb +74 -0
- data/lib/remarkable/active_record/matchers/association_matcher.rb +284 -0
- data/lib/remarkable/active_record/matchers/have_column_matcher.rb +68 -0
- data/lib/remarkable/active_record/matchers/have_default_scope_matcher.rb +68 -0
- data/lib/remarkable/active_record/matchers/have_index_matcher.rb +73 -0
- data/lib/remarkable/active_record/matchers/have_readonly_attributes_matcher.rb +30 -0
- data/lib/remarkable/active_record/matchers/have_scope_matcher.rb +101 -0
- data/lib/remarkable/active_record/matchers/validate_associated_matcher.rb +100 -0
- data/lib/remarkable/active_record/matchers/validate_uniqueness_of_matcher.rb +233 -0
- data/locale/en.yml +264 -0
- metadata +113 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveReadonlyAttributesMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :collection => :attributes, :as => :attribute
|
6
|
+
collection_assertions :is_readonly?
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def is_readonly?
|
11
|
+
readonly = subject_class.readonly_attributes || []
|
12
|
+
return readonly.include?(@attribute.to_s), :actual => readonly.to_a.inspect
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Ensures that the attribute cannot be changed once the record has been
|
17
|
+
# created.
|
18
|
+
#
|
19
|
+
# == Examples
|
20
|
+
#
|
21
|
+
# it { should have_readonly_attributes(:password, :admin_flag) }
|
22
|
+
#
|
23
|
+
def have_readonly_attributes(*attributes, &block)
|
24
|
+
HaveReadonlyAttributesMatcher.new(*attributes, &block).spec(self)
|
25
|
+
end
|
26
|
+
alias :have_readonly_attribute :have_readonly_attributes
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveScopeMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :scope_name
|
6
|
+
assertions :is_scope?, :options_match?
|
7
|
+
|
8
|
+
optionals :with, :splat => true
|
9
|
+
|
10
|
+
# Chained scopes taken from: http://m.onkey.org/2010/1/22/active-record-query-interface
|
11
|
+
optionals :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def is_scope?
|
16
|
+
@scope_object = if @options.key?(:with)
|
17
|
+
@options[:with] = [ @options[:with] ] unless Array === @options[:with]
|
18
|
+
subject_class.send(@scope_name, *@options[:with])
|
19
|
+
else
|
20
|
+
subject_class.send(@scope_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
@scope_object.class == ::ActiveRecord::Relation && @scope_object.arel
|
24
|
+
end
|
25
|
+
|
26
|
+
def options_match?
|
27
|
+
@options.empty? || @scope_object.arel == arel(subject_class, @options.except(:with))
|
28
|
+
end
|
29
|
+
|
30
|
+
def interpolation_options
|
31
|
+
{
|
32
|
+
:options => (subject_class.respond_to?(:scoped) ? arel(subject_class, @options.except(:with)).to_sql : '{}'),
|
33
|
+
:actual => (@scope_object ? @scope_object.arel.to_sql : '{}')
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def arel(model, scopes = nil)
|
39
|
+
return model.scoped unless scopes
|
40
|
+
scopes.inject(model.scoped) do |chain, (cond, option)|
|
41
|
+
chain.send(cond, option)
|
42
|
+
end.arel
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ensures that the model has a named scope that returns an Relation object capable
|
48
|
+
# of building into relational algebra.
|
49
|
+
#
|
50
|
+
# == Options
|
51
|
+
#
|
52
|
+
# * <tt>with</tt> - Options to be sent to the named scope
|
53
|
+
#
|
54
|
+
# All options that the named scope would scope with Arel:
|
55
|
+
# :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
|
56
|
+
#
|
57
|
+
# Matching is done by constructing the Arel objects and testing for equality.
|
58
|
+
#
|
59
|
+
# == Examples
|
60
|
+
#
|
61
|
+
# it { should have_scope(:visible, :where => {:visible => true}) }
|
62
|
+
# it { should have_scope(:visible).where(:visible => true) }
|
63
|
+
#
|
64
|
+
# Passes for
|
65
|
+
#
|
66
|
+
# scope :visible, where(:visible => true)
|
67
|
+
#
|
68
|
+
# Or for
|
69
|
+
#
|
70
|
+
# scope :visible, lambda { where(:visible => true) }
|
71
|
+
#
|
72
|
+
# Or for
|
73
|
+
#
|
74
|
+
# def self.visible
|
75
|
+
# where(:visible => true)
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# You can test lambdas or methods that return ActiveRecord#scoped calls by fixing
|
80
|
+
# a defined parameter.
|
81
|
+
#
|
82
|
+
# it { should have_scope(:recent, :with => 5) }
|
83
|
+
# it { should have_scope(:recent, :with => 1) }
|
84
|
+
#
|
85
|
+
# Passes for
|
86
|
+
#
|
87
|
+
# scope :recent, lambda {|c| limit(c)}
|
88
|
+
#
|
89
|
+
# Or for
|
90
|
+
#
|
91
|
+
# def self.recent(c)
|
92
|
+
# limit(c)
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
def have_scope(*args, &block)
|
96
|
+
HaveScopeMatcher.new(*args, &block).spec(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class ValidateAssociatedMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :collection => :associations, :as => :association, :block => true
|
6
|
+
|
7
|
+
optional :message
|
8
|
+
optional :builder, :block => true
|
9
|
+
|
10
|
+
collection_assertions :find_association?, :is_valid?
|
11
|
+
default_options :message => :invalid
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def find_association?
|
16
|
+
reflection = @subject.class.reflect_on_association(@association)
|
17
|
+
|
18
|
+
raise ScriptError, "Could not find association #{@association} on #{subject_class}." unless reflection
|
19
|
+
|
20
|
+
associated_object = if builder = @options[:builder] || @block
|
21
|
+
builder.call(@subject)
|
22
|
+
elsif [:belongs_to, :has_one].include?(reflection.macro)
|
23
|
+
@subject.send(:"build_#{@association}") rescue nil
|
24
|
+
else
|
25
|
+
@subject.send(@association).build rescue nil
|
26
|
+
end
|
27
|
+
|
28
|
+
raise ScriptError, "The association object #{@association} could not be built. You can give me " <<
|
29
|
+
":builder as option or a block which returns an association." unless associated_object
|
30
|
+
|
31
|
+
raise ScriptError, "The associated object #{@association} is not invalid. You can give me " <<
|
32
|
+
":builder as option or a block which returns an invalid association." if associated_object.valid?
|
33
|
+
|
34
|
+
return true
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_valid?
|
38
|
+
return false if @subject.valid?
|
39
|
+
|
40
|
+
error_message_to_expect = error_message_from_model(@subject, @association, @options[:message])
|
41
|
+
|
42
|
+
assert_contains(@subject.errors[@association], error_message_to_expect)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Ensures that the model is invalid if one of the associations given is
|
47
|
+
# invalid. It tries to build the association automatically. In has_one
|
48
|
+
# and belongs_to cases, it will build it like this:
|
49
|
+
#
|
50
|
+
# @model.build_association
|
51
|
+
# @project.build_manager
|
52
|
+
#
|
53
|
+
# In has_many and has_and_belongs_to_many to cases it will build it like
|
54
|
+
# this:
|
55
|
+
#
|
56
|
+
# @model.association.build
|
57
|
+
# @project.tasks.build
|
58
|
+
#
|
59
|
+
# The object returned MUST be invalid and it's likely the case, since the
|
60
|
+
# associated object is empty when calling build. However, if the associated
|
61
|
+
# object has to be manipulated to be invalid, you will have to give :builder
|
62
|
+
# as option or a block to manipulate it:
|
63
|
+
#
|
64
|
+
# should_validate_associated(:tasks) do |project|
|
65
|
+
# project.tasks.build(:captcha => 'i_am_a_bot')
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# In the case above, the associated object task is only invalid when the
|
69
|
+
# captcha attribute is set. So we give a block to the matcher that tell
|
70
|
+
# exactly how to build an invalid object.
|
71
|
+
#
|
72
|
+
# The example above can also be written as:
|
73
|
+
#
|
74
|
+
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
75
|
+
#
|
76
|
+
# == Options
|
77
|
+
#
|
78
|
+
# * <tt>:builder</tt> - a proc to build the association
|
79
|
+
#
|
80
|
+
# * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
|
81
|
+
# Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
|
82
|
+
#
|
83
|
+
# == Examples
|
84
|
+
#
|
85
|
+
# should_validate_associated :tasks
|
86
|
+
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
87
|
+
#
|
88
|
+
# should_validate_associated :tasks do |m|
|
89
|
+
# m.builder { |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# it { should validate_associated(:tasks) }
|
93
|
+
# it { should validate_associated(:tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }) }
|
94
|
+
#
|
95
|
+
def validate_associated(*args, &block)
|
96
|
+
ValidateAssociatedMatcher.new(*args, &block).spec(self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class ValidateUniquenessOfMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :collection => :attributes, :as => :attribute
|
6
|
+
|
7
|
+
optional :message
|
8
|
+
optional :scope, :splat => true
|
9
|
+
optional :case_sensitive, :allow_nil, :allow_blank, :default => true
|
10
|
+
|
11
|
+
collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?,
|
12
|
+
:valid_with_new_scope?, :allow_nil?, :allow_blank?
|
13
|
+
|
14
|
+
default_options :message => :taken
|
15
|
+
|
16
|
+
before_assert do
|
17
|
+
@options[:scope] = [*@options[:scope]].compact if @options[:scope]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Tries to find an object in the database. If allow_nil and/or allow_blank
|
23
|
+
# is given, we must find a record which is not nil or not blank.
|
24
|
+
#
|
25
|
+
# We should also ensure that the object retrieved from the database
|
26
|
+
# is not the @subject.
|
27
|
+
#
|
28
|
+
# If any of these attempts fail, an error is raised.
|
29
|
+
#
|
30
|
+
def find_first_object?
|
31
|
+
conditions, message = if @options[:allow_nil]
|
32
|
+
[ ["#{@attribute} IS NOT NULL"], " with #{@attribute} not nil" ]
|
33
|
+
elsif @options[:allow_blank]
|
34
|
+
[ ["#{@attribute} != ''"], " with #{@attribute} not blank" ]
|
35
|
+
else
|
36
|
+
[ [], "" ]
|
37
|
+
end
|
38
|
+
|
39
|
+
unless @subject.new_record?
|
40
|
+
primary_key = subject_class.primary_key
|
41
|
+
|
42
|
+
message << " which is different from the subject record (the object being validated is the same as the one in the database)"
|
43
|
+
conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'"
|
44
|
+
end
|
45
|
+
|
46
|
+
options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') }
|
47
|
+
|
48
|
+
return true if @existing = subject_class.find(:first, options)
|
49
|
+
raise ScriptError, "could not find a #{subject_class} record in the database" + message
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set subject scope to be equal to the object found.
|
53
|
+
#
|
54
|
+
def responds_to_scope?
|
55
|
+
(@options[:scope] || []).each do |scope|
|
56
|
+
setter = :"#{scope}="
|
57
|
+
|
58
|
+
return false, :method => setter unless @subject.respond_to?(setter)
|
59
|
+
return false, :method => scope unless @existing.respond_to?(scope)
|
60
|
+
|
61
|
+
@subject.send(setter, @existing.send(scope))
|
62
|
+
end
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if the attribute given is valid and if the validation fails for equal values.
|
67
|
+
#
|
68
|
+
def is_unique?
|
69
|
+
@value = @existing.send(@attribute)
|
70
|
+
return bad?(@value)
|
71
|
+
end
|
72
|
+
|
73
|
+
# If :case_sensitive is given and it's false, we swap the case of the
|
74
|
+
# value used in :is_unique? and see if the test object remains valid.
|
75
|
+
#
|
76
|
+
# If :case_sensitive is given and it's true, we swap the case of the
|
77
|
+
# value used in is_unique? and see if the test object is not valid.
|
78
|
+
#
|
79
|
+
# This validation will only occur if the test object is a String.
|
80
|
+
#
|
81
|
+
def case_sensitive?
|
82
|
+
return true unless @value.is_a?(String)
|
83
|
+
assert_good_or_bad_if_key(:case_sensitive, @value.swapcase)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Now test that the object is valid when changing the scoped attribute.
|
87
|
+
#
|
88
|
+
def valid_with_new_scope?
|
89
|
+
(@options[:scope] || []).each do |scope|
|
90
|
+
setter = :"#{scope}="
|
91
|
+
|
92
|
+
previous_scope_value = @subject.send(scope)
|
93
|
+
@subject.send(setter, new_value_for_scope(scope))
|
94
|
+
return false, :method => scope unless good?(@value)
|
95
|
+
|
96
|
+
@subject.send(setter, previous_scope_value)
|
97
|
+
end
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
# Change the existing object attribute to nil to run allow nil
|
102
|
+
# validations. If we find any problem while updating the @existing
|
103
|
+
# record, it's because we can't save nil values in the database. So it
|
104
|
+
# passes when :allow_nil is false, but should raise an error when
|
105
|
+
# :allow_nil is true
|
106
|
+
#
|
107
|
+
def allow_nil?
|
108
|
+
return true unless @options.key?(:allow_nil)
|
109
|
+
|
110
|
+
begin
|
111
|
+
@existing.update_attribute(@attribute, nil)
|
112
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
113
|
+
raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " <<
|
114
|
+
"but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil]
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
# Change the existing object attribute to blank to run allow blank
|
122
|
+
# validation. It uses the same logic as :allow_nil.
|
123
|
+
#
|
124
|
+
def allow_blank?
|
125
|
+
return true unless @options.key?(:allow_blank)
|
126
|
+
|
127
|
+
begin
|
128
|
+
@existing.update_attribute(@attribute, '')
|
129
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
130
|
+
raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " <<
|
131
|
+
"but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank]
|
132
|
+
return true
|
133
|
+
end
|
134
|
+
|
135
|
+
super
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a value to be used as new scope. It deals with four different
|
139
|
+
# cases: date, time, boolean and stringfiable (everything that can be
|
140
|
+
# converted to a string and the next value makes sense)
|
141
|
+
#
|
142
|
+
def new_value_for_scope(scope)
|
143
|
+
column_type = if @existing.respond_to?(:column_for_attribute)
|
144
|
+
@existing.column_for_attribute(scope)
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
case column_type.type
|
150
|
+
when :int, :integer, :float, :decimal
|
151
|
+
new_value_for_stringfiable_scope(scope)
|
152
|
+
when :datetime, :timestamp, :time
|
153
|
+
Time.now + 10000
|
154
|
+
when :date
|
155
|
+
Date.today + 100
|
156
|
+
when :boolean
|
157
|
+
!@existing.send(scope)
|
158
|
+
else
|
159
|
+
new_value_for_stringfiable_scope(scope)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns a value to be used as scope by generating a range of values
|
164
|
+
# and searching for them in the database.
|
165
|
+
#
|
166
|
+
def new_value_for_stringfiable_scope(scope)
|
167
|
+
values = [(@existing.send(scope) || 999).next.to_s]
|
168
|
+
|
169
|
+
# Generate a range of values to search in the database
|
170
|
+
100.times do
|
171
|
+
values << values.last.next
|
172
|
+
end
|
173
|
+
conditions = { scope => values, @attribute => @value }
|
174
|
+
|
175
|
+
# Get values from the database, get the scope attribute and map them to string.
|
176
|
+
db_values = subject_class.find(:all, :conditions => conditions, :select => scope)
|
177
|
+
db_values.map!{ |r| r.send(scope).to_s }
|
178
|
+
|
179
|
+
if value_to_return = (values - db_values).first
|
180
|
+
value_to_return
|
181
|
+
else
|
182
|
+
raise ScriptError, "Tried to find an unique scope value for #{scope} but I could not. " <<
|
183
|
+
"The conditions hash was #{conditions.inspect} and it returned all records."
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Ensures that the model cannot be saved if one of the attributes listed
|
189
|
+
# is not unique.
|
190
|
+
#
|
191
|
+
# Requires an existing record in the database. If you supply :allow_nil as
|
192
|
+
# option, you need to have in the database a record which is not nil in the
|
193
|
+
# given attributes. The same is required for allow_blank option.
|
194
|
+
#
|
195
|
+
# Notice that the record being validate should not be the same as in the
|
196
|
+
# database. In other words, you can't do this:
|
197
|
+
#
|
198
|
+
# subject { Post.create!(@valid_attributes) }
|
199
|
+
# should_validate_uniqueness_of :title
|
200
|
+
#
|
201
|
+
# But don't worry, if you eventually do that, a helpful error message
|
202
|
+
# will be raised.
|
203
|
+
#
|
204
|
+
# == Options
|
205
|
+
#
|
206
|
+
# * <tt>:scope</tt> - field(s) to scope the uniqueness to.
|
207
|
+
# * <tt>:case_sensitive</tt> - the matcher look for an exact match.
|
208
|
+
# * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
|
209
|
+
# * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
|
210
|
+
# * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
|
211
|
+
# Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
|
212
|
+
#
|
213
|
+
# == Examples
|
214
|
+
#
|
215
|
+
# it { should validate_uniqueness_of(:keyword, :username) }
|
216
|
+
# it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) }
|
217
|
+
# it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
|
218
|
+
#
|
219
|
+
# should_validate_uniqueness_of :keyword, :username
|
220
|
+
# should_validate_uniqueness_of :email, :scope => :name, :case_sensitive => false
|
221
|
+
# should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
|
222
|
+
#
|
223
|
+
# should_validate_uniqueness_of :email do |m|
|
224
|
+
# m.scope = name
|
225
|
+
# m.case_sensitive = false
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
def validate_uniqueness_of(*attributes, &block)
|
229
|
+
ValidateUniquenessOfMatcher.new(*attributes, &block).spec(self)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|