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,284 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class AssociationMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :macro, :collection => :associations, :as => :association
|
6
|
+
|
7
|
+
optionals :through, :source, :source_type, :class_name, :foreign_key, :dependent, :join_table, :as
|
8
|
+
optionals :select, :conditions, :include, :group, :having, :order, :limit, :offset, :finder_sql, :counter_sql
|
9
|
+
optionals :uniq, :readonly, :validate, :autosave, :polymorphic, :counter_cache, :default => true
|
10
|
+
optionals :before_add, :after_add, :before_remove, :after_remove
|
11
|
+
|
12
|
+
collection_assertions :association_exists?, :macro_matches?, :through_exists?, :source_exists?,
|
13
|
+
:klass_exists?, :join_table_exists?, :foreign_key_exists?, :polymorphic_exists?,
|
14
|
+
:counter_cache_exists?, :options_match?
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def association_exists?
|
19
|
+
reflection
|
20
|
+
end
|
21
|
+
|
22
|
+
def macro_matches?
|
23
|
+
reflection.macro == @macro
|
24
|
+
end
|
25
|
+
|
26
|
+
def through_exists?
|
27
|
+
return true unless @options.key?(:through)
|
28
|
+
reflection.through_reflection rescue false
|
29
|
+
end
|
30
|
+
|
31
|
+
def source_exists?
|
32
|
+
return true unless @options.key?(:through)
|
33
|
+
reflection.source_reflection rescue false
|
34
|
+
end
|
35
|
+
|
36
|
+
# Polymorphic associations does not have a klass.
|
37
|
+
#
|
38
|
+
def klass_exists?
|
39
|
+
return true if @options[:polymorphic]
|
40
|
+
reflection.klass rescue nil
|
41
|
+
end
|
42
|
+
|
43
|
+
# has_and_belongs_to_many only works if the tables are in the same
|
44
|
+
# database, so we always look for the table in the subject connection.
|
45
|
+
#
|
46
|
+
def join_table_exists?
|
47
|
+
return true unless reflection.macro == :has_and_belongs_to_many
|
48
|
+
subject_class.connection.tables.include?(reflection.options[:join_table].to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
def foreign_key_exists?
|
52
|
+
return true unless foreign_key_table
|
53
|
+
table_has_column?(foreign_key_table_class, foreign_key_table, reflection_foreign_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def polymorphic_exists?
|
57
|
+
return true unless @options[:polymorphic]
|
58
|
+
klass_table_has_column?(subject_class, reflection_foreign_key.sub(/_id$/, '_type'))
|
59
|
+
end
|
60
|
+
|
61
|
+
def counter_cache_exists?
|
62
|
+
return true unless @options[:counter_cache]
|
63
|
+
klass_table_has_column?(reflection.klass, reflection.counter_cache_column.to_s)
|
64
|
+
end
|
65
|
+
|
66
|
+
def options_match?
|
67
|
+
actual_options = {}
|
68
|
+
|
69
|
+
@options.keys.each do |key|
|
70
|
+
method = :"reflection_#{key}"
|
71
|
+
|
72
|
+
@options[key] = @options[key].to_s
|
73
|
+
actual_options[key] = (respond_to?(method, true) ? send(method) : reflection.options[key]).to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
return @options == actual_options, :actual => actual_options.inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def reflection
|
82
|
+
@reflection ||= subject_class.reflect_on_association(@association.to_sym)
|
83
|
+
end
|
84
|
+
|
85
|
+
def subject_table_name
|
86
|
+
subject_class.table_name.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def reflection_class_name
|
90
|
+
reflection.class_name.to_s rescue nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def reflection_table_name
|
94
|
+
reflection.klass.table_name.to_s rescue nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def reflection_foreign_key
|
98
|
+
(reflection.respond_to?(:foreign_key) ? reflection.foreign_key : reflection.primary_key_name).to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
def table_has_column?(klass, table_name, column)
|
102
|
+
klass.connection.columns(table_name, 'Remarkable column retrieval').any?{|c| c.name == column }
|
103
|
+
end
|
104
|
+
|
105
|
+
def klass_table_has_column?(klass, column)
|
106
|
+
table_has_column?(klass, klass.table_name, column)
|
107
|
+
end
|
108
|
+
|
109
|
+
# In through we don't check the foreign_key, because it's spread
|
110
|
+
# accross the through and the source reflection which should be tested
|
111
|
+
# with their own macros.
|
112
|
+
#
|
113
|
+
# In cases a join table exists (has_and_belongs_to_many and through
|
114
|
+
# associations), we check the foreign key in the join table.
|
115
|
+
#
|
116
|
+
# On belongs to, the foreign_key is in the subject class table and in
|
117
|
+
# other cases it's on the reflection class table.
|
118
|
+
#
|
119
|
+
def foreign_key_table
|
120
|
+
if reflection.options.key?(:through)
|
121
|
+
nil
|
122
|
+
elsif reflection.macro == :has_and_belongs_to_many
|
123
|
+
reflection.options[:join_table]
|
124
|
+
elsif reflection.macro == :belongs_to
|
125
|
+
subject_table_name
|
126
|
+
else
|
127
|
+
reflection_table_name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the foreign key table class to use the proper connection
|
132
|
+
# when searching for the table and foreign key.
|
133
|
+
#
|
134
|
+
def foreign_key_table_class
|
135
|
+
if [:belongs_to, :has_and_belongs_to_many].include?(reflection.macro)
|
136
|
+
subject_class
|
137
|
+
else
|
138
|
+
reflection.klass
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def interpolation_options
|
143
|
+
options = {}
|
144
|
+
options[:macro] = Remarkable.t(@macro, :scope => matcher_i18n_scope, :default => @macro.to_s.gsub("_", ""))
|
145
|
+
options[:options] = @options.inspect
|
146
|
+
|
147
|
+
if @subject && reflection
|
148
|
+
options.merge!(
|
149
|
+
:actual_macro => Remarkable.t(reflection.macro, :scope => matcher_i18n_scope, :default => reflection.macro.to_s),
|
150
|
+
:subject_table => subject_table_name.inspect,
|
151
|
+
:reflection_table => reflection_table_name.inspect,
|
152
|
+
:foreign_key_table => foreign_key_table.inspect,
|
153
|
+
:polymorphic_column => reflection_foreign_key.sub(/_id$/, '_type').inspect,
|
154
|
+
:counter_cache_column => reflection.counter_cache_column.to_s.inspect,
|
155
|
+
:join_table => reflection.options[:join_table].inspect,
|
156
|
+
:foreign_key => reflection_foreign_key.inspect
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
options
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Ensure that the belongs_to relationship exists. Will also test that the
|
165
|
+
# subject table has the association_id column.
|
166
|
+
#
|
167
|
+
# == Options
|
168
|
+
#
|
169
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
170
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the subject table.
|
171
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
172
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
173
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
174
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
175
|
+
# * <tt>:counter_cache</tt> - the expected dependent value for the association.
|
176
|
+
# It also checks if the column actually exists in the table.
|
177
|
+
# * <tt>:polymorphic</tt> - if the association should be polymorphic or not.
|
178
|
+
# When true it also checks for the association_type column in the subject table.
|
179
|
+
#
|
180
|
+
# Plus all supported sql conditions options: :select, :conditions, :order,
|
181
|
+
# :limit, :offset, :include, :group, :having.
|
182
|
+
#
|
183
|
+
# == Examples
|
184
|
+
#
|
185
|
+
# should_belong_to :parent, :polymorphic => true
|
186
|
+
# it { should belong_to(:parent) }
|
187
|
+
#
|
188
|
+
def belong_to(*associations, &block)
|
189
|
+
AssociationMatcher.new(:belongs_to, *associations, &block).spec(self)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Ensures that the has_and_belongs_to_many relationship exists, if the join
|
193
|
+
# table is in place and if the foreign_key column exists.
|
194
|
+
#
|
195
|
+
# == Options
|
196
|
+
#
|
197
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
198
|
+
# * <tt>:join_table</tt> - the expected join table name.
|
199
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the association table.
|
200
|
+
# * <tt>:uniq</tt> - checks wether uniq is true or false.
|
201
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
202
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
203
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
204
|
+
#
|
205
|
+
# Plus all supported sql conditions options: :select, :conditions, :order,
|
206
|
+
# :limit, :offset, :include, :group, :having.
|
207
|
+
#
|
208
|
+
# == Examples
|
209
|
+
#
|
210
|
+
# should_have_and_belong_to_many :posts, :cars
|
211
|
+
# it{ should have_and_belong_to_many :posts, :cars }
|
212
|
+
#
|
213
|
+
def have_and_belong_to_many(*associations, &block)
|
214
|
+
AssociationMatcher.new(:has_and_belongs_to_many, *associations, &block).spec(self)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Ensures that the has_many relationship exists. Will also test that the
|
218
|
+
# associated table has the required columns. It works by default with
|
219
|
+
# polymorphic association (:as does not have to be supplied).
|
220
|
+
#
|
221
|
+
# == Options
|
222
|
+
#
|
223
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
224
|
+
# * <tt>:through</tt> - the expected join model which to perform the query.
|
225
|
+
# It also checks if the through table exists.
|
226
|
+
# * <tt>:source</tt> - the source of the through association.
|
227
|
+
# * <tt>:source_type</tt> - the source type of the through association.
|
228
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
|
229
|
+
# When used with :through, it will check for the foreign key in the join table.
|
230
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
231
|
+
# * <tt>:uniq</tt> - checks wether uniq is true or false.
|
232
|
+
# * <tt>:readonly</tt> - checks wether readonly is true or false.
|
233
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
234
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
235
|
+
#
|
236
|
+
# Plus all supported sql conditions options: :select, :conditions, :order,
|
237
|
+
# :limit, :offset, :include, :group, :having.
|
238
|
+
#
|
239
|
+
# == Examples
|
240
|
+
#
|
241
|
+
# should_have_many :friends
|
242
|
+
# should_have_many :enemies, :through => :friends
|
243
|
+
# should_have_many :enemies, :dependent => :destroy
|
244
|
+
#
|
245
|
+
# it{ should have_many(:friends) }
|
246
|
+
# it{ should have_many(:enemies, :through => :friends) }
|
247
|
+
# it{ should have_many(:enemies, :dependent => :destroy) }
|
248
|
+
#
|
249
|
+
def have_many(*associations, &block)
|
250
|
+
AssociationMatcher.new(:has_many, *associations, &block).spec(self)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Ensures that the has_many relationship exists. Will also test that the
|
254
|
+
# associated table has the required columns. It works by default with
|
255
|
+
# polymorphic association (:as does not have to be supplied).
|
256
|
+
#
|
257
|
+
# == Options
|
258
|
+
#
|
259
|
+
# * <tt>:class_name</tt> - the expected associted class name.
|
260
|
+
# * <tt>:through</tt> - the expected join model which to perform the query.
|
261
|
+
# It also checks if the through table exists.
|
262
|
+
# * <tt>:source</tt> - the source of the through association.
|
263
|
+
# * <tt>:source_type</tt> - the source type of the through association.
|
264
|
+
# * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
|
265
|
+
# When used with :through, it will check for the foreign key in the join table.
|
266
|
+
# * <tt>:dependent</tt> - the expected dependent value for the association.
|
267
|
+
# * <tt>:validate</tt> - checks wether validate is true or false.
|
268
|
+
# * <tt>:autosave</tt> - checks wether autosave is true or false.
|
269
|
+
#
|
270
|
+
# Plus all supported sql conditions options: :select, :conditions, :order,
|
271
|
+
# :limit, :offset, :include, :group, :having.
|
272
|
+
#
|
273
|
+
# == Examples
|
274
|
+
#
|
275
|
+
# should_have_one :universe
|
276
|
+
# it{ should have_one(:universe) }
|
277
|
+
#
|
278
|
+
def have_one(*associations, &block)
|
279
|
+
AssociationMatcher.new(:has_one, *associations, &block).spec(self)
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveColumnMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
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, &block)
|
60
|
+
HaveColumnMatcher.new(*args, &block).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,68 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveDefaultScopeMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments
|
6
|
+
assertions :options_match?
|
7
|
+
|
8
|
+
optionals :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def options_match?
|
13
|
+
default_scope.include?(@options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_scope
|
17
|
+
@default_scope ||= if @subject
|
18
|
+
scopes = subject_class.default_scoping || []
|
19
|
+
scopes.map!{ |s| s[:find] }
|
20
|
+
else
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def interpolation_options
|
26
|
+
{ :options => @options.inspect, :actual => default_scope.inspect }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# Ensures that the model has a default scope with the given options.
|
32
|
+
#
|
33
|
+
# == Options
|
34
|
+
#
|
35
|
+
# All options that the default scope would pass on to find: :conditions,
|
36
|
+
# :include, :joins, :limit, :offset, :order, :select, :readonly, :group,
|
37
|
+
# :having, :from, :lock.
|
38
|
+
#
|
39
|
+
# == Examples
|
40
|
+
#
|
41
|
+
# it { should have_default_scope(:conditions => {:visible => true}) }
|
42
|
+
# it { should have_default_scope.conditions(:visible => true) }
|
43
|
+
#
|
44
|
+
# Passes for:
|
45
|
+
#
|
46
|
+
# default_scope :conditions => { :visible => true }
|
47
|
+
#
|
48
|
+
# If you set two different default scopes, you have to spec them
|
49
|
+
# separatedly. Given the scopes:
|
50
|
+
#
|
51
|
+
# default_scope :conditions => { :visible => true }
|
52
|
+
# default_scope :conditions => { :published => true }
|
53
|
+
#
|
54
|
+
# Then we have the matchers:
|
55
|
+
#
|
56
|
+
# should_have_default_scope :conditions => { :visible => true } # Passes
|
57
|
+
# should_have_default_scope :conditions => { :published => true } # Passes
|
58
|
+
#
|
59
|
+
# should_have_default_scope :conditions => { :published => true,
|
60
|
+
# :visible => true } # Fails
|
61
|
+
#
|
62
|
+
def have_default_scope(*args, &block)
|
63
|
+
HaveDefaultScopeMatcher.new(*args, &block).spec(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module ActiveRecord
|
3
|
+
module Matchers
|
4
|
+
class HaveIndexMatcher < Remarkable::ActiveRecord::Base #:nodoc:
|
5
|
+
arguments :collection => :columns, :as => :column
|
6
|
+
|
7
|
+
optional :table_name
|
8
|
+
optional :unique, :default => true
|
9
|
+
|
10
|
+
collection_assertions :index_exists?, :is_unique?
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def index_exists?
|
15
|
+
!matched_index.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_unique?
|
19
|
+
return true unless @options.key?(:unique)
|
20
|
+
return @options[:unique] == matched_index.unique, :actual => matched_index.unique
|
21
|
+
end
|
22
|
+
|
23
|
+
def matched_index
|
24
|
+
columns = [@column].flatten.map(&:to_s)
|
25
|
+
indexes.detect { |ind| ind.columns == columns }
|
26
|
+
end
|
27
|
+
|
28
|
+
def indexes
|
29
|
+
@indexes ||= ::ActiveRecord::Base.connection.indexes(current_table_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def interpolation_options
|
33
|
+
@subject ? { :table_name => current_table_name } : {}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def current_table_name
|
39
|
+
@options[:table_name] || subject_class.table_name
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensures the database column has specified index.
|
45
|
+
#
|
46
|
+
# == Options
|
47
|
+
#
|
48
|
+
# * <tt>unique</tt> - when supplied, tests if the index is unique or not
|
49
|
+
# * <tt>table_name</tt> - when supplied, tests if the index is defined for the given table
|
50
|
+
#
|
51
|
+
# == Examples
|
52
|
+
#
|
53
|
+
# it { should have_index(:ssn).unique(true) }
|
54
|
+
# it { should have_index([:name, :email]).unique(true) }
|
55
|
+
#
|
56
|
+
# should_have_index :ssn, :unique => true, :limit => 9, :null => false
|
57
|
+
#
|
58
|
+
# should_have_index :ssn do |m|
|
59
|
+
# m.unique
|
60
|
+
# m.limit = 9
|
61
|
+
# m.null = false
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
def have_index(*args, &block)
|
65
|
+
HaveIndexMatcher.new(*args, &block).spec(self)
|
66
|
+
end
|
67
|
+
alias :have_indices :have_index
|
68
|
+
alias :have_db_index :have_index
|
69
|
+
alias :have_db_indices :have_index
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|