benhutton-remarkable_activerecord 4.0.0.alpha6

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