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,73 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class HaveIndexMatcher < Remarkable::ActiveRecord::Base #:nodoc:
5
+ arguments :collection => :columns, :as => :column
6
+
7
+ optional :table_name
8
+ optional :unique, :default => true
9
+
10
+ collection_assertions :index_exists?, :is_unique?
11
+
12
+ protected
13
+
14
+ def index_exists?
15
+ !matched_index.nil?
16
+ end
17
+
18
+ def is_unique?
19
+ return true unless @options.key?(:unique)
20
+ return @options[:unique] == matched_index.unique, :actual => matched_index.unique
21
+ end
22
+
23
+ def matched_index
24
+ columns = [@column].flatten.map(&:to_s)
25
+ indexes.detect { |ind| ind.columns == columns }
26
+ end
27
+
28
+ def indexes
29
+ @indexes ||= ::ActiveRecord::Base.connection.indexes(current_table_name)
30
+ end
31
+
32
+ def interpolation_options
33
+ @subject ? { :table_name => current_table_name } : {}
34
+ end
35
+
36
+ private
37
+
38
+ def current_table_name
39
+ @options[:table_name] || subject_class.table_name
40
+ end
41
+
42
+ end
43
+
44
+ # Ensures the database column has specified index.
45
+ #
46
+ # == Options
47
+ #
48
+ # * <tt>unique</tt> - when supplied, tests if the index is unique or not
49
+ # * <tt>table_name</tt> - when supplied, tests if the index is defined for the given table
50
+ #
51
+ # == Examples
52
+ #
53
+ # it { should have_index(:ssn).unique(true) }
54
+ # it { should have_index([:name, :email]).unique(true) }
55
+ #
56
+ # should_have_index :ssn, :unique => true, :limit => 9, :null => false
57
+ #
58
+ # should_have_index :ssn do |m|
59
+ # m.unique
60
+ # m.limit = 9
61
+ # m.null = false
62
+ # end
63
+ #
64
+ def have_index(*args, &block)
65
+ HaveIndexMatcher.new(*args, &block).spec(self)
66
+ end
67
+ alias :have_indices :have_index
68
+ alias :have_db_index :have_index
69
+ alias :have_db_indices :have_index
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class HaveReadonlyAttributesMatcher < Remarkable::ActiveRecord::Base #:nodoc:
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, &block)
24
+ HaveReadonlyAttributesMatcher.new(*attributes, &block).spec(self)
25
+ end
26
+ alias :have_readonly_attribute :have_readonly_attributes
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,101 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class HaveScopeMatcher < Remarkable::ActiveRecord::Base #:nodoc:
5
+ arguments :scope_name
6
+ assertions :is_scope?, :options_match?
7
+
8
+ optionals :with, :splat => true
9
+
10
+ # Chained scopes taken from: http://m.onkey.org/2010/1/22/active-record-query-interface
11
+ optionals :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
12
+
13
+ protected
14
+
15
+ def is_scope?
16
+ @scope_object = if @options.key?(:with)
17
+ @options[:with] = [ @options[:with] ] unless Array === @options[:with]
18
+ subject_class.send(@scope_name, *@options[:with])
19
+ else
20
+ subject_class.send(@scope_name)
21
+ end
22
+
23
+ @scope_object.class == ::ActiveRecord::Relation && @scope_object.arel
24
+ end
25
+
26
+ def options_match?
27
+ @options.empty? || @scope_object.arel.to_sql == arel(subject_class, @options.except(:with)).to_sql
28
+ end
29
+
30
+ def interpolation_options
31
+ {
32
+ :options => (subject_class.respond_to?(:scoped) ? arel(subject_class, @options.except(:with)).to_sql : '{}'),
33
+ :actual => (@scope_object ? @scope_object.arel.to_sql : '{}')
34
+ }
35
+ end
36
+
37
+ private
38
+ def arel(model, scopes = nil)
39
+ return model.scoped unless scopes
40
+ scopes.inject(model.scoped) do |chain, (cond, option)|
41
+ chain.send(cond, option)
42
+ end.arel
43
+ end
44
+
45
+ end
46
+
47
+ # Ensures that the model has a named scope that returns an Relation object capable
48
+ # of building into relational algebra.
49
+ #
50
+ # == Options
51
+ #
52
+ # * <tt>with</tt> - Options to be sent to the named scope
53
+ #
54
+ # All options that the named scope would scope with Arel:
55
+ # :where, :having, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
56
+ #
57
+ # Matching is done by constructing the Arel objects and testing for equality.
58
+ #
59
+ # == Examples
60
+ #
61
+ # it { should have_scope(:visible, :where => {:visible => true}) }
62
+ # it { should have_scope(:visible).where(:visible => true) }
63
+ #
64
+ # Passes for
65
+ #
66
+ # scope :visible, where(:visible => true)
67
+ #
68
+ # Or for
69
+ #
70
+ # scope :visible, lambda { where(:visible => true) }
71
+ #
72
+ # Or for
73
+ #
74
+ # def self.visible
75
+ # where(:visible => true)
76
+ # end
77
+ #
78
+ #
79
+ # You can test lambdas or methods that return ActiveRecord#scoped calls by fixing
80
+ # a defined parameter.
81
+ #
82
+ # it { should have_scope(:recent, :with => 5) }
83
+ # it { should have_scope(:recent, :with => 1) }
84
+ #
85
+ # Passes for
86
+ #
87
+ # scope :recent, lambda {|c| limit(c)}
88
+ #
89
+ # Or for
90
+ #
91
+ # def self.recent(c)
92
+ # limit(c)
93
+ # end
94
+ #
95
+ def have_scope(*args, &block)
96
+ HaveScopeMatcher.new(*args, &block).spec(self)
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,100 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidateAssociatedMatcher < Remarkable::ActiveRecord::Base #:nodoc:
5
+ arguments :collection => :associations, :as => :association, :block => true
6
+
7
+ optional :message
8
+ optional :builder, :block => true
9
+
10
+ collection_assertions :find_association?, :is_valid?
11
+ default_options :message => :invalid
12
+
13
+ protected
14
+
15
+ def find_association?
16
+ reflection = @subject.class.reflect_on_association(@association)
17
+
18
+ raise ScriptError, "Could not find association #{@association} on #{subject_class}." unless reflection
19
+
20
+ associated_object = if builder = @options[:builder] || @block
21
+ builder.call(@subject)
22
+ elsif [:belongs_to, :has_one].include?(reflection.macro)
23
+ @subject.send(:"build_#{@association}") rescue nil
24
+ else
25
+ @subject.send(@association).build rescue nil
26
+ end
27
+
28
+ raise ScriptError, "The association object #{@association} could not be built. You can give me " <<
29
+ ":builder as option or a block which returns an association." unless associated_object
30
+
31
+ raise ScriptError, "The associated object #{@association} is not invalid. You can give me " <<
32
+ ":builder as option or a block which returns an invalid association." if associated_object.valid?
33
+
34
+ return true
35
+ end
36
+
37
+ def is_valid?
38
+ return false if @subject.valid?
39
+
40
+ error_message_to_expect = error_message_from_model(@subject, @association, @options[:message])
41
+
42
+ assert_contains(@subject.errors[@association], error_message_to_expect)
43
+ end
44
+ end
45
+
46
+ # Ensures that the model is invalid if one of the associations given is
47
+ # invalid. It tries to build the association automatically. In has_one
48
+ # and belongs_to cases, it will build it like this:
49
+ #
50
+ # @model.build_association
51
+ # @project.build_manager
52
+ #
53
+ # In has_many and has_and_belongs_to_many to cases it will build it like
54
+ # this:
55
+ #
56
+ # @model.association.build
57
+ # @project.tasks.build
58
+ #
59
+ # The object returned MUST be invalid and it's likely the case, since the
60
+ # associated object is empty when calling build. However, if the associated
61
+ # object has to be manipulated to be invalid, you will have to give :builder
62
+ # as option or a block to manipulate it:
63
+ #
64
+ # should_validate_associated(:tasks) do |project|
65
+ # project.tasks.build(:captcha => 'i_am_a_bot')
66
+ # end
67
+ #
68
+ # In the case above, the associated object task is only invalid when the
69
+ # captcha attribute is set. So we give a block to the matcher that tell
70
+ # exactly how to build an invalid object.
71
+ #
72
+ # The example above can also be written as:
73
+ #
74
+ # should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
75
+ #
76
+ # == Options
77
+ #
78
+ # * <tt>:builder</tt> - a proc to build the association
79
+ #
80
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
81
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
82
+ #
83
+ # == Examples
84
+ #
85
+ # should_validate_associated :tasks
86
+ # should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
87
+ #
88
+ # should_validate_associated :tasks do |m|
89
+ # m.builder { |p| p.tasks.build(:captcha => 'i_am_a_bot') }
90
+ # end
91
+ #
92
+ # it { should validate_associated(:tasks) }
93
+ # it { should validate_associated(:tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }) }
94
+ #
95
+ def validate_associated(*args, &block)
96
+ ValidateAssociatedMatcher.new(*args, &block).spec(self)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,233 @@
1
+ module Remarkable
2
+ module ActiveRecord
3
+ module Matchers
4
+ class ValidateUniquenessOfMatcher < Remarkable::ActiveRecord::Base #:nodoc:
5
+ arguments :collection => :attributes, :as => :attribute
6
+
7
+ optional :message
8
+ optional :scope, :splat => true
9
+ optional :case_sensitive, :allow_nil, :allow_blank, :default => true
10
+
11
+ collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?,
12
+ :valid_with_new_scope?, :allow_nil?, :allow_blank?
13
+
14
+ default_options :message => :taken
15
+
16
+ before_assert do
17
+ @options[:scope] = [*@options[:scope]].compact if @options[:scope]
18
+ end
19
+
20
+ private
21
+
22
+ # Tries to find an object in the database. If allow_nil and/or allow_blank
23
+ # is given, we must find a record which is not nil or not blank.
24
+ #
25
+ # We should also ensure that the object retrieved from the database
26
+ # is not the @subject.
27
+ #
28
+ # If any of these attempts fail, an error is raised.
29
+ #
30
+ def find_first_object?
31
+ conditions, message = if @options[:allow_nil]
32
+ [ ["#{@attribute} IS NOT NULL"], " with #{@attribute} not nil" ]
33
+ elsif @options[:allow_blank]
34
+ [ ["#{@attribute} != ''"], " with #{@attribute} not blank" ]
35
+ else
36
+ [ [], "" ]
37
+ end
38
+
39
+ unless @subject.new_record?
40
+ primary_key = subject_class.primary_key
41
+
42
+ message << " which is different from the subject record (the object being validated is the same as the one in the database)"
43
+ conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'"
44
+ end
45
+
46
+ options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') }
47
+
48
+ return true if @existing = subject_class.find(:first, options)
49
+ raise ScriptError, "could not find a #{subject_class} record in the database" + message
50
+ end
51
+
52
+ # Set subject scope to be equal to the object found.
53
+ #
54
+ def responds_to_scope?
55
+ (@options[:scope] || []).each do |scope|
56
+ setter = :"#{scope}="
57
+
58
+ return false, :method => setter unless @subject.respond_to?(setter)
59
+ return false, :method => scope unless @existing.respond_to?(scope)
60
+
61
+ @subject.send(setter, @existing.send(scope))
62
+ end
63
+ true
64
+ end
65
+
66
+ # Check if the attribute given is valid and if the validation fails for equal values.
67
+ #
68
+ def is_unique?
69
+ @value = @existing.send(@attribute)
70
+ return bad?(@value)
71
+ end
72
+
73
+ # If :case_sensitive is given and it's false, we swap the case of the
74
+ # value used in :is_unique? and see if the test object remains valid.
75
+ #
76
+ # If :case_sensitive is given and it's true, we swap the case of the
77
+ # value used in is_unique? and see if the test object is not valid.
78
+ #
79
+ # This validation will only occur if the test object is a String.
80
+ #
81
+ def case_sensitive?
82
+ return true unless @value.is_a?(String)
83
+ assert_good_or_bad_if_key(:case_sensitive, @value.swapcase)
84
+ end
85
+
86
+ # Now test that the object is valid when changing the scoped attribute.
87
+ #
88
+ def valid_with_new_scope?
89
+ (@options[:scope] || []).each do |scope|
90
+ setter = :"#{scope}="
91
+
92
+ previous_scope_value = @subject.send(scope)
93
+ @subject.send(setter, new_value_for_scope(scope))
94
+ return false, :method => scope unless good?(@value)
95
+
96
+ @subject.send(setter, previous_scope_value)
97
+ end
98
+ true
99
+ end
100
+
101
+ # Change the existing object attribute to nil to run allow nil
102
+ # validations. If we find any problem while updating the @existing
103
+ # record, it's because we can't save nil values in the database. So it
104
+ # passes when :allow_nil is false, but should raise an error when
105
+ # :allow_nil is true
106
+ #
107
+ def allow_nil?
108
+ return true unless @options.key?(:allow_nil)
109
+
110
+ begin
111
+ @existing.update_attribute(@attribute, nil)
112
+ rescue ::ActiveRecord::StatementInvalid => e
113
+ raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " <<
114
+ "but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil]
115
+ return true
116
+ end
117
+
118
+ super
119
+ end
120
+
121
+ # Change the existing object attribute to blank to run allow blank
122
+ # validation. It uses the same logic as :allow_nil.
123
+ #
124
+ def allow_blank?
125
+ return true unless @options.key?(:allow_blank)
126
+
127
+ begin
128
+ @existing.update_attribute(@attribute, '')
129
+ rescue ::ActiveRecord::StatementInvalid => e
130
+ raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " <<
131
+ "but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank]
132
+ return true
133
+ end
134
+
135
+ super
136
+ end
137
+
138
+ # Returns a value to be used as new scope. It deals with four different
139
+ # cases: date, time, boolean and stringfiable (everything that can be
140
+ # converted to a string and the next value makes sense)
141
+ #
142
+ def new_value_for_scope(scope)
143
+ column_type = if @existing.respond_to?(:column_for_attribute)
144
+ @existing.column_for_attribute(scope)
145
+ else
146
+ nil
147
+ end
148
+
149
+ case column_type.type
150
+ when :int, :integer, :float, :decimal
151
+ new_value_for_stringfiable_scope(scope)
152
+ when :datetime, :timestamp, :time
153
+ Time.now + 10000
154
+ when :date
155
+ Date.today + 100
156
+ when :boolean
157
+ !@existing.send(scope)
158
+ else
159
+ new_value_for_stringfiable_scope(scope)
160
+ end
161
+ end
162
+
163
+ # Returns a value to be used as scope by generating a range of values
164
+ # and searching for them in the database.
165
+ #
166
+ def new_value_for_stringfiable_scope(scope)
167
+ values = [(@existing.send(scope) || 999).next.to_s]
168
+
169
+ # Generate a range of values to search in the database
170
+ 100.times do
171
+ values << values.last.next
172
+ end
173
+ conditions = { scope => values, @attribute => @value }
174
+
175
+ # Get values from the database, get the scope attribute and map them to string.
176
+ db_values = subject_class.find(:all, :conditions => conditions, :select => scope)
177
+ db_values.map!{ |r| r.send(scope).to_s }
178
+
179
+ if value_to_return = (values - db_values).first
180
+ value_to_return
181
+ else
182
+ raise ScriptError, "Tried to find an unique scope value for #{scope} but I could not. " <<
183
+ "The conditions hash was #{conditions.inspect} and it returned all records."
184
+ end
185
+ end
186
+ end
187
+
188
+ # Ensures that the model cannot be saved if one of the attributes listed
189
+ # is not unique.
190
+ #
191
+ # Requires an existing record in the database. If you supply :allow_nil as
192
+ # option, you need to have in the database a record which is not nil in the
193
+ # given attributes. The same is required for allow_blank option.
194
+ #
195
+ # Notice that the record being validate should not be the same as in the
196
+ # database. In other words, you can't do this:
197
+ #
198
+ # subject { Post.create!(@valid_attributes) }
199
+ # should_validate_uniqueness_of :title
200
+ #
201
+ # But don't worry, if you eventually do that, a helpful error message
202
+ # will be raised.
203
+ #
204
+ # == Options
205
+ #
206
+ # * <tt>:scope</tt> - field(s) to scope the uniqueness to.
207
+ # * <tt>:case_sensitive</tt> - the matcher look for an exact match.
208
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
209
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
210
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors[:attribute]</tt>.
211
+ # Regexp, string or symbol. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
212
+ #
213
+ # == Examples
214
+ #
215
+ # it { should validate_uniqueness_of(:keyword, :username) }
216
+ # it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) }
217
+ # it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) }
218
+ #
219
+ # should_validate_uniqueness_of :keyword, :username
220
+ # should_validate_uniqueness_of :email, :scope => :name, :case_sensitive => false
221
+ # should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
222
+ #
223
+ # should_validate_uniqueness_of :email do |m|
224
+ # m.scope = name
225
+ # m.case_sensitive = false
226
+ # end
227
+ #
228
+ def validate_uniqueness_of(*attributes, &block)
229
+ ValidateUniquenessOfMatcher.new(*attributes, &block).spec(self)
230
+ end
231
+ end
232
+ end
233
+ end