hatetepe 0.0.4 → 0.2.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.
@@ -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