hatetepe 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -16
- data/hatetepe.gemspec +1 -2
- data/lib/hatetepe/body.rb +5 -3
- data/lib/hatetepe/builder.rb +6 -3
- data/lib/hatetepe/cli.rb +27 -5
- data/lib/hatetepe/client.rb +157 -82
- data/lib/hatetepe/client/keep_alive.rb +58 -0
- data/lib/hatetepe/client/pipeline.rb +19 -0
- data/lib/hatetepe/connection.rb +42 -0
- data/lib/hatetepe/deferred_status_fix.rb +11 -0
- data/lib/hatetepe/message.rb +4 -4
- data/lib/hatetepe/parser.rb +3 -4
- data/lib/hatetepe/request.rb +19 -6
- data/lib/hatetepe/response.rb +11 -3
- data/lib/hatetepe/server.rb +115 -85
- data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
- data/lib/hatetepe/server/keep_alive.rb +61 -0
- data/lib/hatetepe/server/pipeline.rb +24 -0
- data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +1 -4
- data/spec/integration/cli/start_spec.rb +75 -123
- data/spec/integration/client/keep_alive_spec.rb +74 -0
- data/spec/integration/server/keep_alive_spec.rb +99 -0
- data/spec/spec_helper.rb +41 -16
- data/spec/unit/app_spec.rb +16 -5
- data/spec/unit/builder_spec.rb +4 -4
- data/spec/unit/client/pipeline_spec.rb +40 -0
- data/spec/unit/client_spec.rb +355 -199
- data/spec/unit/connection_spec.rb +64 -0
- data/spec/unit/parser_spec.rb +3 -2
- data/spec/unit/proxy_spec.rb +9 -18
- data/spec/unit/rack_handler_spec.rb +2 -12
- data/spec/unit/server_spec.rb +154 -60
- metadata +31 -36
- data/.rspec +0 -1
- data/.travis.yml +0 -3
- data/.yardopts +0 -1
- 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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
10
|
-
config.before
|
11
|
-
|
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
|
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
|
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
|
39
|
+
config.before :each do
|
37
40
|
EM.instance_variable_set :@spec_hooks, []
|
38
|
-
|
41
|
+
end
|
39
42
|
|
40
|
-
config.after
|
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
|
data/spec/unit/app_spec.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require "spec_helper"
|
2
|
-
require "hatetepe/
|
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
|
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
|
data/spec/unit/builder_spec.rb
CHANGED
@@ -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
|
data/spec/unit/client_spec.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
Hatetepe::
|
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 "
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
144
|
-
let(:
|
81
|
+
describe "#send_request(request)" do
|
82
|
+
let(:entry) { stub "entry" }
|
145
83
|
|
146
|
-
|
147
|
-
client.
|
148
|
-
client.
|
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
|
-
|
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
|
-
[
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
123
|
+
[
|
124
|
+
stub("request_with_response", :response => stub("response")),
|
125
|
+
request,
|
126
|
+
stub("another_request", :response => nil)
|
127
|
+
]
|
164
128
|
}
|
165
|
-
let(:
|
129
|
+
let(:id) { requests[1].object_id }
|
166
130
|
|
167
|
-
before
|
168
|
-
client.
|
169
|
-
client.
|
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
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
195
|
-
let(:
|
196
|
-
let(:
|
147
|
+
describe "#<<(request)" do
|
148
|
+
let(:fiber) { stub "fiber", :resume => nil }
|
149
|
+
let(:app) { stub "app", :call => response }
|
197
150
|
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
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.
|
167
|
+
client.requests.should be_empty
|
208
168
|
end
|
209
169
|
|
210
|
-
it "
|
211
|
-
|
212
|
-
|
213
|
-
|
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 "
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
239
|
-
let
|
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 "
|
242
|
-
|
243
|
-
client.
|
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
|
-
|
248
|
-
|
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
|