http 0.5.1 → 0.6.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of http might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +3 -3
- data/.rspec +3 -2
- data/.rubocop.yml +101 -0
- data/.travis.yml +19 -8
- data/Gemfile +24 -6
- data/LICENSE.txt +1 -1
- data/README.md +144 -29
- data/Rakefile +23 -1
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +14 -14
- data/lib/http.rb +5 -4
- data/lib/http/authorization_header.rb +37 -0
- data/lib/http/authorization_header/basic_auth.rb +24 -0
- data/lib/http/authorization_header/bearer_token.rb +29 -0
- data/lib/http/backports.rb +2 -0
- data/lib/http/backports/base64.rb +6 -0
- data/lib/http/{uri_backport.rb → backports/uri.rb} +10 -10
- data/lib/http/chainable.rb +24 -25
- data/lib/http/client.rb +97 -67
- data/lib/http/content_type.rb +27 -0
- data/lib/http/errors.rb +13 -0
- data/lib/http/headers.rb +154 -0
- data/lib/http/headers/mixin.rb +11 -0
- data/lib/http/mime_type.rb +61 -36
- data/lib/http/mime_type/adapter.rb +24 -0
- data/lib/http/mime_type/json.rb +23 -0
- data/lib/http/options.rb +21 -48
- data/lib/http/redirector.rb +12 -7
- data/lib/http/request.rb +82 -33
- data/lib/http/request/writer.rb +79 -0
- data/lib/http/response.rb +39 -68
- data/lib/http/response/body.rb +62 -0
- data/lib/http/{response_parser.rb → response/parser.rb} +3 -1
- data/lib/http/version.rb +1 -1
- data/logo.png +0 -0
- data/spec/http/authorization_header/basic_auth_spec.rb +29 -0
- data/spec/http/authorization_header/bearer_token_spec.rb +36 -0
- data/spec/http/authorization_header_spec.rb +41 -0
- data/spec/http/backports/base64_spec.rb +13 -0
- data/spec/http/client_spec.rb +181 -0
- data/spec/http/content_type_spec.rb +47 -0
- data/spec/http/headers/mixin_spec.rb +36 -0
- data/spec/http/headers_spec.rb +417 -0
- data/spec/http/options/body_spec.rb +6 -7
- data/spec/http/options/form_spec.rb +4 -5
- data/spec/http/options/headers_spec.rb +9 -17
- data/spec/http/options/json_spec.rb +17 -0
- data/spec/http/options/merge_spec.rb +18 -19
- data/spec/http/options/new_spec.rb +5 -19
- data/spec/http/options/proxy_spec.rb +6 -6
- data/spec/http/options_spec.rb +3 -9
- data/spec/http/redirector_spec.rb +100 -0
- data/spec/http/request/writer_spec.rb +25 -0
- data/spec/http/request_spec.rb +54 -14
- data/spec/http/response/body_spec.rb +24 -0
- data/spec/http/response_spec.rb +61 -32
- data/spec/http_spec.rb +77 -86
- data/spec/spec_helper.rb +25 -2
- data/spec/support/example_server.rb +58 -49
- data/spec/support/proxy_server.rb +27 -11
- metadata +60 -55
- data/lib/http/header.rb +0 -11
- data/lib/http/mime_types/json.rb +0 -19
- data/lib/http/request_stream.rb +0 -77
- data/spec/http/options/callbacks_spec.rb +0 -62
- data/spec/http/options/response_spec.rb +0 -24
- data/spec/http/request_stream_spec.rb +0 -25
@@ -18,7 +18,7 @@ module HTTP
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def http_version
|
21
|
-
@parser.http_version.join(
|
21
|
+
@parser.http_version.join('.')
|
22
22
|
end
|
23
23
|
|
24
24
|
def status_code
|
@@ -53,6 +53,8 @@ module HTTP
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def reset
|
56
|
+
@parser.reset!
|
57
|
+
|
56
58
|
@finished = false
|
57
59
|
@headers = nil
|
58
60
|
@chunk = nil
|
data/lib/http/version.rb
CHANGED
data/logo.png
ADDED
Binary file
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::AuthorizationHeader::BasicAuth do
|
4
|
+
describe '.new' do
|
5
|
+
it 'fails when options is not a Hash' do
|
6
|
+
expect { described_class.new '[FOOBAR]' }.to raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'fails when :pass is not given' do
|
10
|
+
expect { described_class.new :user => '[USER]' }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'fails when :user is not given' do
|
14
|
+
expect { described_class.new :pass => '[PASS]' }.to raise_error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#to_s' do
|
19
|
+
let(:user) { 'foo' }
|
20
|
+
let(:pass) { 'bar' * 100 }
|
21
|
+
let(:user_n_pass) { user + ':' + pass }
|
22
|
+
let(:builder) { described_class.new :user => user, :pass => pass }
|
23
|
+
|
24
|
+
subject { builder.to_s }
|
25
|
+
|
26
|
+
it { should eq "Basic #{Base64.strict_encode64 user_n_pass}" }
|
27
|
+
it { should match(/^Basic [^\s]+$/) }
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::AuthorizationHeader::BearerToken do
|
4
|
+
describe '.new' do
|
5
|
+
it 'fails when options is not a Hash' do
|
6
|
+
expect { described_class.new '[TOKEN]' }.to raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'fails when :token is not given' do
|
10
|
+
expect { described_class.new :encode => true }.to raise_error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#to_s' do
|
15
|
+
let(:token) { 'foobar' * 100 }
|
16
|
+
let(:builder) { described_class.new options.merge :token => token }
|
17
|
+
|
18
|
+
subject { builder.to_s }
|
19
|
+
|
20
|
+
context 'when :encode => true' do
|
21
|
+
let(:options) { {:encode => true} }
|
22
|
+
it { should eq "Bearer #{Base64.strict_encode64 token}" }
|
23
|
+
it { should match(/^Bearer [^\s]+$/) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when :encode => false' do
|
27
|
+
let(:options) { {:encode => false} }
|
28
|
+
it { should eq "Bearer #{token}" }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when :encode not specified' do
|
32
|
+
let(:options) { {} }
|
33
|
+
it { should eq "Bearer #{token}" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::AuthorizationHeader do
|
4
|
+
describe '.build' do
|
5
|
+
context 'with unkown type' do
|
6
|
+
let(:type) { :foobar }
|
7
|
+
let(:opts) { {:foo => :bar} }
|
8
|
+
|
9
|
+
it 'fails' do
|
10
|
+
expect { described_class.build type, opts }.to raise_error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'with :basic type' do
|
15
|
+
let(:type) { :basic }
|
16
|
+
let(:opts) { {:user => 'user', :pass => 'pass'} }
|
17
|
+
|
18
|
+
it 'passes options to BasicAuth' do
|
19
|
+
expect(described_class::BasicAuth).to receive(:new).with(opts)
|
20
|
+
described_class.build type, opts
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with :bearer type' do
|
25
|
+
let(:type) { :bearer }
|
26
|
+
let(:opts) { {:token => 'token', :encode => true} }
|
27
|
+
|
28
|
+
it 'passes options to BearerToken' do
|
29
|
+
expect(described_class::BearerToken).to receive(:new).with(opts)
|
30
|
+
described_class.build type, opts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '.register' do
|
36
|
+
it 'registers given klass in builders registry' do
|
37
|
+
described_class.register :dummy, Class.new { def initialize(*); end }
|
38
|
+
expect { described_class.build(:dummy, 'foobar') }.to_not raise_error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Base64 do
|
4
|
+
specify { expect(Base64).to respond_to :strict_encode64 }
|
5
|
+
|
6
|
+
describe '.strict_encode64' do
|
7
|
+
let(:long_string) { (0...256).map { ('a'..'z').to_a[rand(26)] }.join }
|
8
|
+
|
9
|
+
it 'returns a String without whitespaces' do
|
10
|
+
expect(Base64.strict_encode64 long_string).to_not match(/\s/)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::Client do
|
4
|
+
StubbedClient = Class.new(HTTP::Client) do
|
5
|
+
def perform_without_following_redirects(request, options)
|
6
|
+
stubs.fetch(request.uri.to_s) { super(request, options) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def stubs
|
10
|
+
@stubs ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def stub(stubs)
|
14
|
+
@stubs = stubs
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def redirect_response(location, status = 302)
|
20
|
+
HTTP::Response.new(status, '1.1', {'Location' => location}, '')
|
21
|
+
end
|
22
|
+
|
23
|
+
def simple_response(body, status = 200)
|
24
|
+
HTTP::Response.new(status, '1.1', {}, body)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'following redirects' do
|
28
|
+
it 'returns response of new location' do
|
29
|
+
client = StubbedClient.new(:follow => true).stub(
|
30
|
+
'http://example.com/' => redirect_response('http://example.com/blog'),
|
31
|
+
'http://example.com/blog' => simple_response('OK')
|
32
|
+
)
|
33
|
+
|
34
|
+
expect(client.get('http://example.com/').to_s).to eq 'OK'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'prepends previous request uri scheme and host if needed' do
|
38
|
+
client = StubbedClient.new(:follow => true).stub(
|
39
|
+
'http://example.com/' => redirect_response('/index'),
|
40
|
+
'http://example.com/index' => redirect_response('/index.html'),
|
41
|
+
'http://example.com/index.html' => simple_response('OK')
|
42
|
+
)
|
43
|
+
|
44
|
+
expect(client.get('http://example.com/').to_s).to eq 'OK'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'fails upon endless redirects' do
|
48
|
+
client = StubbedClient.new(:follow => true).stub(
|
49
|
+
'http://example.com/' => redirect_response('/')
|
50
|
+
)
|
51
|
+
|
52
|
+
expect { client.get('http://example.com/') } \
|
53
|
+
.to raise_error(HTTP::Redirector::EndlessRedirectError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'fails if max amount of hops reached' do
|
57
|
+
client = StubbedClient.new(:follow => 5).stub(
|
58
|
+
'http://example.com/' => redirect_response('/1'),
|
59
|
+
'http://example.com/1' => redirect_response('/2'),
|
60
|
+
'http://example.com/2' => redirect_response('/3'),
|
61
|
+
'http://example.com/3' => redirect_response('/4'),
|
62
|
+
'http://example.com/4' => redirect_response('/5'),
|
63
|
+
'http://example.com/5' => redirect_response('/6'),
|
64
|
+
'http://example.com/6' => simple_response('OK')
|
65
|
+
)
|
66
|
+
|
67
|
+
expect { client.get('http://example.com/') } \
|
68
|
+
.to raise_error(HTTP::Redirector::TooManyRedirectsError)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'parsing params' do
|
73
|
+
it 'accepts params within the provided URL' do
|
74
|
+
client = HTTP::Client.new
|
75
|
+
allow(client).to receive(:perform)
|
76
|
+
expect(HTTP::Request).to receive(:new) do |_, uri|
|
77
|
+
params = CGI.parse(URI(uri).query)
|
78
|
+
expect(params).to eq('foo' => ['bar'])
|
79
|
+
end
|
80
|
+
|
81
|
+
client.get('http://example.com/?foo=bar')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'combines GET params from the URI with the passed in params' do
|
85
|
+
client = HTTP::Client.new
|
86
|
+
allow(client).to receive(:perform)
|
87
|
+
expect(HTTP::Request).to receive(:new) do |_, uri|
|
88
|
+
params = CGI.parse(URI(uri).query)
|
89
|
+
expect(params).to eq('foo' => ['bar'], 'baz' => ['quux'])
|
90
|
+
end
|
91
|
+
|
92
|
+
client.get('http://example.com/?foo=bar', :params => {:baz => 'quux'})
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'passing json' do
|
97
|
+
it 'encodes given object' do
|
98
|
+
client = HTTP::Client.new
|
99
|
+
allow(client).to receive(:perform)
|
100
|
+
|
101
|
+
expect(HTTP::Request).to receive(:new) do |*args|
|
102
|
+
expect(args.last).to eq('{"foo":"bar"}')
|
103
|
+
end
|
104
|
+
|
105
|
+
client.get('http://example.com/', :json => {:foo => :bar})
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#request' do
|
110
|
+
context 'with explicitly given `Host` header' do
|
111
|
+
let(:headers) { {'Host' => 'another.example.com'} }
|
112
|
+
let(:client) { described_class.new :headers => headers }
|
113
|
+
|
114
|
+
it 'keeps `Host` header as is' do
|
115
|
+
expect(client).to receive(:perform) do |req, options|
|
116
|
+
expect(req['Host']).to eq 'another.example.com'
|
117
|
+
end
|
118
|
+
|
119
|
+
client.request(:get, 'http://example.com/')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#perform' do
|
125
|
+
let(:client) { described_class.new }
|
126
|
+
|
127
|
+
it 'calls finish_response before actual performance' do
|
128
|
+
TCPSocket.stub(:open) { throw :halt }
|
129
|
+
expect(client).to receive(:finish_response)
|
130
|
+
catch(:halt) { client.head "http://127.0.0.1:#{ExampleService::PORT}/" }
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'calls finish_response once body was fully flushed' do
|
134
|
+
expect(client).to receive(:finish_response).twice.and_call_original
|
135
|
+
client.get("http://127.0.0.1:#{ExampleService::PORT}/").to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with HEAD request' do
|
139
|
+
it 'does not iterates through body' do
|
140
|
+
expect(client).to_not receive(:readpartial)
|
141
|
+
client.head("http://127.0.0.1:#{ExampleService::PORT}/")
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'finishes response after headers were received' do
|
145
|
+
expect(client).to receive(:finish_response).twice.and_call_original
|
146
|
+
client.head("http://127.0.0.1:#{ExampleService::PORT}/")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'when server fully flushes response in one chunk' do
|
151
|
+
before do
|
152
|
+
socket_spy = double
|
153
|
+
|
154
|
+
chunks = [
|
155
|
+
<<-RESPONSE.gsub(/^\s*\| */, '').gsub(/\n/, "\r\n")
|
156
|
+
| HTTP/1.1 200 OK
|
157
|
+
| Content-Type: text/html
|
158
|
+
| Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-11-22)
|
159
|
+
| Date: Mon, 24 Mar 2014 00:32:22 GMT
|
160
|
+
| Content-Length: 15
|
161
|
+
| Connection: Keep-Alive
|
162
|
+
|
|
163
|
+
| <!doctype html>
|
164
|
+
RESPONSE
|
165
|
+
]
|
166
|
+
|
167
|
+
socket_spy.stub(:close) { nil }
|
168
|
+
socket_spy.stub(:closed?) { true }
|
169
|
+
socket_spy.stub(:readpartial) { chunks.shift }
|
170
|
+
socket_spy.stub(:<<) { nil }
|
171
|
+
|
172
|
+
TCPSocket.stub(:open) { socket_spy }
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'properly reads body' do
|
176
|
+
body = client.get("http://127.0.0.1:#{ExampleService::PORT}/").to_s
|
177
|
+
expect(body).to eq '<!doctype html>'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::ContentType do
|
4
|
+
describe '.parse' do
|
5
|
+
context 'with text/plain' do
|
6
|
+
subject { described_class.parse 'text/plain' }
|
7
|
+
its(:mime_type) { should eq 'text/plain' }
|
8
|
+
its(:charset) { should be_nil }
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'with tEXT/plaIN' do
|
12
|
+
subject { described_class.parse 'tEXT/plaIN' }
|
13
|
+
its(:mime_type) { should eq 'text/plain' }
|
14
|
+
its(:charset) { should be_nil }
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with text/plain; charset=utf-8' do
|
18
|
+
subject { described_class.parse 'text/plain; charset=utf-8' }
|
19
|
+
its(:mime_type) { should eq 'text/plain' }
|
20
|
+
its(:charset) { should eq 'utf-8' }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with text/plain; charset="utf-8"' do
|
24
|
+
subject { described_class.parse 'text/plain; charset="utf-8"' }
|
25
|
+
its(:mime_type) { should eq 'text/plain' }
|
26
|
+
its(:charset) { should eq 'utf-8' }
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with text/plain; charSET=utf-8' do
|
30
|
+
subject { described_class.parse 'text/plain; charSET=utf-8' }
|
31
|
+
its(:mime_type) { should eq 'text/plain' }
|
32
|
+
its(:charset) { should eq 'utf-8' }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with text/plain; foo=bar; charset=utf-8' do
|
36
|
+
subject { described_class.parse 'text/plain; foo=bar; charset=utf-8' }
|
37
|
+
its(:mime_type) { should eq 'text/plain' }
|
38
|
+
its(:charset) { should eq 'utf-8' }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with text/plain;charset=utf-8;foo=bar' do
|
42
|
+
subject { described_class.parse 'text/plain;charset=utf-8;foo=bar' }
|
43
|
+
its(:mime_type) { should eq 'text/plain' }
|
44
|
+
its(:charset) { should eq 'utf-8' }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::Headers::Mixin do
|
4
|
+
let :dummy_class do
|
5
|
+
Class.new do
|
6
|
+
include HTTP::Headers::Mixin
|
7
|
+
|
8
|
+
def initialize(headers)
|
9
|
+
@headers = headers
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:headers) { HTTP::Headers.new }
|
15
|
+
let(:dummy) { dummy_class.new headers }
|
16
|
+
|
17
|
+
describe '#headers' do
|
18
|
+
it 'returns @headers instance variable' do
|
19
|
+
expect(dummy.headers).to be headers
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#[]' do
|
24
|
+
it 'proxies to headers#[]' do
|
25
|
+
expect(headers).to receive(:[]).with(:accept)
|
26
|
+
dummy[:accept]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#[]=' do
|
31
|
+
it 'proxies to headers#[]' do
|
32
|
+
expect(headers).to receive(:[]=).with(:accept, 'text/plain')
|
33
|
+
dummy[:accept] = 'text/plain'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,417 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HTTP::Headers do
|
4
|
+
subject(:headers) { described_class.new }
|
5
|
+
|
6
|
+
it 'is Enumerable' do
|
7
|
+
expect(headers).to be_an Enumerable
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#set' do
|
11
|
+
it 'sets header value' do
|
12
|
+
headers.set 'Accept', 'application/json'
|
13
|
+
expect(headers['Accept']).to eq 'application/json'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'normalizes header name' do
|
17
|
+
headers.set :content_type, 'application/json'
|
18
|
+
expect(headers['Content-Type']).to eq 'application/json'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'overwrites previous value' do
|
22
|
+
headers.set :set_cookie, 'hoo=ray'
|
23
|
+
headers.set :set_cookie, 'woo=hoo'
|
24
|
+
expect(headers['Set-Cookie']).to eq 'woo=hoo'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'allows set multiple values' do
|
28
|
+
headers.set :set_cookie, 'hoo=ray'
|
29
|
+
headers.set :set_cookie, %w[hoo=ray woo=hoo]
|
30
|
+
expect(headers['Set-Cookie']).to eq %w[hoo=ray woo=hoo]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#[]=' do
|
35
|
+
it 'sets header value' do
|
36
|
+
headers['Accept'] = 'application/json'
|
37
|
+
expect(headers['Accept']).to eq 'application/json'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'normalizes header name' do
|
41
|
+
headers[:content_type] = 'application/json'
|
42
|
+
expect(headers['Content-Type']).to eq 'application/json'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'overwrites previous value' do
|
46
|
+
headers[:set_cookie] = 'hoo=ray'
|
47
|
+
headers[:set_cookie] = 'woo=hoo'
|
48
|
+
expect(headers['Set-Cookie']).to eq 'woo=hoo'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'allows set multiple values' do
|
52
|
+
headers[:set_cookie] = 'hoo=ray'
|
53
|
+
headers[:set_cookie] = %w[hoo=ray woo=hoo]
|
54
|
+
expect(headers['Set-Cookie']).to eq %w[hoo=ray woo=hoo]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#delete' do
|
59
|
+
before { headers.set 'Content-Type', 'application/json' }
|
60
|
+
|
61
|
+
it 'removes given header' do
|
62
|
+
headers.delete 'Content-Type'
|
63
|
+
expect(headers['Content-Type']).to be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'normalizes header name' do
|
67
|
+
headers.delete :content_type
|
68
|
+
expect(headers['Content-Type']).to be_nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#add' do
|
73
|
+
it 'sets header value' do
|
74
|
+
headers.add 'Accept', 'application/json'
|
75
|
+
expect(headers['Accept']).to eq 'application/json'
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'normalizes header name' do
|
79
|
+
headers.add :content_type, 'application/json'
|
80
|
+
expect(headers['Content-Type']).to eq 'application/json'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'appends new value if header exists' do
|
84
|
+
headers.add :set_cookie, 'hoo=ray'
|
85
|
+
headers.add :set_cookie, 'woo=hoo'
|
86
|
+
expect(headers['Set-Cookie']).to eq %w[hoo=ray woo=hoo]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'allows append multiple values' do
|
90
|
+
headers.add :set_cookie, 'hoo=ray'
|
91
|
+
headers.add :set_cookie, %w[woo=hoo yup=pie]
|
92
|
+
expect(headers['Set-Cookie']).to eq %w[hoo=ray woo=hoo yup=pie]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#get' do
|
97
|
+
before { headers.set 'Content-Type', 'application/json' }
|
98
|
+
|
99
|
+
it 'returns array of associated values' do
|
100
|
+
expect(headers.get 'Content-Type').to eq %w[application/json]
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'normalizes header name' do
|
104
|
+
expect(headers.get :content_type).to eq %w[application/json]
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'when header does not exists' do
|
108
|
+
it 'returns empty array' do
|
109
|
+
expect(headers.get :accept).to eq []
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#[]' do
|
115
|
+
context 'when header does not exists' do
|
116
|
+
it 'returns nil' do
|
117
|
+
expect(headers[:accept]).to be_nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when header has a single value' do
|
122
|
+
before { headers.set 'Content-Type', 'application/json' }
|
123
|
+
|
124
|
+
it 'normalizes header name' do
|
125
|
+
expect(headers[:content_type]).to_not be_nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'returns it returns a single value' do
|
129
|
+
expect(headers[:content_type]).to eq 'application/json'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'when header has a multiple values' do
|
134
|
+
before do
|
135
|
+
headers.add :set_cookie, 'hoo=ray'
|
136
|
+
headers.add :set_cookie, 'woo=hoo'
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'normalizes header name' do
|
140
|
+
expect(headers[:set_cookie]).to_not be_nil
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'returns array of associated values' do
|
144
|
+
expect(headers[:set_cookie]).to eq %w[hoo=ray woo=hoo]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '#to_h' do
|
150
|
+
before do
|
151
|
+
headers.add :content_type, 'application/json'
|
152
|
+
headers.add :set_cookie, 'hoo=ray'
|
153
|
+
headers.add :set_cookie, 'woo=hoo'
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'returns a Hash' do
|
157
|
+
expect(headers.to_h).to be_a Hash
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'returns Hash with normalized keys' do
|
161
|
+
expect(headers.to_h.keys).to match_array %w[Content-Type Set-Cookie]
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'for a header with single value' do
|
165
|
+
it 'provides a value as is' do
|
166
|
+
expect(headers.to_h['Content-Type']).to eq 'application/json'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'for a header with multiple values' do
|
171
|
+
it 'provides an array of values' do
|
172
|
+
expect(headers.to_h['Set-Cookie']).to eq %w[hoo=ray woo=hoo]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe '#to_a' do
|
178
|
+
before do
|
179
|
+
headers.add :content_type, 'application/json'
|
180
|
+
headers.add :set_cookie, 'hoo=ray'
|
181
|
+
headers.add :set_cookie, 'woo=hoo'
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'returns an Array' do
|
185
|
+
expect(headers.to_a).to be_a Array
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'returns Array of key/value pairs with normalized keys' do
|
189
|
+
expect(headers.to_a).to eq [
|
190
|
+
%w[Content-Type application/json],
|
191
|
+
%w[Set-Cookie hoo=ray],
|
192
|
+
%w[Set-Cookie woo=hoo]
|
193
|
+
]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe '#inspect' do
|
198
|
+
before { headers.set :set_cookie, %w[hoo=ray woo=hoo] }
|
199
|
+
subject { headers.inspect }
|
200
|
+
|
201
|
+
it { should eq '#<HTTP::Headers {"Set-Cookie"=>["hoo=ray", "woo=hoo"]}>' }
|
202
|
+
end
|
203
|
+
|
204
|
+
describe '#keys' do
|
205
|
+
before do
|
206
|
+
headers.add :content_type, 'application/json'
|
207
|
+
headers.add :set_cookie, 'hoo=ray'
|
208
|
+
headers.add :set_cookie, 'woo=hoo'
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'returns uniq keys only' do
|
212
|
+
expect(headers.keys).to have_exactly(2).items
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'normalizes keys' do
|
216
|
+
expect(headers.keys).to include('Content-Type', 'Set-Cookie')
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe '#each' do
|
221
|
+
before do
|
222
|
+
headers.add :set_cookie, 'hoo=ray'
|
223
|
+
headers.add :content_type, 'application/json'
|
224
|
+
headers.add :set_cookie, 'woo=hoo'
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'yields each key/value pair separatedly' do
|
228
|
+
expect { |b| headers.each(&b) }.to yield_control.exactly(3).times
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'yields headers in the same order they were added' do
|
232
|
+
expect { |b| headers.each(&b) }.to yield_successive_args(
|
233
|
+
%w[Set-Cookie hoo=ray],
|
234
|
+
%w[Content-Type application/json],
|
235
|
+
%w[Set-Cookie woo=hoo]
|
236
|
+
)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe '.empty?' do
|
241
|
+
subject { headers.empty? }
|
242
|
+
|
243
|
+
context 'initially' do
|
244
|
+
it { should be_true }
|
245
|
+
end
|
246
|
+
|
247
|
+
context 'when header exists' do
|
248
|
+
before { headers.add :accept, 'text/plain' }
|
249
|
+
it { should be_false }
|
250
|
+
end
|
251
|
+
|
252
|
+
context 'when last header was removed' do
|
253
|
+
before do
|
254
|
+
headers.add :accept, 'text/plain'
|
255
|
+
headers.delete :accept
|
256
|
+
end
|
257
|
+
|
258
|
+
it { should be_true }
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe '#hash' do
|
263
|
+
let(:left) { described_class.new }
|
264
|
+
let(:right) { described_class.new }
|
265
|
+
|
266
|
+
it 'equals if two headers equals' do
|
267
|
+
left.add :accept, 'text/plain'
|
268
|
+
right.add :accept, 'text/plain'
|
269
|
+
|
270
|
+
expect(left.hash).to eq right.hash
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
describe '#==' do
|
275
|
+
let(:left) { described_class.new }
|
276
|
+
let(:right) { described_class.new }
|
277
|
+
|
278
|
+
it 'compares header keys and values' do
|
279
|
+
left.add :accept, 'text/plain'
|
280
|
+
right.add :accept, 'text/plain'
|
281
|
+
|
282
|
+
expect(left).to eq right
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'allows comparison with Array of key/value pairs' do
|
286
|
+
left.add :accept, 'text/plain'
|
287
|
+
expect(left).to eq [%w[Accept text/plain]]
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'sensitive to headers order' do
|
291
|
+
left.add :accept, 'text/plain'
|
292
|
+
left.add :cookie, 'woo=hoo'
|
293
|
+
right.add :cookie, 'woo=hoo'
|
294
|
+
right.add :accept, 'text/plain'
|
295
|
+
|
296
|
+
expect(left).to_not eq right
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'sensitive to header values order' do
|
300
|
+
left.add :cookie, 'hoo=ray'
|
301
|
+
left.add :cookie, 'woo=hoo'
|
302
|
+
right.add :cookie, 'woo=hoo'
|
303
|
+
right.add :cookie, 'hoo=ray'
|
304
|
+
|
305
|
+
expect(left).to_not eq right
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
describe '#dup' do
|
310
|
+
before { headers.set :content_type, 'application/json' }
|
311
|
+
|
312
|
+
subject(:dupped) { headers.dup }
|
313
|
+
|
314
|
+
it { should be_a described_class }
|
315
|
+
it { should_not be headers }
|
316
|
+
|
317
|
+
it 'has headers copied' do
|
318
|
+
expect(dupped[:content_type]).to eq 'application/json'
|
319
|
+
end
|
320
|
+
|
321
|
+
context 'modifying a copy' do
|
322
|
+
before { dupped.set :content_type, 'text/plain' }
|
323
|
+
|
324
|
+
it 'modifies dupped copy' do
|
325
|
+
expect(dupped[:content_type]).to eq 'text/plain'
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'does not affects original headers' do
|
329
|
+
expect(headers[:content_type]).to eq 'application/json'
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
describe '#merge!' do
|
335
|
+
before do
|
336
|
+
headers.set :host, 'example.com'
|
337
|
+
headers.set :accept, 'application/json'
|
338
|
+
headers.merge! :accept => 'plain/text', :cookie => %w[hoo=ray woo=hoo]
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'leaves headers not presented in other as is' do
|
342
|
+
expect(headers[:host]).to eq 'example.com'
|
343
|
+
end
|
344
|
+
|
345
|
+
it 'overwrites existing values' do
|
346
|
+
expect(headers[:accept]).to eq 'plain/text'
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'appends other headers, not presented in base' do
|
350
|
+
expect(headers[:cookie]).to eq %w[hoo=ray woo=hoo]
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
describe '#merge' do
|
355
|
+
before do
|
356
|
+
headers.set :host, 'example.com'
|
357
|
+
headers.set :accept, 'application/json'
|
358
|
+
end
|
359
|
+
|
360
|
+
subject(:merged) do
|
361
|
+
headers.merge :accept => 'plain/text', :cookie => %w[hoo=ray woo=hoo]
|
362
|
+
end
|
363
|
+
|
364
|
+
it { should be_a described_class }
|
365
|
+
it { should_not be headers }
|
366
|
+
|
367
|
+
it 'does not affects original headers' do
|
368
|
+
expect(merged.to_h).to_not eq headers.to_h
|
369
|
+
end
|
370
|
+
|
371
|
+
it 'leaves headers not presented in other as is' do
|
372
|
+
expect(merged[:host]).to eq 'example.com'
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'overwrites existing values' do
|
376
|
+
expect(merged[:accept]).to eq 'plain/text'
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'appends other headers, not presented in base' do
|
380
|
+
expect(merged[:cookie]).to eq %w[hoo=ray woo=hoo]
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
describe '.coerce' do
|
385
|
+
let(:dummyClass) { Class.new { def respond_to?(*); end } }
|
386
|
+
|
387
|
+
it 'accepts any object that respond to #to_hash' do
|
388
|
+
hashie = double :to_hash => {'accept' => 'json'}
|
389
|
+
expect(described_class.coerce(hashie)['accept']).to eq 'json'
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'accepts any object that respond to #to_h' do
|
393
|
+
hashie = double :to_h => {'accept' => 'json'}
|
394
|
+
expect(described_class.coerce(hashie)['accept']).to eq 'json'
|
395
|
+
end
|
396
|
+
|
397
|
+
it 'accepts any object that respond to #to_a' do
|
398
|
+
hashie = double :to_a => [%w[accept json]]
|
399
|
+
expect(described_class.coerce(hashie)['accept']).to eq 'json'
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'fails if given object cannot be coerced' do
|
403
|
+
expect { described_class.coerce dummyClass.new }.to raise_error HTTP::Error
|
404
|
+
end
|
405
|
+
|
406
|
+
context 'with duplicate header keys (mixed case)' do
|
407
|
+
let(:headers) { {'Set-Cookie' => 'hoo=ray', 'set-cookie' => 'woo=hoo'} }
|
408
|
+
|
409
|
+
it 'adds all headers' do
|
410
|
+
expect(described_class.coerce(headers).to_a).to match_array([
|
411
|
+
%w[Set-Cookie hoo=ray],
|
412
|
+
%w[Set-Cookie woo=hoo]
|
413
|
+
])
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|