http 4.2.0 → 5.0.2

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +65 -0
  3. data/.gitignore +6 -10
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/style.yml +32 -0
  7. data/.rubocop.yml +8 -110
  8. data/.rubocop_todo.yml +192 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +168 -0
  11. data/Gemfile +18 -10
  12. data/LICENSE.txt +1 -1
  13. data/README.md +17 -20
  14. data/Rakefile +2 -10
  15. data/http.gemspec +5 -5
  16. data/lib/http/chainable.rb +23 -17
  17. data/lib/http/client.rb +52 -35
  18. data/lib/http/connection.rb +12 -8
  19. data/lib/http/content_type.rb +12 -7
  20. data/lib/http/feature.rb +3 -1
  21. data/lib/http/features/auto_deflate.rb +7 -7
  22. data/lib/http/features/auto_inflate.rb +6 -7
  23. data/lib/http/features/instrumentation.rb +1 -1
  24. data/lib/http/features/logging.rb +19 -21
  25. data/lib/http/headers.rb +50 -13
  26. data/lib/http/mime_type/adapter.rb +3 -1
  27. data/lib/http/mime_type/json.rb +1 -0
  28. data/lib/http/options.rb +6 -9
  29. data/lib/http/redirector.rb +4 -2
  30. data/lib/http/request/body.rb +1 -0
  31. data/lib/http/request/writer.rb +8 -3
  32. data/lib/http/request.rb +28 -11
  33. data/lib/http/response/body.rb +6 -4
  34. data/lib/http/response/inflater.rb +1 -1
  35. data/lib/http/response/parser.rb +75 -49
  36. data/lib/http/response/status.rb +4 -3
  37. data/lib/http/response.rb +35 -15
  38. data/lib/http/timeout/global.rb +42 -38
  39. data/lib/http/timeout/null.rb +2 -1
  40. data/lib/http/timeout/per_operation.rb +56 -58
  41. data/lib/http/uri.rb +5 -5
  42. data/lib/http/version.rb +1 -1
  43. data/spec/lib/http/client_spec.rb +173 -35
  44. data/spec/lib/http/connection_spec.rb +8 -5
  45. data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
  46. data/spec/lib/http/features/instrumentation_spec.rb +27 -21
  47. data/spec/lib/http/features/logging_spec.rb +8 -10
  48. data/spec/lib/http/headers_spec.rb +53 -18
  49. data/spec/lib/http/options/headers_spec.rb +1 -1
  50. data/spec/lib/http/options/merge_spec.rb +16 -16
  51. data/spec/lib/http/redirector_spec.rb +59 -1
  52. data/spec/lib/http/request/writer_spec.rb +25 -2
  53. data/spec/lib/http/request_spec.rb +5 -5
  54. data/spec/lib/http/response/body_spec.rb +5 -5
  55. data/spec/lib/http/response/parser_spec.rb +74 -0
  56. data/spec/lib/http/response/status_spec.rb +3 -3
  57. data/spec/lib/http/response_spec.rb +44 -3
  58. data/spec/lib/http_spec.rb +30 -3
  59. data/spec/spec_helper.rb +21 -21
  60. data/spec/support/black_hole.rb +1 -1
  61. data/spec/support/dummy_server/servlet.rb +17 -6
  62. data/spec/support/dummy_server.rb +7 -7
  63. data/spec/support/fuubar.rb +21 -0
  64. data/spec/support/http_handling_shared.rb +4 -4
  65. data/spec/support/simplecov.rb +19 -0
  66. data/spec/support/ssl_helper.rb +4 -4
  67. metadata +24 -16
  68. data/.coveralls.yml +0 -1
  69. data/.travis.yml +0 -37
@@ -1,46 +1,58 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  require "support/http_handling_shared"
5
5
  require "support/dummy_server"
6
6
  require "support/ssl_helper"
7
+ require "logger"
7
8
 
8
9
  RSpec.describe HTTP::Client do
9
10
  run_server(:dummy) { DummyServer.new }
10
11
 
11
- StubbedClient = Class.new(HTTP::Client) do
12
- def perform(request, options)
13
- stubs.fetch(request.uri) { super(request, options) }
14
- end
12
+ before do
13
+ stubbed_client = Class.new(HTTP::Client) do
14
+ def perform(request, options)
15
+ stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
16
+ stubbed ? stubbed.call(request) : super(request, options)
17
+ end
15
18
 
16
- def stubs
17
- @stubs ||= {}
18
- end
19
+ def stubs
20
+ @stubs ||= {}
21
+ end
22
+
23
+ def stub(stubs)
24
+ @stubs = stubs.transform_keys do |k|
25
+ HTTP::URI::NORMALIZER.call(k).to_s
26
+ end
19
27
 
20
- def stub(stubs)
21
- @stubs = stubs.each_with_object({}) do |(k, v), o|
22
- o[HTTP::URI.parse k] = v
28
+ self
23
29
  end
30
+ end
24
31
 
25
- self
32
+ def redirect_response(location, status = 302)
33
+ lambda do |request|
34
+ HTTP::Response.new(
35
+ :status => status,
36
+ :version => "1.1",
37
+ :headers => {"Location" => location},
38
+ :body => "",
39
+ :request => request
40
+ )
41
+ end
26
42
  end
27
- end
28
43
 
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
44
+ def simple_response(body, status = 200)
45
+ lambda do |request|
46
+ HTTP::Response.new(
47
+ :status => status,
48
+ :version => "1.1",
49
+ :body => body,
50
+ :request => request
51
+ )
52
+ end
53
+ end
37
54
 
38
- def simple_response(body, status = 200)
39
- HTTP::Response.new(
40
- :status => status,
41
- :version => "1.1",
42
- :body => body
43
- )
55
+ stub_const("StubbedClient", stubbed_client)
44
56
  end
45
57
 
46
58
  describe "following redirects" do
@@ -98,13 +110,45 @@ RSpec.describe HTTP::Client do
98
110
  end
99
111
 
100
112
  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"
113
+ expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
114
+ to include("url" => "https://httpbin.org/anything/könig")
104
115
  end
105
116
  end
106
117
  end
107
118
 
119
+ describe "following redirects with logging" do
120
+ let(:logger) do
121
+ logger = Logger.new(logdev)
122
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
123
+ logger.level = Logger::INFO
124
+ logger
125
+ end
126
+
127
+ let(:logdev) { StringIO.new }
128
+
129
+ it "logs all requests" do
130
+ client = StubbedClient.new(:follow => true, :features => { :logging => { :logger => logger } }).stub(
131
+ "http://example.com/" => redirect_response("/1"),
132
+ "http://example.com/1" => redirect_response("/2"),
133
+ "http://example.com/2" => redirect_response("/3"),
134
+ "http://example.com/3" => simple_response("OK")
135
+ )
136
+
137
+ expect { client.get("http://example.com/") }.not_to raise_error
138
+
139
+ expect(logdev.string).to eq <<~OUTPUT
140
+ ** INFO **
141
+ > GET http://example.com/
142
+ ** INFO **
143
+ > GET http://example.com/1
144
+ ** INFO **
145
+ > GET http://example.com/2
146
+ ** INFO **
147
+ > GET http://example.com/3
148
+ OUTPUT
149
+ end
150
+ end
151
+
108
152
  describe "parsing params" do
109
153
  let(:client) { HTTP::Client.new }
110
154
  before { allow(client).to receive :perform }
@@ -190,6 +234,22 @@ RSpec.describe HTTP::Client do
190
234
 
191
235
  client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
192
236
  end
237
+
238
+ context "when passing an HTTP::FormData object directly" do
239
+ it "creates url encoded form data object" do
240
+ client = HTTP::Client.new
241
+ form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
242
+
243
+ allow(client).to receive(:perform)
244
+
245
+ expect(HTTP::Request).to receive(:new) do |opts|
246
+ expect(opts[:body]).to be form_data
247
+ expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
248
+ end
249
+
250
+ client.get("http://example.com/", :form => form_data)
251
+ end
252
+ end
193
253
  end
194
254
 
195
255
  describe "passing json" do
@@ -213,9 +273,9 @@ RSpec.describe HTTP::Client do
213
273
  end
214
274
 
215
275
  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"
276
+ url = "https://httpbin.org/anything/ö無"
277
+
278
+ expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
219
279
  end
220
280
  end
221
281
 
@@ -264,6 +324,75 @@ RSpec.describe HTTP::Client do
264
324
  end
265
325
  end
266
326
  end
327
+
328
+ context "Feature" do
329
+ let(:feature_class) do
330
+ Class.new(HTTP::Feature) do
331
+ attr_reader :captured_request, :captured_response, :captured_error
332
+
333
+ def wrap_request(request)
334
+ @captured_request = request
335
+ end
336
+
337
+ def wrap_response(response)
338
+ @captured_response = response
339
+ end
340
+
341
+ def on_error(request, error)
342
+ @captured_request = request
343
+ @captured_error = error
344
+ end
345
+ end
346
+ end
347
+
348
+ it "is given a chance to wrap the Request" do
349
+ feature_instance = feature_class.new
350
+
351
+ response = client.use(:test_feature => feature_instance).
352
+ request(:get, dummy.endpoint)
353
+
354
+ expect(response.code).to eq(200)
355
+ expect(feature_instance.captured_request.verb).to eq(:get)
356
+ expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
357
+ end
358
+
359
+ it "is given a chance to wrap the Response" do
360
+ feature_instance = feature_class.new
361
+
362
+ response = client.use(:test_feature => feature_instance).
363
+ request(:get, dummy.endpoint)
364
+
365
+ expect(feature_instance.captured_response).to eq(response)
366
+ end
367
+
368
+ it "is given a chance to handle an error" do
369
+ sleep_url = "#{dummy.endpoint}/sleep"
370
+ feature_instance = feature_class.new
371
+
372
+ expect do
373
+ client.use(:test_feature => feature_instance).
374
+ timeout(0.2).
375
+ request(:post, sleep_url)
376
+ end.to raise_error(HTTP::TimeoutError)
377
+
378
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
379
+ expect(feature_instance.captured_request.verb).to eq(:post)
380
+ expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
381
+ end
382
+
383
+ it "is given a chance to handle a connection timeout error" do
384
+ allow(TCPSocket).to receive(:open) { sleep 1 }
385
+ sleep_url = "#{dummy.endpoint}/sleep"
386
+ feature_instance = feature_class.new
387
+
388
+ expect do
389
+ client.use(:test_feature => feature_instance).
390
+ timeout(0.001).
391
+ request(:post, sleep_url)
392
+ end.to raise_error(HTTP::TimeoutError)
393
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
394
+ end
395
+ end
267
396
  end
268
397
 
269
398
  include_context "HTTP handling" do
@@ -273,7 +402,8 @@ RSpec.describe HTTP::Client do
273
402
  let(:client) { described_class.new(options.merge(extra_options)) }
274
403
  end
275
404
 
276
- describe "working with SSL" do
405
+ # TODO: https://github.com/httprb/http/issues/627
406
+ xdescribe "working with SSL" do
277
407
  run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
278
408
 
279
409
  let(:extra_options) { {} }
@@ -316,6 +446,14 @@ RSpec.describe HTTP::Client do
316
446
  client.get(dummy.endpoint).to_s
317
447
  end
318
448
 
449
+ it "provides access to the Request from the Response" do
450
+ unique_value = "20190424"
451
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
452
+
453
+ expect(response.request).to be_a(HTTP::Request)
454
+ expect(response.request.headers["X-Value"]).to eq(unique_value)
455
+ end
456
+
319
457
  context "with HEAD request" do
320
458
  it "does not iterates through body" do
321
459
  expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
@@ -406,7 +544,7 @@ RSpec.describe HTTP::Client do
406
544
  BODY
407
545
  end
408
546
 
409
- it "raises HTTP::ConnectionError" do
547
+ xit "raises HTTP::ConnectionError" do
410
548
  expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
411
549
  end
412
550
  end
@@ -3,9 +3,9 @@
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
11
  let(:socket) { double(:connect => nil) }
@@ -20,14 +20,17 @@ RSpec.describe HTTP::Connection do
20
20
  <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
21
21
  | HTTP/1.1 200 OK
22
22
  | Content-Type: text
23
+ | foo_bar: 123
23
24
  |
24
25
  RESPONSE
25
26
  end
26
27
  end
27
28
 
28
- it "reads data in parts" do
29
+ it "populates headers collection, preserving casing" do
29
30
  connection.read_headers!
30
- expect(connection.headers).to eq("Content-Type" => "text")
31
+ expect(connection.headers).to eq("Content-Type" => "text", "foo_bar" => "123")
32
+ expect(connection.headers["Foo-Bar"]).to eq("123")
33
+ expect(connection.headers["foo_bar"]).to eq("123")
31
34
  end
32
35
  end
33
36
 
@@ -11,7 +11,8 @@ RSpec.describe HTTP::Features::AutoInflate do
11
11
  :version => "1.1",
12
12
  :status => 200,
13
13
  :headers => headers,
14
- :connection => connection
14
+ :connection => connection,
15
+ :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
15
16
  )
16
17
  end
17
18
 
@@ -73,7 +74,7 @@ RSpec.describe HTTP::Features::AutoInflate do
73
74
  :status => 200,
74
75
  :headers => {:content_encoding => "gzip"},
75
76
  :connection => connection,
76
- :uri => "https://example.com"
77
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
77
78
  )
78
79
  end
79
80
 
@@ -2,15 +2,36 @@
2
2
 
3
3
  RSpec.describe HTTP::Features::Instrumentation do
4
4
  subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
5
+
5
6
  let(:instrumenter) { TestInstrumenter.new }
6
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
+
7
28
  describe "logging the request" do
8
29
  let(:request) do
9
30
  HTTP::Request.new(
10
- :verb => :post,
11
- :uri => "https://example.com/",
31
+ :verb => :post,
32
+ :uri => "https://example.com/",
12
33
  :headers => {:accept => "application/json"},
13
- :body => '{"hello": "world!"}'
34
+ :body => '{"hello": "world!"}'
14
35
  )
15
36
  end
16
37
 
@@ -25,10 +46,10 @@ RSpec.describe HTTP::Features::Instrumentation do
25
46
  let(:response) do
26
47
  HTTP::Response.new(
27
48
  :version => "1.1",
28
- :uri => "https://example.com",
29
- :status => 200,
49
+ :status => 200,
30
50
  :headers => {:content_type => "application/json"},
31
- :body => '{"success": true}'
51
+ :body => '{"success": true}',
52
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
32
53
  )
33
54
  end
34
55
 
@@ -38,19 +59,4 @@ RSpec.describe HTTP::Features::Instrumentation do
38
59
  expect(instrumenter.output[:finish]).to eq(:response => response)
39
60
  end
40
61
  end
41
-
42
- class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
43
- attr_reader :output
44
- def initialize
45
- @output = {}
46
- end
47
-
48
- def start(_name, payload)
49
- output[:start] = payload
50
- end
51
-
52
- def finish(_name, payload)
53
- output[:finish] = payload
54
- end
55
- end
56
62
  end
@@ -4,10 +4,8 @@ require "logger"
4
4
 
5
5
  RSpec.describe HTTP::Features::Logging do
6
6
  subject(:feature) do
7
- logger = Logger.new(logdev)
8
- logger.formatter = ->(severity, _, _, message) do
9
- format("** %s **\n%s\n", severity, message)
10
- end
7
+ logger = Logger.new(logdev)
8
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
11
9
 
12
10
  described_class.new(:logger => logger)
13
11
  end
@@ -17,10 +15,10 @@ RSpec.describe HTTP::Features::Logging do
17
15
  describe "logging the request" do
18
16
  let(:request) do
19
17
  HTTP::Request.new(
20
- :verb => :post,
21
- :uri => "https://example.com/",
22
- :headers => {:accept => "application/json"},
23
- :body => '{"hello": "world!"}'
18
+ :verb => :post,
19
+ :uri => "https://example.com/",
20
+ :headers => {:accept => "application/json"},
21
+ :body => '{"hello": "world!"}'
24
22
  )
25
23
  end
26
24
 
@@ -44,10 +42,10 @@ RSpec.describe HTTP::Features::Logging do
44
42
  let(:response) do
45
43
  HTTP::Response.new(
46
44
  :version => "1.1",
47
- :uri => "https://example.com",
48
45
  :status => 200,
49
46
  :headers => {:content_type => "application/json"},
50
- :body => '{"success": true}'
47
+ :body => '{"success": true}',
48
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
51
49
  )
52
50
  end
53
51
 
@@ -13,7 +13,7 @@ RSpec.describe HTTP::Headers do
13
13
  expect(headers["Accept"]).to eq "application/json"
14
14
  end
15
15
 
16
- it "normalizes header name" do
16
+ it "allows retrieval via normalized header name" do
17
17
  headers.set :content_type, "application/json"
18
18
  expect(headers["Content-Type"]).to eq "application/json"
19
19
  end
@@ -35,8 +35,15 @@ RSpec.describe HTTP::Headers do
35
35
  to raise_error HTTP::HeaderError
36
36
  end
37
37
 
38
- it "fails with invalid header name" do
39
- expect { headers.set "foo bar", "baz" }.
38
+ ["foo bar", "foo bar: ok\nfoo", "evil-header: evil-value\nfoo"].each do |name|
39
+ it "fails with invalid header name (#{name.inspect})" do
40
+ expect { headers.set name, "baz" }.
41
+ to raise_error HTTP::HeaderError
42
+ end
43
+ end
44
+
45
+ it "fails with invalid header value" do
46
+ expect { headers.set "foo", "bar\nEvil-Header: evil-value" }.
40
47
  to raise_error HTTP::HeaderError
41
48
  end
42
49
  end
@@ -47,7 +54,7 @@ RSpec.describe HTTP::Headers do
47
54
  expect(headers["Accept"]).to eq "application/json"
48
55
  end
49
56
 
50
- it "normalizes header name" do
57
+ it "allows retrieval via normalized header name" do
51
58
  headers[:content_type] = "application/json"
52
59
  expect(headers["Content-Type"]).to eq "application/json"
53
60
  end
@@ -73,7 +80,7 @@ RSpec.describe HTTP::Headers do
73
80
  expect(headers["Content-Type"]).to be_nil
74
81
  end
75
82
 
76
- it "normalizes header name" do
83
+ it "removes header that matches normalized version of specified name" do
77
84
  headers.delete :content_type
78
85
  expect(headers["Content-Type"]).to be_nil
79
86
  end
@@ -83,9 +90,11 @@ RSpec.describe HTTP::Headers do
83
90
  to raise_error HTTP::HeaderError
84
91
  end
85
92
 
86
- it "fails with invalid header name" do
87
- expect { headers.delete "foo bar" }.
88
- to raise_error HTTP::HeaderError
93
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
94
+ it "fails with invalid header name (#{name.inspect})" do
95
+ expect { headers.delete name }.
96
+ to raise_error HTTP::HeaderError
97
+ end
89
98
  end
90
99
  end
91
100
 
@@ -95,13 +104,13 @@ RSpec.describe HTTP::Headers do
95
104
  expect(headers["Accept"]).to eq "application/json"
96
105
  end
97
106
 
98
- it "normalizes header name" do
107
+ it "allows retrieval via normalized header name" do
99
108
  headers.add :content_type, "application/json"
100
109
  expect(headers["Content-Type"]).to eq "application/json"
101
110
  end
102
111
 
103
112
  it "appends new value if header exists" do
104
- headers.add :set_cookie, "hoo=ray"
113
+ headers.add "Set-Cookie", "hoo=ray"
105
114
  headers.add :set_cookie, "woo=hoo"
106
115
  expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
107
116
  end
@@ -117,8 +126,20 @@ RSpec.describe HTTP::Headers do
117
126
  to raise_error HTTP::HeaderError
118
127
  end
119
128
 
120
- it "fails with invalid header name" do
121
- expect { headers.add "foo bar", "baz" }.
129
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
130
+ it "fails with invalid header name (#{name.inspect})" do
131
+ expect { headers.add name, "baz" }.
132
+ to raise_error HTTP::HeaderError
133
+ end
134
+ end
135
+
136
+ it "fails with invalid header value" do
137
+ expect { headers.add "foo", "bar\nEvil-Header: evil-value" }.
138
+ to raise_error HTTP::HeaderError
139
+ end
140
+
141
+ it "fails when header name is not a String or Symbol" do
142
+ expect { headers.add 2, "foo" }.
122
143
  to raise_error HTTP::HeaderError
123
144
  end
124
145
  end
@@ -145,9 +166,11 @@ RSpec.describe HTTP::Headers do
145
166
  to raise_error HTTP::HeaderError
146
167
  end
147
168
 
148
- it "fails with invalid header name" do
149
- expect { headers.get("foo bar") }.
150
- to raise_error HTTP::HeaderError
169
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
170
+ it "fails with invalid header name (#{name.inspect})" do
171
+ expect { headers.get name }.
172
+ to raise_error HTTP::HeaderError
173
+ end
151
174
  end
152
175
  end
153
176
 
@@ -296,8 +319,19 @@ RSpec.describe HTTP::Headers do
296
319
  )
297
320
  end
298
321
 
322
+ it "yields header keys specified as symbols in normalized form" do
323
+ keys = headers.each.map(&:first)
324
+ expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie])
325
+ end
326
+
327
+ it "yields headers specified as strings without conversion" do
328
+ headers.add "X_kEy", "value"
329
+ keys = headers.each.map(&:first)
330
+ expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie X_kEy])
331
+ end
332
+
299
333
  it "returns self instance if block given" do
300
- expect(headers.each { |*| }).to be headers
334
+ expect(headers.each { |*| }).to be headers # rubocop:disable Lint/EmptyBlock
301
335
  end
302
336
 
303
337
  it "returns Enumerator if no block given" do
@@ -472,14 +506,15 @@ RSpec.describe HTTP::Headers do
472
506
  end
473
507
 
474
508
  context "with duplicate header keys (mixed case)" do
475
- let(:headers) { {"Set-Cookie" => "hoo=ray", "set-cookie" => "woo=hoo"} }
509
+ let(:headers) { {"Set-Cookie" => "hoo=ray", "set_cookie" => "woo=hoo", :set_cookie => "ta=da"} }
476
510
 
477
511
  it "adds all headers" do
478
512
  expect(described_class.coerce(headers).to_a).
479
513
  to match_array(
480
514
  [
481
515
  %w[Set-Cookie hoo=ray],
482
- %w[Set-Cookie woo=hoo]
516
+ %w[set_cookie woo=hoo],
517
+ %w[Set-Cookie ta=da]
483
518
  ]
484
519
  )
485
520
  end
@@ -8,7 +8,7 @@ RSpec.describe HTTP::Options, "headers" do
8
8
  end
9
9
 
10
10
  it "may be specified with with_headers" do
11
- opts2 = opts.with_headers("accept" => "json")
11
+ opts2 = opts.with_headers(:accept => "json")
12
12
  expect(opts.headers).to be_empty
13
13
  expect(opts2.headers).to eq([%w[Accept json]])
14
14
  end
@@ -18,28 +18,28 @@ RSpec.describe HTTP::Options, "merge" do
18
18
  # FIXME: yuck :(
19
19
 
20
20
  foo = HTTP::Options.new(
21
- :response => :body,
22
- :params => {:baz => "bar"},
23
- :form => {:foo => "foo"},
24
- :body => "body-foo",
25
- :json => {:foo => "foo"},
26
- :headers => {:accept => "json", :foo => "foo"},
27
- :proxy => {},
28
- :features => {}
21
+ :response => :body,
22
+ :params => {:baz => "bar"},
23
+ :form => {:foo => "foo"},
24
+ :body => "body-foo",
25
+ :json => {:foo => "foo"},
26
+ :headers => {:accept => "json", :foo => "foo"},
27
+ :proxy => {},
28
+ :features => {}
29
29
  )
30
30
 
31
31
  bar = HTTP::Options.new(
32
- :response => :parsed_body,
33
- :persistent => "https://www.googe.com",
34
- :params => {:plop => "plip"},
35
- :form => {:bar => "bar"},
36
- :body => "body-bar",
37
- :json => {:bar => "bar"},
32
+ :response => :parsed_body,
33
+ :persistent => "https://www.googe.com",
34
+ :params => {:plop => "plip"},
35
+ :form => {:bar => "bar"},
36
+ :body => "body-bar",
37
+ :json => {:bar => "bar"},
38
38
  :keep_alive_timeout => 10,
39
39
  :headers => {:accept => "xml", :bar => "bar"},
40
40
  :timeout_options => {:foo => :bar},
41
- :ssl => {:foo => "bar"},
42
- :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}
41
+ :ssl => {:foo => "bar"},
42
+ :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}
43
43
  )
44
44
 
45
45
  expect(foo.merge(bar).to_hash).to eq(