patron 0.14.0 → 1.0.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 +77 -0
- data/README.md +83 -49
- data/ext/patron/extconf.rb +54 -23
- data/ext/patron/session_ext.c +1044 -923
- data/lib/patron/compat.rb +57 -0
- data/lib/patron/error.rb +21 -13
- data/lib/patron/header_parser.rb +27 -25
- data/lib/patron/request.rb +98 -144
- data/lib/patron/response.rb +28 -24
- data/lib/patron/response_decoding.rb +60 -67
- data/lib/patron/session.rb +114 -108
- data/lib/patron/util.rb +14 -12
- data/lib/patron/version.rb +3 -1
- data/lib/patron.rb +9 -15
- metadata +12 -58
- data/.autotest +0 -15
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/ci.yml +0 -79
- data/.gitignore +0 -10
- data/.rspec +0 -0
- data/.travis.yml +0 -13
- data/.yardopts +0 -1
- data/Gemfile +0 -18
- data/Rakefile +0 -31
- data/ext/patron/.gitignore +0 -4
- data/ext/patron/membuffer.c +0 -85
- data/ext/patron/membuffer.h +0 -78
- data/ext/patron/sglib.h +0 -1952
- data/lib/patron/proxy_type.rb +0 -11
- data/patron.gemspec +0 -39
- data/pic.png +0 -0
- data/script/console +0 -11
- data/script/generate_client_certs +0 -65
- data/script/test_server +0 -7
- data/spec/header_parser_spec.rb +0 -101
- data/spec/patron_spec.rb +0 -28
- data/spec/request_spec.rb +0 -89
- data/spec/response_spec.rb +0 -145
- data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
- data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
- data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
- data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
- data/spec/sample_response_headers/sample_http2_header.txt +0 -16
- data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
- data/spec/session_spec.rb +0 -865
- data/spec/session_ssl_spec.rb +0 -431
- data/spec/spec_helper.rb +0 -40
- data/spec/support/certs/cacert.pem +0 -36
- data/spec/support/certs/client.p12 +0 -0
- data/spec/support/certs/client_ca.pem +0 -20
- data/spec/support/certs/client_cert.pem +0 -20
- data/spec/support/certs/client_key.pem +0 -30
- data/spec/support/certs/privkey.pem +0 -51
- data/spec/support/certs/rogue_client.p12 +0 -0
- data/spec/support/config.ru +0 -153
- data/spec/support/fork.rb +0 -70
- data/spec/support/raw_request_capture.rb +0 -93
- data/spec/support/test_server.rb +0 -29
- data/spec/support/valgrind.supp +0 -2
- data/spec/util_spec.rb +0 -71
data/spec/support/config.ru
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
require 'yaml'
|
|
2
|
-
require 'ostruct'
|
|
3
|
-
require 'zlib'
|
|
4
|
-
|
|
5
|
-
## HTTP test server for integration tests
|
|
6
|
-
|
|
7
|
-
Readback = Proc.new {|env|
|
|
8
|
-
# The Patron test suite is originally written to enable the following
|
|
9
|
-
# testing pattern:
|
|
10
|
-
# * Patron does a request
|
|
11
|
-
# * The request gets captured by Webrick
|
|
12
|
-
# * Webrick then takes it's Request object and YAMLs it wholesale (body, headers and all)
|
|
13
|
-
# * ...and sends it back to the client
|
|
14
|
-
# * The test suite then de-YAMLs the request object from the response body and does assertions on it
|
|
15
|
-
#
|
|
16
|
-
# The easiest way to fake it, preserving the entire test suite intact, is to simulate a Webrick
|
|
17
|
-
# request object using an ostruct. Of note is the following:
|
|
18
|
-
#
|
|
19
|
-
# 1) Webrick returns headers in arrays by default, accounting for repeats like with Cookie
|
|
20
|
-
# 2) Webrick does not convert headers to ENV_VARS_FOR_FCI it only downcases them. We have to match that.
|
|
21
|
-
req = Rack::Request.new(env)
|
|
22
|
-
req_headers = env.to_a.select do |(k,v)|
|
|
23
|
-
k.is_a?(String) && v.is_a?(String)
|
|
24
|
-
end.map do |(k,v)|
|
|
25
|
-
[k.downcase.gsub(/^http_/, '').gsub(/_/o, '-'), [v]]
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
fake_webrick_request_object = OpenStruct.new({
|
|
29
|
-
:fullpath => req.fullpath,
|
|
30
|
-
:path => req.path,
|
|
31
|
-
:query_string => req.query_string,
|
|
32
|
-
:request_method => req.request_method.to_s,
|
|
33
|
-
:header => Hash[req_headers],
|
|
34
|
-
:body => env['rack.input'].read,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
body_str = fake_webrick_request_object.to_yaml
|
|
38
|
-
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body_str.bytesize.to_s}, [body_str]]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
GzipServlet = Proc.new {|env|
|
|
42
|
-
raise "Need to have the right Accept-Encoding: header" unless env['HTTP_ACCEPT_ENCODING']
|
|
43
|
-
|
|
44
|
-
body = []
|
|
45
|
-
content_length = 0
|
|
46
|
-
z = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION)
|
|
47
|
-
1024.times do
|
|
48
|
-
chunk = z.deflate('Some highly compressible data')
|
|
49
|
-
content_length += chunk.bytesize
|
|
50
|
-
body << chunk
|
|
51
|
-
end
|
|
52
|
-
fin = z.finish.to_s
|
|
53
|
-
body << fin
|
|
54
|
-
content_length += fin.bytesize
|
|
55
|
-
z.close
|
|
56
|
-
|
|
57
|
-
[200, {'Content-Encoding' => 'deflate', 'Content-Length' => content_length.to_s, 'Vary' => 'Accept-Encoding'}, body]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
TimeoutServlet = Proc.new {|env|
|
|
61
|
-
query_vars = Rack::Utils.parse_nested_query(env.fetch('QUERY_STRING'))
|
|
62
|
-
query_millis = query_vars.fetch('millis').to_i
|
|
63
|
-
sleep(query_millis / 1000.0)
|
|
64
|
-
[200, {'Content-Type' => 'text/plain'}, ['That took a while']]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
SlowServlet = Proc.new {|env|
|
|
68
|
-
body = Enumerator.new do |y|
|
|
69
|
-
y.yield 'x'
|
|
70
|
-
sleep 5
|
|
71
|
-
y.yield 'rest of body'
|
|
72
|
-
end
|
|
73
|
-
[200, {'Content-Type' => 'text/plain'}, body]
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
RedirectServlet = Proc.new {|env|
|
|
77
|
-
url_scheme = env.fetch('rack.url_scheme')
|
|
78
|
-
port = env.fetch('SERVER_PORT')
|
|
79
|
-
[301, {'Location' => "#{url_scheme}://localhost:#{port}/test"}, []]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
EvilRedirectServlet = Proc.new {|env|
|
|
83
|
-
[301, {'Location' => "smtp://mailbox:secret@localhost"}, []]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
BodyReadback = Proc.new {|env|
|
|
87
|
-
readback = {'method' => env['REQUEST_METHOD'], 'body' => env['rack.input'].read, 'content_type' => env.fetch('CONTENT_TYPE')}
|
|
88
|
-
[200, {'Content-Type' => 'text/plain'}, [OpenStruct.new(readback).to_yaml]]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
TestPatchBodyServlet = BodyReadback
|
|
92
|
-
|
|
93
|
-
SetCookieServlet = Proc.new {|env|
|
|
94
|
-
[301, {'Set-Cookie' => 'session_id=foo123', 'Location' => 'http://localhost:9001/test'}, []]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
RepetitiveHeaderServlet = Proc.new {|env|
|
|
98
|
-
# The values of the header must be Strings,
|
|
99
|
-
# consisting of lines (for multiple header values, e.g. multiple
|
|
100
|
-
# <tt>Set-Cookie</tt> values) separated by "\\n".
|
|
101
|
-
[200, {'Set-Cookie' => "a=1\nb=2", 'Content-Type' => 'text/plain'}, ['Hi.']]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
PictureServlet = Proc.new {|env|
|
|
105
|
-
# Rack::File allows us to test Range support as well
|
|
106
|
-
env_with_adjusted_path = env.merge('PATH_INFO' => Rack::Utils.escape('/pic.png'))
|
|
107
|
-
Rack::File.new('./').call(env_with_adjusted_path)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
RedirectToPictureServlet = Proc.new {|env|
|
|
111
|
-
[307, {'Location' => '/picture'}, []]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
WrongContentLengthServlet = Proc.new {|env|
|
|
115
|
-
[200, {'Content-Length' => '1024', 'Content-Type' => 'text/plain'}, ['Hello.']]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Serves a substantial amount of data
|
|
119
|
-
LargeServlet = Proc.new {|env|
|
|
120
|
-
rng = Random.new
|
|
121
|
-
len = 15 * 1024 * 1024
|
|
122
|
-
body = Enumerator.new do |y|
|
|
123
|
-
15.times do
|
|
124
|
-
y.yield(rng.bytes(1024 * 1024))
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
[200, {'Content-Type' => 'binary/octet-stream', 'Content-Length' => len.to_s}, body]
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
# Echoes the subject of the client certificate Puma verified, if any
|
|
131
|
-
PeerCertServlet = Proc.new {|env|
|
|
132
|
-
subject = env['puma.peercert'] ? env['puma.peercert'].subject.to_s : 'none'
|
|
133
|
-
[200, {'Content-Type' => 'text/plain', 'Content-Length' => subject.bytesize.to_s}, [subject]]
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
run Rack::URLMap.new({
|
|
137
|
-
"/" => Proc.new {|env| [200, {'Content-Length' => '2'}, ['Welcome']]},
|
|
138
|
-
"/test" => Readback,
|
|
139
|
-
"/testpost" => BodyReadback,
|
|
140
|
-
"/testpatch" => BodyReadback,
|
|
141
|
-
"/timeout" => TimeoutServlet,
|
|
142
|
-
"/slow" => SlowServlet,
|
|
143
|
-
"/redirect" => RedirectServlet,
|
|
144
|
-
"/evil-redirect" => EvilRedirectServlet,
|
|
145
|
-
"/picture" => PictureServlet,
|
|
146
|
-
"/redirect-to-picture" => RedirectToPictureServlet,
|
|
147
|
-
"/very-large" => LargeServlet,
|
|
148
|
-
"/setcookie" => SetCookieServlet,
|
|
149
|
-
"/repetitiveheader" => RepetitiveHeaderServlet,
|
|
150
|
-
"/wrongcontentlength" => WrongContentLengthServlet,
|
|
151
|
-
"/gzip-compressed" => GzipServlet,
|
|
152
|
-
"/peercert" => PeerCertServlet,
|
|
153
|
-
})
|
data/spec/support/fork.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
require 'thread'
|
|
2
|
-
|
|
3
|
-
class Fork
|
|
4
|
-
def initialize(&block)
|
|
5
|
-
@mu = Mutex.new
|
|
6
|
-
@cond = ConditionVariable.new
|
|
7
|
-
@pstate = nil
|
|
8
|
-
@pid = Process.fork(&block)
|
|
9
|
-
@killed = false
|
|
10
|
-
|
|
11
|
-
# Start monitoring the PID.
|
|
12
|
-
Thread.new { monitor }
|
|
13
|
-
|
|
14
|
-
# Kill the process anyway when the program exits.
|
|
15
|
-
ppid = Process.pid
|
|
16
|
-
at_exit do
|
|
17
|
-
if ppid == Process.pid # Make sure we are not inside another fork spawned by rspec example.
|
|
18
|
-
do_kill("KILL")
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Wait for process to exit.
|
|
24
|
-
def wait(timeout = nil)
|
|
25
|
-
@mu.synchronize do
|
|
26
|
-
next @pstate unless @pstate.nil?
|
|
27
|
-
|
|
28
|
-
@cond.wait(@mu, timeout)
|
|
29
|
-
@pstate
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Signal the process.
|
|
34
|
-
def kill(sig)
|
|
35
|
-
already_killed = @mu.synchronize do
|
|
36
|
-
old = @killed
|
|
37
|
-
@killed = true
|
|
38
|
-
old
|
|
39
|
-
end
|
|
40
|
-
signaled = do_kill(sig)
|
|
41
|
-
Thread.new { reaper } if signaled && !already_killed
|
|
42
|
-
signaled
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
# Signal the process.
|
|
48
|
-
def do_kill(sig)
|
|
49
|
-
Process.kill(sig, @pid)
|
|
50
|
-
true
|
|
51
|
-
rescue Errno::ESRCH # No such process
|
|
52
|
-
false
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Monitor the process state.
|
|
56
|
-
def monitor
|
|
57
|
-
_, pstate = Process.wait2(@pid)
|
|
58
|
-
|
|
59
|
-
@mu.synchronize do
|
|
60
|
-
@pstate = pstate
|
|
61
|
-
@cond.broadcast
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Wait 500 milliseconds and force terminate.
|
|
66
|
-
def reaper
|
|
67
|
-
pstate = wait(0.5)
|
|
68
|
-
do_kill("KILL") unless pstate
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
require 'socket'
|
|
2
|
-
require 'openssl'
|
|
3
|
-
require 'stringio'
|
|
4
|
-
|
|
5
|
-
# Captures the raw HTTP request a client puts on the wire, independent of any
|
|
6
|
-
# Rack/HTTP server. This lets us assert on hop-by-hop details such as
|
|
7
|
-
# Transfer-Encoding which a conformant server (webrick, puma, ...) decodes and
|
|
8
|
-
# never exposes to the application.
|
|
9
|
-
module RawRequestCapture
|
|
10
|
-
CapturedRequest = Struct.new(:request_line, :headers, :body)
|
|
11
|
-
|
|
12
|
-
CERT_DIR = File.join(__dir__, 'certs')
|
|
13
|
-
|
|
14
|
-
# Boots a one-shot capture server on an ephemeral port, yields its base URL to
|
|
15
|
-
# the block (which must perform a single request against it) and returns the
|
|
16
|
-
# CapturedRequest. Pass ssl: true to terminate TLS using the test certificates.
|
|
17
|
-
def capture_request(ssl: false)
|
|
18
|
-
tcp = TCPServer.new('127.0.0.1', 0)
|
|
19
|
-
port = tcp.addr[1]
|
|
20
|
-
server = ssl ? wrap_tls(tcp) : tcp
|
|
21
|
-
captured = nil
|
|
22
|
-
acceptor = Thread.new do
|
|
23
|
-
conn = server.accept
|
|
24
|
-
captured = read_request(conn)
|
|
25
|
-
conn.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
|
|
26
|
-
conn.close
|
|
27
|
-
end
|
|
28
|
-
yield "#{ssl ? 'https' : 'http'}://127.0.0.1:#{port}"
|
|
29
|
-
acceptor.join(5)
|
|
30
|
-
captured
|
|
31
|
-
ensure
|
|
32
|
-
tcp.close if tcp
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def wrap_tls(tcp)
|
|
38
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
|
39
|
-
ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERT_DIR, 'cacert.pem')))
|
|
40
|
-
ctx.key = OpenSSL::PKey.read(File.read(File.join(CERT_DIR, 'privkey.pem')))
|
|
41
|
-
OpenSSL::SSL::SSLServer.new(tcp, ctx)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def read_request(conn)
|
|
45
|
-
buf = +''
|
|
46
|
-
buf << conn.readpartial(4096) until buf.include?("\r\n\r\n")
|
|
47
|
-
head, rest = buf.split("\r\n\r\n", 2)
|
|
48
|
-
request_line, *header_lines = head.split("\r\n")
|
|
49
|
-
headers = header_lines.each_with_object({}) do |line, acc|
|
|
50
|
-
name, value = line.split(': ', 2)
|
|
51
|
-
acc[name.downcase] = value
|
|
52
|
-
end
|
|
53
|
-
# Honour Expect: 100-continue so the client proceeds to send the body.
|
|
54
|
-
conn.write("HTTP/1.1 100 Continue\r\n\r\n") if headers['expect'].to_s =~ /100-continue/i
|
|
55
|
-
CapturedRequest.new(request_line, headers, read_body(conn, headers, rest))
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def read_body(conn, headers, rest)
|
|
59
|
-
if headers['transfer-encoding'] == 'chunked'
|
|
60
|
-
dechunk(drain_chunked(conn, rest))
|
|
61
|
-
elsif (len = headers['content-length'])
|
|
62
|
-
read_exactly(conn, len.to_i, rest)
|
|
63
|
-
else
|
|
64
|
-
rest
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def drain_chunked(conn, rest)
|
|
69
|
-
buf = rest.dup
|
|
70
|
-
buf << conn.readpartial(4096) until buf.include?("0\r\n\r\n")
|
|
71
|
-
buf
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def read_exactly(conn, n, rest)
|
|
75
|
-
buf = rest.dup
|
|
76
|
-
buf << conn.readpartial(4096) while buf.bytesize < n
|
|
77
|
-
buf.byteslice(0, n)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def dechunk(buf)
|
|
81
|
-
io = StringIO.new(buf)
|
|
82
|
-
out = +''
|
|
83
|
-
while (size_line = io.gets("\r\n"))
|
|
84
|
-
size = size_line.strip.split(';', 2).first.to_i(16)
|
|
85
|
-
break if size.zero?
|
|
86
|
-
out << io.read(size).to_s
|
|
87
|
-
io.read(2) # trailing CRLF
|
|
88
|
-
end
|
|
89
|
-
out
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
RSpec.configure { |c| c.include RawRequestCapture }
|
data/spec/support/test_server.rb
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
require 'rack'
|
|
2
|
-
require 'puma'
|
|
3
|
-
require 'rack/handler/puma'
|
|
4
|
-
|
|
5
|
-
class PatronTestServer
|
|
6
|
-
APP = Rack::Builder.new { eval(File.read(File.join(__dir__, 'config.ru'))) }
|
|
7
|
-
|
|
8
|
-
def self.start(ssl = false, port = 9001, client_ca: nil)
|
|
9
|
-
# Reset the RSpec's SIGINT handler that does not really terminate after
|
|
10
|
-
# the first Ctrl+C pressed.
|
|
11
|
-
# Useful to terminate the forked process before Puma is actually started:
|
|
12
|
-
# it happens when running one particular example that does not need Puma
|
|
13
|
-
# so the specs are in fact finished before the Puma started.
|
|
14
|
-
Signal.trap('INT', 'EXIT')
|
|
15
|
-
@ssl = ssl
|
|
16
|
-
keypath = File.join(__dir__, 'certs', 'privkey.pem')
|
|
17
|
-
certpath = File.join(__dir__, 'certs', 'cacert.pem')
|
|
18
|
-
|
|
19
|
-
host = if ssl && client_ca
|
|
20
|
-
# force_peer makes Puma drop the handshake when no valid client certificate is presented
|
|
21
|
-
'ssl://0.0.0.0:%d?key=%s&cert=%s&ca=%s&verify_mode=force_peer' % [port, keypath, certpath, client_ca]
|
|
22
|
-
elsif ssl
|
|
23
|
-
'ssl://0.0.0.0:%d?key=%s&cert=%s' % [port, keypath, certpath]
|
|
24
|
-
else
|
|
25
|
-
'0.0.0.0'
|
|
26
|
-
end
|
|
27
|
-
Rack::Handler::Puma.run(APP, :Port => port.to_i, :Verbose => true, :Host => host)
|
|
28
|
-
end
|
|
29
|
-
end
|
data/spec/support/valgrind.supp
DELETED
data/spec/util_spec.rb
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
require File.expand_path("./spec") + '/spec_helper.rb'
|
|
2
|
-
|
|
3
|
-
describe Patron::Util do
|
|
4
|
-
|
|
5
|
-
describe :build_query_pairs_from_hash do
|
|
6
|
-
|
|
7
|
-
it "correctly serializes a simple hash" do
|
|
8
|
-
hash = {:foo => "bar", "baz" => 42}
|
|
9
|
-
array = Patron::Util.build_query_pairs_from_hash(hash)
|
|
10
|
-
expect(array.size).to be == 2
|
|
11
|
-
expect(array).to include("foo=bar")
|
|
12
|
-
expect(array).to include("baz=42")
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "correctly serializes a more complex hash" do
|
|
16
|
-
hash = {
|
|
17
|
-
:foo => "bar",
|
|
18
|
-
:baz => {
|
|
19
|
-
"quux" => {
|
|
20
|
-
:zing => {
|
|
21
|
-
:ying => 42
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
:blargh => {
|
|
25
|
-
:spaz => "sox",
|
|
26
|
-
:razz => "matazz"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
array = Patron::Util.build_query_pairs_from_hash(hash)
|
|
31
|
-
expect(array.size).to be == 4
|
|
32
|
-
expect(array).to include("foo=bar")
|
|
33
|
-
expect(array).to include("baz[quux][zing][ying]=42")
|
|
34
|
-
expect(array).to include("baz[blargh][spaz]=sox")
|
|
35
|
-
expect(array).to include("baz[blargh][razz]=matazz")
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe :build_query_string_from_hash do
|
|
40
|
-
it "correctly serializes a simple hash" do
|
|
41
|
-
hash = {:foo => "bar", "baz" => 42}
|
|
42
|
-
array = Patron::Util.build_query_string_from_hash(hash).split('&')
|
|
43
|
-
expect(array.size).to be == 2
|
|
44
|
-
expect(array).to include("foo=bar")
|
|
45
|
-
expect(array).to include("baz=42")
|
|
46
|
-
end
|
|
47
|
-
it "correctly serializes a more complex hash" do
|
|
48
|
-
hash = {
|
|
49
|
-
:foo => "bar",
|
|
50
|
-
:baz => {
|
|
51
|
-
"quux" => {
|
|
52
|
-
:zing => {
|
|
53
|
-
:ying => 42
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
:blargh => {
|
|
57
|
-
:spaz => "sox",
|
|
58
|
-
:razz => "matazz"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
array = Patron::Util.build_query_string_from_hash(hash).split('&')
|
|
63
|
-
expect(array.size).to be == 4
|
|
64
|
-
expect(array).to include("foo=bar")
|
|
65
|
-
expect(array).to include("baz[quux][zing][ying]=42")
|
|
66
|
-
expect(array).to include("baz[blargh][spaz]=sox")
|
|
67
|
-
expect(array).to include("baz[blargh][razz]=matazz")
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
end
|