hatetepe 0.0.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ begin
2
+ require "awesome_print"
3
+ rescue LoadError; end
4
+
5
+ require "em-synchrony"
6
+ require "em-synchrony/em-http"
7
+ require "fakefs/safe"
8
+
9
+ RSpec.configure {|config|
10
+ config.before {
11
+ EM.class_eval {
12
+ @spec_hooks = []
13
+ class << self
14
+ attr_reader :spec_hooks
15
+ def synchrony_with_hooks(blk = nil, tail = nil, &block)
16
+ synchrony_without_hooks do
17
+ (blk || block).call
18
+ @spec_hooks.each {|sh| sh.call }
19
+ end
20
+ end
21
+ alias_method :synchrony_without_hooks, :synchrony
22
+ alias_method :synchrony, :synchrony_with_hooks
23
+ end
24
+ }
25
+ }
26
+
27
+ config.after {
28
+ EM.class_eval {
29
+ @spec_hooks = nil
30
+ class << self
31
+ remove_method :spec_hooks
32
+ alias_method :synchrony, :synchrony_without_hooks
33
+ remove_method :synchrony_with_hooks
34
+ end
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,108 @@
1
+ require "spec_helper"
2
+ require "hatetepe/app"
3
+
4
+ describe Hatetepe::App do
5
+ let(:inner_app) { stub "inner app", :call => response }
6
+ let(:app) { Hatetepe::App.new inner_app }
7
+ let(:env) {
8
+ {
9
+ "stream.start" => stub("stream.start", :call => nil),
10
+ "stream.send" => stub("stream.send", :call => nil),
11
+ "stream.close" => stub("stream.close", :call => nil)
12
+ }
13
+ }
14
+
15
+ let(:status) { 123 }
16
+ let(:headers) { stub "headers" }
17
+ let(:body) { [stub("chunk#1"), stub("chunk#2")] }
18
+ let(:response) { [status, headers, body] }
19
+
20
+ context "#initialize(inner_app)" do
21
+ it "keeps the inner app" do
22
+ Hatetepe::App.new(inner_app).app.should equal(inner_app)
23
+ end
24
+ end
25
+
26
+ context "#call(env)" do
27
+ it "sets env[async.callback] before #call'ing inner_app" do
28
+ app.call env
29
+
30
+ app.should_receive(:postprocess) {|e, res|
31
+ e.should equal(env)
32
+ res.should equal(response)
33
+ }
34
+ env["async.callback"].call response
35
+ end
36
+
37
+ it "calls #postprocess with the return of inner_app#call(env)" do
38
+ inner_app.stub :call => response
39
+ app.should_receive(:postprocess) {|e, res|
40
+ e.should equal(env)
41
+ res.should equal(response)
42
+ }
43
+
44
+ app.call env
45
+ end
46
+
47
+ let(:error_response) {
48
+ [500, {"Content-Type" => "text/html"}, ["Internal Server Error"]]
49
+ }
50
+
51
+ it "responds with 500 when catching an exception" do
52
+ inner_app.stub(:call) { raise }
53
+ app.should_receive(:postprocess) {|e, res|
54
+ res.should == error_response
55
+ }
56
+
57
+ app.call env
58
+ end
59
+
60
+ let(:async_response) { [-1, {}, []] }
61
+
62
+ it "catches :async for Thin compatibility" do
63
+ inner_app.stub(:call) { throw :async }
64
+ app.should_receive(:postprocess) {|e, res|
65
+ res.should == async_response
66
+ }
67
+
68
+ app.call env
69
+ end
70
+ end
71
+
72
+ context "#postprocess(env, response)" do
73
+ it "does nothing if the response status is lighter than 0" do
74
+ env["stream.start"].should_not_receive :call
75
+ app.postprocess env, [-1]
76
+ end
77
+
78
+ it "starts the response stream" do
79
+ env["stream.start"].should_receive(:call).with([status, headers])
80
+ app.postprocess env, [status, headers, []]
81
+ end
82
+
83
+ it "streams the body" do
84
+ env["stream.send"].should_receive(:call).with(body[0])
85
+ env["stream.send"].should_receive(:call).with(body[1])
86
+ app.postprocess env, [status, headers, body]
87
+ end
88
+
89
+ it "doesn't stream the body if it equals Rack::STREAMING" do
90
+ env["stream.send"].should_not_receive :call
91
+ app.postprocess env, [status, headers, Rack::STREAMING]
92
+ end
93
+
94
+ it "closes the response stream after streaming the body" do
95
+ env["stream.close"].should_receive :call
96
+ app.postprocess env, [status, headers, body]
97
+ end
98
+
99
+ it "closes the response even if streaming the body fails" do
100
+ body.should_receive(:each).and_raise
101
+ env["stream.close"].should_receive :call
102
+
103
+ proc {
104
+ app.postprocess env, [status, headers, body]
105
+ }.should raise_error
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,198 @@
1
+ require "spec_helper"
2
+ require "hatetepe/body"
3
+
4
+ describe Hatetepe::Body do
5
+ let(:body) { Hatetepe::Body.new }
6
+
7
+ it "is deferrable" do
8
+ body.should respond_to(:callback)
9
+ end
10
+
11
+ context "#initialize" do
12
+ it "leaves the IO stream empty" do
13
+ body.io.length.should be_zero
14
+ end
15
+ end
16
+
17
+ context "#initialize(string)" do
18
+ let(:body) { Hatetepe::Body.new "herp derp" }
19
+
20
+ it "writes the passed string" do
21
+ body.length.should equal(9)
22
+ body.io.read.should == "herp derp"
23
+ end
24
+ end
25
+
26
+ context "#sync" do
27
+ let(:conn) { stub "conn", :paused? => true }
28
+
29
+ it "resumes the source connection if any" do
30
+ body.source = conn
31
+
32
+ conn.should_receive :resume
33
+ Fiber.new { body.succeed }.resume
34
+ body.sync
35
+ end
36
+
37
+ it "forwards to EM::Synchrony.sync(body)" do
38
+ EM::Synchrony.should_receive(:sync).with(body)
39
+ body.sync
40
+ end
41
+ end
42
+
43
+ context "#length" do
44
+ let(:length) { stub "length" }
45
+
46
+ it "forwards to io#length" do
47
+ body.io.stub :length => length
48
+ body.length.should equal(length)
49
+ end
50
+ end
51
+
52
+ context "#empty?" do
53
+ it "returns true if length is zero" do
54
+ body.stub :length => 0
55
+ body.empty?.should be_true
56
+ end
57
+
58
+ it "returns false if length is non-zero" do
59
+ body.stub :length => 42
60
+ body.empty?.should be_false
61
+ end
62
+ end
63
+
64
+ context "#pos" do
65
+ let(:pos) { stub "pos" }
66
+
67
+ it "forwards to io#pos" do
68
+ body.io.stub :pos => pos
69
+ body.pos.should equal(pos)
70
+ end
71
+ end
72
+
73
+ context "#rewind" do
74
+ let(:rewind) { stub "rewind" }
75
+
76
+ it "forwards to io#rewind" do
77
+ body.io.stub :rewind => rewind
78
+ body.rewind.should equal(rewind)
79
+ end
80
+ end
81
+
82
+ context "#close_write" do
83
+ it "forwards to io#close_write" do
84
+ ret = stub("return")
85
+ body.io.should_receive(:close_write) { ret }
86
+
87
+ body.close_write.should equal(ret)
88
+ end
89
+
90
+ it "succeeds the body" do
91
+ body.should_receive :succeed
92
+ body.close_write
93
+ end
94
+ end
95
+
96
+ context "#closed_write?" do
97
+ it "forwards to io#closed_write?" do
98
+ ret = stub("return")
99
+ body.io.should_receive(:closed_write?) { ret }
100
+
101
+ body.closed_write?.should equal(ret)
102
+ end
103
+ end
104
+
105
+ context "#each {|chunk| ... }" do
106
+ it "yields each written chunk until the body succeeds" do
107
+ chunks = ["111", "222"]
108
+ received, succeeded = [], false
109
+
110
+ body << chunks[0]
111
+ Fiber.new {
112
+ body.each {|chunk| received << chunk }
113
+ succeeded = true
114
+ }.resume
115
+ received.should == chunks.values_at(0)
116
+ succeeded.should be_false
117
+
118
+ body << chunks[1]
119
+ received.should == chunks
120
+ succeeded.should be_false
121
+
122
+ body.succeed
123
+ succeeded.should be_true
124
+ end
125
+ end
126
+
127
+ context "#read(length, buffer)" do
128
+ it "waits for the body to succeed" do
129
+ ret, read = nil, false
130
+
131
+ Fiber.new { body.read; read = true }.resume
132
+ read.should be_false
133
+
134
+ body.succeed
135
+ read.should be_true
136
+ end
137
+
138
+ it "rewinds the body" do
139
+ body.succeed
140
+
141
+ body.should_receive :rewind
142
+ body.read
143
+ end
144
+
145
+ it "forwards to io#read" do
146
+ body.succeed
147
+ args, ret = [stub("arg#1"), stub("arg#2")], stub("ret")
148
+
149
+ body.io.should_receive(:read).with(*args) { ret }
150
+ body.read(*args).should equal(ret)
151
+ end
152
+ end
153
+
154
+ context "#gets" do
155
+ it "waits for the body to succeed" do
156
+ ret, read = nil, false
157
+
158
+ Fiber.new { body.gets; read = true }.resume
159
+ read.should be_false
160
+
161
+ body.succeed
162
+ read.should be_true
163
+ end
164
+
165
+ it "rewinds the body" do
166
+ body.succeed
167
+
168
+ body.should_receive :rewind
169
+ body.gets
170
+ end
171
+
172
+ it "forwards to io#gets" do
173
+ body.succeed
174
+ ret = stub("ret")
175
+
176
+ body.io.should_receive(:gets) { ret }
177
+ body.gets.should equal(ret)
178
+ end
179
+ end
180
+
181
+ context "#write(chunk)" do
182
+ it "forwards to io#write" do
183
+ arg, ret = stub("arg"), stub("ret")
184
+ body.io.should_receive(:write).with(arg) { ret }
185
+
186
+ body.write(arg).should equal(ret)
187
+ end
188
+ end
189
+
190
+ context "#<<(chunk)" do
191
+ it "forwards to io#<<" do
192
+ arg, ret = stub("arg"), stub("ret")
193
+ body.io.should_receive(:<<).with(arg) { ret }
194
+
195
+ body.<<(arg).should equal(ret)
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,270 @@
1
+ require "spec_helper"
2
+ require "hatetepe/client"
3
+
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
+ }
18
+
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
30
+ end
31
+
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" }
37
+
38
+ before {
39
+ Hatetepe::Client.stub(:start) { client }
40
+ }
41
+
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
102
+ end
103
+ end
104
+
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
120
+ end
121
+ }
122
+
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
140
+ end
141
+ }
142
+
143
+ context "#initialize(config)" do
144
+ let(:client) { Hatetepe::Client.allocate }
145
+
146
+ it "sets the config" do
147
+ client.send :initialize, config
148
+ client.config.should equal(config)
149
+ end
150
+ end
151
+
152
+ context "#post_init" do
153
+ let(:client) {
154
+ Hatetepe::Client.allocate.tap {|c|
155
+ c.send :initialize, config
156
+ }
157
+ }
158
+ let(:requests) {
159
+ [true, nil, nil].map {|response|
160
+ Hatetepe::Request.new("GET", "/").tap {|request|
161
+ request.response = response
162
+ }
163
+ }
164
+ }
165
+ let(:response) { stub "response" }
166
+
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
177
+ end
178
+
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
185
+ end
186
+
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
191
+ end
192
+ end
193
+
194
+ context "#<<(request)" do
195
+ let(:request) { Hatetepe::Request.new "GET", "/" }
196
+ let(:builder) { client.builder }
197
+
198
+ it "forces a new Host header" do
199
+ builder.should_receive(:header) {|key, value|
200
+ value.should == "foohost:12345"
201
+ }
202
+ client << request
203
+ end
204
+
205
+ it "adds the request to #requests" do
206
+ client << request
207
+ client.requests.last.should equal(request)
208
+ end
209
+
210
+ it "feeds the builder" do
211
+ request.body.write "asdf"
212
+
213
+ builder.should_receive(:request).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)
217
+
218
+ client << request
219
+ end
220
+
221
+ it "wraps the builder feeding within a Fiber" do
222
+ outer, inner = Fiber.current, nil
223
+ builder.should_receive(:request) {
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
+
233
+ client << request
234
+ outer.should_not equal(inner)
235
+ end
236
+ end
237
+
238
+ context "#receive_data(data)" do
239
+ let(:chunk) { stub "chunk" }
240
+
241
+ it "feeds the parser" do
242
+ client.parser.should_receive(:<<).with chunk
243
+ client.receive_data chunk
244
+ end
245
+ end
246
+
247
+ context "#stop" do
248
+ it "closes the connection" do
249
+ client.should_receive :close_connection_after_writing
250
+ client.stop
251
+ end
252
+ end
253
+
254
+ context "#responses" do
255
+ let(:requests) {
256
+ 4.times.map {|i| stub "request##{i}", :response => responses[i] }
257
+ }
258
+ let(:responses) {
259
+ 2.times.map {|i| stub "response##{i}" }
260
+ }
261
+
262
+ before {
263
+ client.stub :requests => requests
264
+ }
265
+
266
+ it "returns all requests' responses" do
267
+ client.responses.should == responses
268
+ end
269
+ end
270
+ end