rspec-rails 2.14.2 → 3.9.1

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 (99) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +4 -2
  5. data/Capybara.md +2 -4
  6. data/Changelog.md +592 -34
  7. data/{License.txt → LICENSE.md} +5 -2
  8. data/README.md +290 -369
  9. data/lib/generators/rspec/controller/controller_generator.rb +1 -0
  10. data/lib/generators/rspec/controller/templates/controller_spec.rb +5 -5
  11. data/lib/generators/rspec/controller/templates/view_spec.rb +2 -2
  12. data/lib/generators/rspec/feature/feature_generator.rb +29 -0
  13. data/lib/generators/rspec/feature/templates/feature_singular_spec.rb +5 -0
  14. data/lib/generators/rspec/feature/templates/feature_spec.rb +5 -0
  15. data/lib/generators/rspec/generators/generator_generator.rb +24 -0
  16. data/lib/generators/rspec/generators/templates/generator_spec.rb +6 -0
  17. data/lib/generators/rspec/helper/helper_generator.rb +1 -0
  18. data/lib/generators/rspec/helper/templates/helper_spec.rb +2 -2
  19. data/lib/generators/rspec/install/install_generator.rb +44 -5
  20. data/lib/generators/rspec/install/templates/spec/rails_helper.rb +78 -0
  21. data/lib/generators/rspec/integration/integration_generator.rb +8 -13
  22. data/lib/generators/rspec/integration/templates/request_spec.rb +4 -9
  23. data/lib/generators/rspec/job/job_generator.rb +12 -0
  24. data/lib/generators/rspec/job/templates/job_spec.rb.erb +7 -0
  25. data/lib/generators/rspec/mailer/mailer_generator.rb +7 -0
  26. data/lib/generators/rspec/mailer/templates/mailer_spec.rb +7 -7
  27. data/lib/generators/rspec/mailer/templates/preview.rb +13 -0
  28. data/lib/generators/rspec/model/model_generator.rb +19 -5
  29. data/lib/generators/rspec/model/templates/fixtures.yml +1 -1
  30. data/lib/generators/rspec/model/templates/model_spec.rb +2 -2
  31. data/lib/generators/rspec/observer/observer_generator.rb +1 -0
  32. data/lib/generators/rspec/observer/templates/observer_spec.rb +2 -2
  33. data/lib/generators/rspec/request/request_generator.rb +10 -0
  34. data/lib/generators/rspec/scaffold/scaffold_generator.rb +68 -138
  35. data/lib/generators/rspec/scaffold/templates/api_controller_spec.rb +165 -0
  36. data/lib/generators/rspec/scaffold/templates/controller_spec.rb +98 -73
  37. data/lib/generators/rspec/scaffold/templates/edit_spec.rb +9 -13
  38. data/lib/generators/rspec/scaffold/templates/index_spec.rb +3 -10
  39. data/lib/generators/rspec/scaffold/templates/new_spec.rb +10 -14
  40. data/lib/generators/rspec/scaffold/templates/routing_spec.rb +21 -12
  41. data/lib/generators/rspec/scaffold/templates/show_spec.rb +4 -11
  42. data/lib/generators/rspec/system/system_generator.rb +26 -0
  43. data/lib/generators/rspec/system/templates/system_spec.rb +9 -0
  44. data/lib/generators/rspec/view/templates/view_spec.rb +2 -2
  45. data/lib/generators/rspec/view/view_generator.rb +1 -0
  46. data/lib/generators/rspec.rb +20 -6
  47. data/lib/rspec/rails/active_record.rb +25 -0
  48. data/lib/rspec/rails/adapters.rb +104 -37
  49. data/lib/rspec/rails/configuration.rb +148 -0
  50. data/lib/rspec/rails/example/controller_example_group.rb +188 -138
  51. data/lib/rspec/rails/example/feature_example_group.rb +63 -20
  52. data/lib/rspec/rails/example/helper_example_group.rb +35 -26
  53. data/lib/rspec/rails/example/job_example_group.rb +23 -0
  54. data/lib/rspec/rails/example/mailer_example_group.rb +30 -14
  55. data/lib/rspec/rails/example/model_example_group.rb +8 -7
  56. data/lib/rspec/rails/example/rails_example_group.rb +3 -1
  57. data/lib/rspec/rails/example/request_example_group.rb +23 -16
  58. data/lib/rspec/rails/example/routing_example_group.rb +49 -40
  59. data/lib/rspec/rails/example/system_example_group.rb +108 -0
  60. data/lib/rspec/rails/example/view_example_group.rb +168 -135
  61. data/lib/rspec/rails/example.rb +2 -33
  62. data/lib/rspec/rails/extensions/active_record/proxy.rb +0 -1
  63. data/lib/rspec/rails/extensions.rb +0 -1
  64. data/lib/rspec/rails/feature_check.rb +64 -0
  65. data/lib/rspec/rails/file_fixture_support.rb +17 -0
  66. data/lib/rspec/rails/fixture_file_upload_support.rb +40 -0
  67. data/lib/rspec/rails/fixture_support.rb +32 -13
  68. data/lib/rspec/rails/matchers/active_job.rb +317 -0
  69. data/lib/rspec/rails/matchers/base_matcher.rb +184 -0
  70. data/lib/rspec/rails/matchers/be_a_new.rb +69 -62
  71. data/lib/rspec/rails/matchers/be_new_record.rb +24 -21
  72. data/lib/rspec/rails/matchers/be_valid.rb +42 -33
  73. data/lib/rspec/rails/matchers/have_enqueued_mail.rb +174 -0
  74. data/lib/rspec/rails/matchers/have_http_status.rb +381 -0
  75. data/lib/rspec/rails/matchers/have_rendered.rb +54 -31
  76. data/lib/rspec/rails/matchers/redirect_to.rb +30 -29
  77. data/lib/rspec/rails/matchers/relation_match_array.rb +1 -1
  78. data/lib/rspec/rails/matchers/routing_matchers.rb +107 -93
  79. data/lib/rspec/rails/matchers.rb +13 -14
  80. data/lib/rspec/rails/tasks/rspec.rake +1 -1
  81. data/lib/rspec/rails/vendor/capybara.rb +10 -4
  82. data/lib/rspec/rails/version.rb +3 -1
  83. data/lib/rspec/rails/view_assigns.rb +18 -18
  84. data/lib/rspec/rails/view_path_builder.rb +29 -0
  85. data/lib/rspec/rails/view_rendering.rb +89 -63
  86. data/lib/rspec/rails/view_spec_methods.rb +56 -0
  87. data/lib/rspec/rails.rb +10 -10
  88. data/lib/rspec-rails.rb +66 -1
  89. data.tar.gz.sig +0 -0
  90. metadata +92 -77
  91. metadata.gz.sig +0 -0
  92. data/lib/autotest/rails_rspec2.rb +0 -85
  93. data/lib/generators/rspec/install/templates/.rspec +0 -1
  94. data/lib/generators/rspec/install/templates/spec/spec_helper.rb.tt +0 -49
  95. data/lib/rspec/rails/extensions/active_record/base.rb +0 -58
  96. data/lib/rspec/rails/matchers/have_extension.rb +0 -36
  97. data/lib/rspec/rails/mocks.rb +0 -274
  98. data/lib/rspec/rails/module_inclusion.rb +0 -19
  99. data/lib/rspec/rails/vendor/webrat.rb +0 -33
@@ -1,40 +1,49 @@
1
- module RSpec::Rails::Matchers
2
- class BeValid < RSpec::Matchers::BuiltIn::Be
3
- def initialize(*args)
4
- @args = args
5
- end
1
+ module RSpec
2
+ module Rails
3
+ module Matchers
4
+ # @private
5
+ class BeValid < RSpec::Matchers::BuiltIn::Be
6
+ def initialize(*args)
7
+ @args = args
8
+ end
6
9
 
7
- # @api private
8
- def matches?(actual)
9
- @actual = actual
10
- actual.valid?(*@args)
11
- end
10
+ def matches?(actual)
11
+ @actual = actual
12
+ actual.valid?(*@args)
13
+ end
12
14
 
13
- # @api private
14
- def failure_message_for_should
15
- message = "expected #{actual.inspect} to be valid"
16
- if actual.respond_to?(:errors)
17
- message << ", but got errors: #{actual.errors.full_messages.join(', ')}"
18
- end
15
+ def failure_message
16
+ message = "expected #{actual.inspect} to be valid"
19
17
 
20
- message
21
- end
18
+ if actual.respond_to?(:errors) && actual.method(:errors).arity < 1
19
+ errors = if actual.errors.respond_to?(:full_messages)
20
+ actual.errors.full_messages
21
+ else
22
+ actual.errors
23
+ end
22
24
 
23
- # @api private
24
- def failure_message_for_should_not
25
- "expected #{actual.inspect} not to be valid"
26
- end
27
- end
25
+ message << ", but got errors: #{errors.map(&:to_s).join(', ')}"
26
+ end
28
27
 
29
- # Passes if the given model instance's `valid?` method is true, meaning all
30
- # of the `ActiveModel::Validations` passed and no errors exist. If a message
31
- # is not given, a default message is shown listing each error.
32
- #
33
- # @example
34
- #
35
- # thing = Thing.new
36
- # thing.should be_valid
37
- def be_valid(*args)
38
- BeValid.new(*args)
28
+ message
29
+ end
30
+
31
+ def failure_message_when_negated
32
+ "expected #{actual.inspect} not to be valid"
33
+ end
34
+ end
35
+
36
+ # @api public
37
+ # Passes if the given model instance's `valid?` method is true, meaning
38
+ # all of the `ActiveModel::Validations` passed and no errors exist. If a
39
+ # message is not given, a default message is shown listing each error.
40
+ #
41
+ # @example
42
+ # thing = Thing.new
43
+ # expect(thing).to be_valid
44
+ def be_valid(*args)
45
+ BeValid.new(*args)
46
+ end
47
+ end
39
48
  end
40
49
  end
@@ -0,0 +1,174 @@
1
+ require "rspec/mocks"
2
+ require "rspec/rails/matchers/active_job"
3
+
4
+ module RSpec
5
+ module Rails
6
+ module Matchers
7
+ # Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
8
+ #
9
+ # rubocop: disable Style/ClassLength
10
+ # @private
11
+ # @see RSpec::Rails::Matchers#have_enqueued_mail
12
+ class HaveEnqueuedMail < ActiveJob::HaveEnqueuedJob
13
+ MAILER_JOB_METHOD = 'deliver_now'.freeze
14
+
15
+ include RSpec::Mocks::ExampleMethods
16
+
17
+ def initialize(mailer_class, method_name)
18
+ super(mailer_job)
19
+ @mailer_class = mailer_class
20
+ @method_name = method_name
21
+ @mail_args = []
22
+ @args = mailer_args
23
+ end
24
+
25
+ def description
26
+ "enqueues #{@mailer_class.name}.#{@method_name}"
27
+ end
28
+
29
+ def with(*args, &block)
30
+ @mail_args = args
31
+ block.nil? ? super(*mailer_args) : super(*mailer_args, &yield_mail_args(block))
32
+ end
33
+
34
+ def matches?(block)
35
+ raise ArgumentError, 'have_enqueued_mail and enqueue_mail only work with block arguments' unless block.respond_to?(:call)
36
+ check_active_job_adapter
37
+ super
38
+ end
39
+
40
+ def failure_message
41
+ "expected to enqueue #{base_message}".tap do |msg|
42
+ msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
43
+ end
44
+ end
45
+
46
+ def failure_message_when_negated
47
+ "expected not to enqueue #{base_message}"
48
+ end
49
+
50
+ private
51
+
52
+ def base_message
53
+ "#{@mailer_class.name}.#{@method_name}".tap do |msg|
54
+ msg << " #{expected_count_message}"
55
+ msg << " with #{@mail_args}," if @mail_args.any?
56
+ msg << " on queue #{@queue}," if @queue
57
+ msg << " at #{@at.inspect}," if @at
58
+ msg << " but enqueued #{@matching_jobs.size}"
59
+ end
60
+ end
61
+
62
+ def expected_count_message
63
+ "#{message_expectation_modifier} #{@expected_number} #{@expected_number == 1 ? 'time' : 'times'}"
64
+ end
65
+
66
+ def mailer_args
67
+ if @mail_args.any?
68
+ base_mailer_args + @mail_args
69
+ else
70
+ mailer_method_arity = @mailer_class.instance_method(@method_name).arity
71
+
72
+ number_of_args = if mailer_method_arity < 0
73
+ (mailer_method_arity + 1).abs
74
+ else
75
+ mailer_method_arity
76
+ end
77
+
78
+ base_mailer_args + Array.new(number_of_args) { anything }
79
+ end
80
+ end
81
+
82
+ def base_mailer_args
83
+ [@mailer_class.name, @method_name.to_s, MAILER_JOB_METHOD]
84
+ end
85
+
86
+ def yield_mail_args(block)
87
+ Proc.new { |*job_args| block.call(*(job_args - base_mailer_args)) }
88
+ end
89
+
90
+ def check_active_job_adapter
91
+ return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
92
+ raise StandardError, "To use HaveEnqueuedMail matcher set `ActiveJob::Base.queue_adapter = :test`"
93
+ end
94
+
95
+ def unmatching_mail_jobs
96
+ @unmatching_jobs.select do |job|
97
+ job[:job] == mailer_job
98
+ end
99
+ end
100
+
101
+ def unmatching_mail_jobs_message
102
+ msg = "Queued deliveries:"
103
+
104
+ unmatching_mail_jobs.each do |job|
105
+ msg << "\n #{mail_job_message(job)}"
106
+ end
107
+
108
+ msg
109
+ end
110
+
111
+ def mail_job_message(job)
112
+ mailer_method = job[:args][0..1].join('.')
113
+
114
+ mailer_args = job[:args][3..-1]
115
+ msg_parts = []
116
+ msg_parts << "with #{mailer_args}" if mailer_args.any?
117
+ msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
118
+ msg_parts << "at #{Time.at(job[:at])}" if job[:at]
119
+
120
+ "#{mailer_method} #{msg_parts.join(', ')}".strip
121
+ end
122
+
123
+ def mailer_job
124
+ ActionMailer::DeliveryJob
125
+ end
126
+ end
127
+ # rubocop: enable Style/ClassLength
128
+
129
+ # @api public
130
+ # Passes if an email has been enqueued inside block.
131
+ # May chain with to specify expected arguments.
132
+ # May chain at_least, at_most or exactly to specify a number of times.
133
+ # May chain at to specify a send time.
134
+ # May chain on_queue to specify a queue.
135
+ #
136
+ # @example
137
+ # expect {
138
+ # MyMailer.welcome(user).deliver_later
139
+ # }.to have_enqueued_mail(MyMailer, :welcome)
140
+ #
141
+ # # Using alias
142
+ # expect {
143
+ # MyMailer.welcome(user).deliver_later
144
+ # }.to enqueue_mail(MyMailer, :welcome)
145
+ #
146
+ # expect {
147
+ # MyMailer.welcome(user).deliver_later
148
+ # }.to have_enqueued_mail(MyMailer, :welcome).with(user)
149
+ #
150
+ # expect {
151
+ # MyMailer.welcome(user).deliver_later
152
+ # MyMailer.welcome(user).deliver_later
153
+ # }.to have_enqueued_mail(MyMailer, :welcome).at_least(:once)
154
+ #
155
+ # expect {
156
+ # MyMailer.welcome(user).deliver_later
157
+ # }.to have_enqueued_mail(MyMailer, :welcome).at_most(:twice)
158
+ #
159
+ # expect {
160
+ # MyMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
161
+ # }.to have_enqueued_mail(MyMailer, :welcome).at(Date.tomorrow.noon)
162
+ #
163
+ # expect {
164
+ # MyMailer.welcome(user).deliver_later(queue: :urgent_mail)
165
+ # }.to have_enqueued_mail(MyMailer, :welcome).on_queue(:urgent_mail)
166
+ def have_enqueued_mail(mailer_class, mail_method_name)
167
+ HaveEnqueuedMail.new(mailer_class, mail_method_name)
168
+ end
169
+ alias_method :have_enqueued_email, :have_enqueued_mail
170
+ alias_method :enqueue_mail, :have_enqueued_mail
171
+ alias_method :enqueue_email, :have_enqueued_mail
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,381 @@
1
+ # The following code inspired and modified from Rails' `assert_response`:
2
+ #
3
+ # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/assertions/response.rb#L22-L38
4
+ #
5
+ # Thank you to all the Rails devs who did the heavy lifting on this!
6
+
7
+ module RSpec
8
+ module Rails
9
+ module Matchers
10
+ # Namespace for various implementations of `have_http_status`.
11
+ #
12
+ # @api private
13
+ module HaveHttpStatus
14
+ # Instantiates an instance of the proper matcher based on the provided
15
+ # `target`.
16
+ #
17
+ # @param target [Object] expected http status or code
18
+ # @return response matcher instance
19
+ def self.matcher_for_status(target)
20
+ if GenericStatus.valid_statuses.include?(target)
21
+ GenericStatus.new(target)
22
+ elsif Symbol === target
23
+ SymbolicStatus.new(target)
24
+ else
25
+ NumericCode.new(target)
26
+ end
27
+ end
28
+
29
+ # @api private
30
+ # Conversion function to coerce the provided object into an
31
+ # `ActionDispatch::TestResponse`.
32
+ #
33
+ # @param obj [Object] object to convert to a response
34
+ # @return [ActionDispatch::TestResponse]
35
+ def as_test_response(obj)
36
+ if ::ActionDispatch::Response === obj
37
+ ::ActionDispatch::TestResponse.from_response(obj)
38
+ elsif ::ActionDispatch::TestResponse === obj
39
+ obj
40
+ elsif obj.respond_to?(:status_code) && obj.respond_to?(:response_headers)
41
+ # Acts As Capybara Session
42
+ # Hack to support `Capybara::Session` without having to load
43
+ # Capybara or catch `NameError`s for the undefined constants
44
+ obj = ActionDispatch::Response.new.tap do |resp|
45
+ resp.status = obj.status_code
46
+ resp.headers.clear
47
+ resp.headers.merge!(obj.response_headers)
48
+ resp.body = obj.body
49
+ resp.request = ActionDispatch::Request.new({})
50
+ end
51
+ ::ActionDispatch::TestResponse.from_response(obj)
52
+ else
53
+ raise TypeError, "Invalid response type: #{obj}"
54
+ end
55
+ end
56
+ module_function :as_test_response
57
+
58
+ # @return [String, nil] a formatted failure message if
59
+ # `@invalid_response` is present, `nil` otherwise
60
+ def invalid_response_type_message
61
+ return unless @invalid_response
62
+ "expected a response object, but an instance of " \
63
+ "#{@invalid_response.class} was received"
64
+ end
65
+
66
+ # @api private
67
+ # Provides an implementation for `have_http_status` matching against
68
+ # numeric http status codes.
69
+ #
70
+ # Not intended to be instantiated directly.
71
+ #
72
+ # @example
73
+ # expect(response).to have_http_status(404)
74
+ #
75
+ # @see RSpec::Rails::Matchers.have_http_status
76
+ class NumericCode < RSpec::Rails::Matchers::BaseMatcher
77
+ include HaveHttpStatus
78
+
79
+ def initialize(code)
80
+ @expected = code.to_i
81
+ @actual = nil
82
+ @invalid_response = nil
83
+ end
84
+
85
+ # @param [Object] response object providing an http code to match
86
+ # @return [Boolean] `true` if the numeric code matched the `response` code
87
+ def matches?(response)
88
+ test_response = as_test_response(response)
89
+ @actual = test_response.response_code.to_i
90
+ expected == @actual
91
+ rescue TypeError => _ignored
92
+ @invalid_response = response
93
+ false
94
+ end
95
+
96
+ # @return [String]
97
+ def description
98
+ "respond with numeric status code #{expected}"
99
+ end
100
+
101
+ # @return [String] explaining why the match failed
102
+ def failure_message
103
+ invalid_response_type_message ||
104
+ "expected the response to have status code #{expected.inspect}" \
105
+ " but it was #{actual.inspect}"
106
+ end
107
+
108
+ # @return [String] explaining why the match failed
109
+ def failure_message_when_negated
110
+ invalid_response_type_message ||
111
+ "expected the response not to have status code " \
112
+ "#{expected.inspect} but it did"
113
+ end
114
+ end
115
+
116
+ # @api private
117
+ # Provides an implementation for `have_http_status` matching against
118
+ # Rack symbol http status codes.
119
+ #
120
+ # Not intended to be instantiated directly.
121
+ #
122
+ # @example
123
+ # expect(response).to have_http_status(:created)
124
+ #
125
+ # @see RSpec::Rails::Matchers.have_http_status
126
+ # @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
127
+ class SymbolicStatus < RSpec::Rails::Matchers::BaseMatcher
128
+ include HaveHttpStatus
129
+
130
+ def initialize(status)
131
+ @expected_status = status
132
+ @actual = nil
133
+ @invalid_response = nil
134
+ set_expected_code!
135
+ end
136
+
137
+ # @param [Object] response object providing an http code to match
138
+ # @return [Boolean] `true` if Rack's associated numeric HTTP code matched
139
+ # the `response` code
140
+ def matches?(response)
141
+ test_response = as_test_response(response)
142
+ @actual = test_response.response_code
143
+ expected == @actual
144
+ rescue TypeError => _ignored
145
+ @invalid_response = response
146
+ false
147
+ end
148
+
149
+ # @return [String]
150
+ def description
151
+ "respond with status code #{pp_expected}"
152
+ end
153
+
154
+ # @return [String] explaining why the match failed
155
+ def failure_message
156
+ invalid_response_type_message ||
157
+ "expected the response to have status code #{pp_expected} but it" \
158
+ " was #{pp_actual}"
159
+ end
160
+
161
+ # @return [String] explaining why the match failed
162
+ def failure_message_when_negated
163
+ invalid_response_type_message ||
164
+ "expected the response not to have status code #{pp_expected} " \
165
+ "but it did"
166
+ end
167
+
168
+ # The initialized expected status symbol
169
+ attr_reader :expected_status
170
+ private :expected_status
171
+
172
+ private
173
+
174
+ # @return [Symbol] representing the actual http numeric code
175
+ def actual_status
176
+ return unless actual
177
+ @actual_status ||= compute_status_from(actual)
178
+ end
179
+
180
+ # Reverse lookup of the Rack status code symbol based on the numeric
181
+ # http code
182
+ #
183
+ # @param code [Fixnum] http status code to look up
184
+ # @return [Symbol] representing the http numeric code
185
+ def compute_status_from(code)
186
+ status, _ = Rack::Utils::SYMBOL_TO_STATUS_CODE.find do |_, c|
187
+ c == code
188
+ end
189
+ status
190
+ end
191
+
192
+ # @return [String] pretty format the actual response status
193
+ def pp_actual
194
+ pp_status(actual_status, actual)
195
+ end
196
+
197
+ # @return [String] pretty format the expected status and associated code
198
+ def pp_expected
199
+ pp_status(expected_status, expected)
200
+ end
201
+
202
+ # @return [String] pretty format the actual response status
203
+ def pp_status(status, code)
204
+ if status
205
+ "#{status.inspect} (#{code})"
206
+ else
207
+ code.to_s
208
+ end
209
+ end
210
+
211
+ # Sets `expected` to the numeric http code based on the Rack
212
+ # `expected_status` status
213
+ #
214
+ # @see Rack::Utils::SYMBOL_TO_STATUS_CODE
215
+ # @raise [ArgumentError] if an associated code could not be found
216
+ def set_expected_code!
217
+ @expected ||=
218
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(expected_status) do
219
+ raise ArgumentError,
220
+ "Invalid HTTP status: #{expected_status.inspect}"
221
+ end
222
+ end
223
+ end
224
+
225
+ # @api private
226
+ # Provides an implementation for `have_http_status` matching against
227
+ # `ActionDispatch::TestResponse` http status category queries.
228
+ #
229
+ # Not intended to be instantiated directly.
230
+ #
231
+ # @example
232
+ # expect(response).to have_http_status(:success)
233
+ # expect(response).to have_http_status(:error)
234
+ # expect(response).to have_http_status(:missing)
235
+ # expect(response).to have_http_status(:redirect)
236
+ #
237
+ # @see RSpec::Rails::Matchers.have_http_status
238
+ # @see ActionDispatch::TestResponse
239
+ class GenericStatus < RSpec::Rails::Matchers::BaseMatcher
240
+ include HaveHttpStatus
241
+
242
+ # @return [Array<Symbol>] of status codes which represent a HTTP status
243
+ # code "group"
244
+ # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
245
+ def self.valid_statuses
246
+ [
247
+ :error, :success, :missing,
248
+ :server_error, :successful, :not_found,
249
+ :redirect
250
+ ]
251
+ end
252
+
253
+ def initialize(type)
254
+ unless self.class.valid_statuses.include?(type)
255
+ raise ArgumentError, "Invalid generic HTTP status: #{type.inspect}"
256
+ end
257
+ @expected = type
258
+ @actual = nil
259
+ @invalid_response = nil
260
+ end
261
+
262
+ # @return [Boolean] `true` if Rack's associated numeric HTTP code matched
263
+ # the `response` code or the named response status
264
+ def matches?(response)
265
+ test_response = as_test_response(response)
266
+ @actual = test_response.response_code
267
+ check_expected_status(test_response, expected)
268
+ rescue TypeError => _ignored
269
+ @invalid_response = response
270
+ false
271
+ end
272
+
273
+ # @return [String]
274
+ def description
275
+ "respond with #{type_message}"
276
+ end
277
+
278
+ # @return [String] explaining why the match failed
279
+ def failure_message
280
+ invalid_response_type_message ||
281
+ "expected the response to have #{type_message} but it was #{actual}"
282
+ end
283
+
284
+ # @return [String] explaining why the match failed
285
+ def failure_message_when_negated
286
+ invalid_response_type_message ||
287
+ "expected the response not to have #{type_message} but it was #{actual}"
288
+ end
289
+
290
+ protected
291
+
292
+ RESPONSE_METHODS = {
293
+ :success => 'successful',
294
+ :error => 'server_error',
295
+ :missing => 'not_found'
296
+ }.freeze
297
+
298
+ def check_expected_status(test_response, expected)
299
+ test_response.send(
300
+ "#{RESPONSE_METHODS.fetch(expected, expected)}?")
301
+ end
302
+
303
+ private
304
+
305
+ # @return [String] formating the expected status and associated code(s)
306
+ def type_message
307
+ @type_message ||= (expected == :error ? "an error" : "a #{expected}") +
308
+ " status code (#{type_codes})"
309
+ end
310
+
311
+ # @return [String] formatting the associated code(s) for the various
312
+ # status code "groups"
313
+ # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
314
+ # @see https://github.com/rack/rack/blob/master/lib/rack/response.rb `Rack::Response`
315
+ def type_codes
316
+ # At the time of this commit the most recent version of
317
+ # `ActionDispatch::TestResponse` defines the following aliases:
318
+ #
319
+ # alias_method :success?, :successful?
320
+ # alias_method :missing?, :not_found?
321
+ # alias_method :redirect?, :redirection?
322
+ # alias_method :error?, :server_error?
323
+ #
324
+ # It's parent `ActionDispatch::Response` includes
325
+ # `Rack::Response::Helpers` which defines the aliased methods as:
326
+ #
327
+ # def successful?; status >= 200 && status < 300; end
328
+ # def redirection?; status >= 300 && status < 400; end
329
+ # def server_error?; status >= 500 && status < 600; end
330
+ # def not_found?; status == 404; end
331
+ #
332
+ # @see https://github.com/rails/rails/blob/ca200378/actionpack/lib/action_dispatch/testing/test_response.rb#L17-L27
333
+ # @see https://github.com/rails/rails/blob/ca200378/actionpack/lib/action_dispatch/http/response.rb#L74
334
+ # @see https://github.com/rack/rack/blob/ce4a3959/lib/rack/response.rb#L119-L122
335
+ @type_codes ||= case expected
336
+ when :error, :server_error
337
+ "5xx"
338
+ when :success, :successful
339
+ "2xx"
340
+ when :missing, :not_found
341
+ "404"
342
+ when :redirect
343
+ "3xx"
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ # @api public
350
+ # Passes if `response` has a matching HTTP status code.
351
+ #
352
+ # The following symbolic status codes are allowed:
353
+ #
354
+ # - `Rack::Utils::SYMBOL_TO_STATUS_CODE`
355
+ # - One of the defined `ActionDispatch::TestResponse` aliases:
356
+ # - `:error`
357
+ # - `:missing`
358
+ # - `:redirect`
359
+ # - `:success`
360
+ #
361
+ # @example Accepts numeric and symbol statuses
362
+ # expect(response).to have_http_status(404)
363
+ # expect(response).to have_http_status(:created)
364
+ # expect(response).to have_http_status(:success)
365
+ # expect(response).to have_http_status(:error)
366
+ # expect(response).to have_http_status(:missing)
367
+ # expect(response).to have_http_status(:redirect)
368
+ #
369
+ # @example Works with standard `response` objects and Capybara's `page`
370
+ # expect(response).to have_http_status(404)
371
+ # expect(page).to have_http_status(:created)
372
+ #
373
+ # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
374
+ # @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
375
+ def have_http_status(target)
376
+ raise ArgumentError, "Invalid HTTP status: nil" unless target
377
+ HaveHttpStatus.matcher_for_status(target)
378
+ end
379
+ end
380
+ end
381
+ end