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.

Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -3
  3. data/.rspec +3 -2
  4. data/.rubocop.yml +101 -0
  5. data/.travis.yml +19 -8
  6. data/Gemfile +24 -6
  7. data/LICENSE.txt +1 -1
  8. data/README.md +144 -29
  9. data/Rakefile +23 -1
  10. data/examples/parallel_requests_with_celluloid.rb +2 -2
  11. data/http.gemspec +14 -14
  12. data/lib/http.rb +5 -4
  13. data/lib/http/authorization_header.rb +37 -0
  14. data/lib/http/authorization_header/basic_auth.rb +24 -0
  15. data/lib/http/authorization_header/bearer_token.rb +29 -0
  16. data/lib/http/backports.rb +2 -0
  17. data/lib/http/backports/base64.rb +6 -0
  18. data/lib/http/{uri_backport.rb → backports/uri.rb} +10 -10
  19. data/lib/http/chainable.rb +24 -25
  20. data/lib/http/client.rb +97 -67
  21. data/lib/http/content_type.rb +27 -0
  22. data/lib/http/errors.rb +13 -0
  23. data/lib/http/headers.rb +154 -0
  24. data/lib/http/headers/mixin.rb +11 -0
  25. data/lib/http/mime_type.rb +61 -36
  26. data/lib/http/mime_type/adapter.rb +24 -0
  27. data/lib/http/mime_type/json.rb +23 -0
  28. data/lib/http/options.rb +21 -48
  29. data/lib/http/redirector.rb +12 -7
  30. data/lib/http/request.rb +82 -33
  31. data/lib/http/request/writer.rb +79 -0
  32. data/lib/http/response.rb +39 -68
  33. data/lib/http/response/body.rb +62 -0
  34. data/lib/http/{response_parser.rb → response/parser.rb} +3 -1
  35. data/lib/http/version.rb +1 -1
  36. data/logo.png +0 -0
  37. data/spec/http/authorization_header/basic_auth_spec.rb +29 -0
  38. data/spec/http/authorization_header/bearer_token_spec.rb +36 -0
  39. data/spec/http/authorization_header_spec.rb +41 -0
  40. data/spec/http/backports/base64_spec.rb +13 -0
  41. data/spec/http/client_spec.rb +181 -0
  42. data/spec/http/content_type_spec.rb +47 -0
  43. data/spec/http/headers/mixin_spec.rb +36 -0
  44. data/spec/http/headers_spec.rb +417 -0
  45. data/spec/http/options/body_spec.rb +6 -7
  46. data/spec/http/options/form_spec.rb +4 -5
  47. data/spec/http/options/headers_spec.rb +9 -17
  48. data/spec/http/options/json_spec.rb +17 -0
  49. data/spec/http/options/merge_spec.rb +18 -19
  50. data/spec/http/options/new_spec.rb +5 -19
  51. data/spec/http/options/proxy_spec.rb +6 -6
  52. data/spec/http/options_spec.rb +3 -9
  53. data/spec/http/redirector_spec.rb +100 -0
  54. data/spec/http/request/writer_spec.rb +25 -0
  55. data/spec/http/request_spec.rb +54 -14
  56. data/spec/http/response/body_spec.rb +24 -0
  57. data/spec/http/response_spec.rb +61 -32
  58. data/spec/http_spec.rb +77 -86
  59. data/spec/spec_helper.rb +25 -2
  60. data/spec/support/example_server.rb +58 -49
  61. data/spec/support/proxy_server.rb +27 -11
  62. metadata +60 -55
  63. data/lib/http/header.rb +0 -11
  64. data/lib/http/mime_types/json.rb +0 -19
  65. data/lib/http/request_stream.rb +0 -77
  66. data/spec/http/options/callbacks_spec.rb +0 -62
  67. data/spec/http/options/response_spec.rb +0 -24
  68. 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
@@ -1,3 +1,3 @@
1
1
  module HTTP
2
- VERSION = "0.5.1" unless defined?(HTTP::VERSION)
2
+ VERSION = '0.6.0.pre'
3
3
  end
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