specstar-remarkable 0.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.
@@ -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 == arel(subject_class, @options.except(:with))
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