puffing-billy 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile.lock +78 -66
- data/README.md +111 -6
- data/lib/billy.rb +6 -4
- data/lib/billy/cache.rb +71 -38
- data/lib/billy/config.rb +10 -2
- data/lib/billy/json_utils.rb +40 -0
- data/lib/billy/proxy.rb +4 -2
- data/lib/billy/proxy_connection.rb +85 -19
- data/lib/billy/proxy_request_stub.rb +1 -1
- data/lib/billy/railtie.rb +9 -0
- data/lib/billy/rspec.rb +0 -1
- data/lib/billy/version.rb +1 -1
- data/lib/puffing-billy.rb +2 -0
- data/lib/puffing-billy/rspec.rb +2 -0
- data/lib/tasks/billy.rake +87 -0
- data/puffing-billy.gemspec +2 -2
- data/spec/features/examples/facebook_api_spec.rb +2 -1
- data/spec/features/examples/tumblr_api_spec.rb +4 -4
- data/spec/lib/billy/cache_spec.rb +37 -0
- data/spec/lib/billy/proxy_request_stub_spec.rb +42 -32
- data/spec/lib/billy/resource_utils_spec.rb +55 -0
- data/spec/lib/proxy_spec.rb +233 -45
- data/spec/support/test_server.rb +44 -28
- metadata +55 -82
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Billy::JSONUtils do
|
4
|
+
describe 'sorting' do
|
5
|
+
describe '#sort_hash_keys' do
|
6
|
+
it 'sorts simple Hashes' do
|
7
|
+
data = {c: 'three',a: 'one',b: 'two'}
|
8
|
+
expected = {a: 'one',b: 'two',c: 'three'}
|
9
|
+
expect(Billy::JSONUtils::sort_hash_keys(data)).to eq expected
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'does not sort simple Arrays' do
|
13
|
+
data = [3,1,2,'two','three','one']
|
14
|
+
expect(Billy::JSONUtils::sort_hash_keys(data)).to eq data
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'does not sort multi-dimensional Arrays' do
|
18
|
+
data = [[3,2,1],[5,4,6],['b','c','a']]
|
19
|
+
expect(Billy::JSONUtils::sort_hash_keys(data)).to eq data
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'sorts multi-dimensional Hashes' do
|
23
|
+
data = {c: {l: 2,m: 3,k: 1},a: {f: 3,e: 2,d: 1},b: {i: 2,h: 1,j: 3}}
|
24
|
+
expected = {a: {d: 1,e: 2,f: 3},b: {h: 1,i: 2,j: 3},c: {k: 1,l: 2,m: 3}}
|
25
|
+
expect(Billy::JSONUtils::sort_hash_keys(data)).to eq expected
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sorts abnormal data structures' do
|
29
|
+
data = {b: [['b','c','a'],{ab: 5,aa: 4, ac: 6},[3,2,1],{ba: true,bc: false, bb: nil}],a: {f: 3,e: 2,d: 1}}
|
30
|
+
expected = {a: {d: 1,e: 2,f: 3},b: [['b','c','a'],{aa: 4,ab: 5,ac: 6},[3,2,1],{ba: true, bb: nil,bc: false}]}
|
31
|
+
expect(Billy::JSONUtils::sort_hash_keys(data)).to eq expected
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'sort_json' do
|
36
|
+
it 'sorts JSON' do
|
37
|
+
data = '{"c":"three","a":"one","b":"two"}'
|
38
|
+
expected = '{"a":"one","b":"two","c":"three"}'
|
39
|
+
expect(Billy::JSONUtils::sort_json(data)).to eq expected
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'json?' do
|
45
|
+
let(:json) { {a: '1'}.to_json }
|
46
|
+
let(:non_json) { 'Not JSON.' }
|
47
|
+
|
48
|
+
it 'identifies JSON' do
|
49
|
+
expect(Billy::JSONUtils::json?(json)).to be_true
|
50
|
+
end
|
51
|
+
it 'identifies non-JSON' do
|
52
|
+
expect(Billy::JSONUtils::json?(non_json)).to be_false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/lib/proxy_spec.rb
CHANGED
@@ -4,23 +4,23 @@ require 'resolv'
|
|
4
4
|
|
5
5
|
shared_examples_for 'a proxy server' do
|
6
6
|
it 'should proxy GET requests' do
|
7
|
-
http.get('/echo').body.
|
7
|
+
expect(http.get('/echo').body).to eql 'GET /echo'
|
8
8
|
end
|
9
9
|
|
10
10
|
it 'should proxy POST requests' do
|
11
|
-
http.post('/echo', :foo => 'bar').body.
|
11
|
+
expect(http.post('/echo', :foo => 'bar').body).to eql "POST /echo\nfoo=bar"
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'should proxy PUT requests' do
|
15
|
-
http.post('/echo', :foo => 'bar').body.
|
15
|
+
expect(http.post('/echo', :foo => 'bar').body).to eql "POST /echo\nfoo=bar"
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'should proxy HEAD requests' do
|
19
|
-
http.head('/echo').headers['HTTP-X-EchoServer'].
|
19
|
+
expect(http.head('/echo').headers['HTTP-X-EchoServer']).to eql 'HEAD /echo'
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'should proxy DELETE requests' do
|
23
|
-
http.delete('/echo').body.
|
23
|
+
expect(http.delete('/echo').body).to eql 'DELETE /echo'
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -28,31 +28,37 @@ shared_examples_for 'a request stub' do
|
|
28
28
|
it 'should stub GET requests' do
|
29
29
|
proxy.stub("#{url}/foo").
|
30
30
|
and_return(:text => 'hello, GET!')
|
31
|
-
http.get('/foo').body.
|
31
|
+
expect(http.get('/foo').body).to eql 'hello, GET!'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should stub GET response statuses' do
|
35
|
+
proxy.stub("#{url}/foo").
|
36
|
+
and_return(:code => 200)
|
37
|
+
expect(http.get('/foo').status).to eql 200
|
32
38
|
end
|
33
39
|
|
34
40
|
it 'should stub POST requests' do
|
35
41
|
proxy.stub("#{url}/bar", :method => :post).
|
36
42
|
and_return(:text => 'hello, POST!')
|
37
|
-
http.post('/bar', :foo => :bar).body.
|
43
|
+
expect(http.post('/bar', :foo => :bar).body).to eql 'hello, POST!'
|
38
44
|
end
|
39
45
|
|
40
46
|
it 'should stub PUT requests' do
|
41
47
|
proxy.stub("#{url}/baz", :method => :put).
|
42
48
|
and_return(:text => 'hello, PUT!')
|
43
|
-
http.put('/baz', :foo => :bar).body.
|
49
|
+
expect(http.put('/baz', :foo => :bar).body).to eql 'hello, PUT!'
|
44
50
|
end
|
45
51
|
|
46
52
|
it 'should stub HEAD requests' do
|
47
53
|
proxy.stub("#{url}/bap", :method => :head).
|
48
54
|
and_return(:headers => {'HTTP-X-Hello' => 'hello, HEAD!'})
|
49
|
-
http.head('/bap').headers['
|
55
|
+
expect(http.head('/bap').headers['http-x-hello']).to eql 'hello, HEAD!'
|
50
56
|
end
|
51
57
|
|
52
58
|
it 'should stub DELETE requests' do
|
53
59
|
proxy.stub("#{url}/bam", :method => :delete).
|
54
60
|
and_return(:text => 'hello, DELETE!')
|
55
|
-
http.delete('/bam').body.
|
61
|
+
expect(http.delete('/bam').body).to eql 'hello, DELETE!'
|
56
62
|
end
|
57
63
|
end
|
58
64
|
|
@@ -60,29 +66,39 @@ shared_examples_for 'a cache' do
|
|
60
66
|
|
61
67
|
context 'whitelisted GET requests' do
|
62
68
|
it 'should not be cached' do
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
69
|
+
assert_noncached_url
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'with ports' do
|
73
|
+
before do
|
74
|
+
rack_app_url = URI(http.url_prefix)
|
75
|
+
Billy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should not be cached ' do
|
79
|
+
assert_noncached_url
|
80
|
+
end
|
70
81
|
end
|
71
82
|
end
|
72
83
|
|
73
|
-
context '
|
84
|
+
context 'non-whitelisted GET requests' do
|
74
85
|
before do
|
75
86
|
Billy.config.whitelist = []
|
76
87
|
end
|
77
88
|
|
78
89
|
it 'should be cached' do
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
assert_cached_url
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'with ports' do
|
94
|
+
before do
|
95
|
+
rack_app_url = URI(http.url_prefix)
|
96
|
+
Billy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port+1}"]
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should be cached' do
|
100
|
+
assert_cached_url
|
101
|
+
end
|
86
102
|
end
|
87
103
|
end
|
88
104
|
|
@@ -93,7 +109,7 @@ shared_examples_for 'a cache' do
|
|
93
109
|
|
94
110
|
it 'should be cached' do
|
95
111
|
r = http.get('/analytics?some_param=5')
|
96
|
-
r.body.
|
112
|
+
expect(r.body).to eql 'GET /analytics'
|
97
113
|
expect {
|
98
114
|
expect {
|
99
115
|
r = http.get('/analytics?some_param=20')
|
@@ -102,9 +118,20 @@ shared_examples_for 'a cache' do
|
|
102
118
|
end
|
103
119
|
end
|
104
120
|
|
121
|
+
context 'path_blacklist GET requests' do
|
122
|
+
before do
|
123
|
+
Billy.config.path_blacklist = ['/api']
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should be cached' do
|
127
|
+
assert_cached_url('/api')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
105
131
|
context "cache persistence" do
|
132
|
+
let(:cached_key) { proxy.cache.key('get',"#{url}/foo","") }
|
106
133
|
let(:cached_file) do
|
107
|
-
f = "
|
134
|
+
f = cached_key + ".yml"
|
108
135
|
File.join(Billy.config.cache_path, f)
|
109
136
|
end
|
110
137
|
|
@@ -119,8 +146,99 @@ shared_examples_for 'a cache' do
|
|
119
146
|
|
120
147
|
it 'should persist' do
|
121
148
|
r = http.get('/foo')
|
122
|
-
File.exists?(cached_file).
|
149
|
+
expect(File.exists?(cached_file)).to be_true
|
123
150
|
end
|
151
|
+
|
152
|
+
it 'should be read initially from persistent cache' do
|
153
|
+
File.open(cached_file, 'w') do |f|
|
154
|
+
cached = {
|
155
|
+
:headers => {},
|
156
|
+
:content => "GET /foo cached"
|
157
|
+
}
|
158
|
+
f.write(cached.to_yaml(:Encoding => :Utf8))
|
159
|
+
end
|
160
|
+
|
161
|
+
r = http.get('/foo')
|
162
|
+
expect(r.body).to eql 'GET /foo cached'
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'cache_request_headers requests' do
|
166
|
+
it 'should not be cached by default' do
|
167
|
+
r = http.get('/foo')
|
168
|
+
saved_cache = Billy.proxy.cache.fetch_from_persistence(cached_key)
|
169
|
+
expect(saved_cache.keys).not_to include :request_headers
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'when enabled' do
|
173
|
+
before do
|
174
|
+
Billy.config.cache_request_headers = true
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should be cached' do
|
178
|
+
r = http.get('/foo')
|
179
|
+
saved_cache = Billy.proxy.cache.fetch_from_persistence(cached_key)
|
180
|
+
expect(saved_cache.keys).to include :request_headers
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'ignore_cache_port requests' do
|
186
|
+
it 'should be cached without port' do
|
187
|
+
r = http.get('/foo')
|
188
|
+
url = URI(r.env[:url])
|
189
|
+
saved_cache = Billy.proxy.cache.fetch_from_persistence(cached_key)
|
190
|
+
|
191
|
+
expect(saved_cache[:url]).to_not eql(url.to_s)
|
192
|
+
expect(saved_cache[:url]).to eql(url.to_s.gsub(":#{url.port}", ''))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'non_whitelisted_requests_disabled requests' do
|
197
|
+
before { Billy.config.non_whitelisted_requests_disabled = true }
|
198
|
+
|
199
|
+
it 'should raise error when disabled' do
|
200
|
+
#TODO: Suppress stderr output: https://gist.github.com/adamstegman/926858
|
201
|
+
expect{http.get('/foo')}.to raise_error(Faraday::Error::ConnectionFailed, "end of file reached")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context 'non_successful_cache_disabled requests' do
|
206
|
+
before do
|
207
|
+
rack_app_url = URI(http_error.url_prefix)
|
208
|
+
Billy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
|
209
|
+
Billy.config.non_successful_cache_disabled = true
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should not cache non-successful response when enabled' do
|
213
|
+
http_error.get('/foo')
|
214
|
+
expect(File.exists?(cached_file)).to be_false
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should cache successful response when enabled' do
|
218
|
+
assert_cached_url
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'non_successful_error_level requests' do
|
223
|
+
before do
|
224
|
+
rack_app_url = URI(http_error.url_prefix)
|
225
|
+
Billy.config.whitelist = ["#{rack_app_url.host}:#{rack_app_url.port}"]
|
226
|
+
Billy.config.non_successful_error_level = :error
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'should raise error for non-successful responses when :error' do
|
230
|
+
# When this config setting is set, the EventMachine running the test servers is killed upon error raising
|
231
|
+
# The `raise` is required to bubble up the error to the test running it
|
232
|
+
# The Faraday error is raised upon `close_connection` so this can be non-pending if we can do one of the following:
|
233
|
+
# 1) Remove the `raise error_message` conditionally for this test
|
234
|
+
# 2) Restart the test servers if they aren't running
|
235
|
+
# 3) Change the test servers to start/stop for each test instead of before all
|
236
|
+
# 4) Remove the test server completely and rely on the server instantiated by the app
|
237
|
+
pending "Unable to test this without affecting the running test servers"
|
238
|
+
expect{http_error.get('/foo')}.to raise_error(Faraday::Error::ConnectionFailed)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
124
242
|
end
|
125
243
|
|
126
244
|
context "disabled" do
|
@@ -128,24 +246,45 @@ shared_examples_for 'a cache' do
|
|
128
246
|
|
129
247
|
it 'shouldnt persist' do
|
130
248
|
r = http.get('/foo')
|
131
|
-
File.exists?(cached_file).
|
249
|
+
expect(File.exists?(cached_file)).to be_false
|
132
250
|
end
|
133
251
|
end
|
134
252
|
end
|
253
|
+
|
254
|
+
def assert_noncached_url(url = '/foo')
|
255
|
+
r = http.get(url)
|
256
|
+
expect(r.body).to eql "GET #{url}"
|
257
|
+
expect {
|
258
|
+
expect {
|
259
|
+
r = http.get(url)
|
260
|
+
}.to change { r.headers['HTTP-X-EchoCount'].to_i }.by(1)
|
261
|
+
}.to_not change { r.body }
|
262
|
+
end
|
263
|
+
|
264
|
+
def assert_cached_url(url = '/foo')
|
265
|
+
r = http.get(url)
|
266
|
+
expect(r.body).to eql "GET #{url}"
|
267
|
+
expect {
|
268
|
+
expect {
|
269
|
+
r = http.get(url)
|
270
|
+
}.to_not change { r.headers['HTTP-X-EchoCount'] }
|
271
|
+
}.to_not change { r.body }
|
272
|
+
end
|
135
273
|
end
|
136
274
|
|
137
275
|
describe Billy::Proxy do
|
138
276
|
|
139
277
|
before do
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
:
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
:
|
278
|
+
# Adding non-valid Faraday options throw an error: https://github.com/arsduo/koala/pull/311
|
279
|
+
# Valid options: :request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class
|
280
|
+
faraday_options = {
|
281
|
+
:proxy => { :uri => proxy.url },
|
282
|
+
:request => { :timeout => 0.5 }
|
283
|
+
}
|
284
|
+
|
285
|
+
@http = Faraday.new @http_url, faraday_options
|
286
|
+
@https = Faraday.new @https_url, faraday_options.merge(:ssl => { :verify => false })
|
287
|
+
@http_error = Faraday.new @error_url, faraday_options
|
149
288
|
end
|
150
289
|
|
151
290
|
context 'proxying' do
|
@@ -165,13 +304,13 @@ describe Billy::Proxy do
|
|
165
304
|
context 'stubbing' do
|
166
305
|
|
167
306
|
context 'HTTP' do
|
168
|
-
let!(:url)
|
307
|
+
let!(:url) { @http_url }
|
169
308
|
let!(:http) { @http }
|
170
309
|
it_should_behave_like 'a request stub'
|
171
310
|
end
|
172
311
|
|
173
312
|
context 'HTTPS' do
|
174
|
-
let!(:url)
|
313
|
+
let!(:url) { @https_url }
|
175
314
|
let!(:http) { @https }
|
176
315
|
it_should_behave_like 'a request stub'
|
177
316
|
end
|
@@ -180,18 +319,67 @@ describe Billy::Proxy do
|
|
180
319
|
|
181
320
|
context 'caching' do
|
182
321
|
|
322
|
+
it 'defaults to nil scope' do
|
323
|
+
expect(proxy.cache.scope).to be_nil
|
324
|
+
end
|
325
|
+
|
183
326
|
context 'HTTP' do
|
184
|
-
let!(:url)
|
185
|
-
let!(:http)
|
327
|
+
let!(:url) { @http_url }
|
328
|
+
let!(:http) { @http }
|
329
|
+
let!(:http_error) { @http_error }
|
186
330
|
it_should_behave_like 'a cache'
|
187
331
|
end
|
188
332
|
|
189
333
|
context 'HTTPS' do
|
190
|
-
let!(:url)
|
191
|
-
let!(:http)
|
334
|
+
let!(:url) { @https_url }
|
335
|
+
let!(:http) { @https }
|
336
|
+
let!(:http_error) { @http_error }
|
192
337
|
it_should_behave_like 'a cache'
|
193
338
|
end
|
194
339
|
|
195
|
-
|
340
|
+
context 'with a cache scope' do
|
341
|
+
let!(:url) { @http_url }
|
342
|
+
let!(:http) { @http }
|
343
|
+
let!(:http_error) { @http_error }
|
344
|
+
|
345
|
+
before do
|
346
|
+
proxy.cache.scope_to "my_cache"
|
347
|
+
end
|
348
|
+
|
349
|
+
after do
|
350
|
+
proxy.cache.use_default_scope
|
351
|
+
end
|
352
|
+
|
353
|
+
it_should_behave_like 'a cache'
|
354
|
+
|
355
|
+
it 'uses the cache scope' do
|
356
|
+
expect(proxy.cache.scope).to eq("my_cache")
|
357
|
+
end
|
196
358
|
|
359
|
+
it 'can be reset to the default scope' do
|
360
|
+
proxy.cache.use_default_scope
|
361
|
+
expect(proxy.cache.scope).to be_nil
|
362
|
+
end
|
363
|
+
|
364
|
+
it 'can execute a block against a cache scope' do
|
365
|
+
expect(proxy.cache.scope).to eq "my_cache"
|
366
|
+
proxy.cache.with_scope "another_cache" do
|
367
|
+
expect(proxy.cache.scope).to eq "another_cache"
|
368
|
+
end
|
369
|
+
expect(proxy.cache.scope).to eq "my_cache"
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'requires a block to be passed to with_scope' do
|
373
|
+
expect {proxy.cache.with_scope "some_scope"}.to raise_error ArgumentError
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'should have different keys for the same request under a different scope' do
|
377
|
+
args = ['get',"#{url}/foo",""]
|
378
|
+
key = proxy.cache.key(*args)
|
379
|
+
proxy.cache.with_scope "another_cache" do
|
380
|
+
expect(proxy.cache.key(*args)).to_not eq key
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
197
385
|
end
|