webmock 3.26.4 → 3.30.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +47 -2
- data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +13 -1
- data/lib/webmock/http_lib_adapters/curb_adapter.rb +1 -1
- data/lib/webmock/json_body_hint.rb +77 -0
- data/lib/webmock/matchers/hash_excluding_matcher.rb +1 -1
- data/lib/webmock/rack_response.rb +0 -10
- data/lib/webmock/request_execution_verifier.rb +22 -0
- data/lib/webmock/request_pattern.rb +12 -27
- data/lib/webmock/request_signature.rb +8 -2
- data/lib/webmock/request_signature_snippet.rb +10 -10
- data/lib/webmock/rspec/matchers/request_pattern_matcher.rb +47 -2
- data/lib/webmock/util/content_type.rb +39 -0
- data/lib/webmock/version.rb +1 -1
- data/lib/webmock/webmock.rb +25 -5
- data/lib/webmock.rb +2 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0be0b6d1129e7014a36a5dbe7a7e77d3a25852d1668f54c455e0be96cd2d1287
|
|
4
|
+
data.tar.gz: db8c3267c3fbf2eef3b69f4b72764a82983e589a6508c9d0be8d2cd53047d484
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad6692051819395acd28a095acb54762b81b379b9d75115f0572351f2693973558978b24392f2086bb2dd6bb3969730881efb0d2dc54210440c9114bc2c867b6
|
|
7
|
+
data.tar.gz: d4c877e82c741f3319e66d803421eb0fb52f3d1a798465bc19c8b55e52d00fc57fca89cd5eb82a52f88d9f0223c2784adcb7110838405a42f05e9e5c26f53561
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# 3.30.0
|
|
4
|
+
|
|
5
|
+
* Allow narrowing an RSpec expectation on a stub with `with`, as in `expect(stub).to have_been_requested.once.with(body: "abc")` (#1128)
|
|
6
|
+
|
|
7
|
+
The expectation counts only the requests that match both the stub and the conditions given to `with`. The stub itself does not change.
|
|
8
|
+
|
|
9
|
+
Thanks to [Sébastien S.](https://github.com/inkstak)
|
|
10
|
+
|
|
11
|
+
* Show the JSON body hint for expectations narrowed with `with` too, and no longer show it when a block is given to the stub or expectation, since checking would mean calling the block
|
|
12
|
+
|
|
13
|
+
* Allow a full URL with a path in `disable_net_connect!(allow: ...)`, as in `allow: "https://github.com/mozilla/geckodriver/releases/latest"` (#977)
|
|
14
|
+
|
|
15
|
+
URLs are compared the same way as in stubs, so the default port, letter case in the host, and the order and encoding of query parameters don't matter. A URL without a path or query allows every path on that scheme, host and port; a trailing slash restricts requests to `/` with no query string. Credentials in either URL are ignored.
|
|
16
|
+
|
|
17
|
+
Thanks to [Henry Blyth](https://github.com/henrahmagix)
|
|
18
|
+
|
|
19
|
+
# 3.29.0
|
|
20
|
+
|
|
21
|
+
* Show a body diff for JSON requests whose content type has a `+json` suffix, such as `application/vnd.api+json`, and for the other content types WebMock already matches as JSON (#734)
|
|
22
|
+
|
|
23
|
+
Thanks to [Olivier Brisse](https://github.com/ouranos)
|
|
24
|
+
|
|
25
|
+
* Ignore the case of JSON, XML and url-encoded content types when matching request bodies, and match bodies of any `+json` or `+xml` media type, such as `image/svg+xml`, as JSON or XML
|
|
26
|
+
|
|
27
|
+
* Suggest declaring a JSON body as a hash when a request fails to match only because the JSON is formatted differently (#649)
|
|
28
|
+
|
|
29
|
+
A body declared as a string must match the request body exactly, including key order and whitespace. When that is the only difference, the error for an unregistered request and the failure message of a request expectation now say so, and suggest `with(body: JSON.parse(json_string))`. Matching JSON strings regardless of key order by default is planned for the next major version (#485).
|
|
30
|
+
|
|
31
|
+
# 3.28.0
|
|
32
|
+
|
|
33
|
+
* Fix `to_rack` apps receiving a `rack.session` they did not set up (#985)
|
|
34
|
+
|
|
35
|
+
WebMock used to add a session hash to the Rack env, shared by every request to the same stub. This confused apps that check whether a session is present, such as Rails API-only apps, and carried session data over between requests. WebMock now leaves `rack.session` and `rack.session.options` unset, as a real server does. If your app needs a session, include session middleware in the app you pass to `to_rack`. `RackResponse#session` and `RackResponse#session_options` have been removed.
|
|
36
|
+
|
|
37
|
+
Thanks to [Jan Graichen](https://github.com/jgraichen)
|
|
38
|
+
|
|
39
|
+
* Fix crash when matching a hash body pattern against a JSON or XML request that has no body
|
|
40
|
+
|
|
41
|
+
Thanks to [Chris](https://github.com/ChrisJr404)
|
|
42
|
+
|
|
43
|
+
* No longer warn about curb versions newer than the last one tested
|
|
44
|
+
|
|
45
|
+
* Document regexp values in `hash_including`
|
|
46
|
+
|
|
47
|
+
Thanks to [Christian Bruckmayer](https://github.com/ChrisBr)
|
|
48
|
+
|
|
49
|
+
* Remove unused `RequestSignatureSnippet#request_params`
|
|
50
|
+
|
|
51
|
+
Thanks to [Daniel Pepper](https://github.com/dpep)
|
|
52
|
+
|
|
53
|
+
# 3.27.0
|
|
54
|
+
|
|
55
|
+
* Fix crash in async-http adapter when a request header has a nil value
|
|
56
|
+
|
|
57
|
+
Thanks to [hikmetba-bit](https://github.com/hikmetba-bit)
|
|
58
|
+
|
|
59
|
+
* The async-http adapter now matches stubs against raw request header values instead of values parsed by protocol-http, consistent with the other adapters
|
|
60
|
+
|
|
61
|
+
* Add RSpec `hash_excluding` support to body matching
|
|
62
|
+
|
|
63
|
+
Thanks to [Chris Gunther](https://github.com/cgunther)
|
|
64
|
+
|
|
65
|
+
* Fix `hash_excluding` ignoring keys given without a value and regexp values, in both query and body matching
|
|
66
|
+
|
|
67
|
+
* Fix crash when matching a request that has more than one `Content-Type` header
|
|
68
|
+
|
|
3
69
|
# 3.26.4
|
|
4
70
|
|
|
5
71
|
* Fix race condition in `HTTPClient` adapter async requests by @koic in #1131
|
data/README.md
CHANGED
|
@@ -176,6 +176,8 @@ RestClient.post('www.example.com', '<data a="1" b="five" />',
|
|
|
176
176
|
content_type: 'application/xml') # ===> Success
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
A body declared as a string must match the request body exactly. For JSON, that includes key order and whitespace, so declare the body as a hash to match it as JSON, for example `with(body: JSON.parse(json_string))`.
|
|
180
|
+
|
|
179
181
|
### Matching request body against partial hash.
|
|
180
182
|
|
|
181
183
|
```ruby
|
|
@@ -184,6 +186,24 @@ stub_request(:post, "www.example.com").
|
|
|
184
186
|
|
|
185
187
|
RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
|
|
186
188
|
:content_type => 'application/x-www-form-urlencoded') # ===> Success
|
|
189
|
+
|
|
190
|
+
stub_request(:post, "www.example.com").
|
|
191
|
+
with(body: hash_including(id: /\d/))
|
|
192
|
+
|
|
193
|
+
RestClient.post('www.example.com', "id=1",
|
|
194
|
+
:content_type => 'application/x-www-form-urlencoded') # ===> Success
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Matching request body against excluding partial hash.
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
stub_request(:post, "www.example.com").
|
|
201
|
+
with(body: hash_excluding({data: {a: '2', b: 'six'}}))
|
|
202
|
+
|
|
203
|
+
RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
|
|
204
|
+
:content_type => 'application/x-www-form-urlencoded') # ===> Success
|
|
205
|
+
RestClient.post('www.example.com', "data[a]=2&data[b]=six&x=1",
|
|
206
|
+
:content_type => 'application/x-www-form-urlencoded') # ===> Failure
|
|
187
207
|
```
|
|
188
208
|
|
|
189
209
|
### Matching custom request headers
|
|
@@ -426,6 +446,8 @@ stub_request(:get, "www.example.com").to_rack(MyRackApp)
|
|
|
426
446
|
RestClient.post('www.example.com') # ===> "Hello"
|
|
427
447
|
```
|
|
428
448
|
|
|
449
|
+
WebMock does not set `rack.session` in the Rack env, just as a real server would not. If your app needs a session, include session middleware in the app you pass to `to_rack`.
|
|
450
|
+
|
|
429
451
|
### Raising errors
|
|
430
452
|
|
|
431
453
|
#### Exception declared by class
|
|
@@ -551,6 +573,21 @@ RestClient.get('www.example.org', '/') # ===> Failure
|
|
|
551
573
|
RestClient.get('www.example.org:8080', '/') # ===> Allowed
|
|
552
574
|
```
|
|
553
575
|
|
|
576
|
+
With a `String` specifying a full URL:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
WebMock.disable_net_connect!(allow: 'https://www.example.org/health')
|
|
580
|
+
|
|
581
|
+
RestClient.get('https://www.example.org/health') # ===> Allowed
|
|
582
|
+
RestClient.get('https://www.example.org/health?full=1') # ===> Failure
|
|
583
|
+
RestClient.get('https://www.example.org/health/db') # ===> Failure
|
|
584
|
+
RestClient.get('https://www.example.org/') # ===> Failure
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
A URL with a path allows only that path and query string. URLs are compared the same way as in stubs, so the default port, letter case in the host, and the order and encoding of query parameters don't matter.
|
|
588
|
+
|
|
589
|
+
A URL without a path or query, such as `https://www.example.org`, allows every path on that scheme, host and port. Adding the trailing slash (`https://www.example.org/`) allows only the root path `/` with no query string. To allow every path under a prefix, use a `Regexp`.
|
|
590
|
+
|
|
554
591
|
With a `Regexp` matching the URI:
|
|
555
592
|
|
|
556
593
|
```ruby
|
|
@@ -615,6 +652,8 @@ This forces WebMock Net::HTTP adapter to always connect on `Net::HTTP.start`. At
|
|
|
615
652
|
WebMock.allow_net_connect!(net_http_connect_on_start: "www.example.com")
|
|
616
653
|
```
|
|
617
654
|
|
|
655
|
+
The path is not known when the connection is made, so a URL with a path given here never matches.
|
|
656
|
+
|
|
618
657
|
## Setting Expectations
|
|
619
658
|
|
|
620
659
|
### Setting expectations in Test::Unit
|
|
@@ -729,11 +768,17 @@ expect(a_request(:post, "www.example.com").
|
|
|
729
768
|
### Setting expectations in RSpec on the stub
|
|
730
769
|
|
|
731
770
|
```ruby
|
|
732
|
-
stub = stub_request(:
|
|
771
|
+
stub = stub_request(:post, "www.example.com")
|
|
772
|
+
|
|
733
773
|
# ... make requests ...
|
|
734
|
-
|
|
774
|
+
|
|
775
|
+
expect(stub).to have_been_requested.twice
|
|
776
|
+
expect(stub).to have_been_requested.once.with(body: {"a" => [1, 2, 3]})
|
|
777
|
+
expect(stub).to have_been_requested.once.with(body: {"a" => [4, 5, 6]})
|
|
735
778
|
```
|
|
736
779
|
|
|
780
|
+
Options and a block given to `with` take the same form as in `stub_request(...).with`. The expectation then counts only the requests that match both the stub and those conditions. The stub itself does not change.
|
|
781
|
+
|
|
737
782
|
## Clearing stubs and request history
|
|
738
783
|
|
|
739
784
|
If you want to reset all current stubs and history of requests use `WebMock.reset!`
|
|
@@ -111,11 +111,23 @@ if defined?(Async::HTTP)
|
|
|
111
111
|
WebMock::RequestSignature.new(
|
|
112
112
|
request.method.downcase.to_sym,
|
|
113
113
|
"#{request.scheme}://#{request.authority}#{request.path}",
|
|
114
|
-
headers: request
|
|
114
|
+
headers: build_request_headers(request),
|
|
115
115
|
body: body
|
|
116
116
|
)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Protocol::HTTP::Headers#to_h parses values through the header policies
|
|
120
|
+
# (since protocol-http 0.56.0) and raises when a value is nil. Build the
|
|
121
|
+
# hash from the raw fields instead, skipping entries with no value since
|
|
122
|
+
# they carry nothing to match against. Keys are downcased so the same
|
|
123
|
+
# header sent in different casings is grouped together.
|
|
124
|
+
def build_request_headers(request)
|
|
125
|
+
request.headers.each.each_with_object({}) do |(key, value), headers|
|
|
126
|
+
next if value.nil?
|
|
127
|
+
(headers[key.downcase] ||= []) << value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
119
131
|
def build_webmock_response(response)
|
|
120
132
|
body = response.read
|
|
121
133
|
response.body = ::Protocol::HTTP::Body::Buffered.wrap(body)
|
|
@@ -13,7 +13,7 @@ if defined?(Curl)
|
|
|
13
13
|
# in curb 1.3.0 through 1.3.5. Fixed in curb 1.3.6. See https://github.com/taf2/curb/issues/478
|
|
14
14
|
unsupported_curb_versions += %w[1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5] if RUBY_VERSION < '2.7'
|
|
15
15
|
|
|
16
|
-
WebMock::VersionChecker.new('Curb', Curl::CURB_VERSION, '0.7.16',
|
|
16
|
+
WebMock::VersionChecker.new('Curb', Curl::CURB_VERSION, '0.7.16', nil, unsupported_curb_versions).check_version!
|
|
17
17
|
|
|
18
18
|
module WebMock
|
|
19
19
|
module HttpLibAdapters
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module WebMock
|
|
6
|
+
# A body declared as a string must match the request body exactly, which for
|
|
7
|
+
# JSON includes key order and whitespace. When a request fails to match only
|
|
8
|
+
# because of that, failure messages suggest declaring the body as a hash.
|
|
9
|
+
# See #485.
|
|
10
|
+
module JsonBodyHint
|
|
11
|
+
ADVICE = "A body declared as a string must match exactly. To match it as JSON, " \
|
|
12
|
+
"declare it as a hash, for example with(body: JSON.parse(json_string))."
|
|
13
|
+
|
|
14
|
+
STUB_HINT = "Hint: the request body is the same JSON as the body of this stub, " \
|
|
15
|
+
"but formatted differently, for example with keys in another order. #{ADVICE}"
|
|
16
|
+
|
|
17
|
+
EXPECTATION_HINT = "Hint: a request was made with the same JSON body as expected, " \
|
|
18
|
+
"but formatted differently, for example with keys in another order. #{ADVICE}"
|
|
19
|
+
|
|
20
|
+
# True when one of the patterns declares its body as a JSON object or array
|
|
21
|
+
# string, the request has a JSON content type and a body that parses to the
|
|
22
|
+
# same JSON but is written differently, and the method, URI and headers of
|
|
23
|
+
# every pattern match the request. Any other patterns the request has to
|
|
24
|
+
# match, such as the conditions of an RSpec expectation, must not declare
|
|
25
|
+
# a body of their own.
|
|
26
|
+
#
|
|
27
|
+
# Building a failure message must not call a block given to with, which
|
|
28
|
+
# could raise and replace the failure, or repeat side effects. So there is
|
|
29
|
+
# no hint when any of the patterns has one.
|
|
30
|
+
#
|
|
31
|
+
# The body is checked first, since that runs no user code, so URI and
|
|
32
|
+
# header matchers are only evaluated when a hint is otherwise possible.
|
|
33
|
+
def self.applies?(request_pattern, request_signature)
|
|
34
|
+
patterns = parts_of(request_pattern)
|
|
35
|
+
return false unless patterns.all? { |pattern| pattern.is_a?(RequestPattern) && pattern.with_block.nil? }
|
|
36
|
+
|
|
37
|
+
with_body = patterns.select(&:body_pattern)
|
|
38
|
+
return false unless with_body.size == 1
|
|
39
|
+
return false unless body_differs_only_in_formatting?(with_body.first.body_pattern, request_signature)
|
|
40
|
+
|
|
41
|
+
patterns.all? { |pattern| matches_except_body?(pattern, request_signature) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A pattern made of parts that must all match, such as an RSpec expectation
|
|
45
|
+
# with conditions, responds to #patterns. A stub stands for its pattern.
|
|
46
|
+
def self.parts_of(request_pattern)
|
|
47
|
+
parts = request_pattern.respond_to?(:patterns) ? request_pattern.patterns : [request_pattern]
|
|
48
|
+
parts.map { |part| part.is_a?(RequestStub) ? part.request_pattern : part }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.body_differs_only_in_formatting?(body_pattern, request_signature)
|
|
52
|
+
expected = body_pattern.pattern
|
|
53
|
+
actual = request_signature.body
|
|
54
|
+
return false unless expected.is_a?(String) && actual.is_a?(String) && expected != actual
|
|
55
|
+
return false unless request_signature.json_headers?
|
|
56
|
+
|
|
57
|
+
expected_json = parse_json(expected)
|
|
58
|
+
return false unless expected_json.is_a?(Hash) || expected_json.is_a?(Array)
|
|
59
|
+
|
|
60
|
+
# eql? compares nested values strictly, so 1 and 1.0 are different.
|
|
61
|
+
expected_json.eql?(parse_json(actual))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.matches_except_body?(request_pattern, request_signature)
|
|
65
|
+
request_pattern.method_pattern.matches?(request_signature.method) &&
|
|
66
|
+
request_pattern.uri_pattern.matches?(request_signature.uri) &&
|
|
67
|
+
(request_pattern.headers_pattern.nil? || request_pattern.headers_pattern.matches?(request_signature.headers))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.parse_json(string)
|
|
71
|
+
::JSON.parse(string)
|
|
72
|
+
rescue ::JSON::JSONError, EncodingError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
private_class_method :parts_of, :body_differs_only_in_formatting?, :matches_except_body?, :parse_json
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -6,7 +6,7 @@ module WebMock
|
|
|
6
6
|
# https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
|
|
7
7
|
class HashExcludingMatcher < HashArgumentMatcher
|
|
8
8
|
def ==(actual)
|
|
9
|
-
super { |key, value| !actual.key?(key) || value
|
|
9
|
+
super { |key, value| !actual.key?(key) || !(value === actual[key]) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def inspect
|
|
@@ -52,8 +52,6 @@ module WebMock
|
|
|
52
52
|
end
|
|
53
53
|
env['rack.url_scheme'] = uri.scheme
|
|
54
54
|
env['rack.run_once'] = true
|
|
55
|
-
env['rack.session'] = session
|
|
56
|
-
env['rack.session.options'] = session_options
|
|
57
55
|
|
|
58
56
|
headers.each do |k, v|
|
|
59
57
|
env["HTTP_#{k.tr('-','_').upcase}"] = v
|
|
@@ -61,13 +59,5 @@ module WebMock
|
|
|
61
59
|
|
|
62
60
|
env
|
|
63
61
|
end
|
|
64
|
-
|
|
65
|
-
def session
|
|
66
|
-
@session ||= {}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def session_options
|
|
70
|
-
@session_options ||= {}
|
|
71
|
-
end
|
|
72
62
|
end
|
|
73
63
|
end
|
|
@@ -56,9 +56,31 @@ module WebMock
|
|
|
56
56
|
def failure_message_phrase(is_negated=false)
|
|
57
57
|
negation = is_negated ? "was not" : "was"
|
|
58
58
|
"The request #{request_pattern} #{negation} expected to execute #{quantity_phrase(is_negated)}but it executed #{times(times_executed)}" +
|
|
59
|
+
(is_negated ? "" : json_body_hint) +
|
|
59
60
|
self.class.executed_requests_message
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
# Only when too few requests matched: with too many, declaring the body
|
|
64
|
+
# as a hash would match even more of them.
|
|
65
|
+
def json_body_hint
|
|
66
|
+
return "" unless WebMock.show_body_diff? && too_few_requests_executed?
|
|
67
|
+
|
|
68
|
+
requests = RequestRegistry.instance.requested_signatures.select do |request_signature, _|
|
|
69
|
+
JsonBodyHint.applies?(request_pattern, request_signature)
|
|
70
|
+
end
|
|
71
|
+
requests.empty? ? "" : "\n\n#{JsonBodyHint::EXPECTATION_HINT}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def too_few_requests_executed?
|
|
75
|
+
if @at_least_times_executed
|
|
76
|
+
times_executed < @at_least_times_executed
|
|
77
|
+
elsif @at_most_times_executed
|
|
78
|
+
false
|
|
79
|
+
else
|
|
80
|
+
times_executed < (@expected_times_executed || 1)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
62
84
|
def quantity_phrase(is_negated=false)
|
|
63
85
|
if @at_least_times_executed
|
|
64
86
|
"at least #{times(@at_least_times_executed)} "
|
|
@@ -14,7 +14,7 @@ module WebMock
|
|
|
14
14
|
|
|
15
15
|
class RequestPattern
|
|
16
16
|
|
|
17
|
-
attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
|
|
17
|
+
attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern, :with_block
|
|
18
18
|
|
|
19
19
|
def initialize(method, uri, options = {})
|
|
20
20
|
@method_pattern = MethodPattern.new(method)
|
|
@@ -33,11 +33,9 @@ module WebMock
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def matches?(request_signature)
|
|
36
|
-
content_type = request_signature.headers['Content-Type'] if request_signature.headers
|
|
37
|
-
content_type = content_type.split(';').first if content_type
|
|
38
36
|
@method_pattern.matches?(request_signature.method) &&
|
|
39
37
|
@uri_pattern.matches?(request_signature.uri) &&
|
|
40
|
-
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type
|
|
38
|
+
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, request_signature.content_type)) &&
|
|
41
39
|
(@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
|
|
42
40
|
(@with_block.nil? || @with_block.call(request_signature))
|
|
43
41
|
end
|
|
@@ -255,18 +253,8 @@ module WebMock
|
|
|
255
253
|
class BodyPattern
|
|
256
254
|
include RSpecMatcherDetector
|
|
257
255
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'application/xml' => :xml,
|
|
261
|
-
'application/json' => :json,
|
|
262
|
-
'text/json' => :json,
|
|
263
|
-
'application/javascript' => :json,
|
|
264
|
-
'text/javascript' => :json,
|
|
265
|
-
'text/html' => :html,
|
|
266
|
-
'application/x-yaml' => :yaml,
|
|
267
|
-
'text/yaml' => :yaml,
|
|
268
|
-
'text/plain' => :plain
|
|
269
|
-
}
|
|
256
|
+
# Kept so existing references to the constant still resolve.
|
|
257
|
+
BODY_FORMATS = WebMock::Util::ContentType::BODY_FORMATS
|
|
270
258
|
|
|
271
259
|
attr_reader :pattern
|
|
272
260
|
|
|
@@ -275,12 +263,16 @@ module WebMock
|
|
|
275
263
|
normalize_hash(pattern)
|
|
276
264
|
elsif rSpecHashIncludingMatcher?(pattern)
|
|
277
265
|
WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(pattern)
|
|
266
|
+
elsif rSpecHashExcludingMatcher?(pattern)
|
|
267
|
+
WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(pattern)
|
|
278
268
|
else
|
|
279
269
|
pattern
|
|
280
270
|
end
|
|
281
271
|
end
|
|
282
272
|
|
|
283
273
|
def matches?(body, content_type = "")
|
|
274
|
+
# Checked on the raw value, so body patterns used with mixed case
|
|
275
|
+
# multipart content types keep working as in earlier versions.
|
|
284
276
|
assert_non_multipart_body(content_type)
|
|
285
277
|
|
|
286
278
|
if (@pattern).is_a?(Hash)
|
|
@@ -304,7 +296,9 @@ module WebMock
|
|
|
304
296
|
private
|
|
305
297
|
|
|
306
298
|
def body_as_hash(body, content_type)
|
|
307
|
-
|
|
299
|
+
return if body.nil?
|
|
300
|
+
|
|
301
|
+
case WebMock::Util::ContentType.body_format(content_type)
|
|
308
302
|
when :json then
|
|
309
303
|
WebMock::Util::Parsers::JSON.parse(body)
|
|
310
304
|
when :xml then
|
|
@@ -316,11 +310,6 @@ module WebMock
|
|
|
316
310
|
nil
|
|
317
311
|
end
|
|
318
312
|
|
|
319
|
-
def body_format(content_type)
|
|
320
|
-
normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
|
|
321
|
-
BODY_FORMATS[normalized_content_type]
|
|
322
|
-
end
|
|
323
|
-
|
|
324
313
|
def assert_non_multipart_body(content_type)
|
|
325
314
|
if content_type =~ %r{^multipart/form-data}
|
|
326
315
|
raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
|
|
@@ -376,7 +365,7 @@ module WebMock
|
|
|
376
365
|
return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
|
|
377
366
|
return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
|
|
378
367
|
|
|
379
|
-
expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if
|
|
368
|
+
expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if WebMock::Util::ContentType.url_encoded?(content_type)
|
|
380
369
|
expected === actual
|
|
381
370
|
end
|
|
382
371
|
|
|
@@ -387,10 +376,6 @@ module WebMock
|
|
|
387
376
|
def normalize_hash(hash)
|
|
388
377
|
Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
|
|
389
378
|
end
|
|
390
|
-
|
|
391
|
-
def url_encoded_body?(content_type)
|
|
392
|
-
content_type =~ %r{^application/x-www-form-urlencoded}
|
|
393
|
-
end
|
|
394
379
|
end
|
|
395
380
|
|
|
396
381
|
class HeadersPattern
|
|
@@ -36,12 +36,18 @@ module WebMock
|
|
|
36
36
|
end
|
|
37
37
|
alias == eql?
|
|
38
38
|
|
|
39
|
+
# A request can carry Content-Type more than once, in which case the
|
|
40
|
+
# normalized header value is an Array. Use its first value.
|
|
41
|
+
def content_type
|
|
42
|
+
Array(headers&.fetch('Content-Type', nil)).first
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
def url_encoded?
|
|
40
|
-
|
|
46
|
+
WebMock::Util::ContentType.url_encoded?(content_type)
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def json_headers?
|
|
44
|
-
|
|
50
|
+
WebMock::Util::ContentType.body_format(content_type) == :json
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
private
|
|
@@ -25,7 +25,10 @@ module WebMock
|
|
|
25
25
|
text = "registered request stubs:\n".dup
|
|
26
26
|
WebMock::StubRegistry.instance.request_stubs.each do |stub|
|
|
27
27
|
text << "\n#{WebMock::StubRequestSnippet.new(stub).to_s(false)}"
|
|
28
|
-
|
|
28
|
+
if WebMock.show_body_diff?
|
|
29
|
+
add_body_diff(stub, text)
|
|
30
|
+
add_json_body_hint(stub, text)
|
|
31
|
+
end
|
|
29
32
|
end
|
|
30
33
|
text
|
|
31
34
|
end
|
|
@@ -37,20 +40,17 @@ module WebMock
|
|
|
37
40
|
text << "\n\n#{body_diff_str}" unless body_diff_str.empty?
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
def add_json_body_hint(stub, text)
|
|
44
|
+
return unless JsonBodyHint.applies?(stub.request_pattern, request_signature)
|
|
45
|
+
|
|
46
|
+
text << "\n\n#{JsonBodyHint::STUB_HINT}\n"
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
def signature_stub_body_diff(stub)
|
|
41
50
|
diff = RequestBodyDiff.new(request_signature, stub).body_diff
|
|
42
51
|
diff.empty? ? "" : "Body diff:\n #{pretty_print_to_string(diff)}"
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
def request_params
|
|
46
|
-
@request_params ||=
|
|
47
|
-
if request_signature.json_headers?
|
|
48
|
-
JSON.parse(request_signature.body)
|
|
49
|
-
else
|
|
50
|
-
""
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
54
|
def pretty_print_to_string(string_to_print)
|
|
55
55
|
StringIO.open("".dup) do |stream|
|
|
56
56
|
PP.pp(string_to_print, stream)
|
|
@@ -52,13 +52,22 @@ module WebMock
|
|
|
52
52
|
self
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Counts only the requests that also match the given conditions, which
|
|
56
|
+
# take the same options and block as RequestPattern#with. Like
|
|
57
|
+
# RequestPattern#with, calling it again adds to the conditions. The stub
|
|
58
|
+
# or request pattern under test is not changed.
|
|
59
|
+
def with(options = {}, &block)
|
|
60
|
+
(@conditions ||= RequestPattern.new(:any, ANY_URI)).with(options, &block)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def matches?(request_pattern)
|
|
56
|
-
@request_execution_verifier.request_pattern = request_pattern
|
|
65
|
+
@request_execution_verifier.request_pattern = with_conditions(request_pattern)
|
|
57
66
|
@request_execution_verifier.matches?
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def does_not_match?(request_pattern)
|
|
61
|
-
@request_execution_verifier.request_pattern = request_pattern
|
|
70
|
+
@request_execution_verifier.request_pattern = with_conditions(request_pattern)
|
|
62
71
|
@request_execution_verifier.does_not_match?
|
|
63
72
|
end
|
|
64
73
|
|
|
@@ -76,5 +85,41 @@ module WebMock
|
|
|
76
85
|
|
|
77
86
|
# RSpec 2 compatibility:
|
|
78
87
|
alias_method :negative_failure_message, :failure_message_when_negated
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
ANY_URI = ->(_uri) { true }
|
|
92
|
+
private_constant :ANY_URI
|
|
93
|
+
|
|
94
|
+
# How the description of conditions starts when nothing was given to
|
|
95
|
+
# #with. What was given follows it, as in ' with body "abc"'.
|
|
96
|
+
EMPTY_CONDITIONS = RequestPattern.new(:any, ANY_URI).to_s.freeze
|
|
97
|
+
private_constant :EMPTY_CONDITIONS
|
|
98
|
+
|
|
99
|
+
def with_conditions(request_pattern)
|
|
100
|
+
@conditions ? PatternWithConditions.new(request_pattern, @conditions) : request_pattern
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Matches a request when both the pattern and the conditions match it.
|
|
104
|
+
class PatternWithConditions
|
|
105
|
+
def initialize(pattern, conditions)
|
|
106
|
+
@pattern = pattern
|
|
107
|
+
@conditions = conditions
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The parts that must all match, so JsonBodyHint can check each of them.
|
|
111
|
+
def patterns
|
|
112
|
+
[@pattern, @conditions]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def matches?(request_signature)
|
|
116
|
+
@pattern.matches?(request_signature) && @conditions.matches?(request_signature)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def to_s
|
|
120
|
+
"#{@pattern} (only requests#{@conditions.to_s.delete_prefix(EMPTY_CONDITIONS)})"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
private_constant :PatternWithConditions
|
|
79
124
|
end
|
|
80
125
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebMock
|
|
4
|
+
module Util
|
|
5
|
+
module ContentType
|
|
6
|
+
BODY_FORMATS = {
|
|
7
|
+
'text/xml' => :xml,
|
|
8
|
+
'application/xml' => :xml,
|
|
9
|
+
'application/json' => :json,
|
|
10
|
+
'text/json' => :json,
|
|
11
|
+
'application/javascript' => :json,
|
|
12
|
+
'text/javascript' => :json,
|
|
13
|
+
'text/html' => :html,
|
|
14
|
+
'application/x-yaml' => :yaml,
|
|
15
|
+
'text/yaml' => :yaml,
|
|
16
|
+
'text/plain' => :plain
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def self.media_type(content_type)
|
|
20
|
+
content_type.to_s.scrub.split(';', 2).first.to_s.strip.downcase
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.url_encoded?(content_type)
|
|
24
|
+
media_type(content_type) == 'application/x-www-form-urlencoded'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.body_format(content_type)
|
|
28
|
+
type = media_type(content_type)
|
|
29
|
+
|
|
30
|
+
BODY_FORMATS.fetch(type) do
|
|
31
|
+
case type
|
|
32
|
+
when %r{\A[^/\s]+/[^/\s]+\+json\z} then :json
|
|
33
|
+
when %r{\A[^/\s]+/[^/\s]+\+xml\z} then :xml
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/webmock/version.rb
CHANGED
data/lib/webmock/webmock.rb
CHANGED
|
@@ -87,14 +87,12 @@ module WebMock
|
|
|
87
87
|
when Array
|
|
88
88
|
allowed.any? { |allowed_item| net_connect_explicit_allowed?(allowed_item, uri) }
|
|
89
89
|
when Regexp
|
|
90
|
-
(uri.to_s
|
|
91
|
-
(uri.
|
|
90
|
+
allowed.match?(uri.to_s) ||
|
|
91
|
+
(uri.port == uri.default_port && allowed.match?(uri.omit(:port).to_s))
|
|
92
92
|
when String
|
|
93
|
-
allowed == uri.to_s ||
|
|
94
93
|
allowed == uri.host ||
|
|
95
94
|
allowed == "#{uri.host}:#{uri.port}" ||
|
|
96
|
-
allowed
|
|
97
|
-
allowed == "#{uri.scheme}://#{uri.host}" && uri.port == uri.default_port
|
|
95
|
+
net_connect_allowed_url?(allowed, uri)
|
|
98
96
|
else
|
|
99
97
|
if allowed.respond_to?(:call)
|
|
100
98
|
allowed.call(uri)
|
|
@@ -102,6 +100,28 @@ module WebMock
|
|
|
102
100
|
end
|
|
103
101
|
end
|
|
104
102
|
|
|
103
|
+
def self.net_connect_allowed_url?(allowed, uri)
|
|
104
|
+
return false unless allowed.match?(%r{\Ahttps?://}i)
|
|
105
|
+
|
|
106
|
+
# Normalization turns an empty path into "/", so check the original URL.
|
|
107
|
+
allowed_uri = Addressable::URI.parse(allowed)
|
|
108
|
+
allow_any_path = allowed_uri.path.empty? && allowed_uri.query.nil?
|
|
109
|
+
|
|
110
|
+
# Connection URIs have no request path to match yet.
|
|
111
|
+
return false if uri.path.empty? && !allowed_uri.path.empty?
|
|
112
|
+
|
|
113
|
+
allowed_uri = WebMock::Util::URI.normalize_uri(allowed_uri)
|
|
114
|
+
request_uri = WebMock::Util::URI.normalize_uri(uri)
|
|
115
|
+
|
|
116
|
+
return false unless allowed_uri.origin == request_uri.origin
|
|
117
|
+
return true if allow_any_path
|
|
118
|
+
|
|
119
|
+
allowed_uri.path == request_uri.path && allowed_uri.query == request_uri.query
|
|
120
|
+
rescue Addressable::URI::InvalidURIError
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
private_class_method :net_connect_allowed_url?
|
|
124
|
+
|
|
105
125
|
def self.show_body_diff!
|
|
106
126
|
Config.instance.show_body_diff = true
|
|
107
127
|
end
|
data/lib/webmock.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'webmock/errors'
|
|
|
13
13
|
require_relative 'webmock/util/query_mapper'
|
|
14
14
|
require_relative 'webmock/util/uri'
|
|
15
15
|
require_relative 'webmock/util/headers'
|
|
16
|
+
require_relative 'webmock/util/content_type'
|
|
16
17
|
require_relative 'webmock/util/hash_counter'
|
|
17
18
|
require_relative 'webmock/util/hash_keys_stringifier'
|
|
18
19
|
require_relative 'webmock/util/values_stringifier'
|
|
@@ -36,6 +37,7 @@ require_relative 'webmock/rack_response'
|
|
|
36
37
|
require_relative 'webmock/stub_request_snippet'
|
|
37
38
|
require_relative 'webmock/request_signature_snippet'
|
|
38
39
|
require_relative 'webmock/request_body_diff'
|
|
40
|
+
require_relative 'webmock/json_body_hint'
|
|
39
41
|
|
|
40
42
|
require_relative 'webmock/assertion_failure'
|
|
41
43
|
require_relative 'webmock/request_execution_verifier'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: webmock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bartosz Blimke
|
|
@@ -318,6 +318,7 @@ files:
|
|
|
318
318
|
- lib/webmock/http_lib_adapters/net_http_response.rb
|
|
319
319
|
- lib/webmock/http_lib_adapters/patron_adapter.rb
|
|
320
320
|
- lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
|
|
321
|
+
- lib/webmock/json_body_hint.rb
|
|
321
322
|
- lib/webmock/matchers/any_arg_matcher.rb
|
|
322
323
|
- lib/webmock/matchers/hash_argument_matcher.rb
|
|
323
324
|
- lib/webmock/matchers/hash_excluding_matcher.rb
|
|
@@ -340,6 +341,7 @@ files:
|
|
|
340
341
|
- lib/webmock/stub_registry.rb
|
|
341
342
|
- lib/webmock/stub_request_snippet.rb
|
|
342
343
|
- lib/webmock/test_unit.rb
|
|
344
|
+
- lib/webmock/util/content_type.rb
|
|
343
345
|
- lib/webmock/util/hash_counter.rb
|
|
344
346
|
- lib/webmock/util/hash_keys_stringifier.rb
|
|
345
347
|
- lib/webmock/util/hash_validator.rb
|
|
@@ -358,9 +360,9 @@ licenses:
|
|
|
358
360
|
- MIT
|
|
359
361
|
metadata:
|
|
360
362
|
bug_tracker_uri: https://github.com/bblimke/webmock/issues
|
|
361
|
-
changelog_uri: https://github.com/bblimke/webmock/blob/v3.
|
|
362
|
-
documentation_uri: https://www.rubydoc.info/gems/webmock/3.
|
|
363
|
-
source_code_uri: https://github.com/bblimke/webmock/tree/v3.
|
|
363
|
+
changelog_uri: https://github.com/bblimke/webmock/blob/v3.30.0/CHANGELOG.md
|
|
364
|
+
documentation_uri: https://www.rubydoc.info/gems/webmock/3.30.0
|
|
365
|
+
source_code_uri: https://github.com/bblimke/webmock/tree/v3.30.0
|
|
364
366
|
wiki_uri: https://github.com/bblimke/webmock/wiki
|
|
365
367
|
rdoc_options: []
|
|
366
368
|
require_paths:
|
|
@@ -376,7 +378,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
376
378
|
- !ruby/object:Gem::Version
|
|
377
379
|
version: '0'
|
|
378
380
|
requirements: []
|
|
379
|
-
rubygems_version: 4.0.
|
|
381
|
+
rubygems_version: 4.0.20
|
|
380
382
|
specification_version: 4
|
|
381
383
|
summary: Library for stubbing HTTP requests in Ruby.
|
|
382
384
|
test_files: []
|