http 4.4.1 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +85 -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 +206 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +200 -3
  11. data/Gemfile +18 -10
  12. data/LICENSE.txt +1 -1
  13. data/README.md +48 -86
  14. data/Rakefile +2 -10
  15. data/SECURITY.md +5 -0
  16. data/http.gemspec +9 -8
  17. data/lib/http/chainable.rb +23 -17
  18. data/lib/http/client.rb +44 -34
  19. data/lib/http/connection.rb +11 -7
  20. data/lib/http/content_type.rb +12 -7
  21. data/lib/http/errors.rb +3 -0
  22. data/lib/http/feature.rb +3 -1
  23. data/lib/http/features/auto_deflate.rb +6 -6
  24. data/lib/http/features/auto_inflate.rb +6 -7
  25. data/lib/http/features/instrumentation.rb +1 -1
  26. data/lib/http/features/logging.rb +19 -21
  27. data/lib/http/headers.rb +50 -13
  28. data/lib/http/mime_type/adapter.rb +3 -1
  29. data/lib/http/mime_type/json.rb +1 -0
  30. data/lib/http/options.rb +5 -8
  31. data/lib/http/redirector.rb +55 -4
  32. data/lib/http/request/body.rb +1 -0
  33. data/lib/http/request/writer.rb +9 -4
  34. data/lib/http/request.rb +28 -11
  35. data/lib/http/response/body.rb +6 -4
  36. data/lib/http/response/inflater.rb +1 -1
  37. data/lib/http/response/parser.rb +74 -62
  38. data/lib/http/response/status.rb +4 -3
  39. data/lib/http/response.rb +44 -18
  40. data/lib/http/timeout/global.rb +20 -36
  41. data/lib/http/timeout/null.rb +2 -1
  42. data/lib/http/timeout/per_operation.rb +32 -55
  43. data/lib/http/uri.rb +51 -6
  44. data/lib/http/version.rb +1 -1
  45. data/spec/lib/http/client_spec.rb +155 -30
  46. data/spec/lib/http/connection_spec.rb +8 -5
  47. data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
  48. data/spec/lib/http/features/instrumentation_spec.rb +27 -21
  49. data/spec/lib/http/features/logging_spec.rb +8 -10
  50. data/spec/lib/http/headers_spec.rb +53 -18
  51. data/spec/lib/http/options/headers_spec.rb +1 -1
  52. data/spec/lib/http/options/merge_spec.rb +16 -16
  53. data/spec/lib/http/redirector_spec.rb +133 -3
  54. data/spec/lib/http/request/body_spec.rb +3 -3
  55. data/spec/lib/http/request/writer_spec.rb +25 -2
  56. data/spec/lib/http/request_spec.rb +5 -5
  57. data/spec/lib/http/response/body_spec.rb +5 -5
  58. data/spec/lib/http/response/parser_spec.rb +33 -4
  59. data/spec/lib/http/response/status_spec.rb +3 -3
  60. data/spec/lib/http/response_spec.rb +80 -3
  61. data/spec/lib/http/uri_spec.rb +39 -0
  62. data/spec/lib/http_spec.rb +30 -3
  63. data/spec/spec_helper.rb +21 -21
  64. data/spec/support/black_hole.rb +1 -1
  65. data/spec/support/dummy_server/servlet.rb +19 -6
  66. data/spec/support/dummy_server.rb +7 -7
  67. data/spec/support/fuubar.rb +21 -0
  68. data/spec/support/http_handling_shared.rb +5 -5
  69. data/spec/support/simplecov.rb +19 -0
  70. data/spec/support/ssl_helper.rb +4 -4
  71. metadata +22 -14
  72. data/.coveralls.yml +0 -1
  73. data/.travis.yml +0 -39
@@ -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(
@@ -6,12 +6,17 @@ RSpec.describe HTTP::Redirector do
6
6
  :status => status,
7
7
  :version => "1.1",
8
8
  :headers => headers,
9
- :body => body
9
+ :body => body,
10
+ :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
10
11
  )
11
12
  end
12
13
 
13
- def redirect_response(status, location)
14
- simple_response status, "", "Location" => location
14
+ def redirect_response(status, location, set_cookie = {})
15
+ res = simple_response status, "", "Location" => location
16
+ set_cookie.each do |name, value|
17
+ res.headers.add("Set-Cookie", "#{name}=#{value}; path=/; httponly; secure; SameSite=none; Secure")
18
+ end
19
+ res
15
20
  end
16
21
 
17
22
  describe "#strict" do
@@ -88,6 +93,87 @@ RSpec.describe HTTP::Redirector do
88
93
  expect(res.to_s).to eq "http://example.com/123"
89
94
  end
90
95
 
96
+ it "returns cookies in response" do
97
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
98
+ hops = [
99
+ redirect_response(301, "http://example.com/1", {"foo" => "42"}),
100
+ redirect_response(301, "http://example.com/2", {"bar" => "53", "deleted" => "foo"}),
101
+ redirect_response(301, "http://example.com/3", {"baz" => "64", "deleted" => ""}),
102
+ redirect_response(301, "http://example.com/4", {"baz" => "65"}),
103
+ simple_response(200, "bar")
104
+ ]
105
+
106
+ request_cookies = [
107
+ {"foo" => "42"},
108
+ {"foo" => "42", "bar" => "53", "deleted" => "foo"},
109
+ {"foo" => "42", "bar" => "53", "baz" => "64"},
110
+ {"foo" => "42", "bar" => "53", "baz" => "65"}
111
+ ]
112
+
113
+ res = redirector.perform(req, hops.shift) do |request|
114
+ req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
115
+ expect(req_cookie).to eq request_cookies.shift
116
+ hops.shift
117
+ end
118
+ expect(res.to_s).to eq "bar"
119
+ cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
120
+ expect(cookies["foo"]).to eq "42"
121
+ expect(cookies["bar"]).to eq "53"
122
+ expect(cookies["baz"]).to eq "65"
123
+ expect(cookies["deleted"]).to eq nil
124
+ end
125
+
126
+ it "returns original cookies in response" do
127
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
128
+ req.headers.set("Cookie", "foo=42; deleted=baz")
129
+ hops = [
130
+ redirect_response(301, "http://example.com/1", {"bar" => "64", "deleted" => ""}),
131
+ simple_response(200, "bar")
132
+ ]
133
+
134
+ request_cookies = [
135
+ {"foo" => "42", "bar" => "64"},
136
+ {"foo" => "42", "bar" => "64"}
137
+ ]
138
+
139
+ res = redirector.perform(req, hops.shift) do |request|
140
+ req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
141
+ expect(req_cookie).to eq request_cookies.shift
142
+ hops.shift
143
+ end
144
+ expect(res.to_s).to eq "bar"
145
+ cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
146
+ expect(cookies["foo"]).to eq "42"
147
+ expect(cookies["bar"]).to eq "64"
148
+ expect(cookies["deleted"]).to eq nil
149
+ end
150
+
151
+ context "with on_redirect callback" do
152
+ let(:options) do
153
+ {
154
+ :on_redirect => proc do |response, location|
155
+ @redirect_response = response
156
+ @redirect_location = location
157
+ end
158
+ }
159
+ end
160
+
161
+ it "calls on_redirect" do
162
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
163
+ hops = [
164
+ redirect_response(301, "http://example.com/1"),
165
+ redirect_response(301, "http://example.com/2"),
166
+ simple_response(200, "foo")
167
+ ]
168
+
169
+ redirector.perform(req, hops.shift) do |prev_req, _|
170
+ expect(@redirect_location.uri.to_s).to eq prev_req.uri.to_s
171
+ expect(@redirect_response.code).to eq 301
172
+ hops.shift
173
+ end
174
+ end
175
+ end
176
+
91
177
  context "following 300 redirect" do
92
178
  context "with strict mode" do
93
179
  let(:options) { {:strict => true} }
@@ -395,5 +481,49 @@ RSpec.describe HTTP::Redirector do
395
481
  end
396
482
  end
397
483
  end
484
+
485
+ describe "changing verbs during redirects" do
486
+ let(:options) { {:strict => false} }
487
+ let(:post_body) { HTTP::Request::Body.new("i might be way longer in real life") }
488
+ let(:cookie) { "dont=eat my cookies" }
489
+
490
+ def a_dangerous_request(verb)
491
+ HTTP::Request.new(
492
+ :verb => verb, :uri => "http://example.com",
493
+ :body => post_body, :headers => {
494
+ "Content-Type" => "meme",
495
+ "Cookie" => cookie
496
+ }
497
+ )
498
+ end
499
+
500
+ def empty_body
501
+ HTTP::Request::Body.new(nil)
502
+ end
503
+
504
+ it "follows without body/content type if it has to change verb" do
505
+ req = a_dangerous_request(:post)
506
+ res = redirect_response 302, "http://example.com/1"
507
+
508
+ redirector.perform(req, res) do |prev_req, _|
509
+ expect(prev_req.body).to eq(empty_body)
510
+ expect(prev_req.headers["Cookie"]).to eq(cookie)
511
+ expect(prev_req.headers["Content-Type"]).to eq(nil)
512
+ simple_response 200
513
+ end
514
+ end
515
+
516
+ it "leaves body/content-type intact if it does not have to change verb" do
517
+ req = a_dangerous_request(:post)
518
+ res = redirect_response 307, "http://example.com/1"
519
+
520
+ redirector.perform(req, res) do |prev_req, _|
521
+ expect(prev_req.body).to eq(post_body)
522
+ expect(prev_req.headers["Cookie"]).to eq(cookie)
523
+ expect(prev_req.headers["Content-Type"]).to eq("meme")
524
+ simple_response 200
525
+ end
526
+ end
527
+ end
398
528
  end
399
529
  end
@@ -118,10 +118,10 @@ RSpec.describe HTTP::Request::Body do
118
118
  end
119
119
 
120
120
  context "when body is a non-Enumerable IO" do
121
- let(:body) { FakeIO.new("a" * 16 * 1024 + "b" * 10 * 1024) }
121
+ let(:body) { FakeIO.new(("a" * 16 * 1024) + ("b" * 10 * 1024)) }
122
122
 
123
123
  it "yields chunks of content" do
124
- expect(chunks.inject("", :+)).to eq "a" * 16 * 1024 + "b" * 10 * 1024
124
+ expect(chunks.inject("", :+)).to eq ("a" * 16 * 1024) + ("b" * 10 * 1024)
125
125
  end
126
126
  end
127
127
 
@@ -148,7 +148,7 @@ RSpec.describe HTTP::Request::Body do
148
148
  end
149
149
 
150
150
  context "when body is an Enumerable IO" do
151
- let(:data) { "a" * 16 * 1024 + "b" * 10 * 1024 }
151
+ let(:data) { ("a" * 16 * 1024) + ("b" * 10 * 1024) }
152
152
  let(:body) { StringIO.new data }
153
153
 
154
154
  it "yields chunks of content" do
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  RSpec.describe HTTP::Request::Writer do
5
5
  let(:io) { StringIO.new }
@@ -22,6 +22,18 @@ RSpec.describe HTTP::Request::Writer do
22
22
  end
23
23
  end
24
24
 
25
+ context "when headers are specified as strings with mixed case" do
26
+ let(:headers) { HTTP::Headers.coerce "content-Type" => "text", "X_MAX" => "200" }
27
+
28
+ it "writes the headers with the same casing" do
29
+ writer.stream
30
+ expect(io.string).to eq [
31
+ "#{headerstart}\r\n",
32
+ "content-Type: text\r\nX_MAX: 200\r\nContent-Length: 0\r\n\r\n"
33
+ ].join
34
+ end
35
+ end
36
+
25
37
  context "when body is nonempty" do
26
38
  let(:body) { HTTP::Request::Body.new("content") }
27
39
 
@@ -35,9 +47,20 @@ RSpec.describe HTTP::Request::Writer do
35
47
  end
36
48
  end
37
49
 
38
- context "when body is empty" do
50
+ context "when body is not set" do
39
51
  let(:body) { HTTP::Request::Body.new(nil) }
40
52
 
53
+ it "doesn't write anything to the socket and doesn't set Content-Length" do
54
+ writer.stream
55
+ expect(io.string).to eq [
56
+ "#{headerstart}\r\n\r\n"
57
+ ].join
58
+ end
59
+ end
60
+
61
+ context "when body is empty" do
62
+ let(:body) { HTTP::Request::Body.new("") }
63
+
41
64
  it "doesn't write anything to the socket and sets Content-Length" do
42
65
  writer.stream
43
66
  expect(io.string).to eq [
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  RSpec.describe HTTP::Request do
5
5
  let(:proxy) { {} }
@@ -8,10 +8,10 @@ RSpec.describe HTTP::Request do
8
8
 
9
9
  subject :request do
10
10
  HTTP::Request.new(
11
- :verb => :get,
12
- :uri => request_uri,
13
- :headers => headers,
14
- :proxy => proxy
11
+ :verb => :get,
12
+ :uri => request_uri,
13
+ :headers => headers,
14
+ :proxy => proxy
15
15
  )
16
16
  end
17
17
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  RSpec.describe HTTP::Response::Body do
4
4
  let(:connection) { double(:sequence_id => 0) }
5
- let(:chunks) { [String.new("Hello, "), String.new("World!")] }
5
+ let(:chunks) { ["Hello, ", "World!"] }
6
6
 
7
7
  before do
8
8
  allow(connection).to receive(:readpartial) { chunks.shift }
@@ -16,7 +16,7 @@ RSpec.describe HTTP::Response::Body do
16
16
  end
17
17
 
18
18
  context "when body empty" do
19
- let(:chunks) { [String.new("")] }
19
+ let(:chunks) { [""] }
20
20
 
21
21
  it "returns responds to empty? with true" do
22
22
  expect(subject).to be_empty
@@ -45,12 +45,12 @@ RSpec.describe HTTP::Response::Body do
45
45
  it "returns content in specified encoding" do
46
46
  body = described_class.new(connection)
47
47
  expect(connection).to receive(:readpartial).
48
- and_return(String.new("content").force_encoding(Encoding::UTF_8))
48
+ and_return(String.new("content", :encoding => Encoding::UTF_8))
49
49
  expect(body.readpartial.encoding).to eq Encoding::BINARY
50
50
 
51
51
  body = described_class.new(connection, :encoding => Encoding::UTF_8)
52
52
  expect(connection).to receive(:readpartial).
53
- and_return(String.new("content").force_encoding(Encoding::BINARY))
53
+ and_return(String.new("content", :encoding => Encoding::BINARY))
54
54
  expect(body.readpartial.encoding).to eq Encoding::UTF_8
55
55
  end
56
56
  end
@@ -59,7 +59,7 @@ RSpec.describe HTTP::Response::Body do
59
59
  let(:chunks) do
60
60
  body = Zlib::Deflate.deflate("Hi, HTTP here ☺")
61
61
  len = body.length
62
- [String.new(body[0, len / 2]), String.new(body[(len / 2)..-1])]
62
+ [body[0, len / 2], body[(len / 2)..]]
63
63
  end
64
64
  subject(:body) do
65
65
  inflater = HTTP::Response::Inflater.new(connection)
@@ -3,14 +3,14 @@
3
3
  RSpec.describe HTTP::Response::Parser do
4
4
  subject(:parser) { described_class.new }
5
5
  let(:raw_response) do
6
- "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMy-Header: val\r\nEmpty-Header: \r\n\r\n{}"
6
+ "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMyHeader: val\r\nEmptyHeader: \r\n\r\n{}"
7
7
  end
8
8
  let(:expected_headers) do
9
9
  {
10
10
  "Content-Length" => "2",
11
11
  "Content-Type" => "application/json",
12
- "My-Header" => "val",
13
- "Empty-Header" => ""
12
+ "MyHeader" => "val",
13
+ "EmptyHeader" => ""
14
14
  }
15
15
  end
16
16
  let(:expected_body) { "{}" }
@@ -32,7 +32,7 @@ RSpec.describe HTTP::Response::Parser do
32
32
  end
33
33
 
34
34
  context "response in many parts" do
35
- let(:parts) { raw_response.split(//) }
35
+ let(:parts) { raw_response.chars }
36
36
 
37
37
  it "parses headers" do
38
38
  expect(subject.headers.to_h).to eq(expected_headers)
@@ -42,4 +42,33 @@ RSpec.describe HTTP::Response::Parser do
42
42
  expect(subject.read(expected_body.size)).to eq(expected_body)
43
43
  end
44
44
  end
45
+
46
+ context "when got 100 Continue response" do
47
+ let :raw_response do
48
+ "HTTP/1.1 100 Continue\r\n\r\n" \
49
+ "HTTP/1.1 200 OK\r\n" \
50
+ "Content-Length: 12\r\n\r\n" \
51
+ "Hello World!"
52
+ end
53
+
54
+ context "when response is feeded in one part" do
55
+ let(:parts) { [raw_response] }
56
+
57
+ it "skips to next non-info response" do
58
+ expect(subject.status_code).to eq(200)
59
+ expect(subject.headers).to eq("Content-Length" => "12")
60
+ expect(subject.read(12)).to eq("Hello World!")
61
+ end
62
+ end
63
+
64
+ context "when response is feeded in many parts" do
65
+ let(:parts) { raw_response.chars }
66
+
67
+ it "skips to next non-info response" do
68
+ expect(subject.status_code).to eq(200)
69
+ expect(subject.headers).to eq("Content-Length" => "12")
70
+ expect(subject.read(12)).to eq("Hello World!")
71
+ end
72
+ end
73
+ end
45
74
  end