puffing-billy 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|