bpardee-net-http-persistent 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,631 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+ require 'net/http/persistent'
4
+ require 'openssl'
5
+ require 'stringio'
6
+ require 'ostruct'
7
+ require 'logger'
8
+
9
+ CMD_SUCCESS = 'success'
10
+ CMD_SLEEP = 'sleep'
11
+ CMD_BAD_RESPONSE = 'bad_response'
12
+ CMD_EOF_ERROR = 'eof_error'
13
+ CMD_CONNRESET = 'connreset'
14
+ CMD_ECHO = 'echo'
15
+
16
+ PASS = 'pass'
17
+ FAIL = 'fail'
18
+
19
+ DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN = 9000
20
+ DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED = 9001
21
+
22
+ $debug = false
23
+
24
+ class Net::HTTP
25
+ def connect
26
+ raise Errno::EHOSTDOWN if open_timeout == DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN
27
+ raise Errno::ECONNREFUSED if open_timeout == DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED
28
+ end
29
+
30
+ def successful_response
31
+ r = Net::HTTPResponse.allocate
32
+ def r.http_version() '1.1' end
33
+ def r.read_body() :read_body end
34
+ yield r if block_given?
35
+ r
36
+ end
37
+
38
+ def request(req, &block)
39
+ $count += 1
40
+ puts "path=#{req.path} count=#{$count}" if $debug
41
+ args = req.path[1..-1].split('/')
42
+ cmd = args.shift
43
+ i = $count % args.size if args.size > 0
44
+ puts "i=#{i}" if $debug
45
+ if cmd == CMD_ECHO
46
+ res = successful_response(&block)
47
+ eval "def res.body() \"#{req.body}\" end"
48
+ return res
49
+ elsif cmd == CMD_SUCCESS || args[i] == PASS
50
+ return successful_response(&block)
51
+ end
52
+ case cmd
53
+ when CMD_SLEEP
54
+ sleep args[i].to_i
55
+ return successful_response(&block)
56
+ when CMD_BAD_RESPONSE
57
+ raise Net::HTTPBadResponse.new('Dummy bad response')
58
+ when CMD_EOF_ERROR
59
+ raise EOFError.new('Dummy EOF error')
60
+ when CMD_CONNRESET
61
+ raise Errno::ECONNRESET
62
+ else
63
+ return successful_response(&block)
64
+ end
65
+ end
66
+ end
67
+
68
+ class Net::HTTP::Persistent
69
+ attr_reader :pool_hash
70
+
71
+ # Make private methods public
72
+ send(:public, *(self.private_instance_methods - Object.private_instance_methods))
73
+ end
74
+
75
+ class TestNetHttpPersistent < MiniTest::Unit::TestCase
76
+
77
+ def uri_for(*args)
78
+ URI.parse("http://example.com/#{args.join('/')}")
79
+ end
80
+
81
+ def request_command(req, *args)
82
+ puts "uri=#{uri_for(args)}" if $debug
83
+ @http.request(uri_for(args), req)
84
+ end
85
+
86
+ def http_and_io(options={})
87
+ io = StringIO.new
88
+ logger = Logger.new(io)
89
+ logger.level = Logger::INFO
90
+ default_options = {:name => 'TestNetHTTPPersistent', :logger => logger, :pool_size => 1}
91
+ http = Net::HTTP::Persistent.new(default_options.merge(options))
92
+ [http, io]
93
+ end
94
+
95
+ def setup
96
+ $count = -1
97
+ @http, @io = http_and_io
98
+ @uri = uri_for CMD_SUCCESS
99
+
100
+ ENV.delete 'http_proxy'
101
+ ENV.delete 'HTTP_PROXY'
102
+ ENV.delete 'http_proxy_user'
103
+ ENV.delete 'HTTP_PROXY_USER'
104
+ ENV.delete 'http_proxy_pass'
105
+ ENV.delete 'HTTP_PROXY_PASS'
106
+ end
107
+
108
+ def teardown
109
+ end
110
+
111
+ def test_initialize
112
+ assert_nil @http.proxy_uri
113
+ end
114
+
115
+ def test_initialize_name
116
+ http = Net::HTTP::Persistent.new(:name => 'name')
117
+ assert_equal 'name', http.name
118
+ end
119
+
120
+ def test_initialize_env
121
+ ENV['HTTP_PROXY'] = 'proxy.example'
122
+ http = Net::HTTP::Persistent.new(:proxy => :ENV)
123
+
124
+ assert_equal URI.parse('http://proxy.example'), http.proxy_uri
125
+ end
126
+
127
+ def test_initialize_uri
128
+ proxy_uri = URI.parse 'http://proxy.example'
129
+
130
+ http = Net::HTTP::Persistent.new(:proxy => proxy_uri)
131
+
132
+ assert_equal proxy_uri, http.proxy_uri
133
+ end
134
+
135
+ def test_connection
136
+ @http.open_timeout = 123
137
+ @http.read_timeout = 321
138
+ pool = @http.pool_for @uri
139
+ assert_same pool, @http.pool_hash['example.com:80']
140
+ pool.with_connection do |c|
141
+ assert c.started?
142
+ refute c.proxy?
143
+
144
+ assert_equal 123, c.open_timeout
145
+ assert_equal 321, c.read_timeout
146
+
147
+ assert_equal 'example.com', c.address
148
+ assert_equal 80, c.port
149
+ end
150
+ end
151
+
152
+ def test_connection_for_cached
153
+ c1, c2 = nil, nil
154
+ pool = @http.pool_for @uri
155
+ assert_same pool, @http.pool_hash['example.com:80']
156
+ pool.with_connection do |c|
157
+ c1 = c
158
+ assert c.started?
159
+ end
160
+ pool.with_connection do |c|
161
+ c2 = c
162
+ assert c.started?
163
+ end
164
+ assert_same c1,c2
165
+ end
166
+
167
+ def test_connection_for_debug_output
168
+ io = StringIO.new
169
+ @http.debug_output = io
170
+
171
+ pool = @http.pool_for @uri
172
+ assert_same pool, @http.pool_hash['example.com:80']
173
+ pool.with_connection do |c|
174
+ assert c.started?
175
+ assert_equal io, c.instance_variable_get(:@debug_output)
176
+ assert_equal 'example.com', c.address
177
+ assert_equal 80, c.port
178
+ end
179
+ end
180
+
181
+ def test_connection_for_hostdown
182
+ @http.open_timeout = DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN
183
+ e = assert_raises Net::HTTP::Persistent::Error do
184
+ request_command(nil, CMD_SUCCESS)
185
+ end
186
+
187
+ assert_match %r%host down%, e.message
188
+ end
189
+
190
+ def test_connection_for_connrefused
191
+ @http.open_timeout = DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED
192
+ e = assert_raises Net::HTTP::Persistent::Error do
193
+ request_command(nil, CMD_SUCCESS)
194
+ end
195
+
196
+ assert_match %r%connection refused%, e.message
197
+ end
198
+
199
+ def test_connection_for_proxy
200
+ uri = URI.parse 'http://proxy.example'
201
+ uri.user = 'johndoe'
202
+ uri.password = 'muffins'
203
+
204
+ http = Net::HTTP::Persistent.new(:proxy => uri)
205
+ pool = http.pool_for(@uri)
206
+ assert_same pool, http.pool_hash['example.com:80:proxy.example:80:johndoe:muffins']
207
+ pool.with_connection do |c|
208
+ assert c.started?
209
+ assert c.proxy?
210
+ end
211
+ end
212
+
213
+ def test_error_message
214
+ 6.times do
215
+ request_command nil, CMD_EOF_ERROR, PASS, PASS, PASS, PASS, FAIL, PASS, PASS
216
+ end
217
+
218
+ assert_match "after 5 requests on", @io.string
219
+ end
220
+
221
+ def test_escape
222
+ assert_nil @http.escape nil
223
+
224
+ assert_equal '%20', @http.escape(' ')
225
+ end
226
+
227
+ def test_finish
228
+ c = Object.new
229
+ def c.finish; @finished = true end
230
+ def c.finished?; @finished end
231
+ def c.start; @started = true end
232
+ def c.started?; @started end
233
+
234
+ @http.finish c
235
+
236
+ refute c.started?
237
+ assert c.finished?
238
+ end
239
+
240
+ def test_finish_io_error
241
+ c = Object.new
242
+ def c.finish; @finished = true; raise IOError end
243
+ def c.finished?; @finished end
244
+ def c.start; @started = true end
245
+ def c.started?; @started end
246
+
247
+ @http.finish c
248
+
249
+ refute c.started?
250
+ assert c.finished?
251
+ end
252
+
253
+ def test_http_version
254
+ assert_nil @http.http_version @uri
255
+
256
+ request_command nil, CMD_SUCCESS
257
+
258
+ assert_equal '1.1', @http.http_version(@uri)
259
+ end
260
+
261
+ def test_idempotent_eh
262
+ assert @http.idempotent? Net::HTTP::Delete.new '/'
263
+ assert @http.idempotent? Net::HTTP::Get.new '/'
264
+ assert @http.idempotent? Net::HTTP::Head.new '/'
265
+ assert @http.idempotent? Net::HTTP::Options.new '/'
266
+ assert @http.idempotent? Net::HTTP::Put.new '/'
267
+ assert @http.idempotent? Net::HTTP::Trace.new '/'
268
+
269
+ refute @http.idempotent? Net::HTTP::Post.new '/'
270
+ end
271
+
272
+ def test_normalize_uri
273
+ assert_equal 'http://example', @http.normalize_uri('example')
274
+ assert_equal 'http://example', @http.normalize_uri('http://example')
275
+ assert_equal 'https://example', @http.normalize_uri('https://example')
276
+ end
277
+
278
+ def test_proxy_from_env
279
+ ENV['HTTP_PROXY'] = 'proxy.example'
280
+ ENV['HTTP_PROXY_USER'] = 'johndoe'
281
+ ENV['HTTP_PROXY_PASS'] = 'muffins'
282
+
283
+ uri = @http.proxy_from_env
284
+
285
+ expected = URI.parse 'http://proxy.example'
286
+ expected.user = 'johndoe'
287
+ expected.password = 'muffins'
288
+
289
+ assert_equal expected, uri
290
+ end
291
+
292
+ def test_proxy_from_env_lower
293
+ ENV['http_proxy'] = 'proxy.example'
294
+ ENV['http_proxy_user'] = 'johndoe'
295
+ ENV['http_proxy_pass'] = 'muffins'
296
+
297
+ uri = @http.proxy_from_env
298
+
299
+ expected = URI.parse 'http://proxy.example'
300
+ expected.user = 'johndoe'
301
+ expected.password = 'muffins'
302
+
303
+ assert_equal expected, uri
304
+ end
305
+
306
+ def test_proxy_from_env_nil
307
+ uri = @http.proxy_from_env
308
+
309
+ assert_nil uri
310
+
311
+ ENV['HTTP_PROXY'] = ''
312
+
313
+ uri = @http.proxy_from_env
314
+
315
+ assert_nil uri
316
+ end
317
+
318
+ def test_remove
319
+ http, io = http_and_io(:pool_size => 3)
320
+ request_command(nil, CMD_SUCCESS)
321
+ pool = http.pool_for(@uri)
322
+ 2.times do
323
+ conns = []
324
+ pool.with_connection do |c1|
325
+ pool.with_connection do |c2|
326
+ conns << c2
327
+ pool.with_connection do |c3|
328
+ conns << c3
329
+ begin
330
+ Timeout.timeout(2) do
331
+ pool.with_connection { |c4| }
332
+ flunk('should NOT have been able to get 4th connection')
333
+ end
334
+ rescue Timeout::Error => e
335
+ pass('successfully failed to get a connection')
336
+ end
337
+ http.remove(pool, c1)
338
+ Timeout.timeout(1) do
339
+ begin
340
+ pool.with_connection do |c4|
341
+ conns << c4
342
+ end
343
+ rescue Timeout::Error => e
344
+ flunk('should have been able to get 4th connection')
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+ pool.with_connection do |c1|
351
+ pool.with_connection do |c2|
352
+ pool.with_connection do |c3|
353
+ assert_equal conns, [c1,c2,c3]
354
+ end
355
+ end
356
+ end
357
+ # Do it a 2nd time with finish returning an IOError
358
+ c1 = conns[0]
359
+ def c1.finish
360
+ super
361
+ raise IOError
362
+ end
363
+ end
364
+ end
365
+
366
+ def test_renew
367
+ http, io = http_and_io(:pool_size => 3)
368
+ request_command(nil, CMD_SUCCESS)
369
+ pool = http.pool_for(@uri)
370
+ 2.times do
371
+ conns = []
372
+ pool.with_connection do |c1|
373
+ pool.with_connection do |c2|
374
+ conns << c2
375
+ pool.with_connection do |c3|
376
+ conns << c3
377
+ new_c1 = http.renew(pool, c1)
378
+ refute_equal(c1, new_c1)
379
+ conns.unshift(new_c1)
380
+ end
381
+ end
382
+ end
383
+ pool.with_connection do |c1|
384
+ pool.with_connection do |c2|
385
+ pool.with_connection do |c3|
386
+ assert_equal conns, [c1,c2,c3]
387
+ end
388
+ end
389
+ end
390
+ # Do it a 2nd time with finish returning an IOError
391
+ c1 = conns[0]
392
+ def c1.finish
393
+ super
394
+ raise IOError
395
+ end
396
+ end
397
+ end
398
+
399
+ def test_renew_with_exception
400
+ http, io = http_and_io(:pool_size => 2)
401
+ pool = http.pool_for(@uri)
402
+ [[DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN, %r%host down%], [DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED, %r%connection refused%]].each do |pair|
403
+ dummy_open_timeout = pair.first
404
+ error_message = pair.last
405
+ pool.with_connection do |c|
406
+ old_c = c
407
+ http.open_timeout = dummy_open_timeout
408
+ e = assert_raises Net::HTTP::Persistent::Error do
409
+ new_c = http.renew pool, c
410
+ end
411
+ assert_match error_message, e.message
412
+
413
+ # Make sure our pool is still in good shape
414
+ http.open_timeout = 5 # Any valid timeout will do
415
+ pool.with_connection do |c1|
416
+ refute_equal old_c, c1
417
+ pool.with_connection do |c2|
418
+ refute_equal old_c, c2
419
+ end
420
+ end
421
+ end
422
+ end
423
+ end
424
+
425
+ def test_request
426
+ @http.headers['user-agent'] = 'test ua'
427
+ req = Net::HTTP::Get.new(@uri.request_uri)
428
+ res = @http.request(@uri, req)
429
+
430
+ assert_kind_of Net::HTTPResponse, res
431
+
432
+ assert_kind_of Net::HTTP::Get, req
433
+ assert_equal @uri.path, req.path
434
+ assert_equal 'keep-alive', req['connection']
435
+ assert_equal '30', req['keep-alive']
436
+ assert_match %r%test ua%, req['user-agent']
437
+ end
438
+
439
+ def test_request_block
440
+ @http.headers['user-agent'] = 'test ua'
441
+ body = nil
442
+
443
+ req = Net::HTTP::Get.new(@uri.request_uri)
444
+ res = @http.request(@uri, req) do |r|
445
+ body = r.read_body
446
+ end
447
+
448
+ assert_kind_of Net::HTTPResponse, res
449
+ refute_nil body
450
+
451
+ assert_kind_of Net::HTTP::Get, req
452
+ assert_equal @uri.path, req.path
453
+ assert_equal 'keep-alive', req['connection']
454
+ assert_equal '30', req['keep-alive']
455
+ assert_match %r%test ua%, req['user-agent']
456
+ end
457
+
458
+ def test_request_bad_response
459
+ e = assert_raises Net::HTTP::Persistent::Error do
460
+ request_command nil, CMD_BAD_RESPONSE, FAIL, FAIL
461
+ end
462
+ assert_match %r%too many bad responses%, e.message
463
+ assert_match %r%Renewing connection because of bad response%, @io.string
464
+ assert_match %r%Removing connection because of too many bad responses%, @io.string
465
+
466
+ res = request_command nil, CMD_SUCCESS
467
+ assert_kind_of Net::HTTPResponse, res
468
+ end
469
+
470
+ def test_request_connreset
471
+ e = assert_raises Net::HTTP::Persistent::Error do
472
+ request_command nil, CMD_CONNRESET, FAIL, FAIL
473
+ end
474
+
475
+ assert_match %r%too many connection resets%, e.message
476
+ assert_match %r%Renewing connection %, @io.string
477
+ assert_match %r%Removing connection %, @io.string
478
+
479
+ res = request_command nil, CMD_SUCCESS
480
+ assert_kind_of Net::HTTPResponse, res
481
+ end
482
+
483
+ def test_request_bad_response_retry
484
+ res = request_command nil, CMD_BAD_RESPONSE, FAIL, PASS
485
+ assert_match %r%Renewing connection because of bad response%, @io.string
486
+ assert_kind_of Net::HTTPResponse, res
487
+ end
488
+
489
+ def test_request_connreset_retry
490
+ res = request_command nil, CMD_CONNRESET, FAIL, PASS
491
+ assert_match %r%Renewing connection %, @io.string
492
+ assert_kind_of Net::HTTPResponse, res
493
+ end
494
+
495
+ def test_request_bad_response_post
496
+ uri = uri_for CMD_BAD_RESPONSE, FAIL, PASS
497
+ post = Net::HTTP::Post.new(uri.request_uri)
498
+ e = assert_raises Net::HTTP::Persistent::Error do
499
+ request_command post, CMD_BAD_RESPONSE, FAIL, PASS
500
+ end
501
+ assert_match %r%too many bad responses%, e.message
502
+ assert_match %r%Removing connection because of too many bad responses%, @io.string
503
+
504
+ res = request_command nil, CMD_SUCCESS
505
+ assert_kind_of Net::HTTPResponse, res
506
+ end
507
+
508
+
509
+ def test_request_connreset_post
510
+ uri = uri_for CMD_CONNRESET, FAIL, PASS
511
+ post = Net::HTTP::Post.new(uri.request_uri)
512
+ e = assert_raises Net::HTTP::Persistent::Error do
513
+ request_command post, CMD_CONNRESET, FAIL, PASS
514
+ end
515
+ assert_match %r%too many connection resets%, e.message
516
+ assert_match %r%Removing connection %, @io.string
517
+
518
+ res = request_command nil, CMD_SUCCESS
519
+ assert_kind_of Net::HTTPResponse, res
520
+ end
521
+
522
+ def test_request_bad_response_post_force_retry
523
+ @http.force_retry = true
524
+ uri = uri_for CMD_BAD_RESPONSE, FAIL, PASS
525
+ post = Net::HTTP::Post.new(uri.request_uri)
526
+ res = request_command post, CMD_BAD_RESPONSE, FAIL, PASS
527
+ assert_match %r%Renewing connection because of bad response%, @io.string
528
+ assert_kind_of Net::HTTPResponse, res
529
+ end
530
+
531
+ def test_request_connreset_post_force_retry
532
+ @http.force_retry = true
533
+ uri = uri_for CMD_CONNRESET, FAIL, PASS
534
+ post = Net::HTTP::Post.new(uri.request_uri)
535
+ res = request_command post, CMD_CONNRESET, FAIL, PASS
536
+ assert_match %r%Renewing connection %, @io.string
537
+ assert_kind_of Net::HTTPResponse, res
538
+ end
539
+
540
+ def test_request_post
541
+ uri = uri_for CMD_ECHO
542
+ post = Net::HTTP::Post.new(uri.request_uri)
543
+ post.body = 'hello Net::HTTP::Persistent'
544
+ res = request_command post, CMD_ECHO
545
+ assert_kind_of Net::HTTPResponse, res
546
+ assert_equal post.body, res.body
547
+ end
548
+
549
+ # def test_shutdown
550
+ # c = connection
551
+ # cs = conns
552
+ # rs = reqs
553
+ #
554
+ # orig = @http
555
+ # @http = Net::HTTP::Persistent.new 'name'
556
+ # c2 = connection
557
+ #
558
+ # orig.shutdown
559
+ #
560
+ # assert c.finished?
561
+ # refute c2.finished?
562
+ #
563
+ # refute_same cs, conns
564
+ # refute_same rs, reqs
565
+ # end
566
+ #
567
+ # def test_shutdown_not_started
568
+ # c = Object.new
569
+ # def c.finish() raise IOError end
570
+ #
571
+ # conns["#{@uri.host}:#{@uri.port}"] = c
572
+ #
573
+ # @http.shutdown
574
+ #
575
+ # assert_nil Thread.current[@http.connection_key]
576
+ # assert_nil Thread.current[@http.request_key]
577
+ # end
578
+ #
579
+ # def test_shutdown_no_connections
580
+ # @http.shutdown
581
+ #
582
+ # assert_nil Thread.current[@http.connection_key]
583
+ # assert_nil Thread.current[@http.request_key]
584
+ # end
585
+
586
+ def test_ssl
587
+ @http.verify_callback = :callback
588
+ c = Net::HTTP.new 'localhost', 80
589
+
590
+ @http.ssl c
591
+
592
+ assert c.use_ssl?
593
+ assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode
594
+ assert_nil c.verify_callback
595
+ end
596
+
597
+ def test_ssl_ca_file
598
+ @http.ca_file = 'ca_file'
599
+ @http.verify_callback = :callback
600
+ c = Net::HTTP.new 'localhost', 80
601
+
602
+ @http.ssl c
603
+
604
+ assert c.use_ssl?
605
+ assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode
606
+ assert_equal :callback, c.verify_callback
607
+ end
608
+
609
+ def test_ssl_certificate
610
+ @http.certificate = :cert
611
+ @http.private_key = :key
612
+ c = Net::HTTP.new 'localhost', 80
613
+
614
+ @http.ssl c
615
+
616
+ assert c.use_ssl?
617
+ assert_equal :cert, c.cert
618
+ assert_equal :key, c.key
619
+ end
620
+
621
+ def test_ssl_verify_mode
622
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
623
+ c = Net::HTTP.new 'localhost', 80
624
+
625
+ @http.ssl c
626
+
627
+ assert c.use_ssl?
628
+ assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode
629
+ end
630
+ end
631
+