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