patron 0.13.3 → 0.14.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/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +79 -0
- data/.travis.yml +10 -7
- data/CHANGELOG.md +32 -1
- data/Gemfile +15 -1
- data/README.md +80 -30
- data/ext/patron/extconf.rb +7 -4
- data/ext/patron/session_ext.c +360 -155
- data/lib/patron/error.rb +4 -0
- data/lib/patron/request.rb +8 -6
- data/lib/patron/response.rb +5 -0
- data/lib/patron/session.rb +19 -4
- data/lib/patron/version.rb +1 -1
- data/patron.gemspec +8 -15
- data/script/generate_client_certs +65 -0
- data/spec/request_spec.rb +12 -0
- data/spec/session_spec.rb +287 -67
- data/spec/session_ssl_spec.rb +163 -27
- data/spec/spec_helper.rb +16 -7
- data/spec/support/certs/client.p12 +0 -0
- data/spec/support/certs/client_ca.pem +20 -0
- data/spec/support/certs/client_cert.pem +20 -0
- data/spec/support/certs/client_key.pem +30 -0
- data/spec/support/certs/rogue_client.p12 +0 -0
- data/spec/support/config.ru +18 -3
- data/spec/support/fork.rb +70 -0
- data/spec/support/raw_request_capture.rb +93 -0
- data/spec/support/test_server.rb +6 -3
- data/spec/support/valgrind.supp +2 -0
- metadata +22 -125
data/spec/session_spec.rb
CHANGED
|
@@ -4,9 +4,18 @@ require 'yaml'
|
|
|
4
4
|
require 'base64'
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'securerandom'
|
|
7
|
+
require 'timeout'
|
|
7
8
|
|
|
8
9
|
describe Patron::Session do
|
|
9
10
|
|
|
11
|
+
def yaml_load(str)
|
|
12
|
+
if RUBY_VERSION >= '3.1.0'
|
|
13
|
+
YAML::safe_load(str, permitted_classes: [OpenStruct])
|
|
14
|
+
else
|
|
15
|
+
YAML::load(str)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
before(:each) do
|
|
11
20
|
@session = Patron::Session.new
|
|
12
21
|
@session.base_url = "http://localhost:9001"
|
|
@@ -59,6 +68,16 @@ describe Patron::Session do
|
|
|
59
68
|
unescaped = @session.unescape(escaped)
|
|
60
69
|
expect(unescaped).to be == string
|
|
61
70
|
end
|
|
71
|
+
|
|
72
|
+
it "unescapes a %00 to a NUL byte instead of cutting the string there" do
|
|
73
|
+
expect(@session.unescape("a%00b").b).to be == "a\0b".b
|
|
74
|
+
expect(@session.escape("a\0b")).to be == "a%00b"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "rejects a file path with an embedded NUL byte" do
|
|
78
|
+
expect { @session.enable_debug("/tmp/x\0y") }.to raise_error(ArgumentError)
|
|
79
|
+
expect { @session.handle_cookies("/tmp/x\0y") }.to raise_error(ArgumentError)
|
|
80
|
+
end
|
|
62
81
|
end
|
|
63
82
|
|
|
64
83
|
it "should raise an error when passed an invalid action" do
|
|
@@ -72,10 +91,23 @@ describe Patron::Session do
|
|
|
72
91
|
|
|
73
92
|
it "should retrieve a url with :get" do
|
|
74
93
|
response = @session.get("/test")
|
|
75
|
-
body =
|
|
94
|
+
body = yaml_load(response.body)
|
|
76
95
|
expect(body.request_method).to be == "GET"
|
|
77
96
|
end
|
|
78
97
|
|
|
98
|
+
it "should expose curl's cumulative transfer timings" do
|
|
99
|
+
timings = @session.get("/test").timings
|
|
100
|
+
|
|
101
|
+
expect(timings.keys).to contain_exactly(
|
|
102
|
+
:namelookup, :connect, :appconnect, :pretransfer, :starttransfer, :redirect, :total
|
|
103
|
+
)
|
|
104
|
+
expect(timings.values).to all(be_a(Float).and(be >= 0))
|
|
105
|
+
expect(timings[:namelookup]).to be <= timings[:connect]
|
|
106
|
+
expect(timings[:connect]).to be <= timings[:starttransfer]
|
|
107
|
+
expect(timings[:starttransfer]).to be <= timings[:total]
|
|
108
|
+
expect(timings[:total]).to be > 0
|
|
109
|
+
end
|
|
110
|
+
|
|
79
111
|
it "should use full base url" do
|
|
80
112
|
@session.base_url = "http://localhost:9001/api/v1"
|
|
81
113
|
response = @session.get("/test")
|
|
@@ -88,38 +120,68 @@ describe Patron::Session do
|
|
|
88
120
|
end
|
|
89
121
|
|
|
90
122
|
it "should download content with :get and a file path" do
|
|
91
|
-
|
|
123
|
+
tf = Tempfile.new
|
|
124
|
+
tf.close
|
|
125
|
+
tmpfile = tf.path
|
|
126
|
+
|
|
92
127
|
response = @session.get_file "/test", tmpfile
|
|
93
128
|
expect(response.body).to be_nil
|
|
94
|
-
body =
|
|
129
|
+
body = yaml_load(File.open(tmpfile).read)
|
|
95
130
|
expect(body.request_method).to be == "GET"
|
|
96
|
-
FileUtils.rm tmpfile
|
|
97
131
|
end
|
|
98
132
|
|
|
99
133
|
it "should download correctly(md5 ok) with get_file" do
|
|
100
|
-
|
|
134
|
+
tf = Tempfile.new
|
|
135
|
+
tf.close
|
|
136
|
+
tmpfile = tf.path
|
|
137
|
+
|
|
101
138
|
response = @session.get_file "/picture", tmpfile
|
|
102
139
|
expect(response.body).to be_nil
|
|
103
140
|
expect(File.size(File.join(File.dirname(__FILE__),"../pic.png"))).to be == File.size(tmpfile)
|
|
104
|
-
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "should follow a 307 redirect with Range headers" do
|
|
144
|
+
tf = Tempfile.new
|
|
145
|
+
tf.close
|
|
146
|
+
tmpfile = tf.path
|
|
147
|
+
|
|
148
|
+
response = @session.get_file "/redirect-to-picture", tmpfile, {'Range' => 'bytes=0-3'}
|
|
149
|
+
expect(response.body).to be_nil
|
|
150
|
+
expect(File.size(tmpfile)).to eq(4)
|
|
105
151
|
end
|
|
106
152
|
|
|
107
153
|
it "downloads a very large file" do
|
|
108
|
-
|
|
154
|
+
tf = Tempfile.new
|
|
155
|
+
tf.close
|
|
156
|
+
tmpfile = tf.path
|
|
157
|
+
|
|
109
158
|
@session.get_file "/very-large", tmpfile
|
|
110
159
|
expect(File.size(tmpfile)).to eq(15 * 1024 * 1024)
|
|
111
160
|
end
|
|
112
161
|
|
|
113
162
|
it "raises an exception if the download body limit is exceeded when using direct-to-file download" do
|
|
114
|
-
|
|
163
|
+
tf = Tempfile.new
|
|
164
|
+
tf.close
|
|
165
|
+
tmpfile = tf.path
|
|
166
|
+
|
|
115
167
|
@session.download_byte_limit = 1024
|
|
116
168
|
@session.buffer_size = 1024
|
|
117
169
|
expect {
|
|
118
170
|
@session.get_file "/very-large", tmpfile
|
|
119
171
|
}.to raise_error(Patron::Error)
|
|
120
172
|
# On Ruby 2.x Patron::Aborted takes precedence, but
|
|
121
|
-
# on 1.9 it will be Patron::PartialFileError
|
|
122
|
-
|
|
173
|
+
# on 1.9 it will be Patron::PartialFileError, so we cannot match on exception.
|
|
174
|
+
#
|
|
175
|
+
# Not clear why, but Patron (or libCURL, or the two in combination)
|
|
176
|
+
# may fetch more bytes than we permit it to - for example,
|
|
177
|
+
# on Darwin the limit is respected exactly, while on Linux in GH actions
|
|
178
|
+
# it does retrieve substantially more.
|
|
179
|
+
# The URL we are hitting is serving a file that is 15 megabytes. On Linux
|
|
180
|
+
# the tempfile fills to 103344 in our tests, so this is probably either
|
|
181
|
+
# OS-dependent or related to timings with the RubyVM context switching.
|
|
182
|
+
# To ensure our limiting works, it is enough to check that the downloaded file
|
|
183
|
+
# is perceptibly smaller than the complete size of 15MB. Let's say < 1MB.
|
|
184
|
+
expect(File.size(tmpfile)).to be < (1024 * 1024)
|
|
123
185
|
end
|
|
124
186
|
|
|
125
187
|
it "raises an exception if the download body limit is exceeded when using download-to-memory" do
|
|
@@ -132,36 +194,44 @@ describe Patron::Session do
|
|
|
132
194
|
# on 1.9 it will be Patron::PartialFileError
|
|
133
195
|
end
|
|
134
196
|
|
|
197
|
+
it "accepts a download body limit above 2 GiB and timeouts given as Floats" do
|
|
198
|
+
@session.download_byte_limit = 5 * 1024 * 1024 * 1024
|
|
199
|
+
@session.dns_cache_timeout = 2.5
|
|
200
|
+
@session.low_speed_time = 2.5
|
|
201
|
+
@session.low_speed_limit = 1.5
|
|
202
|
+
expect(@session.get("/test").status).to be == 200
|
|
203
|
+
end
|
|
204
|
+
|
|
135
205
|
it "should not send the user-agent if it has been deleted from headers" do
|
|
136
206
|
@session.headers.delete 'User-Agent'
|
|
137
207
|
response = @session.get("/test")
|
|
138
|
-
body =
|
|
208
|
+
body = yaml_load(response.body)
|
|
139
209
|
expect(body.header["user-agent"]).to be_nil
|
|
140
210
|
end
|
|
141
211
|
|
|
142
212
|
it "should set the default User-agent" do
|
|
143
213
|
response = @session.get("/test")
|
|
144
|
-
body =
|
|
214
|
+
body = yaml_load(response.body)
|
|
145
215
|
expect(body.header["user-agent"]).to be == [Patron.user_agent_string]
|
|
146
216
|
end
|
|
147
217
|
|
|
148
218
|
it "should include custom headers in a request" do
|
|
149
219
|
response = @session.get("/test", {"User-Agent" => "PatronTest"})
|
|
150
|
-
body =
|
|
220
|
+
body = yaml_load(response.body)
|
|
151
221
|
expect(body.header["user-agent"]).to be == ["PatronTest"]
|
|
152
222
|
end
|
|
153
223
|
|
|
154
224
|
it "should include default headers in a request, if they were defined" do
|
|
155
225
|
@session.headers = {"User-Agent" => "PatronTest"}
|
|
156
226
|
response = @session.get("/test")
|
|
157
|
-
body =
|
|
227
|
+
body = yaml_load(response.body)
|
|
158
228
|
expect(body.header["user-agent"]).to be == ["PatronTest"]
|
|
159
229
|
end
|
|
160
230
|
|
|
161
231
|
it "should merge custom headers with session headers" do
|
|
162
232
|
@session.headers["X-Test"] = "Testing"
|
|
163
233
|
response = @session.get("/test", {"User-Agent" => "PatronTest"})
|
|
164
|
-
body =
|
|
234
|
+
body = yaml_load(response.body)
|
|
165
235
|
expect(body.header["user-agent"]).to be == ["PatronTest"]
|
|
166
236
|
expect(body.header["x-test"]).to be == ["Testing"]
|
|
167
237
|
end
|
|
@@ -181,70 +251,141 @@ describe Patron::Session do
|
|
|
181
251
|
expect {@session.get("/timeout?millis=400")}.to raise_error(Patron::TimeoutError)
|
|
182
252
|
end
|
|
183
253
|
|
|
184
|
-
it "should raise an exception on timeout when reading from a slow resource" do
|
|
185
|
-
@session.timeout =
|
|
254
|
+
it "should raise an exception on timeout when reading from a slow resource and low speed limit is set", :timing do
|
|
255
|
+
@session.timeout = 10 # Greater than the low_speed_time
|
|
186
256
|
@session.low_speed_time = 2
|
|
187
257
|
@session.low_speed_limit = 10
|
|
258
|
+
|
|
259
|
+
# Our test server starts sending the body only after 5 seconds. We should be able to abort
|
|
260
|
+
# by exceeding the low_speed_time.
|
|
261
|
+
started = Time.now.to_f
|
|
188
262
|
expect {@session.get("/slow")}.to raise_error(Patron::TimeoutError)
|
|
263
|
+
delta_s = Time.now.to_f - started
|
|
264
|
+
# For some reason libCURL times out after 3 seconds instead of 2 sometimes.
|
|
265
|
+
expect(delta_s).to be_within(0.1).of(2).or be_within(0.1).of(3)
|
|
189
266
|
end
|
|
190
267
|
|
|
191
|
-
it "is able to terminate the thread that is running a slow request using Thread#kill (uses the custom unblock)" do
|
|
268
|
+
it "is able to terminate the thread that is running a slow request using Thread#kill (uses the custom unblock)", :timing do
|
|
192
269
|
t = Thread.new do
|
|
193
270
|
session = Patron::Session.new
|
|
194
|
-
session.timeout =
|
|
271
|
+
session.timeout = 10 # Greater than the sleep duration
|
|
195
272
|
session.base_url = "http://localhost:9001"
|
|
196
273
|
session.get("/slow")
|
|
197
274
|
end
|
|
198
275
|
|
|
199
|
-
# Our test server starts sending the body only after
|
|
276
|
+
# Our test server starts sending the body only after 5 seconds. We should be able to abort
|
|
200
277
|
# using a signal during that time.
|
|
201
|
-
started = Time.now.
|
|
202
|
-
sleep
|
|
278
|
+
started = Time.now.to_f
|
|
279
|
+
sleep 2 # Less than what it takes for the server to respond
|
|
203
280
|
t.kill # Kill the thread forcibly
|
|
204
|
-
t.join # wrap up the thread. If Patron is still busy there, this join call will still take
|
|
281
|
+
t.join # wrap up the thread. If Patron is still busy there, this join call will still take another 1s.
|
|
205
282
|
|
|
206
|
-
delta_s = Time.now.
|
|
207
|
-
|
|
283
|
+
delta_s = Time.now.to_f - started
|
|
284
|
+
# For some reason libCURL times out after 3 seconds instead of 2 sometimes.
|
|
285
|
+
expect(delta_s).to be_within(0.1).of(2).or be_within(0.1).of(3)
|
|
208
286
|
end
|
|
209
287
|
|
|
210
|
-
it "is able to terminate the
|
|
288
|
+
it "is able to terminate the process that is running a slow request with SIGINT", :timing do
|
|
211
289
|
pid = Process.fork do
|
|
212
290
|
trap('SIGINT') do
|
|
213
291
|
exit # exit the process
|
|
214
292
|
end
|
|
215
293
|
session = Patron::Session.new
|
|
216
|
-
session.timeout =
|
|
294
|
+
session.timeout = 10 # Greater than the sleep duration
|
|
217
295
|
session.base_url = "http://localhost:9001"
|
|
218
296
|
session.get("/slow")
|
|
219
297
|
end
|
|
220
298
|
|
|
221
|
-
# Our test server starts sending the body only after
|
|
299
|
+
# Our test server starts sending the body only after 5 seconds. We should be able to abort
|
|
222
300
|
# using a signal during that time.
|
|
223
|
-
started = Time.now.
|
|
224
|
-
sleep
|
|
225
|
-
Process.kill("INT", pid) # Signal
|
|
226
|
-
Process.wait(pid) # wrap up the process. If Patron is still busy there, this call will still take
|
|
227
|
-
delta_s = Time.now.
|
|
228
|
-
|
|
301
|
+
started = Time.now.to_f
|
|
302
|
+
sleep 2 # Less than what it takes for the server to respond
|
|
303
|
+
Process.kill("INT", pid) # Signal the process...
|
|
304
|
+
Process.wait(pid) # wrap up the process. If Patron is still busy there, this call will still take another 1s.
|
|
305
|
+
delta_s = Time.now.to_f - started
|
|
306
|
+
# For some reason libCURL times out after 3 seconds instead of 2 sometimes.
|
|
307
|
+
expect(delta_s).to be_within(0.1).of(2).or be_within(0.1).of(3)
|
|
229
308
|
end
|
|
230
309
|
|
|
231
310
|
it "receives progress callbacks" do
|
|
232
311
|
session = Patron::Session.new
|
|
233
|
-
session.timeout =
|
|
312
|
+
session.timeout = 10 # # Greater than the /slow respond time (5 seconds)
|
|
234
313
|
session.base_url = "http://localhost:9001"
|
|
235
314
|
callback_args = []
|
|
236
315
|
session.progress_callback = Proc.new {|dltotal, dlnow, ultotal, ulnow|
|
|
237
316
|
callback_args << [dltotal, dlnow, ultotal, ulnow]
|
|
238
317
|
}
|
|
239
318
|
session.get("/slow")
|
|
319
|
+
expect(callback_args).not_to be_empty
|
|
240
320
|
|
|
321
|
+
# Make sure that the callback setting was not reset.
|
|
322
|
+
callback_args = []
|
|
323
|
+
session.get("/slow")
|
|
241
324
|
expect(callback_args).not_to be_empty
|
|
242
325
|
end
|
|
243
326
|
|
|
327
|
+
it "propagates an exception raised in the progress callback and keeps the session usable" do
|
|
328
|
+
session = Patron::Session.new
|
|
329
|
+
session.timeout = 10
|
|
330
|
+
session.base_url = "http://localhost:9001"
|
|
331
|
+
boom = Class.new(StandardError)
|
|
332
|
+
session.progress_callback = Proc.new { raise boom, "from the callback" }
|
|
333
|
+
|
|
334
|
+
expect { session.get("/slow") }.to raise_error(boom, "from the callback")
|
|
335
|
+
|
|
336
|
+
session.progress_callback = nil
|
|
337
|
+
expect(session.get("/test").status).to be == 200
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "lets a throw from the progress callback unwind to the catch around the request" do
|
|
341
|
+
# Timeout.timeout on Rubies before 3.1 unwinds with throw rather than raise
|
|
342
|
+
session = Patron::Session.new
|
|
343
|
+
session.timeout = 10
|
|
344
|
+
session.base_url = "http://localhost:9001"
|
|
345
|
+
session.progress_callback = Proc.new { throw :stop_right_there, :thrown }
|
|
346
|
+
|
|
347
|
+
expect(catch(:stop_right_there) { session.get("/slow") }).to be == :thrown
|
|
348
|
+
|
|
349
|
+
session.progress_callback = nil
|
|
350
|
+
expect(session.get("/test").status).to be == 200
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it "can be interrupted with Timeout.timeout while a progress callback is set", :timing do
|
|
354
|
+
session = Patron::Session.new
|
|
355
|
+
session.timeout = 10
|
|
356
|
+
session.base_url = "http://localhost:9001"
|
|
357
|
+
session.progress_callback = Proc.new { |*| }
|
|
358
|
+
|
|
359
|
+
started = Time.now.to_f
|
|
360
|
+
expect { Timeout.timeout(1) { session.get("/slow") } }.to raise_error(Timeout::Error)
|
|
361
|
+
expect(Time.now.to_f - started).to be < 3
|
|
362
|
+
|
|
363
|
+
session.progress_callback = nil
|
|
364
|
+
expect(session.get("/test").status).to be == 200
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it "raises the right exception class after the heap has been compacted" do
|
|
368
|
+
# The exception classes are looked up from C once at load time; if they
|
|
369
|
+
# were not registered with the GC, compaction would move them away
|
|
370
|
+
skip "no compaction on this Ruby" unless GC.respond_to?(:verify_compaction_references)
|
|
371
|
+
begin
|
|
372
|
+
begin
|
|
373
|
+
GC.verify_compaction_references(expand_heap: true, toward: :empty)
|
|
374
|
+
rescue ArgumentError # expand_heap: is double_heap: on Rubies before 3.2
|
|
375
|
+
GC.verify_compaction_references(double_heap: true, toward: :empty)
|
|
376
|
+
end
|
|
377
|
+
rescue NotImplementedError
|
|
378
|
+
skip "compaction is not supported on this platform"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
session = Patron::Session.new
|
|
382
|
+
expect { session.get("http://localhost:1/") }.to raise_error(Patron::ConnectionFailed)
|
|
383
|
+
end
|
|
384
|
+
|
|
244
385
|
it "should follow redirects by default" do
|
|
245
386
|
@session.max_redirects = 1
|
|
246
387
|
response = @session.get("/redirect")
|
|
247
|
-
body =
|
|
388
|
+
body = yaml_load(response.body)
|
|
248
389
|
expect(response.status).to be == 200
|
|
249
390
|
expect(body.path).to be == "/test"
|
|
250
391
|
end
|
|
@@ -277,26 +418,26 @@ describe Patron::Session do
|
|
|
277
418
|
|
|
278
419
|
it "should send a delete request with :delete" do
|
|
279
420
|
response = @session.delete("/test")
|
|
280
|
-
body =
|
|
421
|
+
body = yaml_load(response.body)
|
|
281
422
|
expect(body.request_method).to be == "DELETE"
|
|
282
423
|
end
|
|
283
424
|
|
|
284
425
|
it "should send a COPY request with :copy" do
|
|
285
426
|
response = @session.copy("/test", "/test2")
|
|
286
|
-
body =
|
|
427
|
+
body = yaml_load(response.body)
|
|
287
428
|
expect(body.request_method).to be == "COPY"
|
|
288
429
|
end
|
|
289
430
|
|
|
290
431
|
it "should include a Destination header in COPY requests" do
|
|
291
432
|
response = @session.copy("/test", "/test2")
|
|
292
|
-
body =
|
|
433
|
+
body = yaml_load(response.body)
|
|
293
434
|
expect(body.header['destination'].first).to be == "/test2"
|
|
294
435
|
end
|
|
295
436
|
|
|
296
437
|
it "should upload data with :get" do
|
|
297
438
|
data = "upload data"
|
|
298
439
|
response = @session.request(:get, "/test", {}, :data => data)
|
|
299
|
-
body =
|
|
440
|
+
body = yaml_load(response.body)
|
|
300
441
|
expect(body.request_method).to be == "GET"
|
|
301
442
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
302
443
|
end
|
|
@@ -304,7 +445,7 @@ describe Patron::Session do
|
|
|
304
445
|
it "should call to_s on the data being uploaded via GET if it is not already a String" do
|
|
305
446
|
data = 12345
|
|
306
447
|
response = @session.request(:get, "/test", {}, :data => data)
|
|
307
|
-
body =
|
|
448
|
+
body = yaml_load(response.body)
|
|
308
449
|
expect(body.request_method).to be == "GET"
|
|
309
450
|
end
|
|
310
451
|
|
|
@@ -315,7 +456,7 @@ describe Patron::Session do
|
|
|
315
456
|
# you can have very deeply going queries, which are still technically GETs
|
|
316
457
|
data = Random.new.bytes(1024 * 24)
|
|
317
458
|
response = @session.request(:get, "/test", {}, :data => data)
|
|
318
|
-
body =
|
|
459
|
+
body = yaml_load(response.body)
|
|
319
460
|
expect(body.request_method).to be == "GET"
|
|
320
461
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
321
462
|
end
|
|
@@ -323,7 +464,7 @@ describe Patron::Session do
|
|
|
323
464
|
it "should upload data with :put" do
|
|
324
465
|
data = Random.new.bytes(1024 * 24)
|
|
325
466
|
response = @session.put("/test", data)
|
|
326
|
-
body =
|
|
467
|
+
body = yaml_load(response.body)
|
|
327
468
|
expect(body.request_method).to be == "PUT"
|
|
328
469
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
329
470
|
end
|
|
@@ -334,7 +475,7 @@ describe Patron::Session do
|
|
|
334
475
|
data.flush; data.rewind
|
|
335
476
|
|
|
336
477
|
response = @session.put("/test", data, {'Expect' => ''})
|
|
337
|
-
body =
|
|
478
|
+
body = yaml_load(response.body)
|
|
338
479
|
expect(body.request_method).to be == "PUT"
|
|
339
480
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
340
481
|
end
|
|
@@ -342,25 +483,45 @@ describe Patron::Session do
|
|
|
342
483
|
it "should upload data with :patch" do
|
|
343
484
|
data = "upload data"
|
|
344
485
|
response = @session.patch("/testpatch", data)
|
|
345
|
-
body =
|
|
486
|
+
body = yaml_load(response.body)
|
|
346
487
|
expect(body.body).to eq("upload data")
|
|
347
488
|
end
|
|
348
489
|
|
|
349
490
|
it "should upload data with :delete" do
|
|
350
491
|
data = "upload data"
|
|
351
492
|
response = @session.request(:delete, "/test", {}, :data => data)
|
|
352
|
-
body =
|
|
493
|
+
body = yaml_load(response.body)
|
|
353
494
|
expect(body.request_method).to be == "DELETE"
|
|
354
495
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
355
496
|
end
|
|
356
497
|
|
|
498
|
+
it "keeps a request body built by #to_s alive for the duration of the transfer" do
|
|
499
|
+
# The body only exists as the return value of #to_s, and libcurl reads it
|
|
500
|
+
# while the GVL is released - GC.stress makes that window reliable
|
|
501
|
+
payload = Random.new.bytes(64 * 1024)
|
|
502
|
+
body_object = Object.new
|
|
503
|
+
body_object.define_singleton_method(:to_s) { payload + '' }
|
|
504
|
+
|
|
505
|
+
begin
|
|
506
|
+
GC.stress = true
|
|
507
|
+
[:post, :put, :delete].each do |verb|
|
|
508
|
+
response = @session.request(verb, "/test", {}, :data => body_object)
|
|
509
|
+
body = yaml_load(response.body)
|
|
510
|
+
expect(body.request_method).to be == verb.to_s.upcase
|
|
511
|
+
expect(body.body.b).to be == payload
|
|
512
|
+
end
|
|
513
|
+
ensure
|
|
514
|
+
GC.stress = false
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
357
518
|
it "should raise when no data is provided to :put" do
|
|
358
519
|
expect { @session.put("/test", nil) }.to raise_error(ArgumentError)
|
|
359
520
|
end
|
|
360
521
|
|
|
361
522
|
it "should upload a file with :put" do
|
|
362
523
|
response = @session.put_file("/test", "LICENSE")
|
|
363
|
-
body =
|
|
524
|
+
body = yaml_load(response.body)
|
|
364
525
|
expect(body.request_method).to be == "PUT"
|
|
365
526
|
end
|
|
366
527
|
|
|
@@ -368,23 +529,33 @@ describe Patron::Session do
|
|
|
368
529
|
expect { @session.put_file("/test", nil) }.to raise_error(ArgumentError)
|
|
369
530
|
end
|
|
370
531
|
|
|
371
|
-
it "should
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
532
|
+
it "should upload a file with :put using chunked transfer-encoding" do
|
|
533
|
+
# Transfer-Encoding is a hop-by-hop header (RFC 7230 3.3.1): a conformant server
|
|
534
|
+
# decodes the chunked framing and never exposes the header to the application, so
|
|
535
|
+
# we capture the raw request to assert how Patron actually frames the upload.
|
|
536
|
+
captured = capture_request do |url|
|
|
537
|
+
session = Patron::Session.new
|
|
538
|
+
session.base_url = url
|
|
539
|
+
session.timeout = 5
|
|
540
|
+
session.put_file("/test", "LICENSE")
|
|
541
|
+
end
|
|
542
|
+
expect(captured.request_line).to start_with("PUT ")
|
|
543
|
+
expect(captured.headers["transfer-encoding"]).to be == "chunked"
|
|
544
|
+
expect(captured.headers).to_not have_key("content-length")
|
|
545
|
+
expect(captured.body.bytesize).to be == File.size("LICENSE")
|
|
375
546
|
end
|
|
376
547
|
|
|
377
548
|
it "should call to_s on the data being uploaded via POST if it is not already a String" do
|
|
378
549
|
data = 12345
|
|
379
550
|
response = @session.post("/testpost", data)
|
|
380
|
-
body =
|
|
551
|
+
body = yaml_load(response.body)
|
|
381
552
|
expect(body['body']).to eq("12345")
|
|
382
553
|
end
|
|
383
554
|
|
|
384
555
|
it "should upload data with :post" do
|
|
385
556
|
data = "upload data"
|
|
386
557
|
response = @session.post("/test", data)
|
|
387
|
-
body =
|
|
558
|
+
body = yaml_load(response.body)
|
|
388
559
|
expect(body.request_method).to be == "POST"
|
|
389
560
|
expect(body.header['content-length']).to be == [data.size.to_s]
|
|
390
561
|
end
|
|
@@ -392,7 +563,7 @@ describe Patron::Session do
|
|
|
392
563
|
it "should POST a hash of arguments as a urlencoded form" do
|
|
393
564
|
data = {:foo => 123, 'baz' => '++hello world++'}
|
|
394
565
|
response = @session.post("/testpost", data)
|
|
395
|
-
body =
|
|
566
|
+
body = yaml_load(response.body)
|
|
396
567
|
expect(body['content_type']).to be == "application/x-www-form-urlencoded"
|
|
397
568
|
expect(body['body']).to match(/baz=%2B%2Bhello%20world%2B%2B/)
|
|
398
569
|
expect(body['body']).to match(/foo=123/)
|
|
@@ -404,42 +575,66 @@ describe Patron::Session do
|
|
|
404
575
|
|
|
405
576
|
it "should upload a file with :post" do
|
|
406
577
|
response = @session.post_file("/test", "LICENSE")
|
|
407
|
-
body =
|
|
578
|
+
body = yaml_load(response.body)
|
|
408
579
|
expect(body.request_method).to be == "POST"
|
|
409
580
|
end
|
|
410
581
|
|
|
411
582
|
it "should upload a multipart with :post" do
|
|
412
583
|
response = @session.post_multipart("/test", { :test_data => "123" }, { :test_file => "LICENSE" } )
|
|
413
|
-
body =
|
|
584
|
+
body = yaml_load(response.body)
|
|
414
585
|
expect(body.request_method).to be == "POST"
|
|
415
586
|
end
|
|
416
587
|
|
|
588
|
+
it "sends multipart field names and binary values intact" do
|
|
589
|
+
# A Symbol key gets stringified into a temporary libcurl must copy,
|
|
590
|
+
# and a value with NUL bytes must survive past the first one
|
|
591
|
+
value = "before\0middle\0after".b
|
|
592
|
+
response = @session.post_multipart("/test", { :sym_key => value }, { :test_file => "LICENSE" })
|
|
593
|
+
body = yaml_load(response.body)
|
|
594
|
+
raw = body.body.b
|
|
595
|
+
expect(raw).to include('name="sym_key"')
|
|
596
|
+
expect(raw).to include(value)
|
|
597
|
+
expect(raw).to include('name="test_file"; filename="LICENSE"')
|
|
598
|
+
expect(raw).to include(File.read("LICENSE").b)
|
|
599
|
+
end
|
|
600
|
+
|
|
417
601
|
it "should raise when no file is provided to :post" do
|
|
418
602
|
expect { @session.post_file("/test", nil) }.to raise_error(ArgumentError)
|
|
419
603
|
end
|
|
420
604
|
|
|
421
|
-
it "should
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
605
|
+
it "should upload a file with :post using chunked transfer-encoding" do
|
|
606
|
+
# See the :put counterpart above: Transfer-Encoding is hop-by-hop and invisible
|
|
607
|
+
# to the application, so we capture the raw request to assert the framing.
|
|
608
|
+
captured = capture_request do |url|
|
|
609
|
+
session = Patron::Session.new
|
|
610
|
+
session.base_url = url
|
|
611
|
+
session.timeout = 5
|
|
612
|
+
session.post_file("/test", "LICENSE")
|
|
613
|
+
end
|
|
614
|
+
expect(captured.request_line).to start_with("POST ")
|
|
615
|
+
expect(captured.headers["transfer-encoding"]).to be == "chunked"
|
|
616
|
+
expect(captured.headers).to_not have_key("content-length")
|
|
617
|
+
expect(captured.body.bytesize).to be == File.size("LICENSE")
|
|
425
618
|
end
|
|
426
619
|
|
|
427
620
|
it "should pass credentials as http basic auth" do
|
|
428
621
|
@session.username = "foo"
|
|
429
622
|
@session.password = "bar"
|
|
430
623
|
response = @session.get("/test")
|
|
431
|
-
body =
|
|
624
|
+
body = yaml_load(response.body)
|
|
432
625
|
expect(body.header['authorization']).to be == [encode_authz("foo", "bar")]
|
|
433
626
|
end
|
|
434
627
|
|
|
435
628
|
it "should store cookies across multiple requests" do
|
|
436
629
|
tf = Tempfile.new('cookiejar')
|
|
630
|
+
tf.close
|
|
437
631
|
cookie_jar_path = tf.path
|
|
438
632
|
|
|
439
633
|
@session.handle_cookies(cookie_jar_path)
|
|
440
634
|
response = @session.get("/setcookie").body
|
|
441
635
|
|
|
442
|
-
cookie_jar_contents = tf.read
|
|
636
|
+
cookie_jar_contents = tf.open.read
|
|
637
|
+
tf.unlink
|
|
443
638
|
expect(cookie_jar_contents).not_to be_empty
|
|
444
639
|
expect(cookie_jar_contents).to include('Netscape HTTP Cookie File')
|
|
445
640
|
end
|
|
@@ -447,12 +642,12 @@ describe Patron::Session do
|
|
|
447
642
|
it "should handle cookies if set" do
|
|
448
643
|
@session.handle_cookies
|
|
449
644
|
response = @session.get("/setcookie").body
|
|
450
|
-
expect(
|
|
645
|
+
expect(yaml_load(response).header['cookie'].first).to be == "session_id=foo123"
|
|
451
646
|
end
|
|
452
647
|
|
|
453
648
|
it "should not handle cookies by default" do
|
|
454
649
|
response = @session.get("/setcookie").body
|
|
455
|
-
expect(
|
|
650
|
+
expect(yaml_load(response).header).to_not include('cookie')
|
|
456
651
|
end
|
|
457
652
|
|
|
458
653
|
it "should ignore a wrong Content-Length when asked to" do
|
|
@@ -473,6 +668,31 @@ describe Patron::Session do
|
|
|
473
668
|
expect { @session.handle_cookies("/trash/clash/foo") }.to raise_error(ArgumentError)
|
|
474
669
|
end
|
|
475
670
|
|
|
671
|
+
it "refuses to run two requests on one Session from two threads at the same time" do
|
|
672
|
+
session = Patron::Session.new
|
|
673
|
+
session.timeout = 10
|
|
674
|
+
session.base_url = "http://localhost:9001"
|
|
675
|
+
|
|
676
|
+
outcomes = Array.new(2) do
|
|
677
|
+
Thread.new do
|
|
678
|
+
begin
|
|
679
|
+
session.get("/slow")
|
|
680
|
+
:completed
|
|
681
|
+
rescue Patron::Error => e
|
|
682
|
+
e
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
end.map(&:value)
|
|
686
|
+
|
|
687
|
+
refused = outcomes.grep(Patron::Error)
|
|
688
|
+
expect(refused.size).to be == 1
|
|
689
|
+
expect(refused.first.message).to include("another thread")
|
|
690
|
+
expect(outcomes - refused).to be == [:completed]
|
|
691
|
+
|
|
692
|
+
# ...and is free again once the in-flight request is done
|
|
693
|
+
expect(session.get("/test").status).to be == 200
|
|
694
|
+
end
|
|
695
|
+
|
|
476
696
|
it "should work with multiple threads" do
|
|
477
697
|
threads = []
|
|
478
698
|
3.times do
|
|
@@ -497,7 +717,7 @@ describe Patron::Session do
|
|
|
497
717
|
|
|
498
718
|
expect {
|
|
499
719
|
response = @session.get("/test")
|
|
500
|
-
body =
|
|
720
|
+
body = yaml_load(response.body)
|
|
501
721
|
}.to_not raise_error
|
|
502
722
|
|
|
503
723
|
expect(body.request_method).to be == "GET"
|
|
@@ -516,13 +736,13 @@ describe Patron::Session do
|
|
|
516
736
|
|
|
517
737
|
it "should serialize query params and append them to the url" do
|
|
518
738
|
response = @session.request(:get, "/test", {}, :query => {:foo => "bar"})
|
|
519
|
-
request =
|
|
739
|
+
request = yaml_load(response.body)
|
|
520
740
|
expect(request.path + '?' + request.query_string).to be == "/test?foo=bar"
|
|
521
741
|
end
|
|
522
742
|
|
|
523
743
|
it "should merge parameters in the :query option with pre-existing query parameters" do
|
|
524
744
|
response = @session.request(:get, "/test?foo=bar", {}, :query => {:baz => "quux"})
|
|
525
|
-
request =
|
|
745
|
+
request = yaml_load(response.body)
|
|
526
746
|
expect(request.path + '?' + request.query_string).to be == "/test?foo=bar&baz=quux"
|
|
527
747
|
end
|
|
528
748
|
|