z-http-request 0.1.0

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.
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