mcmire-shoulda-matchers 2.5.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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +32 -0
  4. data/.yardopts +7 -0
  5. data/Appraisals +45 -0
  6. data/CONTRIBUTING.md +41 -0
  7. data/Gemfile +31 -0
  8. data/Gemfile.lock +166 -0
  9. data/MIT-LICENSE +22 -0
  10. data/NEWS.md +299 -0
  11. data/README.md +163 -0
  12. data/Rakefile +116 -0
  13. data/doc_config/gh-pages/index.html.erb +9 -0
  14. data/doc_config/yard/setup.rb +22 -0
  15. data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
  18. data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
  19. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
  20. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
  21. data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
  22. data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
  23. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
  24. data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
  25. data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
  26. data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
  27. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
  28. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
  29. data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
  30. data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
  31. data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
  32. data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
  33. data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
  34. data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
  35. data/features/rails_integration.feature +113 -0
  36. data/features/step_definitions/rails_steps.rb +162 -0
  37. data/features/support/env.rb +5 -0
  38. data/gemfiles/3.0.gemfile +24 -0
  39. data/gemfiles/3.0.gemfile.lock +150 -0
  40. data/gemfiles/3.1.gemfile +27 -0
  41. data/gemfiles/3.1.gemfile.lock +173 -0
  42. data/gemfiles/3.2.gemfile +27 -0
  43. data/gemfiles/3.2.gemfile.lock +171 -0
  44. data/gemfiles/4.0.0.gemfile +28 -0
  45. data/gemfiles/4.0.0.gemfile.lock +172 -0
  46. data/gemfiles/4.0.1.gemfile +28 -0
  47. data/gemfiles/4.0.1.gemfile.lock +172 -0
  48. data/lib/shoulda-matchers.rb +1 -0
  49. data/lib/shoulda/matchers.rb +11 -0
  50. data/lib/shoulda/matchers/action_controller.rb +17 -0
  51. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
  52. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
  53. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
  54. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
  55. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
  56. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
  57. data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
  58. data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
  59. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
  60. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
  61. data/lib/shoulda/matchers/active_model.rb +30 -0
  62. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
  63. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
  64. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
  65. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
  66. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
  67. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
  68. data/lib/shoulda/matchers/active_model/errors.rb +10 -0
  69. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
  70. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
  71. data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
  72. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  73. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
  74. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
  75. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
  76. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
  77. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
  78. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
  79. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
  80. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
  81. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
  82. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
  83. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
  84. data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
  85. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
  86. data/lib/shoulda/matchers/active_record.rb +22 -0
  87. data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
  88. data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
  89. data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  90. data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
  91. data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
  92. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
  93. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
  94. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
  95. data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
  96. data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
  97. data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
  98. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
  99. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
  100. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
  101. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
  102. data/lib/shoulda/matchers/assertion_error.rb +19 -0
  103. data/lib/shoulda/matchers/error.rb +6 -0
  104. data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
  105. data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
  106. data/lib/shoulda/matchers/rails_shim.rb +50 -0
  107. data/lib/shoulda/matchers/version.rb +6 -0
  108. data/lib/shoulda/matchers/warn.rb +8 -0
  109. data/shoulda-matchers.gemspec +23 -0
  110. data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
  111. data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
  112. data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
  113. data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
  114. data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
  115. data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
  116. data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
  117. data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
  118. data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
  119. data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
  120. data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
  121. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
  122. data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
  123. data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
  124. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
  125. data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
  126. data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
  127. data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
  128. data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
  129. data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
  130. data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
  131. data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
  132. data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
  133. data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
  134. data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
  135. data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
  136. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
  137. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
  138. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
  139. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
  140. data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
  141. data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
  142. data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
  143. data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
  144. data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
  145. data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
  146. data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
  147. data/spec/spec_helper.rb +26 -0
  148. data/spec/support/active_model_versions.rb +13 -0
  149. data/spec/support/active_resource_builder.rb +29 -0
  150. data/spec/support/activemodel_helpers.rb +19 -0
  151. data/spec/support/capture_helpers.rb +19 -0
  152. data/spec/support/class_builder.rb +42 -0
  153. data/spec/support/controller_builder.rb +74 -0
  154. data/spec/support/fail_with_message_including_matcher.rb +33 -0
  155. data/spec/support/fail_with_message_matcher.rb +32 -0
  156. data/spec/support/i18n_faker.rb +10 -0
  157. data/spec/support/mailer_builder.rb +10 -0
  158. data/spec/support/model_builder.rb +81 -0
  159. data/spec/support/rails_versions.rb +18 -0
  160. data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
  161. data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
  162. data/spec/support/test_application.rb +120 -0
  163. data/yard.watchr +5 -0
  164. metadata +281 -0
@@ -0,0 +1,261 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_db_column` matcher tests that the table that backs your model
5
+ # has a specific column.
6
+ #
7
+ # class CreatePhones < ActiveRecord::Migration
8
+ # def change
9
+ # create_table :phones do |t|
10
+ # t.string :supported_ios_version
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # # RSpec
16
+ # describe Phone do
17
+ # it { should have_db_column(:supported_ios_version) }
18
+ # end
19
+ #
20
+ # # Test::Unit
21
+ # class PhoneTest < ActiveSupport::TestCase
22
+ # should have_db_column(:supported_ios_version)
23
+ # end
24
+ #
25
+ # #### Qualifiers
26
+ #
27
+ # ##### of_type
28
+ #
29
+ # Use `of_type` to assert that a column is defined as a certain type.
30
+ #
31
+ # class CreatePhones < ActiveRecord::Migration
32
+ # def change
33
+ # create_table :phones do |t|
34
+ # t.decimal :camera_aperture
35
+ # end
36
+ # end
37
+ # end
38
+ #
39
+ # # RSpec
40
+ # describe Phone do
41
+ # it do
42
+ # should have_db_column(:camera_aperture).of_type(:decimal)
43
+ # end
44
+ # end
45
+ #
46
+ # # Test::Unit
47
+ # class PhoneTest < ActiveSupport::TestCase
48
+ # should have_db_column(:camera_aperture).of_type(:decimal)
49
+ # end
50
+ #
51
+ # ##### with_options
52
+ #
53
+ # Use `with_options` to assert that a column has been defined with
54
+ # certain options (`:precision`, `:limit`, `:default`, `:null`, `:scale`,
55
+ # or `:primary`).
56
+ #
57
+ # class CreatePhones < ActiveRecord::Migration
58
+ # def change
59
+ # create_table :phones do |t|
60
+ # t.decimal :camera_aperture, precision: 1, null: false
61
+ # end
62
+ # end
63
+ # end
64
+ #
65
+ # # RSpec
66
+ # describe Phone do
67
+ # it do
68
+ # should have_db_column(:camera_aperture).
69
+ # with_options(precision: 1, null: false)
70
+ # end
71
+ # end
72
+ #
73
+ # # Test::Unit
74
+ # class PhoneTest < ActiveSupport::TestCase
75
+ # should have_db_column(:camera_aperture).
76
+ # with_options(precision: 1, null: false)
77
+ # end
78
+ #
79
+ # @return [HaveDbColumnMatcher]
80
+ #
81
+ def have_db_column(column)
82
+ HaveDbColumnMatcher.new(column)
83
+ end
84
+
85
+ # @private
86
+ class HaveDbColumnMatcher
87
+ def initialize(column)
88
+ @column = column
89
+ @options = {}
90
+ end
91
+
92
+ def of_type(column_type)
93
+ @options[:column_type] = column_type
94
+ self
95
+ end
96
+
97
+ def with_options(opts = {})
98
+ %w(precision limit default null scale primary).each do |attribute|
99
+ if opts.key?(attribute.to_sym)
100
+ @options[attribute.to_sym] = opts[attribute.to_sym]
101
+ end
102
+ end
103
+ self
104
+ end
105
+
106
+ def matches?(subject)
107
+ @subject = subject
108
+ column_exists? &&
109
+ correct_column_type? &&
110
+ correct_precision? &&
111
+ correct_limit? &&
112
+ correct_default? &&
113
+ correct_null? &&
114
+ correct_scale? &&
115
+ correct_primary?
116
+ end
117
+
118
+ def failure_message
119
+ "Expected #{expectation} (#{@missing})"
120
+ end
121
+ alias failure_message_for_should failure_message
122
+
123
+ def failure_message_when_negated
124
+ "Did not expect #{expectation}"
125
+ end
126
+ alias failure_message_for_should_not failure_message_when_negated
127
+
128
+ def description
129
+ desc = "have db column named #{@column}"
130
+ desc << " of type #{@options[:column_type]}" if @options.key?(:column_type)
131
+ desc << " of precision #{@options[:precision]}" if @options.key?(:precision)
132
+ desc << " of limit #{@options[:limit]}" if @options.key?(:limit)
133
+ desc << " of default #{@options[:default]}" if @options.key?(:default)
134
+ desc << " of null #{@options[:null]}" if @options.key?(:null)
135
+ desc << " of primary #{@options[:primary]}" if @options.key?(:primary)
136
+ desc << " of scale #{@options[:scale]}" if @options.key?(:scale)
137
+ desc
138
+ end
139
+
140
+ protected
141
+
142
+ def column_exists?
143
+ if model_class.column_names.include?(@column.to_s)
144
+ true
145
+ else
146
+ @missing = "#{model_class} does not have a db column named #{@column}."
147
+ false
148
+ end
149
+ end
150
+
151
+ def correct_column_type?
152
+ return true unless @options.key?(:column_type)
153
+
154
+ if matched_column.type.to_s == @options[:column_type].to_s
155
+ true
156
+ else
157
+ @missing = "#{model_class} has a db column named #{@column} " <<
158
+ "of type #{matched_column.type}, not #{@options[:column_type]}."
159
+ false
160
+ end
161
+ end
162
+
163
+ def correct_precision?
164
+ return true unless @options.key?(:precision)
165
+
166
+ if matched_column.precision.to_s == @options[:precision].to_s
167
+ true
168
+ else
169
+ @missing = "#{model_class} has a db column named #{@column} " <<
170
+ "of precision #{matched_column.precision}, " <<
171
+ "not #{@options[:precision]}."
172
+ false
173
+ end
174
+ end
175
+
176
+ def correct_limit?
177
+ return true unless @options.key?(:limit)
178
+
179
+ if matched_column.limit.to_s == @options[:limit].to_s
180
+ true
181
+ else
182
+ @missing = "#{model_class} has a db column named #{@column} " <<
183
+ "of limit #{matched_column.limit}, " <<
184
+ "not #{@options[:limit]}."
185
+ false
186
+ end
187
+ end
188
+
189
+ def correct_default?
190
+ return true unless @options.key?(:default)
191
+
192
+ if matched_column.default.to_s == @options[:default].to_s
193
+ true
194
+ else
195
+ @missing = "#{model_class} has a db column named #{@column} " <<
196
+ "of default #{matched_column.default}, " <<
197
+ "not #{@options[:default]}."
198
+ false
199
+ end
200
+ end
201
+
202
+ def correct_null?
203
+ return true unless @options.key?(:null)
204
+
205
+ if matched_column.null.to_s == @options[:null].to_s
206
+ true
207
+ else
208
+ @missing = "#{model_class} has a db column named #{@column} " <<
209
+ "of null #{matched_column.null}, " <<
210
+ "not #{@options[:null]}."
211
+ false
212
+ end
213
+ end
214
+
215
+ def correct_scale?
216
+ return true unless @options.key?(:scale)
217
+
218
+ if actual_scale.to_s == @options[:scale].to_s
219
+ true
220
+ else
221
+ @missing = "#{model_class} has a db column named #{@column} "
222
+ @missing << "of scale #{actual_scale}, not #{@options[:scale]}."
223
+ false
224
+ end
225
+ end
226
+
227
+ def correct_primary?
228
+ return true unless @options.key?(:primary)
229
+
230
+ if matched_column.primary == @options[:primary]
231
+ true
232
+ else
233
+ @missing = "#{model_class} has a db column named #{@column} "
234
+ if @options[:primary]
235
+ @missing << 'that is not primary, but should be'
236
+ else
237
+ @missing << 'that is primary, but should not be'
238
+ end
239
+ false
240
+ end
241
+ end
242
+
243
+ def matched_column
244
+ model_class.columns.detect { |each| each.name == @column.to_s }
245
+ end
246
+
247
+ def model_class
248
+ @subject.class
249
+ end
250
+
251
+ def actual_scale
252
+ matched_column.scale
253
+ end
254
+
255
+ def expectation
256
+ expected = "#{model_class.name} to #{description}"
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,149 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_db_index` matcher tests that the table that backs your model
5
+ # has a index on a specific column.
6
+ #
7
+ # class CreateBlogs < ActiveRecord::Migration
8
+ # def change
9
+ # create_table :blogs do |t|
10
+ # t.integer :user_id
11
+ # end
12
+ #
13
+ # add_index :blogs, :user_id
14
+ # end
15
+ # end
16
+ #
17
+ # # RSpec
18
+ # describe Blog do
19
+ # it { should have_db_index(:user_id) }
20
+ # end
21
+ #
22
+ # # Test::Unit
23
+ # class BlogTest < ActiveSupport::TestCase
24
+ # should have_db_index(:user_id)
25
+ # end
26
+ #
27
+ # #### Qualifiers
28
+ #
29
+ # ##### unique
30
+ #
31
+ # Use `unique` to assert that the index is unique.
32
+ #
33
+ # class CreateBlogs < ActiveRecord::Migration
34
+ # def change
35
+ # create_table :blogs do |t|
36
+ # t.string :name
37
+ # end
38
+ #
39
+ # add_index :blogs, :name, unique: true
40
+ # end
41
+ # end
42
+ #
43
+ # # RSpec
44
+ # describe Blog do
45
+ # it { should have_db_index(:name).unique(true) }
46
+ # end
47
+ #
48
+ # # Test::Unit
49
+ # class BlogTest < ActiveSupport::TestCase
50
+ # should have_db_index(:name).unique(true)
51
+ # end
52
+ #
53
+ # @return [HaveDbIndexMatcher]
54
+ #
55
+ def have_db_index(columns)
56
+ HaveDbIndexMatcher.new(columns)
57
+ end
58
+
59
+ # @private
60
+ class HaveDbIndexMatcher
61
+ def initialize(columns)
62
+ @columns = normalize_columns_to_array(columns)
63
+ @options = {}
64
+ end
65
+
66
+ def unique(unique)
67
+ @options[:unique] = unique
68
+ self
69
+ end
70
+
71
+ def matches?(subject)
72
+ @subject = subject
73
+ index_exists? && correct_unique?
74
+ end
75
+
76
+ def failure_message
77
+ "Expected #{expectation} (#{@missing})"
78
+ end
79
+ alias failure_message_for_should failure_message
80
+
81
+ def failure_message_when_negated
82
+ "Did not expect #{expectation}"
83
+ end
84
+ alias failure_message_for_should_not failure_message_when_negated
85
+
86
+ def description
87
+ if @options.key?(:unique)
88
+ "have a #{index_type} index on columns #{@columns.join(' and ')}"
89
+ else
90
+ "have an index on columns #{@columns.join(' and ')}"
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def index_exists?
97
+ ! matched_index.nil?
98
+ end
99
+
100
+ def correct_unique?
101
+ return true unless @options.key?(:unique)
102
+
103
+ is_unique = matched_index.unique
104
+
105
+ is_unique = !is_unique unless @options[:unique]
106
+
107
+ unless is_unique
108
+ @missing = "#{table_name} has an index named #{matched_index.name} " <<
109
+ "of unique #{matched_index.unique}, not #{@options[:unique]}."
110
+ end
111
+
112
+ is_unique
113
+ end
114
+
115
+ def matched_index
116
+ indexes.detect { |each| each.columns == @columns }
117
+ end
118
+
119
+ def model_class
120
+ @subject.class
121
+ end
122
+
123
+ def table_name
124
+ model_class.table_name
125
+ end
126
+
127
+ def indexes
128
+ ::ActiveRecord::Base.connection.indexes(table_name)
129
+ end
130
+
131
+ def expectation
132
+ "#{model_class.name} to #{description}"
133
+ end
134
+
135
+ def index_type
136
+ if @options[:unique]
137
+ 'unique'
138
+ else
139
+ 'non-unique'
140
+ end
141
+ end
142
+
143
+ def normalize_columns_to_array(columns)
144
+ Array.wrap(columns).map(&:to_s)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,72 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_readonly_attribute` matcher tests usage of the
5
+ # `attr_readonly` macro.
6
+ #
7
+ # class User < ActiveRecord::Base
8
+ # attr_readonly :password
9
+ # end
10
+ #
11
+ # # RSpec
12
+ # describe User do
13
+ # it { should have_readonly_attribute(:password) }
14
+ # end
15
+ #
16
+ # # Test::Unit
17
+ # class UserTest < ActiveSupport::TestCase
18
+ # should have_readonly_attribute(:password)
19
+ # end
20
+ #
21
+ # @return [HaveReadonlyAttributeMatcher]
22
+ #
23
+ def have_readonly_attribute(value)
24
+ HaveReadonlyAttributeMatcher.new(value)
25
+ end
26
+
27
+ # @private
28
+ class HaveReadonlyAttributeMatcher
29
+ def initialize(attribute)
30
+ @attribute = attribute.to_s
31
+ end
32
+
33
+ attr_reader :failure_message, :failure_message_when_negated
34
+
35
+ alias failure_message_for_should failure_message
36
+ alias failure_message_for_should_not failure_message_when_negated
37
+
38
+ def matches?(subject)
39
+ @subject = subject
40
+ if readonly_attributes.include?(@attribute)
41
+ @failure_message_when_negated = "Did not expect #{@attribute} to be read-only"
42
+ true
43
+ else
44
+ if readonly_attributes.empty?
45
+ @failure_message = "#{class_name} attribute #{@attribute} " <<
46
+ 'is not read-only'
47
+ else
48
+ @failure_message = "#{class_name} is making " <<
49
+ "#{readonly_attributes.to_a.to_sentence} " <<
50
+ "read-only, but not #{@attribute}."
51
+ end
52
+ false
53
+ end
54
+ end
55
+
56
+ def description
57
+ "make #{@attribute} read-only"
58
+ end
59
+
60
+ private
61
+
62
+ def readonly_attributes
63
+ @readonly_attributes ||= (@subject.class.readonly_attributes || [])
64
+ end
65
+
66
+ def class_name
67
+ @subject.class.name
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end