carlosbrando-remarkable 0.0.99 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -5
- data/Manifest.txt +53 -35
- data/PostInstall.txt +1 -6
- data/README.rdoc +109 -15
- data/Rakefile +29 -29
- data/lib/remarkable/active_record/README.markdown +378 -0
- data/lib/remarkable/active_record/active_record.rb +12 -11
- data/lib/remarkable/active_record/helpers.rb +215 -5
- data/lib/remarkable/active_record/macros/associations/association_matcher.rb +242 -0
- data/lib/remarkable/active_record/macros/callbacks/callback_matcher.rb +46 -0
- data/lib/remarkable/active_record/macros/database/column_matcher.rb +122 -0
- data/lib/remarkable/active_record/macros/database/index_matcher.rb +103 -0
- data/lib/remarkable/active_record/macros/validations/allow_mass_assignment_of_matcher.rb +52 -0
- data/lib/remarkable/active_record/macros/validations/ensure_value_in_list_matcher.rb +83 -0
- data/lib/remarkable/active_record/macros/validations/ensure_value_in_range_matcher.rb +172 -0
- data/lib/remarkable/active_record/macros/validations/have_class_methods_matcher.rb +54 -0
- data/lib/remarkable/active_record/macros/validations/have_instance_methods_matcher.rb +54 -0
- data/lib/remarkable/active_record/macros/validations/have_named_scope_matcher.rb +94 -0
- data/lib/remarkable/active_record/macros/validations/have_readonly_attributes_matcher.rb +48 -0
- data/lib/remarkable/active_record/macros/validations/protect_attributes_matcher.rb +51 -0
- data/lib/remarkable/active_record/macros/validations/validate_acceptance_of_matcher.rb +79 -0
- data/lib/remarkable/active_record/macros/validations/validate_associated_matcher.rb +177 -0
- data/lib/remarkable/active_record/macros/validations/validate_confirmation_of_matcher.rb +74 -0
- data/lib/remarkable/active_record/macros/validations/validate_exclusion_of_matcher.rb +38 -0
- data/lib/remarkable/active_record/macros/validations/validate_format_of_matcher.rb +33 -0
- data/lib/remarkable/active_record/macros/validations/validate_inclusion_of_matcher.rb +45 -0
- data/lib/remarkable/active_record/macros/validations/validate_length_of_matcher.rb +248 -0
- data/lib/remarkable/active_record/macros/validations/validate_numericality_of_matcher.rb +206 -0
- data/lib/remarkable/active_record/macros/validations/validate_presence_of_matcher.rb +72 -0
- data/lib/remarkable/active_record/macros/validations/validate_uniqueness_of_matcher.rb +222 -0
- data/lib/remarkable/active_record/macros.rb +52 -0
- data/lib/remarkable/assertions.rb +29 -0
- data/lib/remarkable/controller/README.markdown +147 -0
- data/lib/remarkable/controller/controller.rb +11 -6
- data/lib/remarkable/controller/helpers.rb +4 -38
- data/lib/remarkable/controller/macros/assign_matcher.rb +85 -0
- data/lib/remarkable/controller/macros/filter_params_matcher.rb +63 -0
- data/lib/remarkable/controller/macros/metadata_matcher.rb +63 -0
- data/lib/remarkable/controller/macros/render_with_layout_matcher.rb +75 -0
- data/lib/remarkable/controller/macros/respond_with_content_type_matcher.rb +60 -0
- data/lib/remarkable/controller/macros/respond_with_matcher.rb +62 -0
- data/lib/remarkable/controller/macros/return_from_session_matcher.rb +58 -0
- data/lib/remarkable/controller/macros/route_matcher.rb +75 -0
- data/lib/remarkable/controller/macros/set_the_flash_to_matcher.rb +60 -0
- data/lib/remarkable/controller/macros.rb +78 -0
- data/lib/remarkable/dsl.rb +239 -0
- data/lib/remarkable/example/example_methods.rb +27 -7
- data/lib/remarkable/helpers.rb +28 -0
- data/lib/remarkable/matcher_base.rb +64 -0
- data/lib/remarkable/private_helpers.rb +10 -115
- data/lib/remarkable/rails.rb +27 -0
- data/lib/remarkable.rb +13 -5
- data/remarkable.gemspec +43 -0
- data/spec/controllers/posts_controller_spec.rb +58 -4
- data/spec/controllers/users_controller_spec.rb +1 -0
- data/spec/fixtures/fleas.yml +10 -0
- data/spec/fixtures/users.yml +7 -0
- data/spec/models/address_spec.rb +44 -0
- data/spec/models/dog_spec.rb +64 -3
- data/spec/models/flea_spec.rb +30 -0
- data/spec/models/post_spec.rb +36 -2
- data/spec/models/product_spec.rb +73 -8
- data/spec/models/tag_spec.rb +2 -2
- data/spec/models/tagging_spec.rb +24 -0
- data/spec/models/user_spec.rb +206 -21
- data/spec/other/custom_macros_spec.rb +27 -0
- data/spec/other/my_own_matcher_spec.rb +11 -0
- data/spec/other/private_helpers_spec.rb +31 -0
- data/spec/rails_root/app/controllers/posts_controller.rb +2 -0
- data/spec/rails_root/app/models/address.rb +2 -2
- data/spec/rails_root/app/models/flea.rb +4 -0
- data/spec/rails_root/app/models/pets/dog.rb +12 -0
- data/spec/rails_root/app/models/product.rb +7 -5
- data/spec/rails_root/app/models/tagging.rb +2 -0
- data/spec/rails_root/app/models/user.rb +20 -5
- data/spec/rails_root/app/views/layouts/posts.rhtml +8 -6
- data/spec/rails_root/config/database.yml +1 -2
- data/spec/rails_root/config/environment.rb +3 -1
- data/spec/rails_root/config/environments/{sqlite3.rb → test.rb} +0 -0
- data/spec/rails_root/config/locales/en.yml +8 -0
- data/spec/rails_root/db/migrate/001_create_users.rb +2 -2
- data/spec/rails_root/db/migrate/005_create_dogs.rb +1 -0
- data/spec/rails_root/db/migrate/011_add_fleas_color.rb +10 -0
- data/spec/rails_root/db/migrate/012_add_fleas_address.rb +10 -0
- data/spec/rails_root/spec/remarkable_macros/.keep +0 -0
- data/spec/rails_root/vendor/plugins/my_plugin/remarkable_macros/.keep +0 -0
- data/spec/spec_helper.rb +0 -2
- metadata +63 -43
- data/lib/remarkable/active_record/macros/associations/belong_to.rb +0 -81
- data/lib/remarkable/active_record/macros/associations/have_and_belong_to_many.rb +0 -77
- data/lib/remarkable/active_record/macros/associations/have_many.rb +0 -160
- data/lib/remarkable/active_record/macros/associations/have_one.rb +0 -133
- data/lib/remarkable/active_record/macros/database/have_db_column.rb +0 -81
- data/lib/remarkable/active_record/macros/database/have_db_columns.rb +0 -73
- data/lib/remarkable/active_record/macros/database/have_indices.rb +0 -75
- data/lib/remarkable/active_record/macros/validations/allow_values_for.rb +0 -103
- data/lib/remarkable/active_record/macros/validations/ensure_length_at_least.rb +0 -97
- data/lib/remarkable/active_record/macros/validations/ensure_length_in_range.rb +0 -134
- data/lib/remarkable/active_record/macros/validations/ensure_length_is.rb +0 -106
- data/lib/remarkable/active_record/macros/validations/ensure_value_in_range.rb +0 -117
- data/lib/remarkable/active_record/macros/validations/have_class_methods.rb +0 -74
- data/lib/remarkable/active_record/macros/validations/have_instance_methods.rb +0 -74
- data/lib/remarkable/active_record/macros/validations/have_named_scope.rb +0 -148
- data/lib/remarkable/active_record/macros/validations/have_readonly_attributes.rb +0 -81
- data/lib/remarkable/active_record/macros/validations/only_allow_numeric_values_for.rb +0 -89
- data/lib/remarkable/active_record/macros/validations/protect_attributes.rb +0 -89
- data/lib/remarkable/active_record/macros/validations/require_acceptance_of.rb +0 -94
- data/lib/remarkable/active_record/macros/validations/require_attributes.rb +0 -94
- data/lib/remarkable/active_record/macros/validations/require_unique_attributes.rb +0 -146
- data/lib/remarkable/controller/macros/assign_to.rb +0 -110
- data/lib/remarkable/controller/macros/filter_params.rb +0 -52
- data/lib/remarkable/controller/macros/redirect_to.rb +0 -24
- data/lib/remarkable/controller/macros/render_a_form.rb +0 -23
- data/lib/remarkable/controller/macros/render_template.rb +0 -18
- data/lib/remarkable/controller/macros/render_with_layout.rb +0 -61
- data/lib/remarkable/controller/macros/respond_with.rb +0 -86
- data/lib/remarkable/controller/macros/respond_with_content_type.rb +0 -45
- data/lib/remarkable/controller/macros/return_from_session.rb +0 -45
- data/lib/remarkable/controller/macros/route.rb +0 -91
- data/lib/remarkable/controller/macros/set_the_flash_to.rb +0 -58
- data/spec/rails_root/app/models/dog.rb +0 -5
@@ -0,0 +1,242 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
|
5
|
+
class AssociationMatcher < Remarkable::Matcher::Base
|
6
|
+
def initialize(macro, *associations)
|
7
|
+
@options = associations.extract_options!
|
8
|
+
@macro = macro
|
9
|
+
@associations = associations
|
10
|
+
end
|
11
|
+
|
12
|
+
def through(through)
|
13
|
+
@options[:through] = through
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependent(dependent)
|
18
|
+
@options[:dependent] = dependent
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def matches?(subject)
|
23
|
+
@subject = subject
|
24
|
+
|
25
|
+
assert_matcher_for(@associations) do |association|
|
26
|
+
association_correct?(association)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def failure_message
|
31
|
+
"Expected #{expectation} (#{@missing})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def negative_failure_message
|
35
|
+
"Did not expect #{expectation}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def description
|
39
|
+
description = "#{macro_description} #{@associations.to_sentence}"
|
40
|
+
description += " through #{@options[:through]}" if @options[:through]
|
41
|
+
description += " dependent => #{@options[:dependent]}" if @options[:dependent]
|
42
|
+
description
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def association_correct?(association)
|
48
|
+
@name = association
|
49
|
+
|
50
|
+
association_exists? &&
|
51
|
+
macro_correct? &&
|
52
|
+
foreign_key_exists? &&
|
53
|
+
through_association_valid? &&
|
54
|
+
dependent_correct? &&
|
55
|
+
join_table_exists?
|
56
|
+
end
|
57
|
+
|
58
|
+
def association_exists?
|
59
|
+
if reflection.nil?
|
60
|
+
@missing = "no association called #{@name}"
|
61
|
+
false
|
62
|
+
else
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def macro_correct?
|
68
|
+
if reflection.macro == @macro
|
69
|
+
true
|
70
|
+
else
|
71
|
+
@missing = "actual association type was #{reflection.macro}"
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def foreign_key_exists?
|
77
|
+
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
|
78
|
+
end
|
79
|
+
|
80
|
+
def belongs_foreign_key_missing?
|
81
|
+
@macro == :belongs_to && !class_has_foreign_key?(subject_class)
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_foreign_key_missing?
|
85
|
+
[:has_many, :has_one].include?(@macro) &&
|
86
|
+
!through? &&
|
87
|
+
!class_has_foreign_key?(associated_class)
|
88
|
+
end
|
89
|
+
|
90
|
+
def through_association_valid?
|
91
|
+
@options[:through].nil? || (through_association_exists? && through_association_correct?)
|
92
|
+
end
|
93
|
+
|
94
|
+
def through_association_exists?
|
95
|
+
if through_reflection.nil?
|
96
|
+
@missing = "#{subject_name} does not have any relationship to #{@options[:through]}"
|
97
|
+
false
|
98
|
+
else
|
99
|
+
true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def through_association_correct?
|
104
|
+
if @options[:through] == reflection.options[:through]
|
105
|
+
@missing = "Expected #{subject_name} to have #{@name} through #{@options[:through]}, " <<
|
106
|
+
" but got it through #{reflection.options[:through]}"
|
107
|
+
true
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def dependent_correct?
|
114
|
+
if @options[:dependent].nil? || @options[:dependent].to_s == reflection.options[:dependent].to_s
|
115
|
+
true
|
116
|
+
else
|
117
|
+
@missing = "#{@name} should have #{@options[:dependent]} dependency"
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def join_table_exists?
|
123
|
+
if @macro != :has_and_belongs_to_many ||
|
124
|
+
::ActiveRecord::Base.connection.tables.include?(join_table.to_s)
|
125
|
+
true
|
126
|
+
else
|
127
|
+
@missing = "join table #{join_table} doesn't exist"
|
128
|
+
false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def class_has_foreign_key?(klass)
|
133
|
+
if klass.column_names.include?(foreign_key.to_s)
|
134
|
+
true
|
135
|
+
else
|
136
|
+
@missing = "#{klass} does not have a #{foreign_key} foreign key."
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def join_table
|
142
|
+
reflection.options[:join_table]
|
143
|
+
end
|
144
|
+
|
145
|
+
def associated_class
|
146
|
+
reflection.klass
|
147
|
+
end
|
148
|
+
|
149
|
+
def foreign_key
|
150
|
+
reflection.primary_key_name
|
151
|
+
end
|
152
|
+
|
153
|
+
def through?
|
154
|
+
reflection.options[:through]
|
155
|
+
end
|
156
|
+
|
157
|
+
def reflection
|
158
|
+
subject_class.reflect_on_association(@name)
|
159
|
+
end
|
160
|
+
|
161
|
+
def through_reflection
|
162
|
+
subject_class.reflect_on_association(@options[:through])
|
163
|
+
end
|
164
|
+
|
165
|
+
def expectation
|
166
|
+
"#{subject_name} to have a #{@macro} association called #{@name}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def macro_description
|
170
|
+
case @macro.to_s
|
171
|
+
when 'belongs_to' then 'belong to'
|
172
|
+
when 'has_many' then 'have many'
|
173
|
+
when 'has_one' then 'have one'
|
174
|
+
when 'has_and_belongs_to_many' then
|
175
|
+
'have and belong to many'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Ensure that the belongs_to relationship exists.
|
182
|
+
#
|
183
|
+
# Examples:
|
184
|
+
#
|
185
|
+
# should_belong_to :parent
|
186
|
+
# it { should belong_to(:parent) }
|
187
|
+
#
|
188
|
+
def belong_to(*associations)
|
189
|
+
AssociationMatcher.new(:belongs_to, *associations)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Ensures that the has_many relationship exists. Will also test that the associated table has the required columns. Works with polymorphic associations.
|
193
|
+
#
|
194
|
+
# Options:
|
195
|
+
#
|
196
|
+
# * <tt>:through</tt> - association name for has_many :through
|
197
|
+
# * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
|
198
|
+
#
|
199
|
+
# Examples:
|
200
|
+
#
|
201
|
+
# should_have_many :friends
|
202
|
+
# should_have_many :enemies, :through => :friends
|
203
|
+
# should_have_many :enemies, :dependent => :destroy
|
204
|
+
#
|
205
|
+
# it{ should have_many(:friends) }
|
206
|
+
# it{ should have_many(:enemies, :through => :friends) }
|
207
|
+
# it{ should have_many(:enemies, :dependent => :destroy) }
|
208
|
+
#
|
209
|
+
#
|
210
|
+
def have_many(*associations)
|
211
|
+
AssociationMatcher.new(:has_many, *associations)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Ensure that the has_one relationship exists. Will also test that the associated table has the required columns. Works with polymorphic associations.
|
215
|
+
#
|
216
|
+
# Options:
|
217
|
+
#
|
218
|
+
# * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
|
219
|
+
#
|
220
|
+
# Examples:
|
221
|
+
#
|
222
|
+
# should_have_one :god
|
223
|
+
# it{ should have_one(:god) }
|
224
|
+
#
|
225
|
+
def have_one(*associations)
|
226
|
+
AssociationMatcher.new(:has_one, *associations)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Ensures that the has_and_belongs_to_many relationship exists, and that the join table is in place.
|
230
|
+
#
|
231
|
+
# Examples:
|
232
|
+
#
|
233
|
+
# should_have_and_belong_to_many :posts, :cars
|
234
|
+
# it{ should have_and_belong_to_many :posts, :cars }
|
235
|
+
#
|
236
|
+
def have_and_belong_to_many(*associations)
|
237
|
+
AssociationMatcher.new(:has_and_belongs_to_many, *associations)
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
|
5
|
+
::ActiveRecord::Callbacks::CALLBACKS.each do |callback|
|
6
|
+
define_method("have_#{callback}_callback") do |method|
|
7
|
+
CallbackMatcher.new(callback, method)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class CallbackMatcher < Remarkable::Matcher::Base
|
12
|
+
def initialize(callback, method)
|
13
|
+
@callback = callback
|
14
|
+
@method = method
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(subject)
|
18
|
+
@subject = subject
|
19
|
+
assert_matcher { has_callback? }
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
"have a #{@callback} callback named #{@method}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def has_callback?
|
29
|
+
return true if callbacks_for(@callback).include?(@method)
|
30
|
+
|
31
|
+
@missing = "#{subject_name} does not have a #{@callback} callback named #{@method}"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
def callbacks_for(callback)
|
36
|
+
subject_class.send("#{callback}_callback_chain").collect(&:method)
|
37
|
+
end
|
38
|
+
|
39
|
+
def expectation
|
40
|
+
"#{subject_name} have a #{@callback} callback named #{@method}"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
|
5
|
+
class ColumnMatcher < Remarkable::Matcher::Base
|
6
|
+
def initialize(*columns)
|
7
|
+
@options = columns.extract_options!
|
8
|
+
@columns = columns
|
9
|
+
end
|
10
|
+
|
11
|
+
def type(type)
|
12
|
+
@options[:type] = type
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def primary(value = true)
|
17
|
+
@options[:primary] = value
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def default(default)
|
22
|
+
@options[:default] = default
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def precision(precision)
|
27
|
+
@options[:precision] = precision
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def limit(limit)
|
32
|
+
@options[:limit] = limit
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def null(value = true)
|
37
|
+
@options[:null] = value
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def scale(scale)
|
42
|
+
@options[:scale] = scale
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def sql_type(sql_type)
|
47
|
+
@options[:sql_type] = sql_type
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def matches?(subject)
|
52
|
+
@subject = subject
|
53
|
+
|
54
|
+
assert_matcher_for(@columns) do |column|
|
55
|
+
@column = column
|
56
|
+
has_column? && all_options_correct?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def failure_message
|
61
|
+
"Expected #{expectation} (#{@missing})"
|
62
|
+
end
|
63
|
+
|
64
|
+
def negative_failure_message
|
65
|
+
"Did not expect #{expectation}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def description
|
69
|
+
description = if @columns.size == 1
|
70
|
+
"have column named :#{@columns[0]}"
|
71
|
+
else
|
72
|
+
"have columns #{@columns.to_sentence}"
|
73
|
+
end
|
74
|
+
description << " with options " + @options.inspect unless @options.empty?
|
75
|
+
description
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def column_type
|
81
|
+
subject_class.columns.detect {|c| c.name == @column.to_s }
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_column?
|
85
|
+
return true if column_type
|
86
|
+
@missing = "#{subject_name} does not have column #{@column}"
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
def all_options_correct?
|
91
|
+
@options.each do |option, value|
|
92
|
+
return false unless option_correct?(option, value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def option_correct?(option, expected_value)
|
97
|
+
found_value = column_type.instance_variable_get("@#{option.to_s}").to_s
|
98
|
+
|
99
|
+
if found_value == expected_value.to_s
|
100
|
+
true
|
101
|
+
else
|
102
|
+
@missing = ":#{@column} column on table for #{subject_class} does not match option :#{option}, found '#{found_value}' but expected '#{expected_value}'"
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def expectation
|
108
|
+
"#{subject_name} to have a column named #{@column}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def have_db_column(column, options = {})
|
113
|
+
ColumnMatcher.new(column, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def have_db_columns(*columns)
|
117
|
+
ColumnMatcher.new(*columns)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
|
5
|
+
# Ensures the database column has specified index.
|
6
|
+
#
|
7
|
+
# Options:
|
8
|
+
# * <tt>unique</tt> -
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
# it { should have_index(:ssn).unique(true) }
|
12
|
+
#
|
13
|
+
def have_indices(*columns)
|
14
|
+
IndexMatcher.new(*columns)
|
15
|
+
end
|
16
|
+
alias_method :have_index, :have_indices
|
17
|
+
|
18
|
+
class IndexMatcher < Remarkable::Matcher::Base
|
19
|
+
INDEX_TYPES = { true => "unique", false => "non-unique" }
|
20
|
+
|
21
|
+
def initialize(*columns)
|
22
|
+
load_options(columns)
|
23
|
+
@columns = columns
|
24
|
+
end
|
25
|
+
|
26
|
+
def unique(value = true)
|
27
|
+
@options[:unique] = value
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def table_name=(table_name)
|
32
|
+
@table_name = table_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def matches?(subject)
|
36
|
+
@subject = subject
|
37
|
+
@expected_uniqueness = @options[:unique] ? 'unique' : 'non-unique'
|
38
|
+
|
39
|
+
assert_matcher_for(@columns) do |column|
|
40
|
+
@column = column
|
41
|
+
index_exists? && correct_unique?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def failure_message
|
46
|
+
"Expected to #{expectation} (#{@missing})"
|
47
|
+
end
|
48
|
+
|
49
|
+
def negative_failure_message
|
50
|
+
"Did not expect to #{expectation}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def description
|
54
|
+
"have #{index_type} index on #{table_name} for #{@columns.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def index_exists?
|
60
|
+
return true if matched_index
|
61
|
+
|
62
|
+
@missing = "#{table_name} does not have an index for #{@column}"
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
def correct_unique?
|
67
|
+
return true unless [true, false].include?(@options[:unique])
|
68
|
+
return true if @options[:unique] == matched_index.unique
|
69
|
+
|
70
|
+
@missing = "Expected #{index_type} index but was #{INDEX_TYPES[!@options[:unique]]}."
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
|
74
|
+
def matched_index
|
75
|
+
columns = [@column].flatten.map(&:to_s)
|
76
|
+
indexes.detect { |ind| ind.columns == columns }
|
77
|
+
end
|
78
|
+
|
79
|
+
def index_type
|
80
|
+
INDEX_TYPES[@options[:unique]] || "an"
|
81
|
+
end
|
82
|
+
|
83
|
+
def table_name
|
84
|
+
@table_name ||= subject_class.table_name
|
85
|
+
end
|
86
|
+
|
87
|
+
def indexes
|
88
|
+
@indexes ||= ::ActiveRecord::Base.connection.indexes(table_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def expectation
|
92
|
+
"have #{index_type} index on #{table_name} for #{@column.inspect}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def load_options(options)
|
96
|
+
@options = {
|
97
|
+
:unique => nil
|
98
|
+
}.merge(options.extract_options!)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
class AllowMassAssignmentOfMatcher < Remarkable::Matcher::Base
|
5
|
+
def initialize(*attributes)
|
6
|
+
attributes.extract_options!
|
7
|
+
@attributes = attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
def matches?(subject)
|
11
|
+
@subject = subject
|
12
|
+
|
13
|
+
assert_matcher_for(@attributes) do |attribute|
|
14
|
+
@attribute = attribute
|
15
|
+
allowed_to_mass_update?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
"allow mass assignment of #{@attributes.to_sentence}"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def allowed_to_mass_update?
|
26
|
+
attribute = @attribute.to_sym
|
27
|
+
protected = subject_class.protected_attributes || []
|
28
|
+
accessible = subject_class.accessible_attributes || []
|
29
|
+
|
30
|
+
return true unless protected.include?(attribute.to_s)
|
31
|
+
return true unless accessible.empty? || accessible.include?(attribute.to_s)
|
32
|
+
|
33
|
+
@missing = accessible.empty? ? "#{subject_class} is protecting #{protected.to_a.to_sentence}" :
|
34
|
+
"#{subject_class} has not made #{attribute} accessible"
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
def expectation
|
39
|
+
"to allow mass assignment of #{@attribute}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Ensures that the attribute can be set on mass update.
|
44
|
+
#
|
45
|
+
# it { should allow_mass_assignment_of(:email, :name) }
|
46
|
+
#
|
47
|
+
def allow_mass_assignment_of(*attributes)
|
48
|
+
AllowMassAssignmentOfMatcher.new(*attributes)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module ActiveRecord # :nodoc:
|
3
|
+
module Matchers # :nodoc:
|
4
|
+
class EnsureValueInListMatcher < Remarkable::Matcher::Base
|
5
|
+
include Remarkable::ActiveRecord::Helpers
|
6
|
+
|
7
|
+
def initialize(attribute, behavior, *good_values)
|
8
|
+
@behavior = behavior
|
9
|
+
load_options(good_values.extract_options!)
|
10
|
+
|
11
|
+
@attribute = attribute
|
12
|
+
@good_values = good_values
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(subject)
|
16
|
+
@subject = subject
|
17
|
+
|
18
|
+
assert_matcher_for(@good_values) do |good_value|
|
19
|
+
@good_value = good_value
|
20
|
+
value_valid?
|
21
|
+
end &&
|
22
|
+
assert_matcher do
|
23
|
+
@good_value = 'nil'
|
24
|
+
allow_nil?
|
25
|
+
end &&
|
26
|
+
assert_matcher do
|
27
|
+
@good_value = 'blank'
|
28
|
+
allow_blank?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def description
|
33
|
+
values = @good_values.dup
|
34
|
+
values << 'nil' if @options[:allow_nil]
|
35
|
+
values << 'blank' if @options[:allow_blank]
|
36
|
+
|
37
|
+
if @behavior == :invalid
|
38
|
+
"allow #{@attribute} to be set to #{values.to_sentence}"
|
39
|
+
else
|
40
|
+
"ensure #{@behavior} of #{values.to_sentence} in #{@attribute}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def failure_message
|
45
|
+
"Expected to #{expectation} (#{@missing})"
|
46
|
+
end
|
47
|
+
|
48
|
+
def negative_failure_message
|
49
|
+
"Did not expect to #{expectation}"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def load_options(options = {})
|
55
|
+
@options = {
|
56
|
+
:message => @behavior
|
57
|
+
}.merge(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def value_valid?
|
61
|
+
if @behavior == :exclusion
|
62
|
+
return true if bad?(@good_value)
|
63
|
+
@missing = "#{@attribute} can be set to #{@good_value.inspect}"
|
64
|
+
else
|
65
|
+
return true if good?(@good_value)
|
66
|
+
@missing = "#{@attribute} cannot be set to #{@good_value.inspect}"
|
67
|
+
end
|
68
|
+
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def expectation
|
73
|
+
if @behavior == :invalid
|
74
|
+
"allow #{@attribute} to be set to #{@good_value.inspect}"
|
75
|
+
else
|
76
|
+
"validate #{@behavior} of #{@good_value.inspect} in #{@attribute}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|