remarkable_activerecord 3.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.
- data/CHANGELOG +47 -0
- data/LICENSE +20 -0
- data/README +2 -0
- data/lib/remarkable_activerecord/base.rb +238 -0
- data/lib/remarkable_activerecord/human_names.rb +37 -0
- data/lib/remarkable_activerecord/matchers/allow_mass_assignment_of_matcher.rb +34 -0
- data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +94 -0
- data/lib/remarkable_activerecord/matchers/association_matcher.rb +235 -0
- data/lib/remarkable_activerecord/matchers/have_column_matcher.rb +68 -0
- data/lib/remarkable_activerecord/matchers/have_index_matcher.rb +57 -0
- data/lib/remarkable_activerecord/matchers/have_readonly_attributes_matcher.rb +30 -0
- data/lib/remarkable_activerecord/matchers/have_scope_matcher.rb +80 -0
- data/lib/remarkable_activerecord/matchers/validate_acceptance_of_matcher.rb +51 -0
- data/lib/remarkable_activerecord/matchers/validate_associated_matcher.rb +99 -0
- data/lib/remarkable_activerecord/matchers/validate_confirmation_of_matcher.rb +45 -0
- data/lib/remarkable_activerecord/matchers/validate_exclusion_of_matcher.rb +47 -0
- data/lib/remarkable_activerecord/matchers/validate_inclusion_of_matcher.rb +47 -0
- data/lib/remarkable_activerecord/matchers/validate_length_of_matcher.rb +123 -0
- data/lib/remarkable_activerecord/matchers/validate_numericality_of_matcher.rb +184 -0
- data/lib/remarkable_activerecord/matchers/validate_presence_of_matcher.rb +29 -0
- data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +151 -0
- data/lib/remarkable_activerecord.rb +29 -0
- data/locale/en.yml +253 -0
- data/spec/allow_mass_assignment_of_matcher_spec.rb +57 -0
- data/spec/allow_values_for_matcher_spec.rb +56 -0
- data/spec/association_matcher_spec.rb +616 -0
- data/spec/have_column_matcher_spec.rb +73 -0
- data/spec/have_index_matcher_spec.rb +68 -0
- data/spec/have_readonly_attributes_matcher_spec.rb +47 -0
- data/spec/have_scope_matcher_spec.rb +69 -0
- data/spec/model_builder.rb +101 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/validate_acceptance_of_matcher_spec.rb +68 -0
- data/spec/validate_associated_matcher_spec.rb +122 -0
- data/spec/validate_confirmation_of_matcher_spec.rb +58 -0
- data/spec/validate_exclusion_of_matcher_spec.rb +88 -0
- data/spec/validate_inclusion_of_matcher_spec.rb +84 -0
- data/spec/validate_length_of_matcher_spec.rb +165 -0
- data/spec/validate_numericality_of_matcher_spec.rb +180 -0
- data/spec/validate_presence_of_matcher_spec.rb +52 -0
- data/spec/validate_uniqueness_of_matcher_spec.rb +150 -0
- metadata +112 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class AssociationMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
arguments :macro, :collection => :associations, :as => :association
|
6
|
+
|
7
|
+
optionals :through, :class_name, :foreign_key, :dependent, :join_table, :as
|
8
|
+
optionals :uniq, :readonly, :validate, :autosave, :polymorphic, :counter_cache, :default => true
|
9
|
+
|
10
|
+
# Stores optionals declared above in a CONSTANT to generate assertions
|
11
|
+
ASSOCIATION_OPTIONS = self.matcher_optionals
|
12
|
+
|
13
|
+
collection_assertions :association_exists?, :macro_matches?, :through_exists?,
|
14
|
+
:join_table_exists?, :foreign_key_exists?, :polymorphic_exists?,
|
15
|
+
:counter_cache_exists?
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def association_exists?
|
20
|
+
reflection
|
21
|
+
end
|
22
|
+
|
23
|
+
def macro_matches?
|
24
|
+
reflection.macro == @macro
|
25
|
+
end
|
26
|
+
|
27
|
+
def through_exists?
|
28
|
+
return true unless @options.key?(:through)
|
29
|
+
subject_class.reflect_on_association(@options[:through])
|
30
|
+
end
|
31
|
+
|
32
|
+
def join_table_exists?
|
33
|
+
return true unless has_join_table?
|
34
|
+
::ActiveRecord::Base.connection.tables.include?(reflection_join_table)
|
35
|
+
end
|
36
|
+
|
37
|
+
def foreign_key_exists?
|
38
|
+
table_has_column?(foreign_key_table, reflection_foreign_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def polymorphic_exists?
|
42
|
+
return true unless @options[:polymorphic]
|
43
|
+
table_has_column?(subject_class.table_name, reflection_foreign_key.sub(/_id$/, '_type'))
|
44
|
+
end
|
45
|
+
|
46
|
+
def counter_cache_exists?
|
47
|
+
return true unless @options[:counter_cache]
|
48
|
+
table_has_column?(reflection.klass.table_name, reflection.counter_cache_column.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
ASSOCIATION_OPTIONS.each do |option|
|
52
|
+
collection_assertion :"#{option}_matches?"
|
53
|
+
|
54
|
+
class_eval <<-METHOD, __FILE__, __LINE__
|
55
|
+
def #{option}_matches?
|
56
|
+
return true unless @options.key?(#{option.inspect})
|
57
|
+
actual_value = respond_to?(:reflection_#{option}, true) ? reflection_#{option} : reflection.options[#{option.inspect}].to_s
|
58
|
+
|
59
|
+
return true if @options[#{option.inspect}].to_s == actual_value
|
60
|
+
end
|
61
|
+
METHOD
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def reflection
|
67
|
+
@reflection ||= subject_class.reflect_on_association(@association.to_sym)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Rescue nil to avoid raising errors in invalid through associations
|
71
|
+
def reflection_class_name
|
72
|
+
reflection.class_name.to_s rescue nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def reflection_foreign_key
|
76
|
+
reflection.primary_key_name.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def reflection_join_table
|
80
|
+
(reflection.options[:join_table] || reflection.options[:through]).to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_join_table?
|
84
|
+
reflection.options.key?(:through) || reflection.macro == :has_and_belongs_to_many
|
85
|
+
end
|
86
|
+
|
87
|
+
def table_has_column?(table_name, column)
|
88
|
+
::ActiveRecord::Base.connection.columns(table_name, 'Remarkable column retrieval').any?{|c| c.name == column }
|
89
|
+
end
|
90
|
+
|
91
|
+
# In cases a join table exists (has_and_belongs_to_many and through
|
92
|
+
# associations), we check the foreign key in the join table.
|
93
|
+
#
|
94
|
+
# On belongs to, the foreign_key is in the subject class table and in
|
95
|
+
# other cases it's on the reflection class table.
|
96
|
+
#
|
97
|
+
def foreign_key_table
|
98
|
+
if has_join_table?
|
99
|
+
reflection_join_table
|
100
|
+
elsif reflection.macro == :belongs_to
|
101
|
+
subject_class.table_name
|
102
|
+
else
|
103
|
+
reflection.klass.table_name
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def interpolation_options
|
108
|
+
options = { :macro => Remarkable.t(@macro, :scope => matcher_i18n_scope, :default => @macro.to_s) }
|
109
|
+
|
110
|
+
if @subject && reflection
|
111
|
+
options.merge!(
|
112
|
+
:actual_macro => Remarkable.t(reflection.macro, :scope => matcher_i18n_scope, :default => reflection.macro.to_s),
|
113
|
+
:subject_table => subject_class.table_name.inspect,
|
114
|
+
:reflection_table => reflection.klass.table_name.inspect,
|
115
|
+
:foreign_key_table => foreign_key_table.inspect,
|
116
|
+
:polymorphic_column => reflection_foreign_key.sub(/_id$/, '_type').inspect,
|
117
|
+
:counter_cache_column => reflection.counter_cache_column.to_s.inspect
|
118
|
+
) rescue nil # rescue to allow specs to run properly
|
119
|
+
|
120
|
+
ASSOCIATION_OPTIONS.each do |option|
|
121
|
+
value_to_compare = respond_to?(:"reflection_#{option}", true) ? send(:"reflection_#{option}") : reflection.options[option].to_s
|
122
|
+
options[:"actual_#{option}"] = value_to_compare.inspect
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
options
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Ensure that the belongs_to relationship exists. Will also test that the
|
132
|
+
# subject table has the association_id column.
|
133
|
+
#
|
134
|
+
# == Options
|
135
|
+
#
|
136
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
137
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the subject table.
|
138
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
139
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
140
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
141
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
142
|
+
# * <tt>:counter_cache</tt> - the expected dependent value for the association.
|
143
|
+
# It also checks if the column actually exists in the table.
|
144
|
+
# * <tt>:polymorphic</tt> - if the association should be polymorphic or not.
|
145
|
+
# When true it also checks for the association_type column in the subject table.
|
146
|
+
#
|
147
|
+
# == Examples
|
148
|
+
#
|
149
|
+
# should_belong_to :parent, :polymorphic => true
|
150
|
+
# it { should belong_to(:parent) }
|
151
|
+
#
|
152
|
+
def belong_to(*associations)
|
153
|
+
AssociationMatcher.new(:belongs_to, *associations).spec(self)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Ensures that the has_and_belongs_to_many relationship exists, if the join
|
157
|
+
# table is in place and if the foreign_key column exists.
|
158
|
+
#
|
159
|
+
# == Options
|
160
|
+
#
|
161
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
162
|
+
# * <tt>:join_table</tt> - the expected join table name.
|
163
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the association table.
|
164
|
+
# * <tt>:uniq</tt> - checks wether uniq is true or false.
|
165
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
166
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
167
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
168
|
+
#
|
169
|
+
# == Examples
|
170
|
+
#
|
171
|
+
# should_have_and_belong_to_many :posts, :cars
|
172
|
+
# it{ should have_and_belong_to_many :posts, :cars }
|
173
|
+
#
|
174
|
+
def have_and_belong_to_many(*associations)
|
175
|
+
AssociationMatcher.new(:has_and_belongs_to_many, *associations).spec(self)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Ensures that the has_many relationship exists. Will also test that the
|
179
|
+
# associated table has the required columns. It works by default with
|
180
|
+
# polymorphic association (:as does not have to be supplied).
|
181
|
+
#
|
182
|
+
# == Options
|
183
|
+
#
|
184
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
185
|
+
# * <tt>:through</tt> - the expected join model which to perform the query.
|
186
|
+
# It also checks if the through table exists.
|
187
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
|
188
|
+
# When used with :through, it will check for the foreign key in the join table.
|
189
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
190
|
+
# * <tt>:uniq</tt> - checks wether uniq is true or false.
|
191
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
192
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
193
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
194
|
+
#
|
195
|
+
# == Examples
|
196
|
+
#
|
197
|
+
# should_have_many :friends
|
198
|
+
# should_have_many :enemies, :through => :friends
|
199
|
+
# should_have_many :enemies, :dependent => :destroy
|
200
|
+
#
|
201
|
+
# it{ should have_many(:friends) }
|
202
|
+
# it{ should have_many(:enemies, :through => :friends) }
|
203
|
+
# it{ should have_many(:enemies, :dependent => :destroy) }
|
204
|
+
#
|
205
|
+
def have_many(*associations)
|
206
|
+
AssociationMatcher.new(:has_many, *associations).spec(self)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Ensures that the has_many relationship exists. Will also test that the
|
210
|
+
# associated table has the required columns. It works by default with
|
211
|
+
# polymorphic association (:as does not have to be supplied).
|
212
|
+
#
|
213
|
+
# == Options
|
214
|
+
#
|
215
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
216
|
+
# * <tt>:through</tt> - the expected join model which to perform the query.
|
217
|
+
# It also checks if the through table exists.
|
218
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
|
219
|
+
# When used with :through, it will check for the foreign key in the join table.
|
220
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
221
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
222
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
223
|
+
#
|
224
|
+
# == Examples
|
225
|
+
#
|
226
|
+
# should_have_one :universe
|
227
|
+
# it{ should have_one(:universe) }
|
228
|
+
#
|
229
|
+
def have_one(*associations)
|
230
|
+
AssociationMatcher.new(:has_one, *associations).spec(self)
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveColumnMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
arguments :collection => :columns, :as => :column
|
6
|
+
|
7
|
+
optional :type, :default, :precision, :limit, :scale, :sql_type
|
8
|
+
optional :primary, :null, :default => true
|
9
|
+
|
10
|
+
collection_assertions :column_exists?, :options_match?
|
11
|
+
|
12
|
+
before_assert do
|
13
|
+
@options.each{|k,v| @options[k] = v.to_s}
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def column_exists?
|
19
|
+
!column_type.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def options_match?
|
23
|
+
actual = get_column_options(column_type, @options.keys)
|
24
|
+
return actual == @options, :actual => actual.inspect
|
25
|
+
end
|
26
|
+
|
27
|
+
def column_type
|
28
|
+
subject_class.columns.detect {|c| c.name == @column.to_s }
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_column_options(column, keys)
|
32
|
+
keys.inject({}) do |hash, key|
|
33
|
+
hash[key] = column.instance_variable_get("@#{key}").to_s
|
34
|
+
hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def interpolation_options
|
39
|
+
{ :options => @options.inspect }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensures that a column of the database actually exists.
|
45
|
+
#
|
46
|
+
# == Options
|
47
|
+
#
|
48
|
+
# * All options available in migrations are available:
|
49
|
+
#
|
50
|
+
# :type, :default, :precision, :limit, :scale, :sql_type, :primary, :null
|
51
|
+
#
|
52
|
+
# == Examples
|
53
|
+
#
|
54
|
+
# should_have_column :name, :type => :string, :default => ''
|
55
|
+
#
|
56
|
+
# it { should have_column(:name, :type => :string) }
|
57
|
+
# it { should have_column(:name).type(:string) }
|
58
|
+
#
|
59
|
+
def have_column(*args)
|
60
|
+
HaveColumnMatcher.new(*args).spec(self)
|
61
|
+
end
|
62
|
+
alias :have_columns :have_column
|
63
|
+
alias :have_db_column :have_column
|
64
|
+
alias :have_db_columns :have_column
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveIndexMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
arguments :collection => :columns, :as => :column
|
6
|
+
|
7
|
+
optional :unique, :default => true
|
8
|
+
|
9
|
+
collection_assertions :index_exists?, :is_unique?
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def index_exists?
|
14
|
+
!matched_index.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_unique?
|
18
|
+
return true unless @options.key?(:unique)
|
19
|
+
return @options[:unique] == matched_index.unique, :actual => matched_index.unique
|
20
|
+
end
|
21
|
+
|
22
|
+
def matched_index
|
23
|
+
columns = [@column].flatten.map(&:to_s)
|
24
|
+
indexes.detect { |ind| ind.columns == columns }
|
25
|
+
end
|
26
|
+
|
27
|
+
def indexes
|
28
|
+
@indexes ||= ::ActiveRecord::Base.connection.indexes(subject_class.table_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def interpolation_options
|
32
|
+
@subject ? { :table_name => subject_class.table_name } : {}
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# Ensures the database column has specified index.
|
38
|
+
#
|
39
|
+
# == Options
|
40
|
+
#
|
41
|
+
# * <tt>unique</tt> - when supplied, tests if the index is unique or not
|
42
|
+
#
|
43
|
+
# == Examples
|
44
|
+
#
|
45
|
+
# it { should have_index(:ssn).unique(true) }
|
46
|
+
# it { should have_index([:name, :email]).unique(true) }
|
47
|
+
#
|
48
|
+
def have_index(*args)
|
49
|
+
HaveIndexMatcher.new(*args).spec(self)
|
50
|
+
end
|
51
|
+
alias :have_indices :have_index
|
52
|
+
alias :have_db_index :have_index
|
53
|
+
alias :have_db_indices :have_index
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveReadonlyAttributesMatcher < Remarkable::ActiveRecord::Base
|
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)
|
24
|
+
HaveReadonlyAttributesMatcher.new(*attributes).spec(self)
|
25
|
+
end
|
26
|
+
alias :have_readonly_attribute :have_readonly_attributes
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveScopeMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
arguments :scope_name
|
6
|
+
assertions :is_scope?, :options_match?
|
7
|
+
|
8
|
+
optionals :with, :select, :conditions, :order, :limit, :offset
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def is_scope?
|
13
|
+
@scope_object = if @options[:with]
|
14
|
+
subject_class.send(@scope_name, *@options[:with])
|
15
|
+
else
|
16
|
+
subject_class.send(@scope_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
@scope_object.class == ::ActiveRecord::NamedScope::Scope
|
20
|
+
end
|
21
|
+
|
22
|
+
def options_match?
|
23
|
+
@options.empty? || @scope_object.proxy_options == @options.except(:with)
|
24
|
+
end
|
25
|
+
|
26
|
+
def interpolation_options
|
27
|
+
{ :options => @options.except(:with).inspect,
|
28
|
+
:actual => (@scope_object ? @scope_object.proxy_options.inspect : '{}')
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Ensures that the model has a method named scope that returns a NamedScope
|
35
|
+
# object with the supplied proxy options.
|
36
|
+
#
|
37
|
+
# == Options
|
38
|
+
#
|
39
|
+
# * <tt>with</tt> - Options to be sent to the named scope
|
40
|
+
#
|
41
|
+
# And all other options that the named scope would pass on to find.
|
42
|
+
#
|
43
|
+
# == Examples
|
44
|
+
#
|
45
|
+
# it { should have_scope(:visible, :conditions => {:visible => true}) }
|
46
|
+
# it { should have_scope(:visible).conditions(:visible => true) }
|
47
|
+
#
|
48
|
+
# Passes for
|
49
|
+
#
|
50
|
+
# named_scope :visible, :conditions => {:visible => true}
|
51
|
+
#
|
52
|
+
# Or for
|
53
|
+
#
|
54
|
+
# def self.visible
|
55
|
+
# scoped(:conditions => {:visible => true})
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# You can test lambdas or methods that return ActiveRecord#scoped calls:
|
59
|
+
#
|
60
|
+
# it { should have_scope(:recent, :with => 5) }
|
61
|
+
# it { should have_scope(:recent, :with => 1) }
|
62
|
+
#
|
63
|
+
# Passes for
|
64
|
+
#
|
65
|
+
# named_scope :recent, lambda {|c| {:limit => c}}
|
66
|
+
#
|
67
|
+
# Or for
|
68
|
+
#
|
69
|
+
# def self.recent(c)
|
70
|
+
# scoped(:limit => c)
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
def have_scope(*args)
|
74
|
+
HaveScopeMatcher.new(*args).spec(self)
|
75
|
+
end
|
76
|
+
alias :have_named_scope :have_scope
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class ValidateAcceptanceOfMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
|
6
|
+
arguments :collection => :attributes, :as => :attribute
|
7
|
+
|
8
|
+
optional :accept, :message
|
9
|
+
optional :allow_nil, :default => true
|
10
|
+
|
11
|
+
collection_assertions :requires_acceptance?, :accept_is_valid?, :allow_nil?
|
12
|
+
|
13
|
+
default_options :message => :accepted
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def requires_acceptance?
|
18
|
+
bad?(false)
|
19
|
+
end
|
20
|
+
|
21
|
+
def accept_is_valid?
|
22
|
+
return true unless @options.key?(:accept)
|
23
|
+
good?(@options[:accept])
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ensures that the model cannot be saved if one of the attributes listed is not accepted.
|
29
|
+
#
|
30
|
+
# == Options
|
31
|
+
#
|
32
|
+
# * <tt>:accept</tt> - the expected value to be accepted.
|
33
|
+
# * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
|
34
|
+
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
|
35
|
+
# Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.accepted')</tt>
|
36
|
+
#
|
37
|
+
# == Examples
|
38
|
+
#
|
39
|
+
# should_validate_acceptance_of :eula, :terms
|
40
|
+
# should_validate_acceptance_of :eula, :terms, :accept => true
|
41
|
+
#
|
42
|
+
# it { should validate_acceptance_of(:eula, :terms) }
|
43
|
+
# it { should validate_acceptance_of(:eula, :terms, :accept => true) }
|
44
|
+
#
|
45
|
+
def validate_acceptance_of(*attributes)
|
46
|
+
ValidateAcceptanceOfMatcher.new(*attributes).spec(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class ValidateAssociatedMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
arguments :collection => :associations, :as => :association, :block => :block
|
6
|
+
optional :message, :builder
|
7
|
+
|
8
|
+
collection_assertions :find_association?, :is_valid?
|
9
|
+
default_options :message => :invalid
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def find_association?
|
14
|
+
reflection = @subject.class.reflect_on_association(@association)
|
15
|
+
|
16
|
+
raise ScriptError, "Could not find association #{@association} on #{subject_class}." unless reflection
|
17
|
+
|
18
|
+
associated_object = if builder = @options[:builder] || @block
|
19
|
+
builder.call(@subject)
|
20
|
+
elsif [:belongs_to, :has_one].include?(reflection.macro)
|
21
|
+
@subject.send(:"build_#{@association}") rescue nil
|
22
|
+
else
|
23
|
+
@subject.send(@association).build rescue nil
|
24
|
+
end
|
25
|
+
|
26
|
+
raise ScriptError, "The association object #{@association} could not be built. You can give me " <<
|
27
|
+
":builder as option or a block which returns an association." unless associated_object
|
28
|
+
|
29
|
+
raise ScriptError, "The associated object #{@association} is not invalid. You can give me " <<
|
30
|
+
":builder as option or a block which returns an invalid association." if associated_object.save
|
31
|
+
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_valid?
|
36
|
+
return false if @subject.valid?
|
37
|
+
|
38
|
+
error_message_to_expect = error_message_from_model(@subject, :base, @options[:message])
|
39
|
+
|
40
|
+
# In Rails 2.1.2, the error on association returns a symbol (:invalid)
|
41
|
+
# instead of the message, so we check this case here.
|
42
|
+
@subject.errors.on(@association) == @options[:message] ||
|
43
|
+
assert_contains(@subject.errors.on(@association), error_message_to_expect)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ensures that the model is invalid if one of the associations given is
|
48
|
+
# invalid. It tries to build the association automatically. In has_one
|
49
|
+
# and belongs_to cases, it will build it like this:
|
50
|
+
#
|
51
|
+
# @model.build_association
|
52
|
+
# @project.build_manager
|
53
|
+
#
|
54
|
+
# In has_many and has_and_belongs_to_many to cases it will build it like
|
55
|
+
# this:
|
56
|
+
#
|
57
|
+
# @model.association.build
|
58
|
+
# @project.tasks.build
|
59
|
+
#
|
60
|
+
# The object returned MUST be invalid and it's likely the case, since the
|
61
|
+
# associated object is empty when calling build. However, if the associated
|
62
|
+
# object has to be manipulated to be invalid, you will have to give :builder
|
63
|
+
# as option or a block to manipulate it:
|
64
|
+
#
|
65
|
+
# should_validate_associated(:tasks) do |project|
|
66
|
+
# project.tasks.build(:captcha => 'i_am_a_bot')
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# In the case above, the associated object task is only invalid when the
|
70
|
+
# captcha attribute is set. So we give a block to the matcher that tell
|
71
|
+
# exactly how to build an invalid object.
|
72
|
+
#
|
73
|
+
# The example above can also be written as:
|
74
|
+
#
|
75
|
+
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
76
|
+
#
|
77
|
+
# == Options
|
78
|
+
#
|
79
|
+
# * <tt>:builder</tt> - a proc to build the association
|
80
|
+
#
|
81
|
+
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
|
82
|
+
# Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
|
83
|
+
#
|
84
|
+
# == Examples
|
85
|
+
#
|
86
|
+
# should_validate_associated :tasks
|
87
|
+
# should_validate_associated(:tasks){ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
88
|
+
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
|
89
|
+
#
|
90
|
+
# it { should validate_associated(:tasks) }
|
91
|
+
# it { should validate_associated(:tasks){ |p| p.tasks.build(:captcha => 'i_am_a_bot') } }
|
92
|
+
# it { should validate_associated(:tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }) }
|
93
|
+
#
|
94
|
+
def validate_associated(*args, &block)
|
95
|
+
ValidateAssociatedMatcher.new(*args, &block).spec(self)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class ValidateConfirmationOfMatcher < Remarkable::ActiveRecord::Base
|
5
|
+
|
6
|
+
arguments :collection => :attributes, :as => :attribute
|
7
|
+
|
8
|
+
optional :message
|
9
|
+
collection_assertions :responds_to_confirmation?, :confirms?
|
10
|
+
|
11
|
+
default_options :message => :confirmation
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def responds_to_confirmation?
|
16
|
+
@subject.respond_to?(:"#{@attribute}_confirmation=")
|
17
|
+
end
|
18
|
+
|
19
|
+
def confirms?
|
20
|
+
@subject.send(:"#{@attribute}_confirmation=", 'something')
|
21
|
+
bad?('different')
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensures that the model cannot be saved if one of the attributes is not confirmed.
|
27
|
+
#
|
28
|
+
# == Options
|
29
|
+
#
|
30
|
+
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
|
31
|
+
# Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.confirmation')</tt>
|
32
|
+
#
|
33
|
+
# == Examples
|
34
|
+
#
|
35
|
+
# should_validate_confirmation_of :email, :password
|
36
|
+
#
|
37
|
+
# it { should validate_confirmation_of(:email, :password) }
|
38
|
+
#
|
39
|
+
def validate_confirmation_of(*attributes)
|
40
|
+
ValidateConfirmationOfMatcher.new(*attributes).spec(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|