hatetepe 0.3.1 → 0.4.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 (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