z-http-request 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +17 -0
  8. data/README.md +38 -0
  9. data/Rakefile +3 -0
  10. data/benchmarks/clients.rb +170 -0
  11. data/benchmarks/em-excon.rb +87 -0
  12. data/benchmarks/em-profile.gif +0 -0
  13. data/benchmarks/em-profile.txt +65 -0
  14. data/benchmarks/server.rb +48 -0
  15. data/examples/.gitignore +1 -0
  16. data/examples/digest_auth/client.rb +25 -0
  17. data/examples/digest_auth/server.rb +28 -0
  18. data/examples/fetch.rb +30 -0
  19. data/examples/fibered-http.rb +51 -0
  20. data/examples/multi.rb +25 -0
  21. data/examples/oauth-tweet.rb +35 -0
  22. data/examples/socks5.rb +23 -0
  23. data/lib/z-http/client.rb +318 -0
  24. data/lib/z-http/core_ext/bytesize.rb +6 -0
  25. data/lib/z-http/decoders.rb +254 -0
  26. data/lib/z-http/http_client_options.rb +51 -0
  27. data/lib/z-http/http_connection.rb +214 -0
  28. data/lib/z-http/http_connection_options.rb +44 -0
  29. data/lib/z-http/http_encoding.rb +142 -0
  30. data/lib/z-http/http_header.rb +83 -0
  31. data/lib/z-http/http_status_codes.rb +57 -0
  32. data/lib/z-http/middleware/digest_auth.rb +112 -0
  33. data/lib/z-http/middleware/json_response.rb +15 -0
  34. data/lib/z-http/middleware/oauth.rb +40 -0
  35. data/lib/z-http/middleware/oauth2.rb +28 -0
  36. data/lib/z-http/multi.rb +57 -0
  37. data/lib/z-http/request.rb +23 -0
  38. data/lib/z-http/version.rb +5 -0
  39. data/lib/z-http-request.rb +1 -0
  40. data/lib/z-http.rb +18 -0
  41. data/spec/client_spec.rb +892 -0
  42. data/spec/digest_auth_spec.rb +48 -0
  43. data/spec/dns_spec.rb +44 -0
  44. data/spec/encoding_spec.rb +49 -0
  45. data/spec/external_spec.rb +150 -0
  46. data/spec/fixtures/google.ca +16 -0
  47. data/spec/fixtures/gzip-sample.gz +0 -0
  48. data/spec/gzip_spec.rb +68 -0
  49. data/spec/helper.rb +30 -0
  50. data/spec/middleware_spec.rb +143 -0
  51. data/spec/multi_spec.rb +104 -0
  52. data/spec/pipelining_spec.rb +66 -0
  53. data/spec/redirect_spec.rb +321 -0
  54. data/spec/socksify_proxy_spec.rb +60 -0
  55. data/spec/spec_helper.rb +6 -0
  56. data/spec/ssl_spec.rb +20 -0
  57. data/spec/stallion.rb +296 -0
  58. data/spec/stub_server.rb +42 -0
  59. data/z-http-request.gemspec +33 -0
  60. metadata +248 -0
@@ -0,0 +1,48 @@
1
+ require 'helper'
2
+
3
+ $: << 'lib' << '../lib'
4
+
5
+ require 'z-http/middleware/digest_auth'
6
+
7
+ describe 'Digest Auth Authentication header generation' do
8
+ before :each do
9
+ @reference_header = 'Digest username="digest_username", realm="DigestAuth_REALM", algorithm=MD5, uri="/", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", response="96829962ffc31fa2852f86dc7f9f609b", opaque="BzdNK3gsJ2ixTrBJ"'
10
+ end
11
+
12
+ it 'should generate the correct header' do
13
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
14
+
15
+ params = {
16
+ username: 'digest_username',
17
+ password: 'digest_password'
18
+ }
19
+
20
+ middleware = ZMachine::Middleware::DigestAuth.new(www_authenticate, params)
21
+ middleware.build_auth_digest('GET', '/').should == @reference_header
22
+ end
23
+
24
+ it 'should not generate the same header for a different user' do
25
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
26
+
27
+ params = {
28
+ username: 'digest_username_2',
29
+ password: 'digest_password'
30
+ }
31
+
32
+ middleware = ZMachine::Middleware::DigestAuth.new(www_authenticate, params)
33
+ middleware.build_auth_digest('GET', '/').should_not == @reference_header
34
+ end
35
+
36
+ it 'should not generate the same header if the nounce changes' do
37
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg6", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
38
+
39
+ params = {
40
+ username: 'digest_username_2',
41
+ password: 'digest_password'
42
+ }
43
+
44
+ middleware = ZMachine::Middleware::DigestAuth.new(www_authenticate, params)
45
+ middleware.build_auth_digest('GET', '/').should_not == @reference_header
46
+ end
47
+
48
+ end
data/spec/dns_spec.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'helper'
2
+
3
+ describe ZMachine::HttpRequest do
4
+
5
+ it "should fail gracefully on an invalid host in Location header" do
6
+ ZMachine.run {
7
+ http = ZMachine::HttpRequest.new('http://127.0.0.1:8090/redirect/badhost', :connect_timeout => 0.1).get :redirects => 1
8
+ http.callback { failed(http) }
9
+ http.errback {
10
+ http.error.should match('unable to resolve server address')
11
+ ZMachine.stop
12
+ }
13
+ }
14
+ end
15
+
16
+ # BB: 127.1.1.1 actually loops back, and if you have some process listening
17
+ # on port 80 this spec fails *facepalm*
18
+ # also this does not check any DNS timeout, but a connect timeout ...
19
+ it "should fail GET on DNS timeout" do
20
+ ZMachine.run {
21
+ ZMachine.heartbeat_interval = 0.1
22
+ http = ZMachine::HttpRequest.new('http://127.1.1.1:1/', :connect_timeout => 0.1).get
23
+ http.callback { failed(http) }
24
+ http.errback {
25
+ http.response_header.status.should == 0
26
+ ZMachine.stop
27
+ }
28
+ }
29
+ end
30
+
31
+ it "should fail GET on invalid host" do
32
+ ZMachine.run {
33
+ ZMachine.heartbeat_interval = 0.1
34
+ http = ZMachine::HttpRequest.new('http://somethinglocal/', :connect_timeout => 0.1).get
35
+ http.callback { failed(http) }
36
+ http.errback {
37
+ http.error.should match(/unable to resolve server address/)
38
+ http.response_header.status.should == 0
39
+ ZMachine.stop
40
+ }
41
+ }
42
+ end
43
+
44
+ end
@@ -0,0 +1,49 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'helper'
4
+
5
+ describe ZMachine::HttpEncoding do
6
+ include ZMachine::HttpEncoding
7
+
8
+ it "should transform a basic hash into HTTP POST Params" do
9
+ form_encode_body({:a => "alpha", :b => "beta"}).should == "a=alpha&b=beta"
10
+ end
11
+
12
+ it "should transform a more complex hash into HTTP POST Params" do
13
+ form_encode_body({:a => "a", :b => ["c", "d", "e"]}).should == "a=a&b[0]=c&b[1]=d&b[2]=e"
14
+ end
15
+
16
+ it "should transform a very complex hash into HTTP POST Params" do
17
+ params = form_encode_body({:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]})
18
+ # 1.8.7 does not have ordered hashes.
19
+ params.split(/&/).sort.join('&').should == "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
20
+ end
21
+
22
+ it "should escape values" do
23
+ params = form_encode_body({:stuff => 'string&string'})
24
+ params.should == "stuff=string%26string"
25
+ end
26
+
27
+ it "should escape keys" do
28
+ params = form_encode_body({'bad&str'=> {'key&key' => [:a, :b]}})
29
+ params.should == 'bad%26str[key%26key][0]=a&bad%26str[key%26key][1]=b'
30
+ end
31
+
32
+ it "should escape keys and values" do
33
+ params = form_encode_body({'bad&str'=> {'key&key' => ['bad+&stuff', '[test]']}})
34
+ params.should == "bad%26str[key%26key][0]=bad%2B%26stuff&bad%26str[key%26key][1]=%5Btest%5D"
35
+ end
36
+
37
+ it "should not issue warnings on non-ASCII encodings" do
38
+ # I don't know how to check for ruby warnings.
39
+ params = escape('valö')
40
+ params = escape('valö'.encode('ISO-8859-15'))
41
+ end
42
+
43
+ # xit "should be fast on long string escapes" do
44
+ # s = Time.now
45
+ # 5000.times { |n| form_encode_body({:a => "{a:'b', d:'f', g:['a','b']}"*50}) }
46
+ # (Time.now - s).should satisfy { |t| t < 1.5 }
47
+ # end
48
+
49
+ end
@@ -0,0 +1,150 @@
1
+ require 'helper'
2
+
3
+ requires_connection do
4
+
5
+ describe ZMachine::HttpRequest do
6
+
7
+ it "should follow redirects on HEAD method (external)" do
8
+ ZMachine.run {
9
+ http = ZMachine::HttpRequest.new('http://www.google.com/').head :redirects => 1
10
+ http.errback { failed(http) }
11
+ http.callback {
12
+ http.response_header.status.should == 200
13
+ ZMachine.stop
14
+ }
15
+ }
16
+ end
17
+
18
+ it "should follow redirect to https and initiate the handshake" do
19
+ ZMachine.run {
20
+ http = ZMachine::HttpRequest.new('http://github.com/').get :redirects => 5
21
+
22
+ http.errback { failed(http) }
23
+ http.callback {
24
+ http.response_header.status.should == 200
25
+ ZMachine.stop
26
+ }
27
+ }
28
+ end
29
+
30
+ it "should perform a streaming GET" do
31
+ ZMachine.run {
32
+
33
+ # digg.com uses chunked encoding
34
+ http = ZMachine::HttpRequest.new('http://www.httpwatch.com/httpgallery/chunked/').get
35
+
36
+ http.errback { failed(http) }
37
+ http.callback {
38
+ http.response_header.status.should == 200
39
+ ZMachine.stop
40
+ }
41
+ }
42
+ end
43
+
44
+ it "should handle a 100 continue" do
45
+ ZMachine.run {
46
+ # 8.2.3 Use of the 100 (Continue) Status - http://www.ietf.org/rfc/rfc2616.txt
47
+ #
48
+ # An origin server SHOULD NOT send a 100 (Continue) response if
49
+ # the request message does not include an Expect request-header
50
+ # field with the "100-continue" expectation, and MUST NOT send a
51
+ # 100 (Continue) response if such a request comes from an HTTP/1.0
52
+ # (or earlier) client. There is an exception to this rule: for
53
+ # compatibility with RFC 2068, a server MAY send a 100 (Continue)
54
+ # status in response to an HTTP/1.1 PUT or POST request that does
55
+ # not include an Expect request-header field with the "100-
56
+ # continue" expectation. This exception, the purpose of which is
57
+ # to minimize any client processing delays associated with an
58
+ # undeclared wait for 100 (Continue) status, applies only to
59
+ # HTTP/1.1 requests, and not to requests with any other HTTP-
60
+ # version value.
61
+ #
62
+ # 10.1.1: 100 Continue - http://www.ietf.org/rfc/rfc2068.txt
63
+ # The client may continue with its request. This interim response is
64
+ # used to inform the client that the initial part of the request has
65
+ # been received and has not yet been rejected by the server. The client
66
+ # SHOULD continue by sending the remainder of the request or, if the
67
+ # request has already been completed, ignore this response. The server
68
+ # MUST send a final response after the request has been completed.
69
+
70
+ url = 'http://ws.serviceobjects.com/lv/LeadValidation.asmx/ValidateLead_V2'
71
+ http = ZMachine::HttpRequest.new(url).post :body => {:name => :test}
72
+
73
+ http.errback { failed(http) }
74
+ http.callback {
75
+ http.response_header.status.should == 500
76
+ http.response.should match('Missing')
77
+ ZMachine.stop
78
+ }
79
+ }
80
+ end
81
+
82
+ it "should detect deflate encoding" do
83
+ pending "need an endpoint which supports deflate.. MSN is no longer"
84
+ ZMachine.run {
85
+
86
+ options = {:head => {"accept-encoding" => "deflate"}, :redirects => 5}
87
+ http = ZMachine::HttpRequest.new('http://www.msn.com').get options
88
+
89
+ http.errback { failed(http) }
90
+ http.callback {
91
+ http.response_header.status.should == 200
92
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
93
+
94
+ ZMachine.stop
95
+ }
96
+ }
97
+ end
98
+
99
+ it "should stream chunked gzipped data" do
100
+ ZMachine.run {
101
+ options = {:head => {"accept-encoding" => "gzip"}}
102
+ # GitHub sends chunked gzip, time for a little Inception ;)
103
+ http = ZMachine::HttpRequest.new('https://github.com/igrigorik/z-http-request/commits/master').get options
104
+
105
+ http.errback { failed(http) }
106
+ http.callback {
107
+ http.response_header.status.should == 200
108
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
109
+ http.response.should == ''
110
+
111
+ ZMachine.stop
112
+ }
113
+
114
+ body = ''
115
+ http.stream do |chunk|
116
+ body << chunk
117
+ end
118
+ }
119
+ end
120
+
121
+ context "keepalive" do
122
+ it "should default to non-keepalive" do
123
+ ZMachine.run {
124
+ headers = {'If-Modified-Since' => 'Thu, 05 Aug 2010 22:54:44 GMT'}
125
+ http = ZMachine::HttpRequest.new('http://www.google.com/images/logos/ps_logo2.png').get :head => headers
126
+
127
+ http.errback { fail }
128
+ start = Time.now.to_i
129
+ http.callback {
130
+ (Time.now.to_i - start).should be_within(2).of(0)
131
+ ZMachine.stop
132
+ }
133
+ }
134
+ end
135
+
136
+ it "should work with keep-alive servers" do
137
+ ZMachine.run {
138
+ http = ZMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get :keepalive => true
139
+
140
+ http.errback { failed(http) }
141
+ http.callback {
142
+ http.response_header.status.should == 200
143
+ ZMachine.stop
144
+ }
145
+ }
146
+ end
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,16 @@
1
+ HTTP/1.1 301 Moved Permanently
2
+ Location: http://www.google.ca/
3
+ Content-Type: text/html; charset=UTF-8
4
+ Date: Sun, 09 Jan 2011 02:51:43 GMT
5
+ Expires: Tue, 08 Feb 2011 02:51:43 GMT
6
+ Cache-Control: public, max-age=2592000
7
+ Server: gws
8
+ Content-Length: 218
9
+ X-XSS-Protection: 1; mode=block
10
+
11
+ <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
12
+ <TITLE>301 Moved</TITLE></HEAD><BODY>
13
+ <H1>301 Moved</H1>
14
+ The document has moved
15
+ <A HREF="http://www.google.ca/">here</A>.
16
+ </BODY></HTML>
Binary file
data/spec/gzip_spec.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'helper'
2
+
3
+ describe ZMachine::HttpDecoders::GZip do
4
+
5
+ let(:compressed) {
6
+ compressed = ["1f8b08089668a6500003686900cbc8e402007a7a6fed03000000"].pack("H*")
7
+ }
8
+
9
+ it "should extract the stream of a vanilla gzip" do
10
+ header = ZMachine::HttpDecoders::GZipHeader.new
11
+ stream = header.extract_stream(compressed)
12
+
13
+ stream.unpack("H*")[0].should eq("cbc8e402007a7a6fed03000000")
14
+ end
15
+
16
+ it "should decompress a vanilla gzip" do
17
+ decompressed = ""
18
+
19
+ gz = ZMachine::HttpDecoders::GZip.new do |data|
20
+ decompressed << data
21
+ end
22
+
23
+ gz << compressed
24
+ gz.finalize!
25
+
26
+ decompressed.should eq("hi\n")
27
+ end
28
+
29
+ it "should decompress a vanilla gzip file byte by byte" do
30
+ decompressed = ""
31
+
32
+ gz = ZMachine::HttpDecoders::GZip.new do |data|
33
+ decompressed << data
34
+ end
35
+
36
+ compressed.each_char do |byte|
37
+ gz << byte
38
+ end
39
+
40
+ gz.finalize!
41
+
42
+ decompressed.should eq("hi\n")
43
+ end
44
+
45
+ it "should decompress a large file" do
46
+ decompressed = ""
47
+
48
+ gz = ZMachine::HttpDecoders::GZip.new do |data|
49
+ decompressed << data
50
+ end
51
+
52
+ gz << File.read(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz")
53
+
54
+ gz.finalize!
55
+
56
+ decompressed.size.should eq(32907)
57
+ end
58
+
59
+ it "should fail with a DecoderError if not a gzip file" do
60
+ not_a_gzip = ["1f8c08089668a650000"].pack("H*")
61
+ header = ZMachine::HttpDecoders::GZipHeader.new
62
+
63
+ lambda {
64
+ header.extract_stream(not_a_gzip)
65
+ }.should raise_exception(ZMachine::HttpDecoders::DecoderError)
66
+ end
67
+
68
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'puma'
5
+ require 'z-http'
6
+ require 'multi_json'
7
+
8
+ require 'stallion'
9
+ require 'stub_server'
10
+
11
+ def failed(http = nil)
12
+ ZMachine.stop
13
+ http ? fail(http.error) : fail
14
+ end
15
+
16
+ def requires_connection(&blk)
17
+ blk.call if system('ping -t1 -c1 google.com 2>&1 > /dev/null')
18
+ end
19
+
20
+ def requires_port(port, &blk)
21
+ port_open = true
22
+ begin
23
+ s = TCPSocket.new('localhost', port)
24
+ s.close()
25
+ rescue
26
+ port_open = false
27
+ end
28
+
29
+ blk.call if port_open
30
+ end
@@ -0,0 +1,143 @@
1
+ require 'helper'
2
+
3
+ describe ZMachine::HttpRequest do
4
+
5
+ class EmptyMiddleware; end
6
+
7
+ class GlobalMiddleware
8
+ def response(resp)
9
+ resp.response_header['X-Global'] = 'middleware'
10
+ end
11
+ end
12
+
13
+ it "should accept middleware" do
14
+ ZMachine.run {
15
+ lambda {
16
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090')
17
+ conn.use ResponseMiddleware
18
+ conn.use EmptyMiddleware
19
+
20
+ ZMachine.stop
21
+ }.should_not raise_error
22
+ }
23
+ end
24
+
25
+ context "configuration" do
26
+ class ConfigurableMiddleware
27
+ def initialize(conf, &block)
28
+ @conf = conf
29
+ @block = block
30
+ end
31
+
32
+ def response(resp)
33
+ resp.response_header['X-Conf'] = @conf
34
+ resp.response_header['X-Block'] = @block.call
35
+ end
36
+ end
37
+
38
+ it "should accept middleware initialization parameters" do
39
+ ZMachine.run {
40
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090')
41
+ conn.use ConfigurableMiddleware, 'conf-value' do
42
+ 'block-value'
43
+ end
44
+
45
+ req = conn.get
46
+ req.callback {
47
+ req.response_header['X-Conf'].should match('conf-value')
48
+ req.response_header['X-Block'].should match('block-value')
49
+ ZMachine.stop
50
+ }
51
+ }
52
+ end
53
+ end
54
+
55
+ context "request" do
56
+ class ResponseMiddleware
57
+ def response(resp)
58
+ resp.response_header['X-Header'] = 'middleware'
59
+ resp.response = 'Hello, Middleware!'
60
+ end
61
+ end
62
+
63
+ it "should execute response middleware before user callbacks" do
64
+ ZMachine.run {
65
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090')
66
+ conn.use ResponseMiddleware
67
+
68
+ req = conn.get
69
+ req.callback {
70
+ req.response_header['X-Header'].should match('middleware')
71
+ req.response.should match('Hello, Middleware!')
72
+ ZMachine.stop
73
+ }
74
+ }
75
+ end
76
+
77
+ it "should execute global response middleware before user callbacks" do
78
+ ZMachine.run {
79
+ ZMachine::HttpRequest.use GlobalMiddleware
80
+
81
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090')
82
+
83
+ req = conn.get
84
+ req.callback {
85
+ req.response_header['X-Global'].should match('middleware')
86
+ ZMachine.stop
87
+ }
88
+ }
89
+ end
90
+ end
91
+
92
+ context "request" do
93
+ class RequestMiddleware
94
+ def request(client, head, body)
95
+ head['X-Middleware'] = 'middleware' # insert new header
96
+ body += ' modified' # modify post body
97
+
98
+ [head, body]
99
+ end
100
+ end
101
+
102
+ it "should execute request middleware before dispatching request" do
103
+ ZMachine.run {
104
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090/')
105
+ conn.use RequestMiddleware
106
+
107
+ req = conn.post :body => "data"
108
+ req.callback {
109
+ req.response_header.status.should == 200
110
+ req.response.should match(/data modified/)
111
+ ZMachine.stop
112
+ }
113
+ }
114
+ end
115
+ end
116
+
117
+ context "jsonify" do
118
+ class JSONify
119
+ def request(client, head, body)
120
+ [head, MultiJson.dump(body)]
121
+ end
122
+
123
+ def response(resp)
124
+ resp.response = MultiJson.load(resp.response)
125
+ end
126
+ end
127
+
128
+ it "should use middleware to JSON encode and JSON decode the body" do
129
+ ZMachine.run {
130
+ conn = ZMachine::HttpRequest.new('http://127.0.0.1:8090/')
131
+ conn.use JSONify
132
+
133
+ req = conn.post :body => {:ruby => :hash}
134
+ req.callback {
135
+ req.response_header.status.should == 200
136
+ req.response.should == {"ruby" => "hash"}
137
+ ZMachine.stop
138
+ }
139
+ }
140
+ end
141
+ end
142
+
143
+ end
@@ -0,0 +1,104 @@
1
+ require 'helper'
2
+ require 'stallion'
3
+
4
+ describe ZMachine::MultiRequest do
5
+
6
+ let(:multi) { ZMachine::MultiRequest.new }
7
+ let(:url) { 'http://127.0.0.1:8090/' }
8
+
9
+ it "should submit multiple requests in parallel and return once all of them are complete" do
10
+ ZMachine.run {
11
+ multi.add :a, ZMachine::HttpRequest.new(url).get
12
+ multi.add :b, ZMachine::HttpRequest.new(url).post
13
+ multi.add :c, ZMachine::HttpRequest.new(url).head
14
+ multi.add :d, ZMachine::HttpRequest.new(url).delete
15
+ multi.add :e, ZMachine::HttpRequest.new(url).put
16
+
17
+ multi.callback {
18
+ multi.responses[:callback].size.should == 5
19
+ multi.responses[:callback].each { |name, response|
20
+ [ :a, :b, :c, :d, :e ].should include(name)
21
+ response.response_header.status.should == 200
22
+ }
23
+ multi.responses[:errback].size.should == 0
24
+
25
+ ZMachine.stop
26
+ }
27
+ }
28
+ end
29
+
30
+ it "should require unique keys for each deferrable" do
31
+ lambda do
32
+ multi.add :df1, ZMachine::DefaultDeferrable.new
33
+ multi.add :df1, ZMachine::DefaultDeferrable.new
34
+ end.should raise_error("Duplicate Multi key")
35
+ end
36
+
37
+
38
+ describe "#requests" do
39
+ it "should return the added requests" do
40
+ request1 = double('request1', :callback => nil, :errback => nil)
41
+ request2 = double('request2', :callback => nil, :errback => nil)
42
+
43
+ multi.add :a, request1
44
+ multi.add :b, request2
45
+
46
+ multi.requests.should == {:a => request1, :b => request2}
47
+ end
48
+ end
49
+
50
+ describe "#responses" do
51
+ it "should have an empty :callback hash" do
52
+ multi.responses[:callback].should be_a(Hash)
53
+ multi.responses[:callback].size.should == 0
54
+ end
55
+
56
+ it "should have an empty :errback hash" do
57
+ multi.responses[:errback].should be_a(Hash)
58
+ multi.responses[:errback].size.should == 0
59
+ end
60
+
61
+ it "should provide access to the requests by name" do
62
+ ZMachine.run {
63
+ request1 = ZMachine::HttpRequest.new(url).get
64
+ request2 = ZMachine::HttpRequest.new(url).post
65
+ multi.add :a, request1
66
+ multi.add :b, request2
67
+
68
+ multi.callback {
69
+ multi.responses[:callback][:a].should equal(request1)
70
+ multi.responses[:callback][:b].should equal(request2)
71
+
72
+ ZMachine.stop
73
+ }
74
+ }
75
+ end
76
+ end
77
+
78
+ describe "#finished?" do
79
+ it "should be true when no requests have been added" do
80
+ multi.should be_finished
81
+ end
82
+
83
+ it "should be false while the requests are not finished" do
84
+ ZMachine.run {
85
+ multi.add :a, ZMachine::HttpRequest.new(url).get
86
+ multi.should_not be_finished
87
+
88
+ ZMachine.stop
89
+ }
90
+ end
91
+
92
+ it "should be finished when all requests are finished" do
93
+ ZMachine.run {
94
+ multi.add :a, ZMachine::HttpRequest.new(url).get
95
+ multi.callback {
96
+ multi.should be_finished
97
+
98
+ ZMachine.stop
99
+ }
100
+ }
101
+ end
102
+ end
103
+
104
+ end