excon 1.10.0 → 1.11.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/README.md +31 -0
- data/data/cacert.pem +2 -2
- data/lib/excon/connection.rb +11 -2
- data/lib/excon/error.rb +13 -1
- data/lib/excon/middlewares/mock.rb +2 -0
- data/lib/excon/response.rb +18 -44
- data/lib/excon/socks5.rb +10 -10
- data/lib/excon/ssl_socket.rb +12 -14
- data/lib/excon/version.rb +1 -1
- data/lib/excon.rb +71 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5b23fd4372baad22a291604ac1177dede89a9e4a8b9caa1aa86019f4893b98d
|
|
4
|
+
data.tar.gz: 54bb6046bf8e2359a75e6720b9d8d0ffbdb41dbeeea3a483b78d0e58a2653119
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6032a35accee4a3c447c61b2a1d8bac83352b6fd3c2eb18171f59942614983d5b17bcc18f55e051e4280008ff326acc65f93134f2cfb88d6ce18f604dd51b5ac
|
|
7
|
+
data.tar.gz: 8dfd5595f225591adf29b9d4cba4740825e75b575d894c4a5ebdb3cad468fadefa9b980ca68e7a443a23cb6e04ef72cf457855c760e93b6894f74327674c6b17
|
data/README.md
CHANGED
|
@@ -403,6 +403,37 @@ end
|
|
|
403
403
|
|
|
404
404
|
By default stubs are shared globally, to make stubs unique to each thread, use `Excon.defaults[:stubs] = :local`.
|
|
405
405
|
|
|
406
|
+
### Checking that stubs were called
|
|
407
|
+
|
|
408
|
+
Excon counts the requests each stub answers, so a test can check that the stubs it set up were actually used, instead of passing silently when the request is never made.
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
stub = Excon.stub({ :method => :get, :path => '/users' }, { :body => '[]', :status => 200 })
|
|
412
|
+
|
|
413
|
+
Excon.stub_call_count(stub) # => 0
|
|
414
|
+
Excon.get('http://example.com/users', :mock => true)
|
|
415
|
+
Excon.stub_call_count(stub) # => 1
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`Excon.verify_stubs` raises `Excon::Error::StubNotCalled`, listing the request params of each stub that answered no requests. With no arguments it checks every stub in `Excon.stubs`; pass stubs to check only those, for instance to leave out a catch-all fallback stub that some tests never use.
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
Excon.verify_stubs # check every stub
|
|
422
|
+
Excon.verify_stubs(stub) # check only this stub
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
For example, with RSpec:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
config.after(:each) do
|
|
429
|
+
Excon.verify_stubs
|
|
430
|
+
ensure
|
|
431
|
+
Excon.stubs.clear
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
A stub's count is kept with the stub object `Excon.stub` returns (or `Excon.stub_for` finds), so two identical stubs are counted separately. Counts are only kept for stubs still in `Excon.stubs`, so don't rely on the count of a stub after removing it. Counts follow `Excon.defaults[:stubs]`, so `:local` stubs are counted per thread.
|
|
436
|
+
|
|
406
437
|
## Instrumentation
|
|
407
438
|
|
|
408
439
|
Excon calls can be timed using the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API.
|
data/data/cacert.pem
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
##
|
|
2
2
|
## Bundle of CA Root Certificates
|
|
3
3
|
##
|
|
4
|
-
## Certificate data from Mozilla as of:
|
|
4
|
+
## Certificate data from Mozilla as of: Thu Nov 5 04:12:01 2026 GMT
|
|
5
5
|
##
|
|
6
6
|
## Find updated versions here: https://curl.se/docs/caextract.html
|
|
7
7
|
##
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
## Configure this file as the SSLCACertificateFile.
|
|
17
17
|
##
|
|
18
18
|
## Conversion done with mk-ca-bundle.pl version 1.35.
|
|
19
|
-
## SHA256:
|
|
19
|
+
## SHA256: 3ec2b4d240dc12c747eaf450061db84af757c483bbda302f9e29ceb5d70be510
|
|
20
20
|
##
|
|
21
21
|
|
|
22
22
|
|
data/lib/excon/connection.rb
CHANGED
|
@@ -547,7 +547,11 @@ module Excon
|
|
|
547
547
|
end
|
|
548
548
|
|
|
549
549
|
def raise_socket_error(error)
|
|
550
|
-
|
|
550
|
+
# already a Socket error (e.g. from a proxy handshake): wrapping it
|
|
551
|
+
# again would add nothing but its class name to the message
|
|
552
|
+
if error.is_a?(Excon::Error::Socket)
|
|
553
|
+
raise(error)
|
|
554
|
+
elsif error.message.include?('certificate verify failed')
|
|
551
555
|
raise(Excon::Errors::CertificateError.new(error))
|
|
552
556
|
else
|
|
553
557
|
raise(Excon::Errors::SocketError.new(error))
|
|
@@ -617,7 +621,12 @@ module Excon
|
|
|
617
621
|
when Hash
|
|
618
622
|
# no processing needed
|
|
619
623
|
when String, URI
|
|
620
|
-
uri = @data[:proxy].is_a?(String)
|
|
624
|
+
uri = if @data[:proxy].is_a?(String)
|
|
625
|
+
uri_parser = @data[:uri_parser] || Excon.defaults[:uri_parser]
|
|
626
|
+
uri_parser.parse(@data[:proxy])
|
|
627
|
+
else
|
|
628
|
+
@data[:proxy]
|
|
629
|
+
end
|
|
621
630
|
@data[:proxy] = {
|
|
622
631
|
:host => uri.host,
|
|
623
632
|
:hostname => uri.hostname,
|
data/lib/excon/error.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Excon
|
|
|
5
5
|
@default_status_error = :HTTPStatus
|
|
6
6
|
|
|
7
7
|
class StubNotFound < Error; end
|
|
8
|
+
class StubNotCalled < Error; end
|
|
8
9
|
class InvalidStub < Error; end
|
|
9
10
|
class Warning < Error; end
|
|
10
11
|
|
|
@@ -12,9 +13,14 @@ module Excon
|
|
|
12
13
|
class Socket < Error
|
|
13
14
|
attr_reader :socket_error
|
|
14
15
|
|
|
16
|
+
# socket_error is the error to wrap, or a message for an error that
|
|
17
|
+
# wraps none (whose socket_error is then itself)
|
|
15
18
|
def initialize(socket_error = Excon::Error.new)
|
|
16
19
|
if is_a?(Certificate) || is_a?(Excon::Errors::CertificateError)
|
|
17
20
|
super
|
|
21
|
+
elsif socket_error.is_a?(String)
|
|
22
|
+
super
|
|
23
|
+
@socket_error = self
|
|
18
24
|
else
|
|
19
25
|
super("#{socket_error.message} (#{socket_error.class})")
|
|
20
26
|
set_backtrace(socket_error.backtrace)
|
|
@@ -52,7 +58,9 @@ or:
|
|
|
52
58
|
class Timeout < Error; end
|
|
53
59
|
class ResponseParse < Error; end
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
# Raised when a proxy refuses to open a CONNECT tunnel. A Socket error, so
|
|
62
|
+
# it is retried and rescued as one; socket_error returns the error itself.
|
|
63
|
+
class ProxyConnectionError < Socket
|
|
56
64
|
attr_reader :request, :response
|
|
57
65
|
|
|
58
66
|
def initialize(msg, request = nil, response = nil)
|
|
@@ -62,6 +70,10 @@ or:
|
|
|
62
70
|
end
|
|
63
71
|
end
|
|
64
72
|
|
|
73
|
+
# Raised when a SOCKS5 proxy handshake fails. A Socket error, so it is
|
|
74
|
+
# retried and rescued as one; socket_error returns the error itself.
|
|
75
|
+
class SOCKS5Error < Socket; end
|
|
76
|
+
|
|
65
77
|
class ProxyParse < Error; end
|
|
66
78
|
class TooManyRedirects < Error; end
|
|
67
79
|
|
data/lib/excon/response.rb
CHANGED
|
@@ -126,32 +126,20 @@ module Excon
|
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# without a :response_block (or when :expects would fail), buffer the body
|
|
130
|
+
response_block ||= lambda { |chunk, _remaining, _total| datum[:response][:body] << chunk }
|
|
131
|
+
|
|
129
132
|
if transfer_encoding_chunked
|
|
130
|
-
|
|
131
|
-
while
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
response_block.call(chunk, nil, nil)
|
|
136
|
-
end
|
|
137
|
-
new_line_size = 2 # 2 == "\r\n".length
|
|
138
|
-
while new_line_size > 0
|
|
139
|
-
chunk = socket.read(new_line_size) || raise(EOFError)
|
|
140
|
-
new_line_size -= chunk.length
|
|
141
|
-
end
|
|
133
|
+
while (chunk_size = socket.readline.chomp!.to_i(16)) > 0
|
|
134
|
+
while chunk_size > 0
|
|
135
|
+
chunk = socket.read(chunk_size) || raise(EOFError)
|
|
136
|
+
chunk_size -= chunk.bytesize
|
|
137
|
+
response_block.call(chunk, nil, nil)
|
|
142
138
|
end
|
|
143
|
-
|
|
144
|
-
while
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
chunk_size -= chunk.bytesize
|
|
148
|
-
datum[:response][:body] << chunk
|
|
149
|
-
end
|
|
150
|
-
new_line_size = 2 # 2 == "\r\n".length
|
|
151
|
-
while new_line_size > 0
|
|
152
|
-
chunk = socket.read(new_line_size) || raise(EOFError)
|
|
153
|
-
new_line_size -= chunk.length
|
|
154
|
-
end
|
|
139
|
+
new_line_size = 2 # 2 == "\r\n".length
|
|
140
|
+
while new_line_size > 0
|
|
141
|
+
chunk = socket.read(new_line_size) || raise(EOFError)
|
|
142
|
+
new_line_size -= chunk.length
|
|
155
143
|
end
|
|
156
144
|
end
|
|
157
145
|
parse_headers(socket, datum) # merge trailers into headers
|
|
@@ -161,28 +149,14 @@ module Excon
|
|
|
161
149
|
end
|
|
162
150
|
|
|
163
151
|
if (remaining = content_length)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
remaining -= chunk.bytesize
|
|
169
|
-
end
|
|
170
|
-
else
|
|
171
|
-
while remaining > 0
|
|
172
|
-
chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
|
|
173
|
-
datum[:response][:body] << chunk
|
|
174
|
-
remaining -= chunk.bytesize
|
|
175
|
-
end
|
|
152
|
+
while remaining > 0
|
|
153
|
+
chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
|
|
154
|
+
response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length)
|
|
155
|
+
remaining -= chunk.bytesize
|
|
176
156
|
end
|
|
177
157
|
else
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
response_block.call(chunk, nil, nil)
|
|
181
|
-
end
|
|
182
|
-
else
|
|
183
|
-
while (chunk = socket.read(datum[:chunk_size]))
|
|
184
|
-
datum[:response][:body] << chunk
|
|
185
|
-
end
|
|
158
|
+
while (chunk = socket.read(datum[:chunk_size]))
|
|
159
|
+
response_block.call(chunk, nil, nil)
|
|
186
160
|
end
|
|
187
161
|
end
|
|
188
162
|
end
|
data/lib/excon/socks5.rb
CHANGED
|
@@ -80,7 +80,7 @@ module Excon
|
|
|
80
80
|
version, chosen_method = response.unpack('CC')
|
|
81
81
|
|
|
82
82
|
if version != SOCKS5_VERSION
|
|
83
|
-
raise Excon::Error::
|
|
83
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5 proxy returned invalid version: #{version}"
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
case chosen_method
|
|
@@ -88,13 +88,13 @@ module Excon
|
|
|
88
88
|
# No authentication required
|
|
89
89
|
when SOCKS5_AUTH_USERNAME_PASSWORD
|
|
90
90
|
unless @proxy_user && @proxy_pass
|
|
91
|
-
raise Excon::Error::
|
|
91
|
+
raise Excon::Error::SOCKS5Error, 'SOCKS5 proxy requires authentication but no credentials provided'
|
|
92
92
|
end
|
|
93
93
|
socks5_username_password_auth
|
|
94
94
|
when SOCKS5_NO_ACCEPTABLE_AUTH
|
|
95
|
-
raise Excon::Error::
|
|
95
|
+
raise Excon::Error::SOCKS5Error, 'SOCKS5 proxy: no acceptable authentication methods'
|
|
96
96
|
else
|
|
97
|
-
raise Excon::Error::
|
|
97
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5 proxy: unsupported authentication method #{chosen_method}"
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -114,14 +114,14 @@ module Excon
|
|
|
114
114
|
_, status = response.unpack('CC')
|
|
115
115
|
|
|
116
116
|
unless status == 0x00
|
|
117
|
-
raise Excon::Error::
|
|
117
|
+
raise Excon::Error::SOCKS5Error, 'SOCKS5 proxy authentication failed'
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
# Request connection to target through SOCKS5 proxy
|
|
122
122
|
def socks5_connect(host, port)
|
|
123
123
|
if host.bytesize > MAX_HOSTNAME_LENGTH
|
|
124
|
-
raise Excon::Error::
|
|
124
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5: hostname exceeds maximum length of #{MAX_HOSTNAME_LENGTH} bytes"
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
# Build CONNECT request with domain name (let proxy resolve DNS)
|
|
@@ -135,12 +135,12 @@ module Excon
|
|
|
135
135
|
version, reply, _, atyp = response.unpack('CCCC')
|
|
136
136
|
|
|
137
137
|
if version != SOCKS5_VERSION
|
|
138
|
-
raise Excon::Error::
|
|
138
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5 proxy returned invalid version: #{version}"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
unless reply == SOCKS5_SUCCESS
|
|
142
142
|
error_msg = SOCKS5_ERRORS[reply] || "Unknown error (#{reply})"
|
|
143
|
-
raise Excon::Error::
|
|
143
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5 proxy connect failed: #{error_msg}"
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
# Read and discard bound address (not needed for CONNECT)
|
|
@@ -157,7 +157,7 @@ module Excon
|
|
|
157
157
|
when SOCKS5_ATYP_IPV6
|
|
158
158
|
socks5_read_exactly(16 + 2) # 16 bytes IP + 2 bytes port
|
|
159
159
|
else
|
|
160
|
-
raise Excon::Error::
|
|
160
|
+
raise Excon::Error::SOCKS5Error, "SOCKS5 proxy returned unknown address type: #{atyp}"
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
|
@@ -183,7 +183,7 @@ module Excon
|
|
|
183
183
|
when :wait_readable
|
|
184
184
|
IO.select([@socket], nil, nil, deadline ? [deadline - Time.now, 0].max : nil)
|
|
185
185
|
when nil, ''
|
|
186
|
-
raise Excon::Error::
|
|
186
|
+
raise Excon::Error::SOCKS5Error, 'SOCKS5 proxy connection closed unexpectedly'
|
|
187
187
|
else
|
|
188
188
|
data << chunk
|
|
189
189
|
end
|
data/lib/excon/ssl_socket.rb
CHANGED
|
@@ -86,11 +86,8 @@ module Excon
|
|
|
86
86
|
|
|
87
87
|
if client_cert_data && client_key_data
|
|
88
88
|
ssl_context.cert = OpenSSL::X509::Certificate.new client_cert_data
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else
|
|
92
|
-
ssl_context.key = OpenSSL::PKey::RSA.new(client_key_data, client_key_pass)
|
|
93
|
-
end
|
|
89
|
+
ssl_context.key = OpenSSL::PKey.read(client_key_data, client_key_pass)
|
|
90
|
+
# jruby-openssl 0.16.x (bundled with JRuby 10.0) lacks Certificate.load
|
|
94
91
|
if client_chain_data && OpenSSL::X509::Certificate.respond_to?(:load)
|
|
95
92
|
ssl_context.extra_chain_cert = OpenSSL::X509::Certificate.load(client_chain_data)
|
|
96
93
|
elsif client_chain_data
|
|
@@ -101,11 +98,7 @@ module Excon
|
|
|
101
98
|
end
|
|
102
99
|
elsif @data.key?(:certificate) && @data.key?(:private_key)
|
|
103
100
|
ssl_context.cert = OpenSSL::X509::Certificate.new(@data[:certificate])
|
|
104
|
-
|
|
105
|
-
ssl_context.key = OpenSSL::PKey.read(@data[:private_key], client_key_pass)
|
|
106
|
-
else
|
|
107
|
-
ssl_context.key = OpenSSL::PKey::RSA.new(@data[:private_key], client_key_pass)
|
|
108
|
-
end
|
|
101
|
+
ssl_context.key = OpenSSL::PKey.read(@data[:private_key], client_key_pass)
|
|
109
102
|
end
|
|
110
103
|
|
|
111
104
|
if @data[:proxy]
|
|
@@ -129,10 +122,15 @@ module Excon
|
|
|
129
122
|
# write out the proxy setup request
|
|
130
123
|
@socket.write(request)
|
|
131
124
|
|
|
132
|
-
# eat the proxy's connection response
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
# eat the proxy's connection response; any 2xx means the tunnel is
|
|
126
|
+
# open (RFC 9110, section 9.3.6)
|
|
127
|
+
response = Excon::Response.parse(self, method: 'CONNECT')
|
|
128
|
+
unless (200..299).cover?(response[:response][:status])
|
|
129
|
+
# the error keeps the request, which may be logged, so leave out
|
|
130
|
+
# the proxy credentials (which :ssl_proxy_headers may also carry)
|
|
131
|
+
redacted_request = request.gsub(/^(Proxy-Authorization:)[^\r\n]*/i, "\\1 #{Excon::REDACTED}")
|
|
132
|
+
raise(Excon::Errors::ProxyConnectionError.new('proxy connection could not be established',
|
|
133
|
+
redacted_request, response))
|
|
136
134
|
end
|
|
137
135
|
end
|
|
138
136
|
|
data/lib/excon/version.rb
CHANGED
data/lib/excon.rb
CHANGED
|
@@ -48,6 +48,9 @@ require 'excon/unix_socket'
|
|
|
48
48
|
|
|
49
49
|
# Define defaults first so they will be available to other files
|
|
50
50
|
module Excon
|
|
51
|
+
# guards the stub call counts, which threads share when stubs are :global
|
|
52
|
+
STUB_CALL_COUNTS_MUTEX = Mutex.new
|
|
53
|
+
|
|
51
54
|
class << self
|
|
52
55
|
# @return [Hash] defaults for Excon connections
|
|
53
56
|
def defaults
|
|
@@ -178,19 +181,21 @@ module Excon
|
|
|
178
181
|
else
|
|
179
182
|
raise(ArgumentError, 'stub requires either response_params OR a block')
|
|
180
183
|
end
|
|
184
|
+
prune_stub_call_counts
|
|
181
185
|
stubs.unshift(stub)
|
|
182
186
|
stub
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
# get a stub matching params or nil
|
|
186
190
|
# @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
|
|
187
|
-
# @return [
|
|
191
|
+
# @return [Array] the matching stub from Excon.stubs, a [request_params, response_params] pair
|
|
188
192
|
def stub_for(request_params={})
|
|
189
193
|
if (method = request_params.delete(:method))
|
|
190
194
|
request_params[:method] = method.to_s.downcase.to_sym
|
|
191
195
|
end
|
|
192
196
|
request_params[:query] = Utils.query_string(request_params)[1...] if request_params.key?(:query)
|
|
193
|
-
Excon.stubs.each do |
|
|
197
|
+
Excon.stubs.each do |entry|
|
|
198
|
+
stub, = entry
|
|
194
199
|
captures = { headers: {} }
|
|
195
200
|
headers_match = !stub.key?(:headers) || stub[:headers].keys.all? do |key|
|
|
196
201
|
case value = stub[:headers][key]
|
|
@@ -226,7 +231,7 @@ module Excon
|
|
|
226
231
|
end
|
|
227
232
|
if headers_match && non_headers_match
|
|
228
233
|
request_params[:captures] = captures
|
|
229
|
-
return
|
|
234
|
+
return entry
|
|
230
235
|
end
|
|
231
236
|
end
|
|
232
237
|
nil
|
|
@@ -248,7 +253,44 @@ module Excon
|
|
|
248
253
|
def unstub(request_params = {})
|
|
249
254
|
return unless (stub = stub_for(request_params))
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
STUB_CALL_COUNTS_MUTEX.synchronize { stub_call_counts.delete(stub) }
|
|
257
|
+
Excon.stubs.delete_at(Excon.stubs.index { |entry| entry.equal?(stub) })
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# number of requests a stub has answered
|
|
261
|
+
# counts are only kept for stubs still in Excon.stubs; don't rely on the
|
|
262
|
+
# count of a stub after removing it
|
|
263
|
+
# @param stub [Array] a stub, as returned by Excon.stub or Excon.stub_for
|
|
264
|
+
# @return [Integer] how many times the stub has answered a request
|
|
265
|
+
def stub_call_count(stub)
|
|
266
|
+
STUB_CALL_COUNTS_MUTEX.synchronize { stub_call_counts.fetch(stub, 0) }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# raise if any of the given stubs, or of all stubs, has not answered a request
|
|
270
|
+
# @param stubs [Array<Array>] stubs to check, as returned by Excon.stub; omit to check every stub in Excon.stubs
|
|
271
|
+
# @raise [Excon::Error::StubNotCalled] listing the request params of each stub that was never called
|
|
272
|
+
# @return [true] if every stub checked has been called
|
|
273
|
+
def verify_stubs(*stubs)
|
|
274
|
+
stubs = Excon.stubs.dup if stubs.empty?
|
|
275
|
+
uncalled = stubs.select { |stub| stub_call_count(stub).zero? }
|
|
276
|
+
return true if uncalled.empty?
|
|
277
|
+
|
|
278
|
+
message = StringIO.new
|
|
279
|
+
message.puts("#{uncalled.length} of #{stubs.length} stubs not called")
|
|
280
|
+
uncalled.each do |request_params, _|
|
|
281
|
+
message.puts('{')
|
|
282
|
+
Excon::PrettyPrinter.pp(message, request_params)
|
|
283
|
+
message.puts('}')
|
|
284
|
+
end
|
|
285
|
+
raise(Excon::Error::StubNotCalled.new(message.string))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# record that a stub answered a request, used by Excon::Middleware::Mock
|
|
289
|
+
# @param stub [Array] the stub, as returned by Excon.stub_for
|
|
290
|
+
def record_stub_call(stub)
|
|
291
|
+
STUB_CALL_COUNTS_MUTEX.synchronize do
|
|
292
|
+
stub_call_counts[stub] = stub_call_counts.fetch(stub, 0) + 1
|
|
293
|
+
end
|
|
252
294
|
end
|
|
253
295
|
|
|
254
296
|
# Generic non-persistent HTTP methods
|
|
@@ -259,5 +301,30 @@ module Excon
|
|
|
259
301
|
end
|
|
260
302
|
DEF
|
|
261
303
|
end
|
|
304
|
+
|
|
305
|
+
private
|
|
306
|
+
|
|
307
|
+
# call counts for stubs, keyed by the stub object itself (not by equality,
|
|
308
|
+
# so two identical stubs are counted apart); kept alongside Excon.stubs
|
|
309
|
+
def stub_call_counts
|
|
310
|
+
case Excon.defaults[:stubs]
|
|
311
|
+
when :global
|
|
312
|
+
@stub_call_counts ||= {}.compare_by_identity
|
|
313
|
+
when :local
|
|
314
|
+
Thread.current[:_excon_stub_call_counts] ||= {}.compare_by_identity
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# drop counts for stubs no longer in Excon.stubs (e.g. after Excon.stubs.clear),
|
|
319
|
+
# so they don't hold on to removed stubs
|
|
320
|
+
def prune_stub_call_counts
|
|
321
|
+
STUB_CALL_COUNTS_MUTEX.synchronize do
|
|
322
|
+
counts = stub_call_counts
|
|
323
|
+
next if counts.empty?
|
|
324
|
+
|
|
325
|
+
live = Excon.stubs.each_with_object({}.compare_by_identity) { |stub, set| set[stub] = true }
|
|
326
|
+
counts.delete_if { |stub, _| !live.key?(stub) }
|
|
327
|
+
end
|
|
328
|
+
end
|
|
262
329
|
end
|
|
263
330
|
end
|