http 3.1.0 → 5.3.1

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 (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
@@ -1,5 +1,8 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "cgi"
5
+ require "logger"
3
6
 
4
7
  require "support/http_handling_shared"
5
8
  require "support/dummy_server"
@@ -8,39 +11,50 @@ require "support/ssl_helper"
8
11
  RSpec.describe HTTP::Client do
9
12
  run_server(:dummy) { DummyServer.new }
10
13
 
11
- StubbedClient = Class.new(HTTP::Client) do
12
- def perform(request, options)
13
- stubs.fetch(request.uri) { super(request, options) }
14
- end
14
+ before do
15
+ stubbed_client = Class.new(HTTP::Client) do
16
+ def perform(request, options)
17
+ stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
18
+ stubbed ? stubbed.call(request) : super(request, options)
19
+ end
15
20
 
16
- def stubs
17
- @stubs ||= {}
18
- end
21
+ def stubs
22
+ @stubs ||= {}
23
+ end
24
+
25
+ def stub(stubs)
26
+ @stubs = stubs.transform_keys do |k|
27
+ HTTP::URI::NORMALIZER.call(k).to_s
28
+ end
19
29
 
20
- def stub(stubs)
21
- @stubs = stubs.each_with_object({}) do |(k, v), o|
22
- o[HTTP::URI.parse k] = v
30
+ self
23
31
  end
32
+ end
24
33
 
25
- self
34
+ def redirect_response(location, status = 302)
35
+ lambda do |request|
36
+ HTTP::Response.new(
37
+ :status => status,
38
+ :version => "1.1",
39
+ :headers => {"Location" => location},
40
+ :body => "",
41
+ :request => request
42
+ )
43
+ end
26
44
  end
27
- end
28
45
 
29
- def redirect_response(location, status = 302)
30
- HTTP::Response.new(
31
- :status => status,
32
- :version => "1.1",
33
- :headers => {"Location" => location},
34
- :body => ""
35
- )
36
- end
46
+ def simple_response(body, status = 200)
47
+ lambda do |request|
48
+ HTTP::Response.new(
49
+ :status => status,
50
+ :version => "1.1",
51
+ :body => body,
52
+ :request => request
53
+ )
54
+ end
55
+ end
37
56
 
38
- def simple_response(body, status = 200)
39
- HTTP::Response.new(
40
- :status => status,
41
- :version => "1.1",
42
- :body => body
43
- )
57
+ stub_const("StubbedClient", stubbed_client)
44
58
  end
45
59
 
46
60
  describe "following redirects" do
@@ -98,13 +112,45 @@ RSpec.describe HTTP::Client do
98
112
  end
99
113
 
100
114
  it "works like a charm in real world" do
101
- url = "http://git.io/jNeY"
102
- client = HTTP.follow
103
- expect(client.get(url).to_s).to include "support for non-ascii URIs"
115
+ expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
116
+ to include("url" => "https://httpbin.org/anything/könig")
104
117
  end
105
118
  end
106
119
  end
107
120
 
121
+ describe "following redirects with logging" do
122
+ let(:logger) do
123
+ logger = Logger.new(logdev)
124
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
125
+ logger.level = Logger::INFO
126
+ logger
127
+ end
128
+
129
+ let(:logdev) { StringIO.new }
130
+
131
+ it "logs all requests" do
132
+ client = StubbedClient.new(:follow => true, :features => { :logging => { :logger => logger } }).stub(
133
+ "http://example.com/" => redirect_response("/1"),
134
+ "http://example.com/1" => redirect_response("/2"),
135
+ "http://example.com/2" => redirect_response("/3"),
136
+ "http://example.com/3" => simple_response("OK")
137
+ )
138
+
139
+ expect { client.get("http://example.com/") }.not_to raise_error
140
+
141
+ expect(logdev.string).to eq <<~OUTPUT
142
+ ** INFO **
143
+ > GET http://example.com/
144
+ ** INFO **
145
+ > GET http://example.com/1
146
+ ** INFO **
147
+ > GET http://example.com/2
148
+ ** INFO **
149
+ > GET http://example.com/3
150
+ OUTPUT
151
+ end
152
+ end
153
+
108
154
  describe "parsing params" do
109
155
  let(:client) { HTTP::Client.new }
110
156
  before { allow(client).to receive :perform }
@@ -190,6 +236,22 @@ RSpec.describe HTTP::Client do
190
236
 
191
237
  client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
192
238
  end
239
+
240
+ context "when passing an HTTP::FormData object directly" do
241
+ it "creates url encoded form data object" do
242
+ client = HTTP::Client.new
243
+ form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
244
+
245
+ allow(client).to receive(:perform)
246
+
247
+ expect(HTTP::Request).to receive(:new) do |opts|
248
+ expect(opts[:body]).to be form_data
249
+ expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
250
+ end
251
+
252
+ client.get("http://example.com/", :form => form_data)
253
+ end
254
+ end
193
255
  end
194
256
 
195
257
  describe "passing json" do
@@ -199,6 +261,7 @@ RSpec.describe HTTP::Client do
199
261
 
200
262
  expect(HTTP::Request).to receive(:new) do |opts|
201
263
  expect(opts[:body]).to eq '{"foo":"bar"}'
264
+ expect(opts[:headers]["Content-Type"]).to eq "application/json; charset=utf-8"
202
265
  end
203
266
 
204
267
  client.get("http://example.com/", :json => {:foo => :bar})
@@ -213,9 +276,9 @@ RSpec.describe HTTP::Client do
213
276
  end
214
277
 
215
278
  it "works like a charm in real world" do
216
- url = "https://github.com/httprb/http.rb/pull/197/ö無"
217
- client = HTTP.follow
218
- expect(client.get(url).to_s).to include "support for non-ascii URIs"
279
+ url = "https://httpbin.org/anything/ö無"
280
+
281
+ expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
219
282
  end
220
283
  end
221
284
 
@@ -234,7 +297,7 @@ RSpec.describe HTTP::Client do
234
297
 
235
298
  context "when :auto_deflate was specified" do
236
299
  let(:headers) { {"Content-Length" => "12"} }
237
- let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
300
+ let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}}, :body => "foo" }
238
301
 
239
302
  it "deletes Content-Length header" do
240
303
  expect(client).to receive(:perform) do |req, _|
@@ -251,6 +314,87 @@ RSpec.describe HTTP::Client do
251
314
 
252
315
  client.request(:get, "http://example.com/")
253
316
  end
317
+
318
+ context "and there is no body" do
319
+ let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
320
+
321
+ it "doesn't set Content-Encoding header" do
322
+ expect(client).to receive(:perform) do |req, _|
323
+ expect(req.headers).not_to include "Content-Encoding"
324
+ end
325
+
326
+ client.request(:get, "http://example.com/")
327
+ end
328
+ end
329
+ end
330
+
331
+ context "Feature" do
332
+ let(:feature_class) do
333
+ Class.new(HTTP::Feature) do
334
+ attr_reader :captured_request, :captured_response, :captured_error
335
+
336
+ def wrap_request(request)
337
+ @captured_request = request
338
+ end
339
+
340
+ def wrap_response(response)
341
+ @captured_response = response
342
+ end
343
+
344
+ def on_error(request, error)
345
+ @captured_request = request
346
+ @captured_error = error
347
+ end
348
+ end
349
+ end
350
+
351
+ it "is given a chance to wrap the Request" do
352
+ feature_instance = feature_class.new
353
+
354
+ response = client.use(:test_feature => feature_instance).
355
+ request(:get, dummy.endpoint)
356
+
357
+ expect(response.code).to eq(200)
358
+ expect(feature_instance.captured_request.verb).to eq(:get)
359
+ expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
360
+ end
361
+
362
+ it "is given a chance to wrap the Response" do
363
+ feature_instance = feature_class.new
364
+
365
+ response = client.use(:test_feature => feature_instance).
366
+ request(:get, dummy.endpoint)
367
+
368
+ expect(feature_instance.captured_response).to eq(response)
369
+ end
370
+
371
+ it "is given a chance to handle an error" do
372
+ sleep_url = "#{dummy.endpoint}/sleep"
373
+ feature_instance = feature_class.new
374
+
375
+ expect do
376
+ client.use(:test_feature => feature_instance).
377
+ timeout(0.2).
378
+ request(:post, sleep_url)
379
+ end.to raise_error(HTTP::TimeoutError)
380
+
381
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
382
+ expect(feature_instance.captured_request.verb).to eq(:post)
383
+ expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
384
+ end
385
+
386
+ it "is given a chance to handle a connection timeout error" do
387
+ allow(TCPSocket).to receive(:open) { sleep 1 }
388
+ sleep_url = "#{dummy.endpoint}/sleep"
389
+ feature_instance = feature_class.new
390
+
391
+ expect do
392
+ client.use(:test_feature => feature_instance).
393
+ timeout(0.001).
394
+ request(:post, sleep_url)
395
+ end.to raise_error(HTTP::ConnectTimeoutError)
396
+ expect(feature_instance.captured_error).to be_a(HTTP::ConnectTimeoutError)
397
+ end
254
398
  end
255
399
  end
256
400
 
@@ -261,7 +405,8 @@ RSpec.describe HTTP::Client do
261
405
  let(:client) { described_class.new(options.merge(extra_options)) }
262
406
  end
263
407
 
264
- describe "working with SSL" do
408
+ # TODO: https://github.com/httprb/http/issues/627
409
+ xdescribe "working with SSL" do
265
410
  run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
266
411
 
267
412
  let(:extra_options) { {} }
@@ -304,6 +449,14 @@ RSpec.describe HTTP::Client do
304
449
  client.get(dummy.endpoint).to_s
305
450
  end
306
451
 
452
+ it "provides access to the Request from the Response" do
453
+ unique_value = "20190424"
454
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
455
+
456
+ expect(response.request).to be_a(HTTP::Request)
457
+ expect(response.request.headers["X-Value"]).to eq(unique_value)
458
+ end
459
+
307
460
  context "with HEAD request" do
308
461
  it "does not iterates through body" do
309
462
  expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
@@ -394,7 +547,7 @@ RSpec.describe HTTP::Client do
394
547
  BODY
395
548
  end
396
549
 
397
- it "raises HTTP::ConnectionError" do
550
+ xit "raises HTTP::ConnectionError" do
398
551
  expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
399
552
  end
400
553
  end
@@ -3,16 +3,36 @@
3
3
  RSpec.describe HTTP::Connection do
4
4
  let(:req) do
5
5
  HTTP::Request.new(
6
- :verb => :get,
7
- :uri => "http://example.com/",
8
- :headers => {}
6
+ :verb => :get,
7
+ :uri => "http://example.com/",
8
+ :headers => {}
9
9
  )
10
10
  end
11
- let(:socket) { double(:connect => nil) }
11
+ let(:socket) { double(:connect => nil, :close => nil) }
12
12
  let(:timeout_class) { double(:new => socket) }
13
13
  let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) }
14
14
  let(:connection) { HTTP::Connection.new(req, opts) }
15
15
 
16
+ describe "#initialize times out" do
17
+ let(:req) do
18
+ HTTP::Request.new(
19
+ :verb => :get,
20
+ :uri => "https://example.com/",
21
+ :headers => {}
22
+ )
23
+ end
24
+
25
+ before do
26
+ expect(socket).to receive(:start_tls).and_raise(HTTP::TimeoutError)
27
+ expect(socket).to receive(:closed?) { false }
28
+ expect(socket).to receive(:close)
29
+ end
30
+
31
+ it "closes the connection" do
32
+ expect { connection }.to raise_error(HTTP::TimeoutError)
33
+ end
34
+ end
35
+
16
36
  describe "#read_headers!" do
17
37
  before do
18
38
  connection.instance_variable_set(:@pending_response, true)
@@ -20,14 +40,17 @@ RSpec.describe HTTP::Connection do
20
40
  <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
21
41
  | HTTP/1.1 200 OK
22
42
  | Content-Type: text
43
+ | foo_bar: 123
23
44
  |
24
45
  RESPONSE
25
46
  end
26
47
  end
27
48
 
28
- it "reads data in parts" do
49
+ it "populates headers collection, preserving casing" do
29
50
  connection.read_headers!
30
- expect(connection.headers).to eq("Content-Type" => "text")
51
+ expect(connection.headers).to eq("Content-Type" => "text", "foo_bar" => "123")
52
+ expect(connection.headers["Foo-Bar"]).to eq("123")
53
+ expect(connection.headers["foo_bar"]).to eq("123")
31
54
  end
32
55
  end
33
56
 
@@ -55,9 +78,11 @@ RSpec.describe HTTP::Connection do
55
78
  connection.read_headers!
56
79
  buffer = String.new
57
80
  while (s = connection.readpartial(3))
81
+ expect(connection.finished_request?).to be false if s != ""
58
82
  buffer << s
59
83
  end
60
84
  expect(buffer).to eq "1234567890"
85
+ expect(connection.finished_request?).to be true
61
86
  end
62
87
  end
63
88
  end
@@ -1,68 +1,85 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe HTTP::Features::AutoInflate do
4
- subject { HTTP::Features::AutoInflate.new }
4
+ subject(:feature) { HTTP::Features::AutoInflate.new }
5
+
5
6
  let(:connection) { double }
6
- let(:headers) { {} }
7
+ let(:headers) { {} }
8
+
7
9
  let(:response) do
8
10
  HTTP::Response.new(
9
11
  :version => "1.1",
10
12
  :status => 200,
11
13
  :headers => headers,
12
- :connection => connection
14
+ :connection => connection,
15
+ :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
13
16
  )
14
17
  end
15
18
 
16
- describe "stream_for" do
19
+ describe "#wrap_response" do
20
+ subject(:result) { feature.wrap_response(response) }
21
+
17
22
  context "when there is no Content-Encoding header" do
18
- it "returns connection" do
19
- stream = subject.stream_for(connection, response)
20
- expect(stream).to eq(connection)
23
+ it "returns original request" do
24
+ expect(result).to be response
21
25
  end
22
26
  end
23
27
 
24
28
  context "for identity Content-Encoding header" do
25
- let(:headers) { {:content_encoding => "not-supported"} }
29
+ let(:headers) { {:content_encoding => "identity"} }
26
30
 
27
- it "returns connection" do
28
- stream = subject.stream_for(connection, response)
29
- expect(stream).to eq(connection)
31
+ it "returns original request" do
32
+ expect(result).to be response
30
33
  end
31
34
  end
32
35
 
33
36
  context "for unknown Content-Encoding header" do
34
37
  let(:headers) { {:content_encoding => "not-supported"} }
35
38
 
36
- it "returns connection" do
37
- stream = subject.stream_for(connection, response)
38
- expect(stream).to eq(connection)
39
+ it "returns original request" do
40
+ expect(result).to be response
39
41
  end
40
42
  end
41
43
 
42
44
  context "for deflate Content-Encoding header" do
43
45
  let(:headers) { {:content_encoding => "deflate"} }
44
46
 
45
- it "returns HTTP::Response::Inflater instance - connection wrapper" do
46
- stream = subject.stream_for(connection, response)
47
- expect(stream).to be_instance_of HTTP::Response::Inflater
47
+ it "returns a HTTP::Response wrapping the inflated response body" do
48
+ expect(result.body).to be_instance_of HTTP::Response::Body
48
49
  end
49
50
  end
50
51
 
51
52
  context "for gzip Content-Encoding header" do
52
53
  let(:headers) { {:content_encoding => "gzip"} }
53
54
 
54
- it "returns HTTP::Response::Inflater instance - connection wrapper" do
55
- stream = subject.stream_for(connection, response)
56
- expect(stream).to be_instance_of HTTP::Response::Inflater
55
+ it "returns a HTTP::Response wrapping the inflated response body" do
56
+ expect(result.body).to be_instance_of HTTP::Response::Body
57
57
  end
58
58
  end
59
59
 
60
60
  context "for x-gzip Content-Encoding header" do
61
61
  let(:headers) { {:content_encoding => "x-gzip"} }
62
62
 
63
- it "returns HTTP::Response::Inflater instance - connection wrapper" do
64
- stream = subject.stream_for(connection, response)
65
- expect(stream).to be_instance_of HTTP::Response::Inflater
63
+ it "returns a HTTP::Response wrapping the inflated response body" do
64
+ expect(result.body).to be_instance_of HTTP::Response::Body
65
+ end
66
+ end
67
+
68
+ # TODO(ixti): We should refactor API to either make uri non-optional,
69
+ # or add reference to request into response object (better).
70
+ context "when response has uri" do
71
+ let(:response) do
72
+ HTTP::Response.new(
73
+ :version => "1.1",
74
+ :status => 200,
75
+ :headers => {:content_encoding => "gzip"},
76
+ :connection => connection,
77
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
78
+ )
79
+ end
80
+
81
+ it "preserves uri in wrapped response" do
82
+ expect(result.uri).to eq HTTP::URI.parse("https://example.com")
66
83
  end
67
84
  end
68
85
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Features::Instrumentation do
4
+ subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
5
+
6
+ let(:instrumenter) { TestInstrumenter.new }
7
+
8
+ before do
9
+ test_instrumenter = Class.new(HTTP::Features::Instrumentation::NullInstrumenter) do
10
+ attr_reader :output
11
+
12
+ def initialize
13
+ @output = {}
14
+ end
15
+
16
+ def start(_name, payload)
17
+ output[:start] = payload
18
+ end
19
+
20
+ def finish(_name, payload)
21
+ output[:finish] = payload
22
+ end
23
+ end
24
+
25
+ stub_const("TestInstrumenter", test_instrumenter)
26
+ end
27
+
28
+ describe "logging the request" do
29
+ let(:request) do
30
+ HTTP::Request.new(
31
+ :verb => :post,
32
+ :uri => "https://example.com/",
33
+ :headers => {:accept => "application/json"},
34
+ :body => '{"hello": "world!"}'
35
+ )
36
+ end
37
+
38
+ it "should log the request" do
39
+ feature.wrap_request(request)
40
+
41
+ expect(instrumenter.output[:start]).to eq(:request => request)
42
+ end
43
+ end
44
+
45
+ describe "logging the response" do
46
+ let(:response) do
47
+ HTTP::Response.new(
48
+ :version => "1.1",
49
+ :status => 200,
50
+ :headers => {:content_type => "application/json"},
51
+ :body => '{"success": true}',
52
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
53
+ )
54
+ end
55
+
56
+ it "should log the response" do
57
+ feature.wrap_response(response)
58
+
59
+ expect(instrumenter.output[:finish]).to eq(:response => response)
60
+ end
61
+ end
62
+
63
+ describe "logging errors" do
64
+ let(:request) do
65
+ HTTP::Request.new(
66
+ :verb => :post,
67
+ :uri => "https://example.com/",
68
+ :headers => {:accept => "application/json"},
69
+ :body => '{"hello": "world!"}'
70
+ )
71
+ end
72
+
73
+ let(:error) { HTTP::TimeoutError.new }
74
+
75
+ it "should log the error" do
76
+ feature.on_error(request, error)
77
+
78
+ expect(instrumenter.output[:finish]).to eq(:request => request, :error => error)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ RSpec.describe HTTP::Features::Logging do
6
+ subject(:feature) do
7
+ logger = Logger.new(logdev)
8
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
9
+
10
+ described_class.new(:logger => logger)
11
+ end
12
+
13
+ let(:logdev) { StringIO.new }
14
+
15
+ describe "logging the request" do
16
+ let(:request) do
17
+ HTTP::Request.new(
18
+ :verb => :post,
19
+ :uri => "https://example.com/",
20
+ :headers => {:accept => "application/json"},
21
+ :body => '{"hello": "world!"}'
22
+ )
23
+ end
24
+
25
+ it "should log the request" do
26
+ feature.wrap_request(request)
27
+
28
+ expect(logdev.string).to eq <<~OUTPUT
29
+ ** INFO **
30
+ > POST https://example.com/
31
+ ** DEBUG **
32
+ Accept: application/json
33
+ Host: example.com
34
+ User-Agent: http.rb/#{HTTP::VERSION}
35
+
36
+ {"hello": "world!"}
37
+ OUTPUT
38
+ end
39
+ end
40
+
41
+ describe "logging the response" do
42
+ let(:response) do
43
+ HTTP::Response.new(
44
+ :version => "1.1",
45
+ :status => 200,
46
+ :headers => {:content_type => "application/json"},
47
+ :body => '{"success": true}',
48
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
49
+ )
50
+ end
51
+
52
+ it "should log the response" do
53
+ feature.wrap_response(response)
54
+
55
+ expect(logdev.string).to eq <<~OUTPUT
56
+ ** INFO **
57
+ < 200 OK
58
+ ** DEBUG **
59
+ Content-Type: application/json
60
+
61
+ {"success": true}
62
+ OUTPUT
63
+ end
64
+ end
65
+ end