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.
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 = YAML::load(response.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
- tmpfile = "/tmp/patron_test.yaml"
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 = YAML::load_file(tmpfile)
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
- tmpfile = "/tmp/picture"
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
- FileUtils.rm tmpfile
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
- tmpfile = "/tmp/large-succeeded"
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
- tmpfile = "/tmp/large-aborted"
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
- expect(File.size(tmpfile)).to be < (1024 * 3)
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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = 40
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 = 40
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 20 seconds. We should be able to abort
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.to_i
202
- sleep 5 # Less than what it takes for the server to respond
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 15s.
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.to_i - started
207
- expect(delta_s).to be_within(2).of(5)
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 thread that is running a slow request" do
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 = 40
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 20 seconds. We should be able to abort
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.to_i
224
- sleep 5 # Less than what it takes for the server to respond
225
- Process.kill("INT", pid) # Signal ourselves...
226
- Process.wait(pid) # wrap up the process. If Patron is still busy there, this call will still take 15s.
227
- delta_s = Time.now.to_i - started
228
- expect(delta_s).to be_within(2).of(5)
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 = 40
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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 use chunked encoding when uploading a file with :put" do
372
- response = @session.put_file("/test", "LICENSE")
373
- body = YAML::load(response.body)
374
- expect(body.header['transfer-encoding'].first).to be == "chunked"
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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 = YAML::load(response.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 use chunked encoding when uploading a file with :post" do
422
- response = @session.post_file("/test", "LICENSE")
423
- body = YAML::load(response.body)
424
- expect(body.header['transfer-encoding'].first).to be == "chunked"
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 = YAML::load(response.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(YAML::load(response).header['cookie'].first).to be == "session_id=foo123"
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(YAML::load(response).header).to_not include('cookie')
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 = YAML::load(response.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 = YAML::load(response.body)
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 = YAML::load(response.body)
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