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.
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