ratnikov-shoulda 2.0.6.3 → 2.9.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 (60) hide show
  1. data/README.rdoc +3 -2
  2. data/Rakefile +1 -1
  3. data/lib/shoulda/active_record/assertions.rb +10 -31
  4. data/lib/shoulda/active_record/helpers.rb +40 -0
  5. data/lib/shoulda/active_record/macros.rb +171 -325
  6. data/lib/shoulda/active_record/matchers/allow_mass_assignment_of_matcher.rb +83 -0
  7. data/lib/shoulda/active_record/matchers/allow_value_matcher.rb +102 -0
  8. data/lib/shoulda/active_record/matchers/association_matcher.rb +226 -0
  9. data/lib/shoulda/active_record/matchers/ensure_inclusion_of_matcher.rb +87 -0
  10. data/lib/shoulda/active_record/matchers/ensure_length_of_matcher.rb +141 -0
  11. data/lib/shoulda/active_record/matchers/have_db_column_matcher.rb +169 -0
  12. data/lib/shoulda/active_record/matchers/have_index_matcher.rb +105 -0
  13. data/lib/shoulda/active_record/matchers/have_named_scope_matcher.rb +125 -0
  14. data/lib/shoulda/active_record/matchers/have_readonly_attribute_matcher.rb +59 -0
  15. data/lib/shoulda/active_record/matchers/validate_acceptance_of_matcher.rb +41 -0
  16. data/lib/shoulda/active_record/matchers/validate_numericality_of_matcher.rb +39 -0
  17. data/lib/shoulda/active_record/matchers/validate_presence_of_matcher.rb +60 -0
  18. data/lib/shoulda/active_record/matchers/validate_uniqueness_of_matcher.rb +148 -0
  19. data/lib/shoulda/active_record/matchers/validation_matcher.rb +56 -0
  20. data/lib/shoulda/active_record/matchers.rb +42 -0
  21. data/lib/shoulda/active_record.rb +4 -0
  22. data/lib/shoulda/assertions.rb +12 -0
  23. data/lib/shoulda/autoload_macros.rb +46 -0
  24. data/lib/shoulda/rails.rb +1 -8
  25. data/lib/shoulda/rspec.rb +5 -0
  26. data/lib/shoulda/tasks/list_tests.rake +6 -1
  27. data/lib/shoulda/test_unit.rb +19 -0
  28. data/lib/shoulda.rb +5 -17
  29. data/rails/init.rb +1 -1
  30. data/test/README +2 -2
  31. data/test/fail_macros.rb +2 -2
  32. data/test/matchers/allow_mass_assignment_of_matcher_test.rb +68 -0
  33. data/test/matchers/allow_value_matcher_test.rb +41 -0
  34. data/test/matchers/association_matcher_test.rb +258 -0
  35. data/test/matchers/ensure_inclusion_of_matcher_test.rb +80 -0
  36. data/test/matchers/ensure_length_of_matcher_test.rb +158 -0
  37. data/test/matchers/have_db_column_matcher_test.rb +169 -0
  38. data/test/matchers/have_index_matcher_test.rb +74 -0
  39. data/test/matchers/have_named_scope_matcher_test.rb +65 -0
  40. data/test/matchers/have_readonly_attributes_matcher_test.rb +29 -0
  41. data/test/matchers/validate_acceptance_of_matcher_test.rb +44 -0
  42. data/test/matchers/validate_numericality_of_matcher_test.rb +52 -0
  43. data/test/matchers/validate_presence_of_matcher_test.rb +86 -0
  44. data/test/matchers/validate_uniqueness_of_matcher_test.rb +141 -0
  45. data/test/model_builder.rb +61 -0
  46. data/test/other/autoload_macro_test.rb +18 -0
  47. data/test/other/helpers_test.rb +58 -0
  48. data/test/rails_root/config/database.yml +1 -1
  49. data/test/rails_root/config/environments/{sqlite3.rb → test.rb} +0 -0
  50. data/test/rails_root/test/shoulda_macros/custom_macro.rb +6 -0
  51. data/test/rails_root/vendor/gems/gem_with_macro-0.0.1/shoulda_macros/gem_macro.rb +6 -0
  52. data/test/rails_root/vendor/plugins/plugin_with_macro/shoulda_macros/plugin_macro.rb +6 -0
  53. data/test/test_helper.rb +3 -1
  54. data/test/unit/address_test.rb +1 -1
  55. data/test/unit/dog_test.rb +1 -1
  56. data/test/unit/post_test.rb +4 -4
  57. data/test/unit/product_test.rb +2 -2
  58. data/test/unit/tag_test.rb +2 -1
  59. data/test/unit/user_test.rb +8 -7
  60. metadata +49 -3
@@ -0,0 +1,169 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures the database column exists.
6
+ #
7
+ # Options:
8
+ # * <tt>of_type</tt> - db column type (:integer, :string, etc.)
9
+ # * <tt>with_options</tt> - same options available in migrations
10
+ # (:default, :null, :limit, :precision, :scale)
11
+ #
12
+ # Examples:
13
+ # it { should_not have_db_column(:admin).of_type(:boolean) }
14
+ # it { should have_db_column(:salary).
15
+ # of_type(:decimal).
16
+ # with_options(:precision => 10, :scale => 2) }
17
+ #
18
+ def have_db_column(column)
19
+ HaveDbColumnMatcher.new(:have_db_column, column)
20
+ end
21
+
22
+ class HaveDbColumnMatcher # :nodoc:
23
+ def initialize(macro, column)
24
+ @macro = macro
25
+ @column = column
26
+ end
27
+
28
+ def of_type(column_type)
29
+ @column_type = column_type
30
+ self
31
+ end
32
+
33
+ def with_options(opts = {})
34
+ @precision = opts[:precision]
35
+ @limit = opts[:limit]
36
+ @default = opts[:default]
37
+ @null = opts[:null]
38
+ @scale = opts[:scale]
39
+ self
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject
44
+ column_exists? &&
45
+ correct_column_type? &&
46
+ correct_precision? &&
47
+ correct_limit? &&
48
+ correct_default? &&
49
+ correct_null? &&
50
+ correct_scale?
51
+ end
52
+
53
+ def failure_message
54
+ "Expected #{expectation} (#{@missing})"
55
+ end
56
+
57
+ def negative_failure_message
58
+ "Did not expect #{expectation}"
59
+ end
60
+
61
+ def description
62
+ desc = "have db column named #{@column}"
63
+ desc << " of type #{@column_type}" unless @column_type.nil?
64
+ desc << " of precision #{@precision}" unless @precision.nil?
65
+ desc << " of limit #{@limit}" unless @limit.nil?
66
+ desc << " of default #{@default}" unless @default.nil?
67
+ desc << " of null #{@null}" unless @null.nil?
68
+ desc << " of primary #{@primary}" unless @primary.nil?
69
+ desc << " of scale #{@scale}" unless @scale.nil?
70
+ desc
71
+ end
72
+
73
+ protected
74
+
75
+ def column_exists?
76
+ if model_class.column_names.include?(@column.to_s)
77
+ true
78
+ else
79
+ @missing = "#{model_class} does not have a db column named #{@column}."
80
+ false
81
+ end
82
+ end
83
+
84
+ def correct_column_type?
85
+ return true if @column_type.nil?
86
+ if matched_column.type.to_s == @column_type.to_s
87
+ true
88
+ else
89
+ @missing = "#{model_class} has a db column named #{@column} " <<
90
+ "of type #{matched_column.type}, not #{@column_type}."
91
+ false
92
+ end
93
+ end
94
+
95
+ def correct_precision?
96
+ return true if @precision.nil?
97
+ if matched_column.precision.to_s == @precision.to_s
98
+ true
99
+ else
100
+ @missing = "#{model_class} has a db column named #{@column} " <<
101
+ "of precision #{matched_column.precision}, " <<
102
+ "not #{@precision}."
103
+ false
104
+ end
105
+ end
106
+
107
+ def correct_limit?
108
+ return true if @limit.nil?
109
+ if matched_column.limit.to_s == @limit.to_s
110
+ true
111
+ else
112
+ @missing = "#{model_class} has a db column named #{@column} " <<
113
+ "of limit #{matched_column.limit}, " <<
114
+ "not #{@limit}."
115
+ false
116
+ end
117
+ end
118
+
119
+ def correct_default?
120
+ return true if @default.nil?
121
+ if matched_column.default.to_s == @default.to_s
122
+ true
123
+ else
124
+ @missing = "#{model_class} has a db column named #{@column} " <<
125
+ "of default #{matched_column.default}, " <<
126
+ "not #{@default}."
127
+ false
128
+ end
129
+ end
130
+
131
+ def correct_null?
132
+ return true if @null.nil?
133
+ if matched_column.null.to_s == @null.to_s
134
+ true
135
+ else
136
+ @missing = "#{model_class} has a db column named #{@column} " <<
137
+ "of null #{matched_column.null}, " <<
138
+ "not #{@null}."
139
+ false
140
+ end
141
+ end
142
+
143
+ def correct_scale?
144
+ return true if @scale.nil?
145
+ if matched_column.scale.to_s == @scale.to_s
146
+ true
147
+ else
148
+ @missing = "#{model_class} has a db column named #{@column} " <<
149
+ "of scale #{matched_column.scale}, not #{@scale}."
150
+ false
151
+ end
152
+ end
153
+
154
+ def matched_column
155
+ model_class.columns.detect { |each| each.name == @column.to_s }
156
+ end
157
+
158
+ def model_class
159
+ @subject.class
160
+ end
161
+
162
+ def expectation
163
+ expected = "#{model_class.name} to #{description}"
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,105 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures that there are DB indices on the given columns or tuples of
6
+ # columns.
7
+ #
8
+ # Options:
9
+ # * <tt>unique</tt> - whether or not the index has a unique
10
+ # constraint. Use <tt>true</tt> to explicitly test for a unique
11
+ # constraint. Use <tt>false</tt> to explicitly test for a non-unique
12
+ # constraint. Use <tt>nil</tt> if you don't care whether the index is
13
+ # unique or not. Default = <tt>nil</tt>
14
+ #
15
+ # Examples:
16
+ #
17
+ # it { should have_index(:age) }
18
+ # it { should have_index([:commentable_type, :commentable_id]) }
19
+ # it { should have_index(:ssn).unique(true) }
20
+ #
21
+ def have_index(columns)
22
+ HaveIndexMatcher.new(:have_index, columns)
23
+ end
24
+
25
+ class HaveIndexMatcher # :nodoc:
26
+ def initialize(macro, columns)
27
+ @macro = macro
28
+ @columns = normalize_columns_to_array(columns)
29
+ end
30
+
31
+ def unique(unique)
32
+ @unique = unique
33
+ self
34
+ end
35
+
36
+ def matches?(subject)
37
+ @subject = subject
38
+ index_exists? && correct_unique?
39
+ end
40
+
41
+ def failure_message
42
+ "Expected #{expectation} (#{@missing})"
43
+ end
44
+
45
+ def negative_failure_message
46
+ "Did not expect #{expectation}"
47
+ end
48
+
49
+ def description
50
+ "have a #{index_type} index on columns #{@columns}"
51
+ end
52
+
53
+ protected
54
+
55
+ def index_exists?
56
+ ! matched_index.nil?
57
+ end
58
+
59
+ def correct_unique?
60
+ return true if @unique.nil?
61
+ if matched_index.unique == @unique
62
+ true
63
+ else
64
+ @missing = "#{table_name} has an index named #{matched_index.name} " <<
65
+ "of unique #{matched_index.unique}, not #{@unique}."
66
+ false
67
+ end
68
+ end
69
+
70
+ def matched_index
71
+ indexes.detect { |each| each.columns == @columns }
72
+ end
73
+
74
+ def model_class
75
+ @subject.class
76
+ end
77
+
78
+ def table_name
79
+ model_class.table_name
80
+ end
81
+
82
+ def indexes
83
+ ::ActiveRecord::Base.connection.indexes(table_name)
84
+ end
85
+
86
+ def expectation
87
+ expected = "#{model_class.name} to #{description}"
88
+ end
89
+
90
+ def index_type
91
+ @unique ? "unique" : "non-unique"
92
+ end
93
+
94
+ def normalize_columns_to_array(columns)
95
+ if columns.class == Array
96
+ columns.collect { |each| each.to_s }
97
+ else
98
+ [columns.to_s]
99
+ end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,125 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures that the model has a method named scope_call that returns a
6
+ # NamedScope object with the proxy options set to the options you supply.
7
+ # scope_call can be either a symbol, or a Ruby expression in a String
8
+ # which will be evaled. The eval'd method call has access to all the same
9
+ # instance variables that an example would.
10
+ #
11
+ # Options:
12
+ #
13
+ # * <tt>in_context</tt> - Any of the options that the named scope would
14
+ # pass on to find.
15
+ #
16
+ # Example:
17
+ #
18
+ # it { should have_named_scope(:visible).
19
+ # finding(:conditions => {:visible => true}) }
20
+ #
21
+ # Passes for
22
+ #
23
+ # named_scope :visible, :conditions => {:visible => true}
24
+ #
25
+ # Or for
26
+ #
27
+ # def self.visible
28
+ # scoped(:conditions => {:visible => true})
29
+ # end
30
+ #
31
+ # You can test lambdas or methods that return ActiveRecord#scoped calls:
32
+ #
33
+ # it { should have_named_scope('recent(5)').finding(:limit => 5) }
34
+ # it { should have_named_scope('recent(1)').finding(:limit => 1) }
35
+ #
36
+ # Passes for
37
+ # named_scope :recent, lambda {|c| {:limit => c}}
38
+ #
39
+ # Or for
40
+ #
41
+ # def self.recent(c)
42
+ # scoped(:limit => c)
43
+ # end
44
+ #
45
+ def have_named_scope(scope_call)
46
+ HaveNamedScopeMatcher.new(scope_call).in_context(self)
47
+ end
48
+
49
+ class HaveNamedScopeMatcher # :nodoc:
50
+
51
+ def initialize(scope_call)
52
+ @scope_call = scope_call.to_s
53
+ end
54
+
55
+ def finding(finding)
56
+ @finding = finding
57
+ self
58
+ end
59
+
60
+ def in_context(context)
61
+ @context = context
62
+ self
63
+ end
64
+
65
+ def matches?(subject)
66
+ @subject = subject
67
+ call_succeeds? && returns_scope? && finds_correct_scope?
68
+ end
69
+
70
+ def failure_message
71
+ "Expected #{@missing_expectation}"
72
+ end
73
+
74
+ def negative_failure_message
75
+ "Didn't expect a named scope for #{@scope_call}"
76
+ end
77
+
78
+ def description
79
+ result = "have a named scope for #{@scope_call}"
80
+ result << " finding #{@finding.inspect}" unless @finding.nil?
81
+ result
82
+ end
83
+
84
+ private
85
+
86
+ def call_succeeds?
87
+ scope
88
+ true
89
+ rescue Exception => exception
90
+ @missing_expectation = "#{@subject.class.name} " <<
91
+ "to respond to #{@scope_call} " <<
92
+ "but raised error: #{exception.inspect}"
93
+ false
94
+ end
95
+
96
+ def scope
97
+ @scope ||= @context.instance_eval("#{@subject.class.name}.#{@scope_call}")
98
+ end
99
+
100
+ def returns_scope?
101
+ if ::ActiveRecord::NamedScope::Scope === scope
102
+ true
103
+ else
104
+ @missing_expectation = "#{@scope_call} to return a scope"
105
+ false
106
+ end
107
+ end
108
+
109
+ def finds_correct_scope?
110
+ return true if @finding.nil?
111
+ if @finding == scope.proxy_options
112
+ true
113
+ else
114
+ @missing_expectation = "#{@scope_call} to return results scoped to "
115
+ @missing_expectation << "#{@finding.inspect} but was scoped to "
116
+ @missing_expectation << scope.proxy_options.inspect
117
+ false
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,59 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures that the attribute cannot be changed once the record has been
6
+ # created.
7
+ #
8
+ # it { should have_readonly_attributes(:password) }
9
+ #
10
+ def have_readonly_attribute(value)
11
+ HaveReadonlyAttributeMatcher.new(value)
12
+ end
13
+
14
+ class HaveReadonlyAttributeMatcher # :nodoc:
15
+
16
+ def initialize(attribute)
17
+ @attribute = attribute.to_s
18
+ end
19
+
20
+ def matches?(subject)
21
+ @subject = subject
22
+ if readonly_attributes.include?(@attribute)
23
+ @negative_failure_message =
24
+ "Did not expect #{@attribute} to be read-only"
25
+ true
26
+ else
27
+ if readonly_attributes.empty?
28
+ @failure_message = "#{class_name} attribute #{@attribute} " <<
29
+ "is not read-only"
30
+ else
31
+ @failure_message = "#{class_name} is making " <<
32
+ "#{readonly_attributes.to_sentence} " <<
33
+ "read-only, but not #{@attribute}."
34
+ end
35
+ false
36
+ end
37
+ end
38
+
39
+ attr_reader :failure_message, :negative_failure_message
40
+
41
+ def description
42
+ "make #{@attribute} read-only"
43
+ end
44
+
45
+ private
46
+
47
+ def readonly_attributes
48
+ @readonly_attributes ||= (@subject.class.readonly_attributes || [])
49
+ end
50
+
51
+ def class_name
52
+ @subject.class.name
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,41 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures that the model cannot be saved the given attribute is not
6
+ # accepted.
7
+ #
8
+ # Options:
9
+ # * <tt>with_message</tt> - value the test expects to find in
10
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
11
+ # translation for <tt>:accepted</tt>.
12
+ #
13
+ # Example:
14
+ # it { should validate_acceptance_of(:eula) }
15
+ #
16
+ def validate_acceptance_of(attr)
17
+ ValidateAcceptanceOfMatcher.new(attr)
18
+ end
19
+
20
+ class ValidateAcceptanceOfMatcher < ValidationMatcher # :nodoc:
21
+
22
+ def with_message(message)
23
+ @expected_message = message if message
24
+ self
25
+ end
26
+
27
+ def matches?(subject)
28
+ super(subject)
29
+ @expected_message ||= :accepted
30
+ disallows_value_of(false, @expected_message)
31
+ end
32
+
33
+ def description
34
+ "require #{@attribute} to be accepted"
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensure that the attribute is numeric
6
+ #
7
+ # Options:
8
+ # * <tt>with_message</tt> - value the test expects to find in
9
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
10
+ # translation for <tt>:not_a_number</tt>.
11
+ #
12
+ # Example:
13
+ # it { should validate_numericality_of(:age) }
14
+ #
15
+ def validate_numericality_of(attr)
16
+ ValidateNumericalityOfMatcher.new(attr)
17
+ end
18
+
19
+ class ValidateNumericalityOfMatcher < ValidationMatcher # :nodoc:
20
+
21
+ def with_message(message)
22
+ @expected_message = message if message
23
+ self
24
+ end
25
+
26
+ def matches?(subject)
27
+ super(subject)
28
+ @expected_message ||= :not_a_number
29
+ disallows_value_of('abcd', @expected_message)
30
+ end
31
+
32
+ def description
33
+ "only allow numeric values for #{@attribute}"
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Matchers
4
+
5
+ # Ensures that the model is not valid if the given attribute is not
6
+ # present.
7
+ #
8
+ # Options:
9
+ # * <tt>with_message</tt> - value the test expects to find in
10
+ # <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
11
+ # Defaults to the translation for <tt>:blank</tt>.
12
+ #
13
+ # Examples:
14
+ # it { should validate_presence_of(:name) }
15
+ # it { should validate_presence_of(:name).
16
+ # with_message(/is not optional/) }
17
+ #
18
+ def validate_presence_of(attr)
19
+ ValidatePresenceOfMatcher.new(attr)
20
+ end
21
+
22
+ class ValidatePresenceOfMatcher < ValidationMatcher # :nodoc:
23
+
24
+ def with_message(message)
25
+ @expected_message = message if message
26
+ self
27
+ end
28
+
29
+ def matches?(subject)
30
+ super(subject)
31
+ @expected_message ||= :blank
32
+ disallows_value_of(blank_value, @expected_message)
33
+ end
34
+
35
+ def description
36
+ "require #{@attribute} to be set"
37
+ end
38
+
39
+ private
40
+
41
+ def blank_value
42
+ if collection?
43
+ []
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def collection?
50
+ if reflection = @subject.class.reflect_on_association(@attribute)
51
+ [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
52
+ else
53
+ false
54
+ end
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end