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.
- 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 +192 -0
- data/.yardopts +1 -1
- data/CHANGES.md +168 -0
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +17 -20
- data/Rakefile +2 -10
- data/http.gemspec +5 -5
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +52 -35
- data/lib/http/connection.rb +12 -8
- data/lib/http/content_type.rb +12 -7
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +7 -7
- 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 +6 -9
- data/lib/http/redirector.rb +4 -2
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +8 -3
- 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 +75 -49
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +35 -15
- data/lib/http/timeout/global.rb +42 -38
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +56 -58
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +173 -35
- 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 +59 -1
- 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 +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +44 -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 +4 -4
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +24 -16
- data/.coveralls.yml +0 -1
- 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
|
-
|
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
|
@@ -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
|
-
|
102
|
-
|
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
|
217
|
-
|
218
|
-
expect(
|
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
|
-
|
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
|
-
|
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
|
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(
|