remarkable_activerecord 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CHANGELOG +47 -0
  2. data/LICENSE +20 -0
  3. data/README +2 -0
  4. data/lib/remarkable_activerecord/base.rb +238 -0
  5. data/lib/remarkable_activerecord/human_names.rb +37 -0
  6. data/lib/remarkable_activerecord/matchers/allow_mass_assignment_of_matcher.rb +34 -0
  7. data/lib/remarkable_activerecord/matchers/allow_values_for_matcher.rb +94 -0
  8. data/lib/remarkable_activerecord/matchers/association_matcher.rb +235 -0
  9. data/lib/remarkable_activerecord/matchers/have_column_matcher.rb +68 -0
  10. data/lib/remarkable_activerecord/matchers/have_index_matcher.rb +57 -0
  11. data/lib/remarkable_activerecord/matchers/have_readonly_attributes_matcher.rb +30 -0
  12. data/lib/remarkable_activerecord/matchers/have_scope_matcher.rb +80 -0
  13. data/lib/remarkable_activerecord/matchers/validate_acceptance_of_matcher.rb +51 -0
  14. data/lib/remarkable_activerecord/matchers/validate_associated_matcher.rb +99 -0
  15. data/lib/remarkable_activerecord/matchers/validate_confirmation_of_matcher.rb +45 -0
  16. data/lib/remarkable_activerecord/matchers/validate_exclusion_of_matcher.rb +47 -0
  17. data/lib/remarkable_activerecord/matchers/validate_inclusion_of_matcher.rb +47 -0
  18. data/lib/remarkable_activerecord/matchers/validate_length_of_matcher.rb +123 -0
  19. data/lib/remarkable_activerecord/matchers/validate_numericality_of_matcher.rb +184 -0
  20. data/lib/remarkable_activerecord/matchers/validate_presence_of_matcher.rb +29 -0
  21. data/lib/remarkable_activerecord/matchers/validate_uniqueness_of_matcher.rb +151 -0
  22. data/lib/remarkable_activerecord.rb +29 -0
  23. data/locale/en.yml +253 -0
  24. data/spec/allow_mass_assignment_of_matcher_spec.rb +57 -0
  25. data/spec/allow_values_for_matcher_spec.rb +56 -0
  26. data/spec/association_matcher_spec.rb +616 -0
  27. data/spec/have_column_matcher_spec.rb +73 -0
  28. data/spec/have_index_matcher_spec.rb +68 -0
  29. data/spec/have_readonly_attributes_matcher_spec.rb +47 -0
  30. data/spec/have_scope_matcher_spec.rb +69 -0
  31. data/spec/model_builder.rb +101 -0
  32. data/spec/rcov.opts +2 -0
  33. data/spec/spec.opts +4 -0
  34. data/spec/spec_helper.rb +27 -0
  35. data/spec/validate_acceptance_of_matcher_spec.rb +68 -0
  36. data/spec/validate_associated_matcher_spec.rb +122 -0
  37. data/spec/validate_confirmation_of_matcher_spec.rb +58 -0
  38. data/spec/validate_exclusion_of_matcher_spec.rb +88 -0
  39. data/spec/validate_inclusion_of_matcher_spec.rb +84 -0
  40. data/spec/validate_length_of_matcher_spec.rb +165 -0
  41. data/spec/validate_numericality_of_matcher_spec.rb +180 -0
  42. data/spec/validate_presence_of_matcher_spec.rb +52 -0
  43. data/spec/validate_uniqueness_of_matcher_spec.rb +150 -0
  44. 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