jubilee 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.rbenv-version +1 -0
  2. data/Gemfile +16 -0
  3. data/Gemfile.lock +54 -0
  4. data/Guardfile +24 -0
  5. data/README.md +61 -0
  6. data/Rakefile +94 -0
  7. data/VERSION +1 -0
  8. data/bin/jubilee +6 -0
  9. data/bin/jubilee_d +10 -0
  10. data/examples/jubilee/keystore.jks +0 -0
  11. data/examples/jubilee/server-keystore.jks +0 -0
  12. data/examples/ssl/ServerTest.java +19 -0
  13. data/examples/ssl/webroot/index.html +10 -0
  14. data/jars/netty-3.6.0.Beta1.jar +0 -0
  15. data/jars/vertx-core-1.3.0.final.jar +0 -0
  16. data/java/.idea/ant.xml +7 -0
  17. data/java/.idea/libraries/jruby.xml +9 -0
  18. data/java/.idea/libraries/netty_3_6_0_Beta1.xml +9 -0
  19. data/java/.idea/libraries/vertx_core_1_3_0_final.xml +9 -0
  20. data/java/src/jubilee/JubileeService.java +21 -0
  21. data/java/src/org/jruby/jubilee/Const.java +148 -0
  22. data/java/src/org/jruby/jubilee/RackApplication.java +78 -0
  23. data/java/src/org/jruby/jubilee/RackEnvironment.java +13 -0
  24. data/java/src/org/jruby/jubilee/RackErrors.java +44 -0
  25. data/java/src/org/jruby/jubilee/RackInput.java +62 -0
  26. data/java/src/org/jruby/jubilee/RackResponse.java +16 -0
  27. data/java/src/org/jruby/jubilee/Server.java +104 -0
  28. data/java/src/org/jruby/jubilee/deploy/Starter.java +26 -0
  29. data/java/src/org/jruby/jubilee/impl/DefaultRackEnvironment.java +98 -0
  30. data/java/src/org/jruby/jubilee/impl/NullIO.java +111 -0
  31. data/java/src/org/jruby/jubilee/impl/RubyIORackErrors.java +68 -0
  32. data/java/src/org/jruby/jubilee/impl/RubyIORackInput.java +164 -0
  33. data/lib/jubilee.rb +11 -0
  34. data/lib/jubilee/application.rb +13 -0
  35. data/lib/jubilee/cli.rb +74 -0
  36. data/lib/jubilee/configuration.rb +52 -0
  37. data/lib/jubilee/const.rb +39 -0
  38. data/lib/jubilee/jubilee.jar +0 -0
  39. data/lib/jubilee/response.rb +64 -0
  40. data/lib/jubilee/server.rb +16 -0
  41. data/lib/rack/handler/jubilee.rb +43 -0
  42. data/test/.rbenv-version +1 -0
  43. data/test/config/app.rb +5 -0
  44. data/test/jubilee/test_cli.rb +11 -0
  45. data/test/jubilee/test_config.rb +14 -0
  46. data/test/jubilee/test_persistent.rb +238 -0
  47. data/test/jubilee/test_rack_server.rb +116 -0
  48. data/test/jubilee/test_server.rb +68 -0
  49. data/test/sinatra_app/app.rb +31 -0
  50. data/test/sinatra_app/config.ru +6 -0
  51. data/test/sinatra_app/public/test.html +10 -0
  52. data/test/sinatra_app/unicorn.conf.rb +29 -0
  53. data/test/test_helper.rb +21 -0
  54. metadata +160 -0
@@ -0,0 +1,39 @@
1
+ module Jubilee
2
+ module Const
3
+ JUBILEE_VERSION = VERSION = "0.1.0".freeze
4
+ HTTP_11 = "HTTP/1.1".freeze
5
+ HTTP_10 = "HTTP/1.0".freeze
6
+
7
+ SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
8
+ SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
9
+ GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
10
+ SERVER_NAME = "SERVER_NAME".freeze
11
+ SERVER_PORT = "SERVER_PORT".freeze
12
+
13
+ CGI_VER = "CGI/1.2".freeze
14
+
15
+ RACK_INPUT = "rack.input".freeze
16
+
17
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
18
+ GET = 'GET'.freeze
19
+ POST = "POST".freeze
20
+ REQUEST_PATH = "REQUEST_PATH".freeze
21
+ REQUEST_URI = "REQUEST_URI".freeze
22
+ PATH_INFO = "PATH_INFO".freeze
23
+ QUERY_STRING = "QUERY_STRING".freeze
24
+
25
+ CONTENT_LENGTH = "Content-Length".freeze
26
+ TRANSFER_ENCODING = "Transfer-Encoding".freeze
27
+
28
+ HTTP_VERSION = "HTTP_VERSION".freeze
29
+ HTTP_HOST = "HTTP_HOST".freeze
30
+ HTTP_USER_AGENT = "HTTP_USER_AGENT".freeze
31
+ HTTP_ACCEPT = "HTTP_ACCEPT".freeze
32
+ HTTP_COOKIE = "HTTP_COOKIE".freeze
33
+ HTTP_ACCEPT_LANGUAGE = "HTTP_ACCEPT_LANGUAGE".freeze
34
+ HTTP_ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze
35
+ HTTP_CONNECTION = "HTTP_CONNECTION".freeze
36
+
37
+ STATUS_WITH_NO_ENTITY_BODY = Hash[Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.map{|s| [s, true]}]
38
+ end
39
+ end
Binary file
@@ -0,0 +1,64 @@
1
+ require 'java'
2
+ module Jubilee
3
+ class Response
4
+ include Const
5
+ include org.jruby.jubilee.RackResponse
6
+
7
+ def initialize(array)
8
+ @status, @headers, @body = *array
9
+ @content_length = nil
10
+ if @body.kind_of? Array and @body.size == 1
11
+ @content_length = @body[0].bytesize
12
+ end
13
+ end
14
+
15
+ def respond(response)
16
+ no_body = @status < 200 || STATUS_WITH_NO_ENTITY_BODY[@status]
17
+ write_status(response)
18
+ write_headers(response)
19
+ if no_body
20
+ response.end
21
+ else
22
+ if @body.respond_to?(:to_path)
23
+ response.sendFile(@body.to_path)
24
+ else
25
+ write_body(response)
26
+ response.end
27
+ end
28
+ end
29
+ ensure
30
+ @body.close if @body.respond_to?(:close)
31
+ end
32
+
33
+ private
34
+ def write_status(response)
35
+ response.statusCode = @status
36
+ end
37
+
38
+ def write_headers(response)
39
+ @headers.each do |key, value|
40
+ case key
41
+ when CONTENT_LENGTH
42
+ @content_length = value
43
+ next
44
+ when TRANSFER_ENCODING
45
+ @allow_chunked = false
46
+ @content_length = nil
47
+ end
48
+ response.putHeader(key, value)
49
+ end
50
+ end
51
+
52
+ def write_body(response)
53
+ if @content_length
54
+ response.putHeader(CONTENT_LENGTH, @content_length.to_s)
55
+ else
56
+ response.setChunked(true)
57
+ end
58
+
59
+ @body.each do |part|
60
+ response.write(part)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ module Jubilee
2
+ class Server < VertxServer
3
+ def initialize(app, opts = {})
4
+ options = {port: 3215, ssl: false}.merge(opts)
5
+ if (options[:ssl])
6
+ if options[:keystore].nil?
7
+ raise ArgumentError, "Please provide a keystore for ssl"
8
+ else
9
+ super(Application.new(app), options[:port], options[:ssl], options[:keystore], options[:keystore_password])
10
+ end
11
+ else
12
+ super(Application.new(app), options[:port])
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ require 'rack/handler'
2
+ require 'jubilee'
3
+ require 'java'
4
+
5
+ module Rack
6
+ module Handler
7
+ module Jubilee
8
+ DEFAULT_OPTIONS = {
9
+ :host => '0.0.0.0',
10
+ :port => 3000,
11
+ :verbose => false
12
+ }
13
+ def self.run(app, options = {})
14
+ options = DEFAULT_OPTIONS.merge(options)
15
+
16
+ if options[:verbose]
17
+ app = Rack::CommonLogger.new(app, STDOUT)
18
+ end
19
+
20
+ if options[:environment]
21
+ ENV["RACK_ENV"] = options[:environment].to_s
22
+ end
23
+
24
+ @server = ::Jubilee::Server.new(app, options)
25
+
26
+ puts "Jubilee starting..."
27
+ puts "Environment: #{ENV['RACK_ENV']}"
28
+
29
+ yield @server if block_given?
30
+
31
+ @server.start
32
+ @starter = org.jruby.jubilee.deploy.Starter.new
33
+ @starter.block
34
+ end
35
+
36
+ def self.shutdown
37
+ @server.stop{ @starter.unblock }
38
+ exit
39
+ end
40
+ end
41
+ register :jubilee, Jubilee
42
+ end
43
+ end
@@ -0,0 +1 @@
1
+ jruby-1.7.0
@@ -0,0 +1,5 @@
1
+ class App
2
+ def call(env)
3
+ [200, {"Content-Type" => "text/plain"}, ["embeded app"]]
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require 'test_helper'
2
+ require 'jubilee/cli'
3
+
4
+ class TestJubileeCLI < MiniTest::Unit::TestCase
5
+ def test_parse_options
6
+ cli = Jubilee::CLI.new(["app.rb"])
7
+ cli.parse_options
8
+ assert_equal "app.rb", cli.options[:rackup]
9
+ assert_equal 3215, cli.options[:port]
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+ require 'jubilee/configuration'
3
+
4
+ class TestConfig < MiniTest::Unit::TestCase
5
+ def setup
6
+ @config = Jubilee::Configuration.new({rackup: "config/app.rb"})
7
+ end
8
+ def test_load
9
+ @config.load
10
+ resp = [200, {"Content-Type" => "text/plain"}, ["embeded app"]]
11
+ skip "hard to test because of Rack::Lint"
12
+ #assert_equal resp, @config.app.call({})
13
+ end
14
+ end
@@ -0,0 +1,238 @@
1
+ require 'test_helper'
2
+ require 'timeout'
3
+ require 'socket'
4
+ class TestPersistent < MiniTest::Unit::TestCase
5
+ def setup
6
+ @valid_request = "GET / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n"
7
+ @close_request = "GET / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n"
8
+ @http10_request = "GET / HTTP/1.0\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n"
9
+ @keep_request = "GET / HTTP/1.0\r\nHost: test.com\r\nContent-Type: text/plain\r\nConnection: Keep-Alive\r\n\r\n"
10
+
11
+ @valid_post = "POST / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello"
12
+ @valid_no_body = "GET / HTTP/1.1\r\nHost: test.com\r\nX-Status: 204\r\nContent-Type: text/plain\r\n\r\n"
13
+
14
+ @headers = { "X-Header" => "Works" }
15
+ @body = ["Hello"]
16
+ @inputs = []
17
+
18
+ @simple = lambda do |env|
19
+ @inputs << env['rack.input']
20
+ status = Integer(env['HTTP_X_STATUS'] || 200)
21
+ [status, @headers, @body]
22
+ end
23
+
24
+ @host = "127.0.0.1"
25
+ @port = 3215
26
+
27
+ @server = Jubilee::Server.new @simple
28
+ @server.start
29
+
30
+ @client = TCPSocket.new @host, @port
31
+ end
32
+
33
+ def teardown
34
+ @client.close
35
+ sleep 0.1 # in case server shutdown before request is submitted
36
+ @server.stop
37
+ end
38
+
39
+ def lines(count, s=@client)
40
+ str = ""
41
+ timeout(5) do
42
+ count.times { str << s.gets }
43
+ end
44
+ str
45
+ end
46
+
47
+ def test_one_with_content_length
48
+ @client << @valid_request
49
+ sz = @body[0].size.to_s
50
+
51
+ assert_match Regexp.new("HTTP/1.1 200 OK\r\nContent-Length: #{sz}\r\nX-Header: Works\r\n\r\n", true), lines(4)
52
+ assert_equal "Hello", @client.read(5)
53
+ end
54
+
55
+ def test_two_back_to_back
56
+ @client << @valid_request
57
+ sz = @body[0].size.to_s
58
+
59
+ assert_match Regexp.new("HTTP/1.1 200 OK\r\nContent-Length: #{sz}\r\nX-Header: Works\r\n\r\n", true), lines(4)
60
+ assert_equal "Hello", @client.read(5)
61
+
62
+ @client << @valid_request
63
+ sz = @body[0].size.to_s
64
+
65
+ assert_match Regexp.new("HTTP/1.1 200 OK\r\nContent-Length: #{sz}\r\nX-Header: Works\r\n\r\n", true), lines(4)
66
+ assert_equal "Hello", @client.read(5)
67
+ end
68
+
69
+ def test_post_then_get
70
+ @client << @valid_post
71
+ sz = @body[0].size.to_s
72
+
73
+ assert_match Regexp.new("HTTP/1.1 200 OK\r\nContent-Length: #{sz}\r\nX-Header: Works\r\n\r\n", true), lines(4)
74
+ assert_equal "Hello", @client.read(5)
75
+
76
+ @client << @valid_request
77
+ sz = @body[0].size.to_s
78
+
79
+ assert_match Regexp.new("HTTP/1.1 200 OK\r\nContent-Length: #{sz}\r\nX-Header: Works\r\n\r\n", true), lines(4)
80
+ assert_equal "Hello", @client.read(5)
81
+ end
82
+
83
+ #def test_no_body_then_get
84
+ # @client << @valid_no_body
85
+ # assert_equal "HTTP/1.1 204 No Content\r\nX-Header: Works\r\n\r\n", lines(3)
86
+
87
+ # @client << @valid_request
88
+ # sz = @body[0].size.to_s
89
+
90
+ # assert_equal "HTTP/1.1 200 OK\r\nX-Header: Works\r\nContent-Length: #{sz}\r\n\r\n", lines(4)
91
+ # assert_equal "Hello", @client.read(5)
92
+ #end
93
+
94
+ def test_chunked
95
+ @body << "Chunked"
96
+
97
+ @client << @valid_request
98
+
99
+ assert_equal "HTTP/1.1 200 OK\r\nx-header: Works\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n7\r\nChunked\r\n0\r\n\r\n", lines(10)
100
+ end
101
+
102
+ def test_no_chunked_in_http10
103
+ @body << "Chunked"
104
+
105
+ @client << @http10_request
106
+
107
+ assert_equal "HTTP/1.0 200 OK\r\nConnection: close\r\nx-header: Works\r\n\r\n", lines(4)
108
+ assert_equal "HelloChunked", @client.read
109
+ end
110
+
111
+ def test_hex
112
+ str = "This is longer and will be in hex"
113
+ @body << str
114
+
115
+ @client << @valid_request
116
+
117
+ assert_equal "HTTP/1.1 200 OK\r\nx-header: Works\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n#{str.size.to_s(16)}\r\n#{str}\r\n0\r\n\r\n", lines(10)
118
+
119
+ end
120
+
121
+ def test_client11_close
122
+ @client << @close_request
123
+ sz = @body[0].size.to_s
124
+
125
+ assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(5)
126
+ assert_equal "Hello", @client.read(5)
127
+ end
128
+
129
+ def test_client10_close
130
+ @client << @http10_request
131
+ sz = @body[0].size.to_s
132
+
133
+ assert_equal "HTTP/1.0 200 OK\r\nConnection: close\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(5)
134
+ assert_equal "Hello", @client.read(5)
135
+ end
136
+
137
+ def test_one_with_keep_alive_header
138
+ @client << @keep_request
139
+ sz = @body[0].size.to_s
140
+
141
+ assert_equal "HTTP/1.0 200 OK\r\nConnection: keep-alive\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(5)
142
+ assert_equal "Hello", @client.read(5)
143
+ end
144
+
145
+ def test_persistent_timeout
146
+ @server.persistent_timeout = 2
147
+ @client << @valid_request
148
+ sz = @body[0].size.to_s
149
+
150
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4)
151
+ assert_equal "Hello", @client.read(5)
152
+
153
+ sleep 3
154
+
155
+ assert_raises EOFError do
156
+ @client.read_nonblock(1)
157
+ end
158
+ end
159
+
160
+ def test_app_sets_content_length
161
+ @body = ["hello", " world"]
162
+ @headers['Content-Length'] = "11"
163
+
164
+ @client << @valid_request
165
+
166
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: 11\r\nx-header: Works\r\n\r\n",
167
+ lines(4)
168
+ assert_equal "hello world", @client.read(11)
169
+ end
170
+
171
+ def test_allow_app_to_chunk_itself
172
+ skip "vertx doesn't support chunk self yet"
173
+ @headers = {'Transfer-Encoding' => "chunked" }
174
+
175
+ @body = ["5\r\nhello\r\n0\r\n\r\n"]
176
+
177
+ @client << @valid_request
178
+
179
+ assert_equal "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n", lines(7)
180
+ end
181
+
182
+
183
+ def test_two_requests_in_one_chunk
184
+ @server.persistent_timeout = 3
185
+
186
+ req = @valid_request.to_s
187
+ req << "GET /second HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n"
188
+
189
+ @client << req
190
+
191
+ sz = @body[0].size.to_s
192
+
193
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4)
194
+ assert_equal "Hello", @client.read(5)
195
+
196
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4)
197
+ assert_equal "Hello", @client.read(5)
198
+ end
199
+
200
+ def test_second_request_not_in_first_req_body
201
+ @server.persistent_timeout = 3
202
+
203
+ req = @valid_request.to_s
204
+ req << "GET /second HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n"
205
+
206
+ @client << req
207
+
208
+ sz = @body[0].size.to_s
209
+
210
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4)
211
+ assert_equal "Hello", @client.read(5)
212
+
213
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4)
214
+ assert_equal "Hello", @client.read(5)
215
+
216
+ # Since rack process request before the body is ready, we cannot
217
+ # utilize this optimization
218
+ # assert_kind_of Jubilee::NullIO, @inputs[0]
219
+ # assert_kind_of Jubilee::NullIO, @inputs[1]
220
+ end
221
+
222
+ def test_keepalive_doesnt_starve_clients
223
+ sz = @body[0].size.to_s
224
+
225
+ @client << @valid_request
226
+
227
+ c2 = TCPSocket.new @host, @port
228
+ c2 << @valid_request
229
+
230
+ out = IO.select([c2], nil, nil, 1)
231
+
232
+ assert out, "select returned nil"
233
+ assert_equal c2, out.first.first
234
+
235
+ assert_equal "HTTP/1.1 200 OK\r\ncontent-length: #{sz}\r\nx-header: Works\r\n\r\n", lines(4, c2)
236
+ assert_equal "Hello", c2.read(5)
237
+ end
238
+ end
@@ -0,0 +1,116 @@
1
+ require 'test_helper'
2
+ require 'rack/lint'
3
+ require 'rack/commonlogger'
4
+
5
+ class TestRackServer < MiniTest::Unit::TestCase
6
+
7
+ class ErrorChecker
8
+ def initialize(app)
9
+ @app = app
10
+ @exception = nil
11
+ @env = nil
12
+ end
13
+
14
+ attr_reader :exception, :env
15
+
16
+ def call(env)
17
+ begin
18
+ @env = env
19
+ return @app.call(env)
20
+ rescue Exception => e
21
+ @exception = e
22
+
23
+ [
24
+ 500,
25
+ { "X-Exception" => e.message, "X-Exception-Class" => e.class.to_s },
26
+ ["Error detected"]
27
+ ]
28
+ end
29
+ end
30
+ end
31
+
32
+ class ServerLint < Rack::Lint
33
+ def call(env)
34
+ assert("No env given") { env }
35
+ check_env env
36
+
37
+ @app.call(env)
38
+ end
39
+ end
40
+
41
+ def setup
42
+ @valid_request = "GET / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n"
43
+
44
+ @simple = lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] }
45
+ @checker = ErrorChecker.new ServerLint.new(@simple)
46
+ end
47
+
48
+ def teardown
49
+ @server.stop
50
+ end
51
+
52
+ def test_lint
53
+ @server = Jubilee::Server.new @checker
54
+
55
+ @server.start
56
+
57
+ hit(['http://127.0.0.1:3215/test'])
58
+
59
+ if exc = @checker.exception
60
+ raise exc
61
+ end
62
+ end
63
+
64
+ def test_large_post_body
65
+ @checker = ErrorChecker.new ServerLint.new(@simple)
66
+ @server = Jubilee::Server.new @checker
67
+
68
+ @server.start
69
+
70
+ big = "x" * (1024 * 16)
71
+
72
+ Net::HTTP.post_form URI.parse('http://127.0.0.1:3215/test'),
73
+ { "big" => big }
74
+
75
+ if exc = @checker.exception
76
+ raise exc
77
+ end
78
+ end
79
+
80
+ def test_path_info
81
+ input = nil
82
+ @server = Jubilee::Server.new (lambda { |env| input = env; @simple.call(env) })
83
+ @server.start
84
+
85
+ hit(['http://127.0.0.1:3215/test/a/b/c'])
86
+
87
+ assert_equal "/test/a/b/c", input['PATH_INFO']
88
+ end
89
+
90
+ def test_query_string
91
+ input = nil
92
+ @server = Jubilee::Server.new (lambda { |env| input = env; @simple.call(env) })
93
+ @server.start
94
+
95
+ hit(['http://127.0.0.1:3215/test/a/b/c?foo=bar'])
96
+
97
+ assert_equal "foo=bar", input['QUERY_STRING']
98
+ end
99
+
100
+ def test_post_data
101
+ require 'rack/request'
102
+ input = nil
103
+ @server = Jubilee::Server.new (lambda { |env| input = env; @simple.call(env) })
104
+ @server.start
105
+
106
+ req = Net::HTTP::Post::Multipart.new("/", "foo" => "bar")
107
+ resp = Net::HTTP.start('localhost', 3215) do |http|
108
+ http.request req
109
+ end
110
+
111
+ #Net::HTTP.post_form URI.parse('http://127.0.0.1:3215/test'), { "foo" => "bar" }
112
+
113
+ request = Rack::Request.new input
114
+ assert_equal "bar", request.params["foo"]
115
+ end
116
+ end