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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +83 -49
  4. data/ext/patron/extconf.rb +54 -23
  5. data/ext/patron/session_ext.c +1044 -923
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +21 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -144
  10. data/lib/patron/response.rb +28 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +114 -108
  13. data/lib/patron/util.rb +14 -12
  14. data/lib/patron/version.rb +3 -1
  15. data/lib/patron.rb +9 -15
  16. metadata +12 -58
  17. data/.autotest +0 -15
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -79
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -0
  22. data/.travis.yml +0 -13
  23. data/.yardopts +0 -1
  24. data/Gemfile +0 -18
  25. data/Rakefile +0 -31
  26. data/ext/patron/.gitignore +0 -4
  27. data/ext/patron/membuffer.c +0 -85
  28. data/ext/patron/membuffer.h +0 -78
  29. data/ext/patron/sglib.h +0 -1952
  30. data/lib/patron/proxy_type.rb +0 -11
  31. data/patron.gemspec +0 -39
  32. data/pic.png +0 -0
  33. data/script/console +0 -11
  34. data/script/generate_client_certs +0 -65
  35. data/script/test_server +0 -7
  36. data/spec/header_parser_spec.rb +0 -101
  37. data/spec/patron_spec.rb +0 -28
  38. data/spec/request_spec.rb +0 -89
  39. data/spec/response_spec.rb +0 -145
  40. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  41. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  42. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  43. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  44. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  45. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  46. data/spec/session_spec.rb +0 -865
  47. data/spec/session_ssl_spec.rb +0 -431
  48. data/spec/spec_helper.rb +0 -40
  49. data/spec/support/certs/cacert.pem +0 -36
  50. data/spec/support/certs/client.p12 +0 -0
  51. data/spec/support/certs/client_ca.pem +0 -20
  52. data/spec/support/certs/client_cert.pem +0 -20
  53. data/spec/support/certs/client_key.pem +0 -30
  54. data/spec/support/certs/privkey.pem +0 -51
  55. data/spec/support/certs/rogue_client.p12 +0 -0
  56. data/spec/support/config.ru +0 -153
  57. data/spec/support/fork.rb +0 -70
  58. data/spec/support/raw_request_capture.rb +0 -93
  59. data/spec/support/test_server.rb +0 -29
  60. data/spec/support/valgrind.supp +0 -2
  61. data/spec/util_spec.rb +0 -71
data/spec/session_spec.rb DELETED
@@ -1,865 +0,0 @@
1
- require File.expand_path("./spec") + '/spec_helper.rb'
2
- require 'webrick'
3
- require 'yaml'
4
- require 'base64'
5
- require 'fileutils'
6
- require 'securerandom'
7
- require 'timeout'
8
-
9
- describe Patron::Session do
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
-
19
- before(:each) do
20
- @session = Patron::Session.new
21
- @session.base_url = "http://localhost:9001"
22
- end
23
-
24
- context 'when trying a non-HTTP(s) URL' do
25
- forbidden_protos = %w( smb tftp imap smtp telnet dict ftp sftp scp file gopher )
26
- forbidden_protos.each do |forbidden_proto|
27
- it "should deny a #{forbidden_proto.upcase} request" do
28
- expect {
29
- @session.base_url = '%s://localhost' % forbidden_proto
30
- @session.get('')
31
- }.to raise_error(Patron::UnsupportedProtocol)
32
- end
33
- end
34
- end
35
-
36
- it 'does not follow a redirect to a non-HTTP/HTTPS URL' do
37
- # The "/evil-redirect" servlet tries to do a redirect to SMTP,
38
- # which can lead to exploits. By default, libCURL will just follow
39
- # that redirect.
40
- expect {
41
- @session.get('/evil-redirect')
42
- }.to raise_error(Patron::UnsupportedProtocol)
43
- end
44
-
45
- it "should work when forcing ipv4" do
46
- @session.force_ipv4 = true
47
- expect { @session.get("/test") }.to_not raise_error
48
- end
49
-
50
- describe '.escape and #escape' do
51
- it 'makes escape() and unescape() available on the class' do
52
- string = "foo~bar baz/"
53
- escaped = described_class.escape(string)
54
- unescaped = described_class.unescape(escaped)
55
- expect(unescaped).to be == string
56
- end
57
-
58
- it "should escape and unescape strings symetrically" do
59
- string = "foo~bar baz/"
60
- escaped = @session.escape(string)
61
- unescaped = @session.unescape(escaped)
62
- expect(unescaped).to be == string
63
- end
64
-
65
- it "should make e and unescape strings symetrically" do
66
- string = "foo~bar baz/"
67
- escaped = @session.escape(string)
68
- unescaped = @session.unescape(escaped)
69
- expect(unescaped).to be == string
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
81
- end
82
-
83
- it "should raise an error when passed an invalid action" do
84
- expect { @session.request(:bogus, "/test", {}) }.to raise_error(ArgumentError)
85
- end
86
-
87
- it "should raise an error when no URL is provided" do
88
- @session.base_url = nil
89
- expect {@session.get(nil)}.to raise_error(ArgumentError)
90
- end
91
-
92
- it "should retrieve a url with :get" do
93
- response = @session.get("/test")
94
- body = yaml_load(response.body)
95
- expect(body.request_method).to be == "GET"
96
- end
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
-
111
- it "should use full base url" do
112
- @session.base_url = "http://localhost:9001/api/v1"
113
- response = @session.get("/test")
114
- expect(response.url).to be == "http://localhost:9001/api/v1/test"
115
- end
116
-
117
- it 'should ignore #base_url when a full URL is provided' do
118
- @session.base_url = "http://example.com:123"
119
- expect { @session.get("http://localhost:9001/test") }.to_not raise_error(URI::InvalidURIError)
120
- end
121
-
122
- it "should download content with :get and a file path" do
123
- tf = Tempfile.new
124
- tf.close
125
- tmpfile = tf.path
126
-
127
- response = @session.get_file "/test", tmpfile
128
- expect(response.body).to be_nil
129
- body = yaml_load(File.open(tmpfile).read)
130
- expect(body.request_method).to be == "GET"
131
- end
132
-
133
- it "should download correctly(md5 ok) with get_file" do
134
- tf = Tempfile.new
135
- tf.close
136
- tmpfile = tf.path
137
-
138
- response = @session.get_file "/picture", tmpfile
139
- expect(response.body).to be_nil
140
- expect(File.size(File.join(File.dirname(__FILE__),"../pic.png"))).to be == File.size(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)
151
- end
152
-
153
- it "downloads a very large file" do
154
- tf = Tempfile.new
155
- tf.close
156
- tmpfile = tf.path
157
-
158
- @session.get_file "/very-large", tmpfile
159
- expect(File.size(tmpfile)).to eq(15 * 1024 * 1024)
160
- end
161
-
162
- it "raises an exception if the download body limit is exceeded when using direct-to-file download" do
163
- tf = Tempfile.new
164
- tf.close
165
- tmpfile = tf.path
166
-
167
- @session.download_byte_limit = 1024
168
- @session.buffer_size = 1024
169
- expect {
170
- @session.get_file "/very-large", tmpfile
171
- }.to raise_error(Patron::Error)
172
- # On Ruby 2.x Patron::Aborted takes precedence, but
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)
185
- end
186
-
187
- it "raises an exception if the download body limit is exceeded when using download-to-memory" do
188
- @session.download_byte_limit = 1024
189
- @session.buffer_size = 1024
190
- expect {
191
- @session.get "/very-large"
192
- }.to raise_error(Patron::Error)
193
- # On Ruby 2.x Patron::Aborted takes precedence, but
194
- # on 1.9 it will be Patron::PartialFileError
195
- end
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
-
205
- it "should not send the user-agent if it has been deleted from headers" do
206
- @session.headers.delete 'User-Agent'
207
- response = @session.get("/test")
208
- body = yaml_load(response.body)
209
- expect(body.header["user-agent"]).to be_nil
210
- end
211
-
212
- it "should set the default User-agent" do
213
- response = @session.get("/test")
214
- body = yaml_load(response.body)
215
- expect(body.header["user-agent"]).to be == [Patron.user_agent_string]
216
- end
217
-
218
- it "should include custom headers in a request" do
219
- response = @session.get("/test", {"User-Agent" => "PatronTest"})
220
- body = yaml_load(response.body)
221
- expect(body.header["user-agent"]).to be == ["PatronTest"]
222
- end
223
-
224
- it "should include default headers in a request, if they were defined" do
225
- @session.headers = {"User-Agent" => "PatronTest"}
226
- response = @session.get("/test")
227
- body = yaml_load(response.body)
228
- expect(body.header["user-agent"]).to be == ["PatronTest"]
229
- end
230
-
231
- it "should merge custom headers with session headers" do
232
- @session.headers["X-Test"] = "Testing"
233
- response = @session.get("/test", {"User-Agent" => "PatronTest"})
234
- body = yaml_load(response.body)
235
- expect(body.header["user-agent"]).to be == ["PatronTest"]
236
- expect(body.header["x-test"]).to be == ["Testing"]
237
- end
238
-
239
- it "accepts the DNS cache timeout option" do
240
- @session.dns_cache_timeout = 60
241
- @session.get("/")
242
- end
243
-
244
- it "should raise an exception on timeout" do
245
- @session.timeout = 1
246
- expect {@session.get("/timeout?millis=1100")}.to raise_error(Patron::TimeoutError)
247
- end
248
-
249
- it "should raise an exception on a sub-second timeout" do
250
- @session.timeout = 0.3
251
- expect {@session.get("/timeout?millis=400")}.to raise_error(Patron::TimeoutError)
252
- end
253
-
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
256
- @session.low_speed_time = 2
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
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)
266
- end
267
-
268
- it "is able to terminate the thread that is running a slow request using Thread#kill (uses the custom unblock)", :timing do
269
- t = Thread.new do
270
- session = Patron::Session.new
271
- session.timeout = 10 # Greater than the sleep duration
272
- session.base_url = "http://localhost:9001"
273
- session.get("/slow")
274
- end
275
-
276
- # Our test server starts sending the body only after 5 seconds. We should be able to abort
277
- # using a signal during that time.
278
- started = Time.now.to_f
279
- sleep 2 # Less than what it takes for the server to respond
280
- t.kill # Kill the thread forcibly
281
- t.join # wrap up the thread. If Patron is still busy there, this join call will still take another 1s.
282
-
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)
286
- end
287
-
288
- it "is able to terminate the process that is running a slow request with SIGINT", :timing do
289
- pid = Process.fork do
290
- trap('SIGINT') do
291
- exit # exit the process
292
- end
293
- session = Patron::Session.new
294
- session.timeout = 10 # Greater than the sleep duration
295
- session.base_url = "http://localhost:9001"
296
- session.get("/slow")
297
- end
298
-
299
- # Our test server starts sending the body only after 5 seconds. We should be able to abort
300
- # using a signal during that time.
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)
308
- end
309
-
310
- it "receives progress callbacks" do
311
- session = Patron::Session.new
312
- session.timeout = 10 # # Greater than the /slow respond time (5 seconds)
313
- session.base_url = "http://localhost:9001"
314
- callback_args = []
315
- session.progress_callback = Proc.new {|dltotal, dlnow, ultotal, ulnow|
316
- callback_args << [dltotal, dlnow, ultotal, ulnow]
317
- }
318
- session.get("/slow")
319
- expect(callback_args).not_to be_empty
320
-
321
- # Make sure that the callback setting was not reset.
322
- callback_args = []
323
- session.get("/slow")
324
- expect(callback_args).not_to be_empty
325
- end
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
-
385
- it "should follow redirects by default" do
386
- @session.max_redirects = 1
387
- response = @session.get("/redirect")
388
- body = yaml_load(response.body)
389
- expect(response.status).to be == 200
390
- expect(body.path).to be == "/test"
391
- end
392
-
393
- it "should not keep the Location header from the redirecting response" do
394
- @session.max_redirects = 1
395
- response = @session.get("/redirect")
396
- expect(response.headers['Location']).to be_nil
397
- end
398
-
399
- it "should include redirect count in response" do
400
- @session.max_redirects = 1
401
- response = @session.get("/redirect")
402
- expect(response.redirect_count).to be == 1
403
- end
404
-
405
- it "should not follow redirects when configured to do so" do
406
- @session.max_redirects = 0
407
- response = @session.get("/redirect")
408
- expect(response.status).to be == 301
409
- expect(response.body).to be_empty
410
- end
411
-
412
- it "should retrieve URL metadata with :head" do
413
- response = @session.head("/test")
414
- expect(response.status).to be == 200
415
- expect(response.body).to be_empty
416
- expect(response.headers).to_not be_empty
417
- end
418
-
419
- it "should send a delete request with :delete" do
420
- response = @session.delete("/test")
421
- body = yaml_load(response.body)
422
- expect(body.request_method).to be == "DELETE"
423
- end
424
-
425
- it "should send a COPY request with :copy" do
426
- response = @session.copy("/test", "/test2")
427
- body = yaml_load(response.body)
428
- expect(body.request_method).to be == "COPY"
429
- end
430
-
431
- it "should include a Destination header in COPY requests" do
432
- response = @session.copy("/test", "/test2")
433
- body = yaml_load(response.body)
434
- expect(body.header['destination'].first).to be == "/test2"
435
- end
436
-
437
- it "should upload data with :get" do
438
- data = "upload data"
439
- response = @session.request(:get, "/test", {}, :data => data)
440
- body = yaml_load(response.body)
441
- expect(body.request_method).to be == "GET"
442
- expect(body.header['content-length']).to be == [data.size.to_s]
443
- end
444
-
445
- it "should call to_s on the data being uploaded via GET if it is not already a String" do
446
- data = 12345
447
- response = @session.request(:get, "/test", {}, :data => data)
448
- body = yaml_load(response.body)
449
- expect(body.request_method).to be == "GET"
450
- end
451
-
452
- it "should upload data with :get" do
453
- # Sending a request body with a GET request is a technique seldom used,
454
- # but it does get used nevertheless - for instance, it is a usual
455
- # practice when interacting with an ElasticSearch cluster where
456
- # you can have very deeply going queries, which are still technically GETs
457
- data = Random.new.bytes(1024 * 24)
458
- response = @session.request(:get, "/test", {}, :data => data)
459
- body = yaml_load(response.body)
460
- expect(body.request_method).to be == "GET"
461
- expect(body.header['content-length']).to be == [data.size.to_s]
462
- end
463
-
464
- it "should upload data with :put" do
465
- data = Random.new.bytes(1024 * 24)
466
- response = @session.put("/test", data)
467
- body = yaml_load(response.body)
468
- expect(body.request_method).to be == "PUT"
469
- expect(body.header['content-length']).to be == [data.size.to_s]
470
- end
471
-
472
- it "should upload a Tempfile with :put" do
473
- data = Tempfile.new 'data-buffer'
474
- data << Random.new.bytes(1024 * 64)
475
- data.flush; data.rewind
476
-
477
- response = @session.put("/test", data, {'Expect' => ''})
478
- body = yaml_load(response.body)
479
- expect(body.request_method).to be == "PUT"
480
- expect(body.header['content-length']).to be == [data.size.to_s]
481
- end
482
-
483
- it "should upload data with :patch" do
484
- data = "upload data"
485
- response = @session.patch("/testpatch", data)
486
- body = yaml_load(response.body)
487
- expect(body.body).to eq("upload data")
488
- end
489
-
490
- it "should upload data with :delete" do
491
- data = "upload data"
492
- response = @session.request(:delete, "/test", {}, :data => data)
493
- body = yaml_load(response.body)
494
- expect(body.request_method).to be == "DELETE"
495
- expect(body.header['content-length']).to be == [data.size.to_s]
496
- end
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
-
518
- it "should raise when no data is provided to :put" do
519
- expect { @session.put("/test", nil) }.to raise_error(ArgumentError)
520
- end
521
-
522
- it "should upload a file with :put" do
523
- response = @session.put_file("/test", "LICENSE")
524
- body = yaml_load(response.body)
525
- expect(body.request_method).to be == "PUT"
526
- end
527
-
528
- it "should raise when no file is provided to :put" do
529
- expect { @session.put_file("/test", nil) }.to raise_error(ArgumentError)
530
- end
531
-
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")
546
- end
547
-
548
- it "should call to_s on the data being uploaded via POST if it is not already a String" do
549
- data = 12345
550
- response = @session.post("/testpost", data)
551
- body = yaml_load(response.body)
552
- expect(body['body']).to eq("12345")
553
- end
554
-
555
- it "should upload data with :post" do
556
- data = "upload data"
557
- response = @session.post("/test", data)
558
- body = yaml_load(response.body)
559
- expect(body.request_method).to be == "POST"
560
- expect(body.header['content-length']).to be == [data.size.to_s]
561
- end
562
-
563
- it "should POST a hash of arguments as a urlencoded form" do
564
- data = {:foo => 123, 'baz' => '++hello world++'}
565
- response = @session.post("/testpost", data)
566
- body = yaml_load(response.body)
567
- expect(body['content_type']).to be == "application/x-www-form-urlencoded"
568
- expect(body['body']).to match(/baz=%2B%2Bhello%20world%2B%2B/)
569
- expect(body['body']).to match(/foo=123/)
570
- end
571
-
572
- it "should raise when no data is provided to :post" do
573
- expect { @session.post("/test", nil) }.to raise_error(ArgumentError)
574
- end
575
-
576
- it "should upload a file with :post" do
577
- response = @session.post_file("/test", "LICENSE")
578
- body = yaml_load(response.body)
579
- expect(body.request_method).to be == "POST"
580
- end
581
-
582
- it "should upload a multipart with :post" do
583
- response = @session.post_multipart("/test", { :test_data => "123" }, { :test_file => "LICENSE" } )
584
- body = yaml_load(response.body)
585
- expect(body.request_method).to be == "POST"
586
- end
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
-
601
- it "should raise when no file is provided to :post" do
602
- expect { @session.post_file("/test", nil) }.to raise_error(ArgumentError)
603
- end
604
-
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")
618
- end
619
-
620
- it "should pass credentials as http basic auth" do
621
- @session.username = "foo"
622
- @session.password = "bar"
623
- response = @session.get("/test")
624
- body = yaml_load(response.body)
625
- expect(body.header['authorization']).to be == [encode_authz("foo", "bar")]
626
- end
627
-
628
- it "should store cookies across multiple requests" do
629
- tf = Tempfile.new('cookiejar')
630
- tf.close
631
- cookie_jar_path = tf.path
632
-
633
- @session.handle_cookies(cookie_jar_path)
634
- response = @session.get("/setcookie").body
635
-
636
- cookie_jar_contents = tf.open.read
637
- tf.unlink
638
- expect(cookie_jar_contents).not_to be_empty
639
- expect(cookie_jar_contents).to include('Netscape HTTP Cookie File')
640
- end
641
-
642
- it "should handle cookies if set" do
643
- @session.handle_cookies
644
- response = @session.get("/setcookie").body
645
- expect(yaml_load(response).header['cookie'].first).to be == "session_id=foo123"
646
- end
647
-
648
- it "should not handle cookies by default" do
649
- response = @session.get("/setcookie").body
650
- expect(yaml_load(response).header).to_not include('cookie')
651
- end
652
-
653
- it "should ignore a wrong Content-Length when asked to" do
654
- expect {
655
- @session.ignore_content_length = true
656
- @session.get("/wrongcontentlength", { 'Connection' => 'close' })
657
- }.to_not raise_error
658
- end
659
-
660
- it "should fail by default with a Content-Length too high" do
661
- expect {
662
- @session.ignore_content_length = nil
663
- @session.get("/wrongcontentlength", { 'Connection' => 'close' })
664
- }.to raise_error(Patron::PartialFileError)
665
- end
666
-
667
- it "should raise exception if cookie store is not writable or readable" do
668
- expect { @session.handle_cookies("/trash/clash/foo") }.to raise_error(ArgumentError)
669
- end
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
-
696
- it "should work with multiple threads" do
697
- threads = []
698
- 3.times do
699
- threads << Thread.new do
700
- session = Patron::Session.new
701
- session.base_url = "http://localhost:9001"
702
- session.post_file("/test", "LICENSE")
703
- end
704
- end
705
- threads.each {|t| t.join }
706
- end
707
-
708
- it "should limit the buffer_size" do
709
- # Buffer size is tricky to test, as it only affects the buffer size for each
710
- # read and it's not really visible at this, higher level. It's also only a
711
- # suggestion rather than a command so it may not even take affect. Currently
712
- # we just test that the response completes without any issues, it would be nice
713
- # to have a more robust test here.
714
- @session.buffer_size = 1
715
-
716
- body = nil
717
-
718
- expect {
719
- response = @session.get("/test")
720
- body = yaml_load(response.body)
721
- }.to_not raise_error
722
-
723
- expect(body.request_method).to be == "GET"
724
- end
725
-
726
- it "should automatically decompress using Content-Encoding if requested" do
727
- @session.automatic_content_encoding = true
728
- response = @session.get('/gzip-compressed')
729
-
730
- expect(response.headers['Content-Length']).to eq('125')
731
-
732
- body = response.body
733
- expect(body).to match(/Some highly compressible data/)
734
- expect(body.bytesize).to eq(29696)
735
- end
736
-
737
- it "should serialize query params and append them to the url" do
738
- response = @session.request(:get, "/test", {}, :query => {:foo => "bar"})
739
- request = yaml_load(response.body)
740
- expect(request.path + '?' + request.query_string).to be == "/test?foo=bar"
741
- end
742
-
743
- it "should merge parameters in the :query option with pre-existing query parameters" do
744
- response = @session.request(:get, "/test?foo=bar", {}, :query => {:baz => "quux"})
745
- request = yaml_load(response.body)
746
- expect(request.path + '?' + request.query_string).to be == "/test?foo=bar&baz=quux"
747
- end
748
-
749
- def encode_authz(user, passwd)
750
- "Basic " + Base64.encode64("#{user}:#{passwd}").strip
751
- end
752
-
753
- describe 'when using a subclass with a custom Response' do
754
-
755
- class CustomResponse
756
- attr_reader :constructor_args
757
- def initialize(*constructor_args)
758
- @constructor_args = constructor_args
759
- end
760
- end
761
-
762
- class CustomizedSession < Patron::Session
763
- def response_class
764
- CustomResponse
765
- end
766
- end
767
-
768
- it 'instantiates the customized response object' do
769
- @session = CustomizedSession.new
770
- @session.base_url = "http://localhost:9001"
771
- response = @session.request(:get, "/test", {}, :query => {:foo => "bar"})
772
-
773
- expect(response).to be_kind_of(CustomResponse)
774
- expect(response.constructor_args.length).to eq(6)
775
- end
776
- end
777
-
778
- describe 'when instantiating with hash arguments' do
779
-
780
- let(:args) { {
781
- :timeout => 10,
782
- :dns_cache_timeout => 10,
783
- :base_url => 'http://localhost:9001',
784
- :headers => {'User-Agent' => 'myapp/1.0'}
785
- } }
786
-
787
- let(:session) { Patron::Session.new(args) }
788
-
789
- it 'sets the base_url' do
790
- expect(session.base_url).to be == args[:base_url]
791
- end
792
-
793
- it 'sets timeout' do
794
- expect(session.timeout).to be == args[:timeout]
795
- end
796
-
797
- it 'sets DNS cache timeout' do
798
- expect(session.dns_cache_timeout).to be == args[:dns_cache_timeout]
799
- end
800
-
801
- it 'sets headers' do
802
- expect(session.headers).to be == args[:headers]
803
- end
804
-
805
- context 'when given an incorrect accessor name' do
806
- let(:args) { { :not_a_real_accessor => 'http://localhost:9001' }}
807
- it 'raises no method error' do
808
- expect { session }.to raise_error NoMethodError
809
- end
810
- end
811
-
812
- end
813
-
814
- describe 'when instantiating with a block' do
815
- args = {
816
- :timeout => 10,
817
- :base_url => 'http://localhost:9001',
818
- :headers => {'User-Agent' => 'myapp/1.0'}
819
- }
820
-
821
- session = Patron::Session.new do |patron|
822
- patron.timeout = args[:timeout]
823
- patron.base_url = args[:base_url]
824
- patron.headers = args[:headers]
825
- end
826
-
827
- it 'sets the base_url' do
828
- expect(session.base_url).to be == args[:base_url]
829
- end
830
-
831
- it 'sets timeout' do
832
- expect(session.timeout).to be == args[:timeout]
833
- end
834
-
835
- it 'sets headers' do
836
- expect(session.headers).to be == args[:headers]
837
- end
838
-
839
- context 'when given an incorrect accessor name' do
840
- it 'raises no method error' do
841
- expect {
842
- Patron::Session.new do |patron|
843
- patron.timeoutttt = args[:timeout]
844
- end
845
- }.to raise_error NoMethodError
846
- end
847
- end
848
- end
849
-
850
- # ------------------------------------------------------------------------
851
- describe 'when debug is enabled' do
852
- it 'it should not clobber stderr' do
853
- rdev = STDERR.stat.rdev
854
-
855
- retval = @session.enable_debug
856
- expect(retval).to eq(@session)
857
- expect(STDERR.stat.rdev).to be == rdev
858
-
859
- retval = @session.enable_debug
860
- expect(retval).to eq(@session)
861
- expect(STDERR.stat.rdev).to be == rdev
862
- end
863
- end
864
-
865
- end