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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.document +1 -1
- data/.yardopts +4 -2
- data/Capybara.md +2 -4
- data/Changelog.md +592 -34
- data/{License.txt → LICENSE.md} +5 -2
- data/README.md +290 -369
- data/lib/generators/rspec/controller/controller_generator.rb +1 -0
- data/lib/generators/rspec/controller/templates/controller_spec.rb +5 -5
- data/lib/generators/rspec/controller/templates/view_spec.rb +2 -2
- data/lib/generators/rspec/feature/feature_generator.rb +29 -0
- data/lib/generators/rspec/feature/templates/feature_singular_spec.rb +5 -0
- data/lib/generators/rspec/feature/templates/feature_spec.rb +5 -0
- data/lib/generators/rspec/generators/generator_generator.rb +24 -0
- data/lib/generators/rspec/generators/templates/generator_spec.rb +6 -0
- data/lib/generators/rspec/helper/helper_generator.rb +1 -0
- data/lib/generators/rspec/helper/templates/helper_spec.rb +2 -2
- data/lib/generators/rspec/install/install_generator.rb +44 -5
- data/lib/generators/rspec/install/templates/spec/rails_helper.rb +78 -0
- data/lib/generators/rspec/integration/integration_generator.rb +8 -13
- data/lib/generators/rspec/integration/templates/request_spec.rb +4 -9
- data/lib/generators/rspec/job/job_generator.rb +12 -0
- data/lib/generators/rspec/job/templates/job_spec.rb.erb +7 -0
- data/lib/generators/rspec/mailer/mailer_generator.rb +7 -0
- data/lib/generators/rspec/mailer/templates/mailer_spec.rb +7 -7
- data/lib/generators/rspec/mailer/templates/preview.rb +13 -0
- data/lib/generators/rspec/model/model_generator.rb +19 -5
- data/lib/generators/rspec/model/templates/fixtures.yml +1 -1
- data/lib/generators/rspec/model/templates/model_spec.rb +2 -2
- data/lib/generators/rspec/observer/observer_generator.rb +1 -0
- data/lib/generators/rspec/observer/templates/observer_spec.rb +2 -2
- data/lib/generators/rspec/request/request_generator.rb +10 -0
- data/lib/generators/rspec/scaffold/scaffold_generator.rb +68 -138
- data/lib/generators/rspec/scaffold/templates/api_controller_spec.rb +165 -0
- data/lib/generators/rspec/scaffold/templates/controller_spec.rb +98 -73
- data/lib/generators/rspec/scaffold/templates/edit_spec.rb +9 -13
- data/lib/generators/rspec/scaffold/templates/index_spec.rb +3 -10
- data/lib/generators/rspec/scaffold/templates/new_spec.rb +10 -14
- data/lib/generators/rspec/scaffold/templates/routing_spec.rb +21 -12
- data/lib/generators/rspec/scaffold/templates/show_spec.rb +4 -11
- data/lib/generators/rspec/system/system_generator.rb +26 -0
- data/lib/generators/rspec/system/templates/system_spec.rb +9 -0
- data/lib/generators/rspec/view/templates/view_spec.rb +2 -2
- data/lib/generators/rspec/view/view_generator.rb +1 -0
- data/lib/generators/rspec.rb +20 -6
- data/lib/rspec/rails/active_record.rb +25 -0
- data/lib/rspec/rails/adapters.rb +104 -37
- data/lib/rspec/rails/configuration.rb +148 -0
- data/lib/rspec/rails/example/controller_example_group.rb +188 -138
- data/lib/rspec/rails/example/feature_example_group.rb +63 -20
- data/lib/rspec/rails/example/helper_example_group.rb +35 -26
- data/lib/rspec/rails/example/job_example_group.rb +23 -0
- data/lib/rspec/rails/example/mailer_example_group.rb +30 -14
- data/lib/rspec/rails/example/model_example_group.rb +8 -7
- data/lib/rspec/rails/example/rails_example_group.rb +3 -1
- data/lib/rspec/rails/example/request_example_group.rb +23 -16
- data/lib/rspec/rails/example/routing_example_group.rb +49 -40
- data/lib/rspec/rails/example/system_example_group.rb +108 -0
- data/lib/rspec/rails/example/view_example_group.rb +168 -135
- data/lib/rspec/rails/example.rb +2 -33
- data/lib/rspec/rails/extensions/active_record/proxy.rb +0 -1
- data/lib/rspec/rails/extensions.rb +0 -1
- data/lib/rspec/rails/feature_check.rb +64 -0
- data/lib/rspec/rails/file_fixture_support.rb +17 -0
- data/lib/rspec/rails/fixture_file_upload_support.rb +40 -0
- data/lib/rspec/rails/fixture_support.rb +32 -13
- data/lib/rspec/rails/matchers/active_job.rb +317 -0
- data/lib/rspec/rails/matchers/base_matcher.rb +184 -0
- data/lib/rspec/rails/matchers/be_a_new.rb +69 -62
- data/lib/rspec/rails/matchers/be_new_record.rb +24 -21
- data/lib/rspec/rails/matchers/be_valid.rb +42 -33
- data/lib/rspec/rails/matchers/have_enqueued_mail.rb +174 -0
- data/lib/rspec/rails/matchers/have_http_status.rb +381 -0
- data/lib/rspec/rails/matchers/have_rendered.rb +54 -31
- data/lib/rspec/rails/matchers/redirect_to.rb +30 -29
- data/lib/rspec/rails/matchers/relation_match_array.rb +1 -1
- data/lib/rspec/rails/matchers/routing_matchers.rb +107 -93
- data/lib/rspec/rails/matchers.rb +13 -14
- data/lib/rspec/rails/tasks/rspec.rake +1 -1
- data/lib/rspec/rails/vendor/capybara.rb +10 -4
- data/lib/rspec/rails/version.rb +3 -1
- data/lib/rspec/rails/view_assigns.rb +18 -18
- data/lib/rspec/rails/view_path_builder.rb +29 -0
- data/lib/rspec/rails/view_rendering.rb +89 -63
- data/lib/rspec/rails/view_spec_methods.rb +56 -0
- data/lib/rspec/rails.rb +10 -10
- data/lib/rspec-rails.rb +66 -1
- data.tar.gz.sig +0 -0
- metadata +92 -77
- metadata.gz.sig +0 -0
- data/lib/autotest/rails_rspec2.rb +0 -85
- data/lib/generators/rspec/install/templates/.rspec +0 -1
- data/lib/generators/rspec/install/templates/spec/spec_helper.rb.tt +0 -49
- data/lib/rspec/rails/extensions/active_record/base.rb +0 -58
- data/lib/rspec/rails/matchers/have_extension.rb +0 -36
- data/lib/rspec/rails/mocks.rb +0 -274
- data/lib/rspec/rails/module_inclusion.rb +0 -19
- data/lib/rspec/rails/vendor/webrat.rb +0 -33
@@ -1,40 +1,49 @@
|
|
1
|
-
module RSpec
|
2
|
-
|
3
|
-
|
4
|
-
@
|
5
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
10
|
+
def matches?(actual)
|
11
|
+
@actual = actual
|
12
|
+
actual.valid?(*@args)
|
13
|
+
end
|
12
14
|
|
13
|
-
|
14
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|