http 4.4.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +8 -110
- data/.rubocop_todo.yml +205 -0
- data/.yardopts +1 -1
- data/CHANGES.md +188 -3
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +47 -86
- data/Rakefile +2 -10
- data/SECURITY.md +5 -0
- data/http.gemspec +9 -8
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +44 -34
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +3 -0
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -7
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +5 -8
- data/lib/http/redirector.rb +51 -2
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +9 -4
- data/lib/http/request.rb +28 -11
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +74 -62
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +44 -18
- data/lib/http/timeout/global.rb +20 -36
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +32 -55
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +153 -30
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
- data/spec/lib/http/features/instrumentation_spec.rb +27 -21
- data/spec/lib/http/features/logging_spec.rb +8 -10
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +107 -3
- data/spec/lib/http/request/body_spec.rb +3 -3
- data/spec/lib/http/request/writer_spec.rb +25 -2
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +33 -4
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +80 -3
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +5 -5
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +23 -15
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
@@ -104,6 +116,39 @@ RSpec.describe HTTP::Client do
|
|
104
116
|
end
|
105
117
|
end
|
106
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
|
+
|
107
152
|
describe "parsing params" do
|
108
153
|
let(:client) { HTTP::Client.new }
|
109
154
|
before { allow(client).to receive :perform }
|
@@ -193,7 +238,7 @@ RSpec.describe HTTP::Client do
|
|
193
238
|
context "when passing an HTTP::FormData object directly" do
|
194
239
|
it "creates url encoded form data object" do
|
195
240
|
client = HTTP::Client.new
|
196
|
-
form_data = HTTP::FormData::Multipart.new(:foo => "bar")
|
241
|
+
form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
|
197
242
|
|
198
243
|
allow(client).to receive(:perform)
|
199
244
|
|
@@ -279,6 +324,75 @@ RSpec.describe HTTP::Client do
|
|
279
324
|
end
|
280
325
|
end
|
281
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::ConnectTimeoutError)
|
393
|
+
expect(feature_instance.captured_error).to be_a(HTTP::ConnectTimeoutError)
|
394
|
+
end
|
395
|
+
end
|
282
396
|
end
|
283
397
|
|
284
398
|
include_context "HTTP handling" do
|
@@ -288,7 +402,8 @@ RSpec.describe HTTP::Client do
|
|
288
402
|
let(:client) { described_class.new(options.merge(extra_options)) }
|
289
403
|
end
|
290
404
|
|
291
|
-
|
405
|
+
# TODO: https://github.com/httprb/http/issues/627
|
406
|
+
xdescribe "working with SSL" do
|
292
407
|
run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
|
293
408
|
|
294
409
|
let(:extra_options) { {} }
|
@@ -331,6 +446,14 @@ RSpec.describe HTTP::Client do
|
|
331
446
|
client.get(dummy.endpoint).to_s
|
332
447
|
end
|
333
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
|
+
|
334
457
|
context "with HEAD request" do
|
335
458
|
it "does not iterates through body" do
|
336
459
|
expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
|
@@ -421,7 +544,7 @@ RSpec.describe HTTP::Client do
|
|
421
544
|
BODY
|
422
545
|
end
|
423
546
|
|
424
|
-
|
547
|
+
xit "raises HTTP::ConnectionError" do
|
425
548
|
expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
|
426
549
|
end
|
427
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
|
7
|
-
:uri
|
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 "
|
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
|
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
|
11
|
-
:uri
|
31
|
+
:verb => :post,
|
32
|
+
:uri => "https://example.com/",
|
12
33
|
:headers => {:accept => "application/json"},
|
13
|
-
:body
|
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
|
-
:
|
29
|
-
:status => 200,
|
49
|
+
:status => 200,
|
30
50
|
:headers => {:content_type => "application/json"},
|
31
|
-
:body
|
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
|
8
|
-
logger.formatter = ->(severity, _, _, message)
|
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
|
21
|
-
:uri
|
22
|
-
:headers
|
23
|
-
:body
|
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 "
|
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
|
-
|
39
|
-
|
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 "
|
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 "
|
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
|
-
|
87
|
-
|
88
|
-
|
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 "
|
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
|
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
|
-
|
121
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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", "
|
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[
|
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(
|
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
|
22
|
-
:params
|
23
|
-
:form
|
24
|
-
:body
|
25
|
-
:json
|
26
|
-
:headers
|
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
|
33
|
-
:persistent
|
34
|
-
:params
|
35
|
-
:form
|
36
|
-
:body
|
37
|
-
:json
|
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
|
42
|
-
:proxy
|
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(
|