sitehub 0.4.3 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +31 -0
  3. data/.gitignore +2 -1
  4. data/.reek +41 -0
  5. data/.simplecov +7 -0
  6. data/Gemfile.lock +61 -33
  7. data/README.md +4 -0
  8. data/Rakefile +1 -1
  9. data/circle.yml +1 -1
  10. data/lib/sitehub/builder.rb +19 -36
  11. data/lib/sitehub/collection/split_route_collection.rb +18 -13
  12. data/lib/sitehub/collection/split_route_collection/split.rb +6 -4
  13. data/lib/sitehub/constants.rb +2 -1
  14. data/lib/sitehub/constants/http_header_keys.rb +2 -0
  15. data/lib/sitehub/constants/rack_http_header_keys.rb +2 -0
  16. data/lib/sitehub/cookie.rb +4 -13
  17. data/lib/sitehub/cookie/attribute.rb +10 -9
  18. data/lib/sitehub/cookie/flag.rb +5 -8
  19. data/lib/sitehub/cookie_rewriting.rb +12 -5
  20. data/lib/sitehub/downstream_client.rb +37 -0
  21. data/lib/sitehub/equality.rb +28 -0
  22. data/lib/sitehub/forward_proxy.rb +19 -62
  23. data/lib/sitehub/forward_proxy_builder.rb +70 -49
  24. data/lib/sitehub/getter_setter_methods.rb +21 -0
  25. data/lib/sitehub/http_headers.rb +45 -48
  26. data/lib/sitehub/location_rewriter.rb +29 -0
  27. data/lib/sitehub/location_rewriters.rb +23 -0
  28. data/lib/sitehub/memoize.rb +25 -0
  29. data/lib/sitehub/middleware.rb +16 -6
  30. data/lib/sitehub/middleware/error_handling.rb +20 -0
  31. data/lib/sitehub/middleware/forward_proxies.rb +54 -0
  32. data/lib/sitehub/{logging.rb → middleware/logging.rb} +0 -0
  33. data/lib/sitehub/middleware/logging/access_logger.rb +36 -0
  34. data/lib/sitehub/middleware/logging/error_logger.rb +38 -0
  35. data/lib/sitehub/middleware/logging/log_entry.rb +16 -0
  36. data/lib/sitehub/middleware/logging/log_stash.rb +12 -0
  37. data/lib/sitehub/middleware/logging/log_wrapper.rb +24 -0
  38. data/lib/sitehub/middleware/logging/request_log.rb +74 -0
  39. data/lib/sitehub/middleware/reverse_proxy.rb +37 -0
  40. data/lib/sitehub/middleware/transaction_id.rb +18 -0
  41. data/lib/sitehub/nil_location_rewriter.rb +7 -0
  42. data/lib/sitehub/nil_proxy.rb +11 -0
  43. data/lib/sitehub/request.rb +101 -0
  44. data/lib/sitehub/request_mapping.rb +16 -18
  45. data/lib/sitehub/resolver.rb +1 -1
  46. data/lib/sitehub/response.rb +10 -0
  47. data/lib/sitehub/string_utils.rb +13 -0
  48. data/lib/sitehub/version.rb +1 -1
  49. data/sitehub.gemspec +4 -1
  50. data/spec/equality_spec.rb +32 -0
  51. data/spec/sitehub/builder_spec.rb +29 -22
  52. data/spec/sitehub/collection/route_collection_spec.rb +15 -14
  53. data/spec/sitehub/collection/split_route_collection/split_spec.rb +26 -0
  54. data/spec/sitehub/collection/split_route_collection_spec.rb +15 -3
  55. data/spec/sitehub/cookie/flag_spec.rb +1 -1
  56. data/spec/sitehub/cookie_rewriting_spec.rb +6 -10
  57. data/spec/sitehub/downstream_client_spec.rb +72 -0
  58. data/spec/sitehub/equality_spec.rb +32 -0
  59. data/spec/sitehub/forward_proxy_builder_spec.rb +92 -55
  60. data/spec/sitehub/forward_proxy_spec.rb +29 -97
  61. data/spec/sitehub/http_headers_spec.rb +32 -52
  62. data/spec/sitehub/integration_spec.rb +1 -1
  63. data/spec/sitehub/location_rewriter_spec.rb +46 -0
  64. data/spec/sitehub/{path_directives_spec.rb → location_rewriters_spec.rb} +8 -8
  65. data/spec/sitehub/memoize_spec.rb +56 -0
  66. data/spec/sitehub/middleware/error_handling_spec.rb +34 -0
  67. data/spec/sitehub/middleware/forward_proxies_spec.rb +105 -0
  68. data/spec/sitehub/middleware/logging/access_logger_spec.rb +51 -0
  69. data/spec/sitehub/middleware/logging/error_logger_spec.rb +84 -0
  70. data/spec/sitehub/middleware/logging/log_entry_spec.rb +33 -0
  71. data/spec/sitehub/middleware/logging/log_stash_spec.rb +21 -0
  72. data/spec/sitehub/middleware/logging/log_wrapper_spec.rb +31 -0
  73. data/spec/sitehub/middleware/logging/request_log_spec.rb +108 -0
  74. data/spec/sitehub/middleware/reverse_proxy_spec.rb +113 -0
  75. data/spec/sitehub/middleware/transaction_id_spec.rb +30 -0
  76. data/spec/sitehub/middleware_spec.rb +23 -13
  77. data/spec/sitehub/nil_location_rewriter_spec.rb +10 -0
  78. data/spec/sitehub/nil_proxy_spec.rb +14 -0
  79. data/spec/sitehub/request_mapping_spec.rb +21 -23
  80. data/spec/sitehub/request_spec.rb +228 -0
  81. data/spec/sitehub/resolver_spec.rb +2 -5
  82. data/spec/sitehub/response_spec.rb +30 -0
  83. data/spec/spec_helper.rb +12 -6
  84. data/spec/support/async/middleware.rb +1 -0
  85. data/spec/support/patch/rack/response.rb +7 -5
  86. data/spec/support/shared_contexts.rb +3 -0
  87. data/spec/support/shared_contexts/http_proxy_rules_context.rb +36 -0
  88. data/spec/support/shared_contexts/middleware_context.rb +6 -6
  89. data/spec/support/shared_contexts/module_spec_context.rb +7 -0
  90. data/spec/support/shared_contexts/rack_request_context.rb +18 -0
  91. data/spec/support/shared_contexts/rack_test_context.rb +0 -1
  92. data/spec/support/shared_examples.rb +3 -0
  93. data/spec/support/shared_examples/memoized_helpers.rb +7 -0
  94. data/spec/support/shared_examples/prohibited_http_header_filter.rb +16 -0
  95. data/spec/support/silent_warnings.rb +1 -1
  96. data/tasks/code_quality.rake +6 -0
  97. metadata +99 -29
  98. data/lib/sitehub/forward_proxies.rb +0 -49
  99. data/lib/sitehub/logging/access_logger.rb +0 -78
  100. data/lib/sitehub/logging/error_logger.rb +0 -36
  101. data/lib/sitehub/logging/log_entry.rb +0 -15
  102. data/lib/sitehub/logging/log_stash.rb +0 -10
  103. data/lib/sitehub/logging/log_wrapper.rb +0 -23
  104. data/lib/sitehub/path_directive.rb +0 -32
  105. data/lib/sitehub/path_directives.rb +0 -22
  106. data/lib/sitehub/reverse_proxy.rb +0 -57
  107. data/lib/sitehub/string_sanitiser.rb +0 -7
  108. data/lib/sitehub/transaction_id.rb +0 -16
  109. data/spec/sitehub/error_handling_spec.rb +0 -20
  110. data/spec/sitehub/forward_proxies_spec.rb +0 -103
  111. data/spec/sitehub/logging/access_logger_spec.rb +0 -128
  112. data/spec/sitehub/logging/error_logger_spec.rb +0 -78
  113. data/spec/sitehub/logging/log_entry_spec.rb +0 -31
  114. data/spec/sitehub/logging/log_stash_spec.rb +0 -19
  115. data/spec/sitehub/logging/log_wrapper_spec.rb +0 -29
  116. data/spec/sitehub/path_directive_spec.rb +0 -47
  117. data/spec/sitehub/reverse_proxy_spec.rb +0 -111
  118. data/spec/sitehub/transaction_id_spec.rb +0 -28
  119. data/spec/support/async/response_handler.rb +0 -16
  120. data/spec/support/shared_contexts/async_context.rb +0 -14
@@ -0,0 +1,30 @@
1
+ require 'sitehub/middleware/transaction_id'
2
+
3
+ class SiteHub
4
+ module Middleware
5
+ describe TransactionId do
6
+ let(:transaction_id) { Constants::RackHttpHeaderKeys::TRANSACTION_ID }
7
+ subject do
8
+ described_class.new(proc {})
9
+ end
10
+ it 'adds a unique identifier to the request' do
11
+ uuid = UUID.generate(:compact)
12
+ expect(UUID).to receive(:generate).with(:compact).and_return(uuid)
13
+ env = {}
14
+ subject.call(env)
15
+
16
+ expect(env[transaction_id]).to eq(uuid)
17
+ end
18
+
19
+ context 'transaction id header already exists' do
20
+ it 'leaves it intact' do
21
+ expect(UUID).to_not receive(:generate)
22
+ env = { transaction_id => :exiting_id }
23
+ subject.call(env)
24
+
25
+ expect(env[transaction_id]).to eq(:exiting_id)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,13 +1,8 @@
1
1
  require 'sitehub/middleware'
2
2
  class SiteHub
3
3
  describe Middleware do
4
- include_context :middleware_test
5
-
6
- subject do
7
- Object.new.tap do |o|
8
- o.extend(described_class)
9
- end
10
- end
4
+ include_context :middleware_test, :module_spec
5
+ include_context :module_spec
11
6
 
12
7
  describe '#use' do
13
8
  it 'stores the middleware to be used by the forward proxies' do
@@ -18,6 +13,21 @@ class SiteHub
18
13
  end
19
14
  end
20
15
 
16
+ describe '#middleware?' do
17
+ context 'middleware defined' do
18
+ it 'returns true' do
19
+ subject.use :middleware
20
+ expect(subject.middleware?).to eq(true)
21
+ end
22
+ end
23
+
24
+ context 'no middleware defined' do
25
+ it 'returns true' do
26
+ expect(subject.middleware?).to eq(false)
27
+ end
28
+ end
29
+ end
30
+
21
31
  describe '#apply_middleware' do
22
32
  context 'middleware defined' do
23
33
  it 'wraps the supplied app in the middleware' do
@@ -28,15 +38,15 @@ class SiteHub
28
38
  end
29
39
 
30
40
  it 'wraps the supplied app in the middleware in the order they were supplied' do
31
- middleware_1 = create_middleware
32
- middleware_2 = create_middleware
33
- subject.use middleware_1
34
- subject.use middleware_2
41
+ first_middleware = create_middleware
42
+ second_middleware = create_middleware
43
+ subject.use first_middleware
44
+ subject.use second_middleware
35
45
 
36
46
  result = subject.apply_middleware(:app)
37
47
 
38
- expect(result).to be_a(middleware_1)
39
- expect(result).to be_using(middleware_2)
48
+ expect(result).to be_a(first_middleware)
49
+ expect(result).to be_using(second_middleware)
40
50
  end
41
51
 
42
52
  context 'args supplied' do
@@ -0,0 +1,10 @@
1
+ require 'sitehub/location_rewriters_spec'
2
+ class SiteHub
3
+ describe NilLocationRewriter do
4
+ describe 'apply' do
5
+ it 'returns the location parameter' do
6
+ expect(subject.apply(:location, :source_url)).to eq(:location)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ require 'sitehub/nil_proxy'
2
+ class SiteHub
3
+ describe NilProxy do
4
+ describe '#call' do
5
+ let(:app) do
6
+ described_class.new
7
+ end
8
+
9
+ it 'returns a 404' do
10
+ expect(get('/').status).to eq(404)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -2,34 +2,24 @@ require 'sitehub/request_mapping'
2
2
 
3
3
  class SiteHub
4
4
  describe RequestMapping do
5
+ let(:mapped_url) { 'http://downstream_url' }
6
+
7
+ subject do
8
+ described_class.new(source_url: 'http://upstream.com/articles/123',
9
+ mapped_url: mapped_url,
10
+ mapped_path: %r{/articles/(.*)})
11
+ end
12
+
5
13
  describe '#initialize' do
6
- let(:mapped_url) { 'http://downstream_url' }
7
- subject do
8
- described_class.new(source_url: 'http://upstream.com/articles/123',
9
- mapped_url: mapped_url,
10
- mapped_path: %r{/articles/(.*)})
11
- end
12
14
  it 'duplicates the mapped url as we mutate it' do
13
15
  expect(subject.mapped_url).to eq(mapped_url)
14
16
  expect(subject.mapped_url).to_not be(mapped_url)
15
17
  end
16
18
  end
17
19
 
18
- describe '#cookie_path' do
19
- subject do
20
- described_class.new(source_url: 'http://upstream.com/articles/123',
21
- mapped_url: 'http://downstream_url/$1/view',
22
- mapped_path: %r{/articles/(.*)})
23
- end
24
-
25
- context 'mapped_path is a regexp' do
26
- it 'returns the literal part of the mapped path' do
27
- expect(subject.cookie_path).to eq('/articles')
28
- end
29
- end
30
- end
31
-
32
20
  describe '#computed_uri' do
21
+ it_behaves_like 'a memoized helper'
22
+
33
23
  context 'mapped_path is a regexp' do
34
24
  subject do
35
25
  described_class.new(source_url: 'http://upstream.com/articles/123',
@@ -37,7 +27,7 @@ class SiteHub
37
27
  mapped_path: %r{/articles/(.*)})
38
28
  end
39
29
  it 'returns the computed uri' do
40
- expect(subject.computed_uri).to eq('http://downstream_url/123/view')
30
+ expect(subject.computed_uri).to eq(URI('http://downstream_url/123/view'))
41
31
  end
42
32
  end
43
33
 
@@ -49,7 +39,7 @@ class SiteHub
49
39
  end
50
40
 
51
41
  it 'returns the mapped url' do
52
- expect(subject.computed_uri).to eq('http://downstream_url/articles')
42
+ expect(subject.computed_uri).to eq(URI('http://downstream_url/articles'))
53
43
  end
54
44
  end
55
45
 
@@ -60,9 +50,17 @@ class SiteHub
60
50
  mapped_path: %r{/(.*)})
61
51
  end
62
52
  it 'keeps the querystring' do
63
- expect(subject.computed_uri).to eq('http://downstream_url/articles?param=value')
53
+ expect(subject.computed_uri).to eq(URI('http://downstream_url/articles?param=value'))
64
54
  end
65
55
  end
66
56
  end
57
+
58
+ describe '#host' do
59
+ it_behaves_like 'a memoized helper'
60
+
61
+ it 'returns the host' do
62
+ expect(subject.host).to eq('upstream.com')
63
+ end
64
+ end
67
65
  end
68
66
  end
@@ -0,0 +1,228 @@
1
+ # rubocop:disable Metrics/ClassLength
2
+ class SiteHub
3
+ describe Request do
4
+ HttpHeaderKeys = Constants::HttpHeaderKeys
5
+ RackHttpHeaderKeys = Constants::RackHttpHeaderKeys
6
+
7
+ include_context :rack_request
8
+ include_context :http_proxy_rules
9
+
10
+ let(:rack_env) { env_for(method: :get) }
11
+
12
+ subject(:request) do
13
+ described_class.new(env: rack_env)
14
+ end
15
+
16
+ describe '#initialize' do
17
+ it 'sets the time' do
18
+ time = Time.now
19
+ expect(subject.time).to eq(time)
20
+ end
21
+ end
22
+
23
+ describe '#map' do
24
+ it 'sets mapped_url and mapped_path' do
25
+ path = 'path'
26
+ url = 'url'
27
+ subject.map(path, url)
28
+ expect(subject.mapped_path).to be(path)
29
+ expect(subject.mapped_url).to be(url)
30
+ end
31
+ end
32
+
33
+ describe '#request_method' do
34
+ let(:rack_env) { env_for(method: :get) }
35
+
36
+ it_behaves_like 'a memoized helper'
37
+
38
+ it 'returns the request method' do
39
+ expect(subject.request_method).to be(:get)
40
+ end
41
+ end
42
+
43
+ describe '#body' do
44
+ it_behaves_like 'a memoized helper'
45
+
46
+ let(:rack_env) { env_for(method: :post, params_or_body: 'body') }
47
+ it 'returns the request body' do
48
+ expect(subject.body).to eq('body')
49
+ end
50
+ end
51
+
52
+ describe '#headers' do
53
+ it_behaves_like 'prohibited_header_filter' do
54
+ let(:rack_env) { format_http_to_rack_headers(prohibited_headers.merge(permitted_header => 'value')) }
55
+ subject do
56
+ request.headers
57
+ end
58
+ end
59
+
60
+ it_behaves_like 'a memoized helper'
61
+
62
+ let(:x_forwarded_host) { HttpHeaderKeys::X_FORWARDED_HOST_HEADER }
63
+
64
+ context 'host header' do
65
+ context 'request mapped' do
66
+ it 'is set to the host identified in mapped url' do
67
+ expected_uri = URI('http://host:9292/somewhere')
68
+ subject.map('/mapped_path', expected_uri.to_s)
69
+ expect(subject.headers[HttpHeaderKeys::HOST_HEADER]).to eq("#{expected_uri.host}:#{expected_uri.port}")
70
+ end
71
+ end
72
+ context 'request not mapped' do
73
+ it 'is not set' do
74
+ expect(subject.headers[HttpHeaderKeys::HOST_HEADER]).to be_nil
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'x-forwarded-host header' do
80
+ context 'header not present' do
81
+ it 'assigns it to the requested host' do
82
+ expect(subject.headers[x_forwarded_host]).to eq('example.org')
83
+ end
84
+ end
85
+
86
+ context 'header already present' do
87
+ let(:rack_env) { env_for(env: { format_as_rack_header_name(x_forwarded_host) => 'first.host,second.host' }) }
88
+
89
+ it 'appends the host to the existing value' do
90
+ expect(subject.headers[x_forwarded_host]).to eq('first.host,second.host,example.org')
91
+ end
92
+ end
93
+ end
94
+
95
+ # used for identifying the originating IP address of a request.
96
+ context 'x-forwarded-for' do
97
+ let(:x_forwarded_for) { HttpHeaderKeys::X_FORWARDED_FOR_HEADER }
98
+
99
+ context 'header not present' do
100
+ it 'introduces it assigned to the value the remote-addr http header' do
101
+ expect(subject.headers[x_forwarded_for]).to eq(rack_env['REMOTE_ADDR'])
102
+ end
103
+ end
104
+
105
+ context 'already present' do
106
+ let(:rack_env) { env_for(env: { format_as_rack_header_name(x_forwarded_for) => 'first_host_ip' }) }
107
+
108
+ it 'appends the value of the remote-addr header to it' do
109
+ expect(subject.headers[x_forwarded_for]).to eq("first_host_ip,#{rack_env['REMOTE_ADDR']}")
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '#mapping' do
116
+ it_behaves_like 'a memoized helper'
117
+
118
+ it 'returns a RequestMapping' do
119
+ mapped_url = 'source_url'
120
+ mapped_path = 'mapped_path'
121
+ subject.map(mapped_path, mapped_url)
122
+
123
+ expected_mapping = RequestMapping.new(source_url: subject.url, mapped_url: mapped_url, mapped_path: mapped_path)
124
+ expect(subject.mapping).to eq(expected_mapping)
125
+ end
126
+ end
127
+
128
+ describe '#mapped?' do
129
+ it_behaves_like 'a memoized helper'
130
+
131
+ context 'request has been mapped' do
132
+ it 'returns true' do
133
+ subject.map('mapped_path', 'mapped_url')
134
+
135
+ expect(subject.mapped?).to eq(true)
136
+ end
137
+ end
138
+
139
+ context 'request has not been mapped' do
140
+ it 'returns false' do
141
+ subject.map(nil, nil)
142
+ expect(subject.mapped?).to eq(false)
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#remote_user' do
148
+ let(:rack_env) { env_for(env: { 'REMOTE_USER' => 'user' }) }
149
+ it 'returns the value of REMOTE_USER rack header' do
150
+ expect(subject.remote_user).to eq('user')
151
+ end
152
+ end
153
+
154
+ describe '#transation_id' do
155
+ let(:transaction_id) { HttpHeaderKeys::TRANSACTION_ID }
156
+ let(:rack_env) { env_for(env: { format_as_rack_header_name(transaction_id) => :transaction_id }) }
157
+
158
+ it 'returns the value of transaction_id header' do
159
+ expect(subject.transaction_id).to eq(:transaction_id)
160
+ end
161
+ end
162
+
163
+ describe '#http_version' do
164
+ let(:http_version) { RackHttpHeaderKeys::HTTP_VERSION }
165
+ let(:rack_env) { env_for(env: { http_version => :version }) }
166
+
167
+ it 'returns the value of http_version header' do
168
+ expect(subject.http_version).to eq(:version)
169
+ end
170
+ end
171
+
172
+ describe '#source_address' do
173
+ context 'x-forwarded-for header set' do
174
+ let(:x_forwarded_for) { HttpHeaderKeys::X_FORWARDED_FOR_HEADER }
175
+ let(:address) { 'first_host_ip' }
176
+ let(:rack_env) { env_for(env: { format_as_rack_header_name(x_forwarded_for) => address }) }
177
+
178
+ it 'appends the value of the remote-addr header to it' do
179
+ expect(subject.source_address).to eq(address)
180
+ end
181
+ end
182
+
183
+ context 'x-forwarded-for header not set' do
184
+ it 'returns REMOTE_ADDR' do
185
+ expect(subject.source_address).to eq(rack_env['REMOTE_ADDR'])
186
+ end
187
+ end
188
+ end
189
+
190
+ describe '#url' do
191
+ it_behaves_like 'a memoized helper'
192
+
193
+ it 'returns the source url' do
194
+ expect(subject.url).to eq(subject.rack_request.url)
195
+ end
196
+ end
197
+
198
+ describe '#params' do
199
+ it_behaves_like 'a memoized helper'
200
+
201
+ let(:params) { { 'param' => 'value' } }
202
+ let(:rack_env) { env_for(params_or_body: params) }
203
+ it 'returns the request params' do
204
+ expect(subject.params).to eq(params)
205
+ end
206
+ end
207
+
208
+ describe '#path' do
209
+ it_behaves_like 'a memoized helper'
210
+
211
+ let(:path) { '/path' }
212
+ let(:rack_env) { env_for(path: path) }
213
+ it 'returns the request path' do
214
+ expect(subject.path).to eq(path)
215
+ end
216
+ end
217
+
218
+ describe '#query_string' do
219
+ it_behaves_like 'a memoized helper'
220
+
221
+ let(:query_string) { 'param=value' }
222
+ let(:rack_env) { env_for(path: "/?#{query_string}") }
223
+ it 'returns the request path' do
224
+ expect(subject.query_string).to eq(query_string)
225
+ end
226
+ end
227
+ end
228
+ end
@@ -1,11 +1,8 @@
1
1
  require 'sitehub/resolver'
2
2
  class SiteHub
3
3
  describe Resolver do
4
- subject do
5
- Object.new.tap do |o|
6
- o.extend(described_class)
7
- end
8
- end
4
+ include_context :module_spec
5
+
9
6
  describe '#resolve' do
10
7
  it 'returns self' do
11
8
  expect(subject.resolve).to be(subject)
@@ -0,0 +1,30 @@
1
+ class SiteHub
2
+ describe Response do
3
+ subject do
4
+ described_class.new([], 200, {})
5
+ end
6
+
7
+ it 'extends Rack::Response' do
8
+ expect(subject).to be_a(Rack::Response)
9
+ end
10
+
11
+ describe '#initialize' do
12
+ it 'sets the response time' do
13
+ expect(subject.time).to eq(Time.now)
14
+ end
15
+ end
16
+
17
+ describe 'time' do
18
+ it 'returns the same time every time' do
19
+ first_return = subject.time
20
+ expect(subject.time).to eq(first_return)
21
+ end
22
+ end
23
+
24
+ describe '#headers' do
25
+ it 'is an alias of header' do
26
+ expect(subject.method(:header)).to eq(subject.method(:headers))
27
+ end
28
+ end
29
+ end
30
+ end