hatetepe 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +21 -16
  2. data/hatetepe.gemspec +1 -2
  3. data/lib/hatetepe/body.rb +5 -3
  4. data/lib/hatetepe/builder.rb +6 -3
  5. data/lib/hatetepe/cli.rb +27 -5
  6. data/lib/hatetepe/client.rb +157 -82
  7. data/lib/hatetepe/client/keep_alive.rb +58 -0
  8. data/lib/hatetepe/client/pipeline.rb +19 -0
  9. data/lib/hatetepe/connection.rb +42 -0
  10. data/lib/hatetepe/deferred_status_fix.rb +11 -0
  11. data/lib/hatetepe/message.rb +4 -4
  12. data/lib/hatetepe/parser.rb +3 -4
  13. data/lib/hatetepe/request.rb +19 -6
  14. data/lib/hatetepe/response.rb +11 -3
  15. data/lib/hatetepe/server.rb +115 -85
  16. data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
  17. data/lib/hatetepe/server/keep_alive.rb +61 -0
  18. data/lib/hatetepe/server/pipeline.rb +24 -0
  19. data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
  20. data/lib/hatetepe/version.rb +1 -1
  21. data/lib/rack/handler/hatetepe.rb +1 -4
  22. data/spec/integration/cli/start_spec.rb +75 -123
  23. data/spec/integration/client/keep_alive_spec.rb +74 -0
  24. data/spec/integration/server/keep_alive_spec.rb +99 -0
  25. data/spec/spec_helper.rb +41 -16
  26. data/spec/unit/app_spec.rb +16 -5
  27. data/spec/unit/builder_spec.rb +4 -4
  28. data/spec/unit/client/pipeline_spec.rb +40 -0
  29. data/spec/unit/client_spec.rb +355 -199
  30. data/spec/unit/connection_spec.rb +64 -0
  31. data/spec/unit/parser_spec.rb +3 -2
  32. data/spec/unit/proxy_spec.rb +9 -18
  33. data/spec/unit/rack_handler_spec.rb +2 -12
  34. data/spec/unit/server_spec.rb +154 -60
  35. metadata +31 -36
  36. data/.rspec +0 -1
  37. data/.travis.yml +0 -3
  38. data/.yardopts +0 -1
  39. data/lib/hatetepe/pipeline.rb +0 -27
@@ -0,0 +1,99 @@
1
+ require "spec_helper"
2
+ require "hatetepe/cli"
3
+ require "hatetepe/server"
4
+
5
+ describe Hatetepe::Server, "with Keep-Alive" do
6
+ before do
7
+ $stderr = StringIO.new
8
+
9
+ FakeFS.activate!
10
+ File.open "config.ru", "w" do |f|
11
+ f.write 'run proc {|env| [200, {"Content-Type" => "text/plain"}, []] }'
12
+ end
13
+ end
14
+
15
+ after do
16
+ $stderr = STDERR
17
+
18
+ FakeFS.deactivate!
19
+ FakeFS::FileSystem.clear
20
+ end
21
+
22
+ let :client do
23
+ Hatetepe::Client.start :host => "127.0.0.1", :port => 30001
24
+ end
25
+
26
+ let :server do
27
+ Hatetepe::Server.any_instance
28
+ end
29
+
30
+ it "keeps the connection open for 1 seconds by default" do
31
+ command "-p 30001", 1.1 do
32
+ client
33
+ EM::Synchrony.sleep 0.95
34
+ client.should_not be_closed
35
+ EM::Synchrony.sleep 0.1
36
+ client.should be_closed
37
+ end
38
+ end
39
+
40
+ describe "and :timeout option" do
41
+ it "times out the connection after the specified amount of time" do
42
+ command "-p 30001 -t 0.5", 0.6 do
43
+ client
44
+ EM::Synchrony.sleep 0.45
45
+ client.should_not be_closed
46
+ EM::Synchrony.sleep 0.1
47
+ client.should be_closed_by_remote
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "and :timeout option set to 0" do
53
+ it "keeps the connection open until the client closes it" do
54
+ command "-p 30001 -t 0", 2 do
55
+ client
56
+ EM::Synchrony.sleep 1.95
57
+ client.should_not be_closed
58
+ end
59
+ end
60
+ end
61
+
62
+ it "closes the connection if the client sends Connection: close" do
63
+ command "-p 30001" do
64
+ client.get("/", "Connection" => "close").tap do |response|
65
+ response.headers["Connection"].should == "close"
66
+ EM::Synchrony.sync response.body
67
+ client.should be_closed_by_remote
68
+ end
69
+ end
70
+ end
71
+
72
+ it "sends Connection: keep-alive if the client also sends it" do
73
+ command "-p 30001" do
74
+ client.get("/", "Connection" => "keep-alive").tap do |response|
75
+ response.headers["Connection"].should == "keep-alive"
76
+ end
77
+ end
78
+ end
79
+
80
+ ["1.0", "0.9"].each do |version|
81
+ describe "and an HTTP #{version} client" do
82
+ after { ENV.delete "DEBUG_KEEP_ALIVE" }
83
+
84
+ it "closes the connection after one request" do
85
+ pending "http_parser.rb doesn't parse HTTP/0.9" if version == "0.9"
86
+
87
+ ENV["DEBUG_KEEP_ALIVE"] = "yes please"
88
+
89
+ command "-p 30001" do
90
+ client.get("/", {"Connection" => ""}, nil, version).tap do |response|
91
+ response.headers["Connection"].should == "close"
92
+ EM::Synchrony.sync response.body
93
+ client.should be_closed_by_remote
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -3,41 +3,66 @@ begin
3
3
  rescue LoadError; end
4
4
 
5
5
  require "em-synchrony"
6
- require "em-synchrony/em-http"
7
6
  require "fakefs/safe"
8
7
 
9
- RSpec.configure {|config|
10
- config.before(:all) {
11
- EM.class_eval {
8
+ RSpec.configure do |config|
9
+ config.before :each do
10
+ ENV["RACK_ENV"] = "testing"
11
+ end
12
+
13
+ config.before :all do
14
+ EM.class_eval do
12
15
  class << self
13
16
  attr_reader :spec_hooks
14
17
  def synchrony_with_hooks(blk = nil, tail = nil, &block)
15
18
  synchrony_without_hooks do
16
19
  (blk || block).call
17
- @spec_hooks.each {|sh| sh.call }
20
+ @spec_hooks.each &:call
18
21
  end
19
22
  end
20
23
  alias_method :synchrony_without_hooks, :synchrony
21
24
  alias_method :synchrony, :synchrony_with_hooks
22
25
  end
23
- }
24
- }
26
+ end
27
+ end
25
28
 
26
- config.after(:all) {
27
- EM.class_eval {
29
+ config.after :all do
30
+ EM.class_eval do
28
31
  class << self
29
32
  remove_method :spec_hooks
30
33
  alias_method :synchrony, :synchrony_without_hooks
31
34
  remove_method :synchrony_with_hooks
32
35
  end
33
- }
34
- }
36
+ end
37
+ end
35
38
 
36
- config.before(:each) {
39
+ config.before :each do
37
40
  EM.instance_variable_set :@spec_hooks, []
38
- }
41
+ end
39
42
 
40
- config.after(:each) {
43
+ config.after :each do
41
44
  EM.instance_variable_set :@spec_hooks, nil
42
- }
43
- }
45
+ end
46
+
47
+ def secure_reactor(timeout = 0.05, &expectations)
48
+ finished = false
49
+ location = caller[0]
50
+
51
+ EM.spec_hooks << proc do
52
+ EM.add_timer(timeout) do
53
+ EM.stop
54
+ fail "Timeout exceeded" unless finished
55
+ end
56
+ end
57
+ EM.spec_hooks << proc do
58
+ expectations.call
59
+ finished = true
60
+ EM.next_tick { EM.stop }
61
+ end
62
+ end
63
+
64
+ def command(opts, timeout = 0.5, &expectations)
65
+ secure_reactor timeout, &expectations
66
+ Hatetepe::CLI.start opts.split
67
+ end
68
+ end
@@ -1,9 +1,9 @@
1
1
  require "spec_helper"
2
- require "hatetepe/app"
2
+ require "hatetepe/server"
3
3
 
4
- describe Hatetepe::App do
4
+ describe Hatetepe::Server::App do
5
5
  let(:inner_app) { stub "inner app", :call => response }
6
- let(:app) { Hatetepe::App.new inner_app }
6
+ let(:app) { Hatetepe::Server::App.new inner_app }
7
7
  let(:env) {
8
8
  {
9
9
  "stream.start" => proc {},
@@ -19,7 +19,7 @@ describe Hatetepe::App do
19
19
 
20
20
  context "#initialize(inner_app)" do
21
21
  it "keeps the inner app" do
22
- Hatetepe::App.new(inner_app).app.should equal(inner_app)
22
+ Hatetepe::Server::App.new(inner_app).app.should equal(inner_app)
23
23
  end
24
24
  end
25
25
 
@@ -48,7 +48,9 @@ describe Hatetepe::App do
48
48
  [500, {"Content-Type" => "text/html"}, ["Internal Server Error"]]
49
49
  }
50
50
 
51
- it "responds with 500 when catching an exception" do
51
+ it "responds with 500 when catching an error" do
52
+ ENV.delete "RACK_ENV"
53
+
52
54
  inner_app.stub(:call) { raise }
53
55
  app.should_receive(:postprocess) {|e, res|
54
56
  res.should == error_response
@@ -57,6 +59,15 @@ describe Hatetepe::App do
57
59
  app.call env
58
60
  end
59
61
 
62
+ describe "if server's :env option is testing" do
63
+ let(:error) { StandardError.new }
64
+
65
+ it "doesn't catch errors" do
66
+ inner_app.stub(:call) { raise error }
67
+ expect { app.call env }.to raise_error(error)
68
+ end
69
+ end
70
+
60
71
  let(:async_response) { [-1, {}, []] }
61
72
 
62
73
  it "catches :async for Thin compatibility" do
@@ -78,7 +78,7 @@ describe Hatetepe::Builder do
78
78
  before { builder.send :initialize }
79
79
 
80
80
  it "is a shortcut for #request_line, #headers, #body, #complete" do
81
- builder.should_receive(:request_line).with req[0], req[1]
81
+ builder.should_receive(:request_line).with req[0], req[1], "1.1"
82
82
  builder.should_receive(:headers).with req[2]
83
83
  builder.should_receive(:body).with req[3]
84
84
  builder.should_receive :complete
@@ -86,7 +86,7 @@ describe Hatetepe::Builder do
86
86
  end
87
87
 
88
88
  it "doesn't require a body (fourth element)" do
89
- builder.should_receive(:request_line).with req[0], req[1]
89
+ builder.should_receive(:request_line).with req[0], req[1], "1.1"
90
90
  builder.should_receive(:headers).with req[2]
91
91
  builder.should_not_receive :body
92
92
  builder.request req[0..2]
@@ -142,7 +142,7 @@ describe Hatetepe::Builder do
142
142
  before { builder.send :initialize }
143
143
 
144
144
  it "is a shortcut for #response_line, #headers, #body, #complete" do
145
- builder.should_receive(:response_line).with res[0]
145
+ builder.should_receive(:response_line).with res[0], "1.1"
146
146
  builder.should_receive(:headers).with res[1]
147
147
  builder.should_receive(:body).with res[2]
148
148
  builder.should_receive :complete
@@ -150,7 +150,7 @@ describe Hatetepe::Builder do
150
150
  end
151
151
 
152
152
  it "doesn't require a body (third element)" do
153
- builder.should_receive(:response_line).with res[0]
153
+ builder.should_receive(:response_line).with res[0], "1.1"
154
154
  builder.should_receive(:headers).with res[1]
155
155
  builder.should_not_receive :body
156
156
  builder.response res[0..1]
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+ require "hatetepe/client"
3
+
4
+ describe Hatetepe::Client::Pipeline do
5
+ let(:app) { stub "app", :call => nil }
6
+ let(:pipeline) { Hatetepe::Client::Pipeline.new app }
7
+
8
+ describe "#initialize(app)" do
9
+ it "sets the app" do
10
+ pipeline.app.should equal(app)
11
+ end
12
+ end
13
+
14
+ let(:requests) {
15
+ [stub("previous_request"), stub("request")]
16
+ }
17
+ let(:lock) { stub "lock" }
18
+ let(:pending) { {requests.first.object_id => lock} }
19
+ let(:client) do
20
+ stub "client", :requests => requests, :pending_transmission => pending
21
+ end
22
+ let(:response) { stub "response" }
23
+
24
+ before do
25
+ requests.last.stub :connection => client
26
+ EM::Synchrony.stub :sync
27
+ end
28
+
29
+ describe "#call(request)" do
30
+ it "waits until the previous request has been transmitted" do
31
+ EM::Synchrony.should_receive(:sync).with lock
32
+ pipeline.call requests.last
33
+ end
34
+
35
+ it "calls the app" do
36
+ app.should_receive(:call).with(requests.last) { response }
37
+ pipeline.call(requests.last).should equal(response)
38
+ end
39
+ end
40
+ end
@@ -2,252 +2,408 @@ require "spec_helper"
2
2
  require "hatetepe/client"
3
3
 
4
4
  describe Hatetepe::Client do
5
- let(:client) {
6
- Hatetepe::Client.allocate.tap {|c|
7
- c.send :initialize, config
8
- c.stub :send_data
9
- c.post_init
10
- }
11
- }
12
- let(:config) {
13
- {
14
- :host => stub("host", :to_s => "foohost"),
15
- :port => stub("port", :to_s => "12345")
16
- }
17
- }
5
+ let(:client) do
6
+ Hatetepe::Client.allocate.tap {|c| c.send :initialize, config }
7
+ end
8
+ let(:config) { stub "config" }
18
9
 
19
- context ".start(config)" do
20
- it "attaches a socket to the EventMachine reactor" do
21
- EM.should_receive(:connect) {|host, port, klass, cfg|
22
- host.should equal(cfg[:host])
23
- port.should equal(cfg[:port])
24
- klass.should equal(Hatetepe::Client)
25
- cfg.should equal(config)
26
- client
27
- }
28
- Hatetepe::Client.start(config).should equal(client)
29
- end
10
+ let(:uri) { "http://example.net:8080/foo" }
11
+ let(:parsed_uri) { URI.parse uri }
12
+ let(:request) { Hatetepe::Request.new *request_as_array }
13
+ let(:request_as_array) { ["GET", "/foo", {"Host" => "example.net:8080"}, [], "1.1"] }
14
+ let(:headers) { {} }
15
+ let(:body) { stub "body" }
16
+ let(:response) { Hatetepe::Response.new 200 }
17
+
18
+ it "inherits from Hatetepe::Connection" do
19
+ client.should be_a(Hatetepe::Connection)
30
20
  end
31
21
 
32
- context ".request(verb, uri, headers, body)" do
33
- let(:verb) { stub "verb" }
34
- let(:uri) { "http://foo.bar/baz?key=value" }
35
- let(:headers) { stub "headers", :[]= => nil, :[] => nil }
36
- let(:body) { stub "body" }
22
+ describe "#initialize(config)" do
23
+ it "sets the config" do
24
+ client.config.should equal(config)
25
+ end
37
26
 
38
- before {
39
- Hatetepe::Client.stub(:start) { client }
40
- }
27
+ it "creates the builder and parser" do
28
+ client.parser.should be_a(Hatetepe::Parser)
29
+ client.builder.should be_a(Hatetepe::Builder)
30
+ end
41
31
 
42
- it "starts a client" do
43
- Fiber.new {
44
- Hatetepe::Client.should_receive(:start) {|config|
45
- config[:port].should == URI.parse(uri).port
46
- config[:host].should == URI.parse(uri).host
47
- client
48
- }
49
- Hatetepe::Client.request "GET", uri
50
- }.resume
51
- end
52
-
53
- let(:user_agent) { stub "user agent" }
54
-
55
- it "sets an appropriate User-Agent header if there is none" do
56
- Fiber.new {
57
- client.should_receive(:<<) {|request|
58
- request.headers["User-Agent"].should == "hatetepe/#{Hatetepe::VERSION}"
59
- }
60
- Hatetepe::Client.request "GET", uri
61
-
62
- client.should_receive(:<<) {|request|
63
- request.headers["User-Agent"].should equal(user_agent)
64
- }
65
- Hatetepe::Client.request "GET", uri, "User-Agent" => user_agent
66
- }.resume
67
- end
68
-
69
- it "uses an empty, write-closed Body as default" do
70
- Fiber.new {
71
- client.should_receive(:<<) {|request|
72
- request.body.closed_write?.should be_true
73
- request.body.should be_empty
74
- }
75
- Hatetepe::Client.request verb, uri
76
- }.resume
77
- end
78
-
79
- it "sends the request" do
80
- Fiber.new {
81
- client.should_receive(:<<) {|request|
82
- request.verb.should equal(verb)
83
- request.uri.should == URI.parse(uri).request_uri
84
- request.headers.should equal(headers)
85
- request.body.should equal(body)
86
- }
87
- Hatetepe::Client.request verb, uri, headers, body
88
- }.resume
89
- end
90
-
91
- it "waits for the request to succeed" do
92
- request, succeeded = nil, false
93
- Fiber.new {
94
- client.should_receive(:<<) {|req| request = req }
95
- Hatetepe::Client.request verb, uri
96
- succeeded = true
97
- }.resume
98
-
99
- succeeded.should be_false
100
- request.succeed
101
- succeeded.should be_true
32
+ it "creates the requests list" do
33
+ client.requests.should be_an(Array)
34
+ client.requests.should be_empty
35
+ end
36
+
37
+ it "creates the lists of requests pending transmission or response" do
38
+ client.pending_transmission.should be_a(Hash)
39
+ client.pending_transmission.should be_empty
40
+ client.pending_response.should be_a(Hash)
41
+ client.pending_response.should be_empty
42
+ end
43
+
44
+ it "builds the app" do
45
+ client.app.should be_a(Hatetepe::Client::KeepAlive)
46
+ client.app.app.should be_a(Hatetepe::Client::Pipeline)
47
+ client.app.app.app.should == client.method(:send_request)
102
48
  end
103
49
  end
104
50
 
105
- [:get, :head].each {|verb|
106
- context ".#{verb}(uri, headers)" do
107
- let(:uri) { stub "uri" }
108
- let(:headers) { stub "headers" }
109
- let(:response) { stub "response" }
110
-
111
- it "forwards to .request('#{verb.to_s.upcase}')" do
112
- Hatetepe::Client.should_receive(:request) {|verb, urii, hedders|
113
- verb.should == verb.to_s.upcase
114
- urii.should equal(uri)
115
- hedders.should equal(headers)
116
- response
117
- }
118
- Hatetepe::Client.send(verb, uri, headers).should equal(response)
119
- end
51
+ describe "#post_init" do
52
+ it "wires the builder and parser" do
53
+ client.post_init
54
+ client.builder.on_write[0].should == client.method(:send_data)
55
+ client.parser.on_response[0].should == client.method(:receive_response)
120
56
  end
121
- }
57
+
58
+ it "enables processing" do
59
+ client.post_init
60
+ client.processing_enabled.should be_true
61
+ end
62
+ end
122
63
 
123
- [:options, :post, :put, :delete, :trace, :connect].each {|verb|
124
- context ".#{verb}(uri, headers, body)" do
125
- let(:uri) { stub "uri" }
126
- let(:headers) { stub "headers" }
127
- let(:response) { stub "response" }
128
- let(:body) { stub "body" }
129
-
130
- it "forwards to .request('#{verb.to_s.upcase}')" do
131
- Hatetepe::Client.should_receive(:request) {|verb, urii, hedders, bodeh|
132
- verb.should == verb.to_s.upcase
133
- urii.should equal(uri)
134
- hedders.should equal(headers)
135
- bodeh.should equal(body)
136
- response
137
- }
138
- Hatetepe::Client.send(verb, uri, headers, body).should equal(response)
139
- end
64
+ describe "#receive_data(data)" do
65
+ let(:data) { stub "data" }
66
+
67
+ it "feeds the data into the parser" do
68
+ client.parser.should_receive(:<<).with data
69
+ client.receive_data data
70
+ end
71
+
72
+ let(:error) { StandardError.new "alarm! eindringlinge! alarm!" }
73
+
74
+ it "stops the client if it catches an error" do
75
+ client.parser.should_receive(:<<).and_raise error
76
+ client.should_receive :close_connection
77
+ proc { client.receive_data data }.should raise_error(error)
140
78
  end
141
- }
79
+ end
142
80
 
143
- context "#initialize(config)" do
144
- let(:client) { Hatetepe::Client.allocate }
81
+ describe "#send_request(request)" do
82
+ let(:entry) { stub "entry" }
145
83
 
146
- it "sets the config" do
147
- client.send :initialize, config
148
- client.config.should equal(config)
84
+ before do
85
+ client.pending_transmission[request.object_id] = entry
86
+ client.builder.stub :request
87
+ entry.stub :succeed
88
+ EM::Synchrony.stub :sync
89
+ end
90
+
91
+ it "feeds the request into the builder" do
92
+ client.builder.should_receive(:request).with request_as_array
93
+ client.send_request request
94
+ end
95
+
96
+ it "succeeds the request's entry in the pending transmission list" do
97
+ entry.should_receive :succeed
98
+ client.send_request request
99
+ end
100
+
101
+ it "adds the request to the pending response list and waits" do
102
+ EM::Synchrony.should_receive(:sync) do |syncee|
103
+ syncee.should respond_to(:succeed)
104
+ client.pending_response[request.object_id].should equal(syncee)
105
+ end
106
+ client.send_request request
107
+ end
108
+
109
+ it "returns the waiting result" do
110
+ EM::Synchrony.should_receive(:sync).and_return response
111
+ client.send_request(request).should equal(response)
112
+ end
113
+
114
+ it "makes sure the request gets removed from the pending response list" do
115
+ EM::Synchrony.should_receive(:sync).and_raise StandardError
116
+ client.send_request request rescue nil
117
+ client.pending_response.should be_empty
149
118
  end
150
119
  end
151
120
 
152
- context "#post_init" do
153
- let(:client) {
154
- Hatetepe::Client.allocate.tap {|c|
155
- c.send :initialize, config
156
- }
157
- }
121
+ describe "#receive_response(response)" do
158
122
  let(:requests) {
159
- [true, nil, nil].map {|response|
160
- Hatetepe::Request.new("GET", "/").tap {|request|
161
- request.response = response
162
- }
163
- }
123
+ [
124
+ stub("request_with_response", :response => stub("response")),
125
+ request,
126
+ stub("another_request", :response => nil)
127
+ ]
164
128
  }
165
- let(:response) { stub "response", :body => Hatetepe::Body.new }
129
+ let(:id) { requests[1].object_id }
166
130
 
167
- before {
168
- client.post_init
169
- client.requests.push *requests
170
- }
171
-
172
- context "'s on_response handler" do
173
- it "associates the response with a request" do
174
- client.parser.on_response[0].call response
175
- requests[1].response.should equal(response)
176
- end
131
+ before do
132
+ client.stub :requests => requests
133
+ client.pending_response[id] = stub("entry", :succeed => nil)
177
134
  end
178
135
 
179
- context "'s on_headers handler" do
180
- it "succeeds the response's request" do
181
- requests[1].response = response
182
- requests[1].should_receive(:succeed).with response
183
- client.parser.on_headers[0].call
184
- end
136
+ it "succeeds the pending response list entry of the first request without a response" do
137
+ client.pending_response[id].should_receive(:succeed).with response
138
+ client.receive_response response
185
139
  end
186
140
 
187
- context "'s on_write handler" do
188
- it "forwards to EM's send_data" do
189
- client.builder.on_write[0].should == client.method(:send_data)
190
- end
141
+ it "associates the response with the corresponding request" do
142
+ client.receive_response response
143
+ request.response.should equal(response)
191
144
  end
192
145
  end
193
146
 
194
- context "#<<(request)" do
195
- let(:request) { Hatetepe::Request.new "GET", "/" }
196
- let(:builder) { client.builder }
147
+ describe "#<<(request)" do
148
+ let(:fiber) { stub "fiber", :resume => nil }
149
+ let(:app) { stub "app", :call => response }
197
150
 
198
- it "forces a new Host header" do
199
- builder.should_receive(:header) {|key, value|
200
- value.should == "foohost:12345"
201
- }
151
+ before do
152
+ client.processing_enabled = true
153
+ client.stub :app => app
154
+ Fiber.stub(:new) {|blk| blk.call; fiber }
155
+ end
156
+
157
+ it "sets the request's connection" do
158
+ request.should_receive(:connection=).with client
202
159
  client << request
203
160
  end
204
161
 
205
- it "adds the request to #requests" do
162
+ it "adds the request to the requests list" do
163
+ app.should_receive :call do
164
+ client.requests[-1].should equal(request)
165
+ end
206
166
  client << request
207
- client.requests.last.should equal(request)
167
+ client.requests.should be_empty
208
168
  end
209
169
 
210
- it "feeds the builder" do
211
- request.body.write "asdf"
212
-
213
- builder.should_receive(:request_line).with request.verb, request.uri
214
- builder.should_receive(:headers).with request.headers
215
- builder.should_receive(:body).with request.body
216
- builder.should_receive(:complete)
170
+ it "fails and ignores the request if processing is disabled" do
171
+ client.processing_enabled = false
172
+ request.should_receive :fail
173
+ app.should_not_receive :call
217
174
 
218
175
  client << request
176
+ request.connection.should equal(client)
219
177
  end
220
178
 
221
- it "wraps the builder feeding within a Fiber" do
222
- outer, inner = Fiber.current, nil
223
- builder.should_receive(:request_line) {
224
- inner = Fiber.current
225
- }
226
-
227
- builder.stub :headers
228
- builder.stub :body
229
- builder.should_receive(:complete) {
230
- inner.should equal(Fiber.current)
231
- }
232
-
179
+ it "adds the request to the pending transmission list" do
180
+ app.should_receive :call do |req|
181
+ client.pending_transmission[req.object_id].should respond_to(:succeed)
182
+ end
183
+ client << request
184
+ end
185
+
186
+ it "calls the app" do
187
+ app.should_receive(:call).with request
233
188
  client << request
234
- outer.should_not equal(inner)
189
+ end
190
+
191
+ it "sets the response" do
192
+ request.should_receive(:response=).with response
193
+ client << request
194
+ end
195
+
196
+ it "succeeds the request if the response status indicates success" do
197
+ request.should_receive(:succeed).with response
198
+ client << request
199
+ end
200
+
201
+ it "fails the request if the response status indicates failure" do
202
+ response.status = 403
203
+ request.should_receive(:fail).with response
204
+ client << request
205
+ end
206
+
207
+ it "fails the request if no response has been received" do
208
+ app.stub :call => nil
209
+ request.should_receive(:fail).with nil
210
+ client << request
211
+ end
212
+
213
+ it "makes sure the request gets removed from the pending transmission list" do
214
+ app.should_receive(:call).and_raise StandardError
215
+ client << request rescue nil
216
+ client.pending_transmission.should be_empty
235
217
  end
236
218
  end
237
219
 
238
- context "#receive_data(data)" do
239
- let(:chunk) { stub "chunk" }
220
+ describe "#request(verb, uri, headers, body)" do
221
+ let :config do
222
+ {
223
+ :host => "example.org",
224
+ :port => 8080
225
+ }
226
+ end
227
+
228
+ before do
229
+ EM::Synchrony.stub :sync
230
+ client.stub :<<
231
+ end
232
+
233
+ it "sets a Host header if none is set" do
234
+ client.should_receive :<< do |request|
235
+ request.headers["Host"].should == "example.org:8080"
236
+ end
237
+ client.request :get, uri
238
+ end
239
+
240
+ it "sets the User-Agent header" do
241
+ client.should_receive :<< do |request|
242
+ request.headers["User-Agent"].should == "hatetepe/#{Hatetepe::VERSION}"
243
+ end
244
+ client.request :get, uri
245
+ end
246
+
247
+ let(:user_agent) { stub "user-agent" }
248
+
249
+ it "doesn't override an existing User-Agent header" do
250
+ client.should_receive :<< do |request|
251
+ request.headers["User-Agent"].should equal(user_agent)
252
+ end
253
+ client.request :get, uri, "User-Agent" => user_agent
254
+ end
255
+
256
+ describe "with Content-Type: application/x-www-form-urlencoded" do
257
+ let :headers do
258
+ {"Content-Type" => "application/x-www-form-urlencoded"}
259
+ end
260
+
261
+ let :body do
262
+ [
263
+ stub("body#1", :length => 12),
264
+ stub("body#2", :length => 13),
265
+ stub("body#3", :length => 14)
266
+ ]
267
+ end
268
+
269
+ it "computes the body's length" do
270
+ client.should_receive :<< do |request|
271
+ request.headers["Content-Length"].should equal(39)
272
+ end
273
+ client.request :get, uri, headers, body
274
+ end
275
+
276
+ it "sets Content-Length to 0 if no body was passed" do
277
+ client.should_receive :<< do |request|
278
+ request.headers["Content-Length"].should equal(0)
279
+ end
280
+ client.request :get, uri, headers
281
+ end
282
+ end
283
+
284
+ it "doesn't close the body" do
285
+ body.should_not_receive :close_write
286
+ client.request :get, uri, {}, body
287
+ end
288
+
289
+ it "passes the request to #<<" do
290
+ client.should_receive :<< do |request|
291
+ request.verb.should == "GET"
292
+ request.uri.should == uri
293
+ request.headers.should == headers
294
+ request.body.should == [body]
295
+ end
296
+ client.request :get, uri, headers, body
297
+ end
240
298
 
241
- it "feeds the parser" do
242
- client.parser.should_receive(:<<).with chunk
243
- client.receive_data chunk
299
+ it "waits until the requests succeeds" do
300
+ EM::Synchrony.should_receive(:sync).with kind_of(Hatetepe::Request)
301
+ client.request :get, uri
302
+ end
303
+
304
+ it "closes the response body if the request's method was HEAD" do
305
+ Hatetepe::Request.any_instance.stub :response => response
306
+ response.body.should_receive :close_write
307
+ client.request :head, uri
308
+ end
309
+
310
+ it "returns the response" do
311
+ Hatetepe::Request.any_instance.stub :response => response
312
+ client.request(:get, uri).should equal(response)
244
313
  end
245
314
  end
246
315
 
247
- context "#stop" do
248
- it "closes the connection" do
316
+ describe "#stop" do
317
+ before do
318
+ response.stub :body => stub("body")
319
+ client.stub :requests => [request,
320
+ stub("another_request", :response => response)]
321
+ client.stub :close_connection
322
+ end
323
+
324
+ it "waits for the last request to complete and then stops" do
325
+ EM::Synchrony.should_receive(:sync).with(client.requests.last) { response }
326
+ EM::Synchrony.should_receive(:sync).with response.body
249
327
  client.should_receive :close_connection
250
328
  client.stop
251
329
  end
252
330
  end
331
+
332
+ describe "#wrap_body(body)" do
333
+ let(:body) { stub "body" }
334
+
335
+ it "doesn't modify a body that responds to #each" do
336
+ body.stub :each
337
+ client.wrap_body(body).should equal(body)
338
+ end
339
+
340
+ it "makes a body that responds to #read enumerable" do
341
+ body.stub :read => stub("#read")
342
+ client.wrap_body(body).should == [body.read]
343
+ end
344
+
345
+ it "makes other bodies enumerable" do
346
+ client.wrap_body(body).should == [body]
347
+ end
348
+
349
+ it "makes an empty body enumerable" do
350
+ client.wrap_body(nil).should == []
351
+ end
352
+ end
353
+
354
+ describe ".start(config)" do
355
+ let(:config) { {:host => "0.0.0.0", :port => 1234} }
356
+ let(:client) { stub "client" }
357
+
358
+ it "starts an EventMachine connection and returns it" do
359
+ EM.should_receive(:connect).with(config[:host], config[:port],
360
+ Hatetepe::Client, config) { client }
361
+ Hatetepe::Client.start(config).should equal(client)
362
+ end
363
+ end
364
+
365
+ describe ".request(verb, uri, headers, body)" do
366
+ let(:client) { stub "client" }
367
+
368
+ before do
369
+ Hatetepe::Client.stub :start => client
370
+ client.stub :request => response, :stop => nil
371
+ end
372
+
373
+ it "starts a client" do
374
+ Hatetepe::Client.should_receive(:start).with :host => parsed_uri.host,
375
+ :port => parsed_uri.port
376
+ Hatetepe::Client.request :get, uri
377
+ end
378
+
379
+ it "feeds the request into the client and returns the response" do
380
+ client.should_receive(:request).with(:get, parsed_uri.request_uri,
381
+ headers, body) { response }
382
+ Hatetepe::Client.request(:get, uri, headers, body).should equal(response)
383
+ end
384
+
385
+ it "stops the client when the response has finished" do
386
+ client.should_receive :stop
387
+ Hatetepe::Client.request :get, uri
388
+ end
389
+ end
390
+
391
+ [:get, :head, :post, :put, :delete,
392
+ :options, :trace, :connect].each do |verb|
393
+ describe "##{verb}(uri, headers, body)" do
394
+ it "delegates to #request" do
395
+ client.should_receive(:request).with(verb, uri, headers, body) { response }
396
+ client.send(verb, uri, headers, body).should equal(response)
397
+ end
398
+ end
399
+
400
+ describe ".#{verb}(uri, headers, body)" do
401
+ let(:client) { Hatetepe::Client }
402
+
403
+ it "delegates to .request" do
404
+ client.should_receive(:request).with(verb, uri, headers, body) { response }
405
+ client.send(verb, uri, headers, body).should equal(response)
406
+ end
407
+ end
408
+ end
253
409
  end