rack-contrib 0.9.0 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-contrib might be problematic. Click here for more details.
- data/README.rdoc +16 -0
- data/Rakefile +6 -1
- data/lib/rack/contrib.rb +10 -1
- data/lib/rack/contrib/accept_format.rb +46 -0
- data/lib/rack/contrib/backstage.rb +20 -0
- data/lib/rack/contrib/config.rb +16 -0
- data/lib/rack/contrib/csshttprequest.rb +39 -0
- data/lib/rack/contrib/deflect.rb +137 -0
- data/lib/rack/contrib/jsonp.rb +18 -15
- data/lib/rack/contrib/not_found.rb +18 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +10 -10
- data/lib/rack/contrib/relative_redirect.rb +44 -0
- data/lib/rack/contrib/response_cache.rb +59 -0
- data/lib/rack/contrib/route_exceptions.rb +20 -19
- data/lib/rack/contrib/signals.rb +63 -0
- data/rack-contrib.gemspec +22 -3
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/spec_rack_accept_format.rb +72 -0
- data/test/spec_rack_backstage.rb +26 -0
- data/test/spec_rack_callbacks.rb +2 -2
- data/test/spec_rack_config.rb +22 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_jsonp.rb +23 -10
- data/test/spec_rack_lighttpd_script_name_fix.rb +1 -1
- data/test/spec_rack_not_found.rb +17 -0
- data/test/spec_rack_post_body_content_type_parser.rb +5 -5
- data/test/spec_rack_profiler.rb +31 -26
- data/test/spec_rack_relative_redirect.rb +78 -0
- data/test/spec_rack_response_cache.rb +137 -0
- metadata +30 -3
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
# Rack::RelativeRedirect is a simple middleware that converts relative paths in
|
4
|
+
# redirects in absolute urls, so they conform to RFC2616. It allows the user to
|
5
|
+
# specify the absolute path to use (with a sensible default), and handles
|
6
|
+
# relative paths (those that don't start with a slash) as well.
|
7
|
+
class Rack::RelativeRedirect
|
8
|
+
SCHEME_MAP = {'http'=>'80', 'https'=>'443'}
|
9
|
+
# The default proc used if a block is not provided to .new
|
10
|
+
# Just uses the url scheme of the request and the server name.
|
11
|
+
DEFAULT_ABSOLUTE_PROC = proc do |env, res|
|
12
|
+
port = env['SERVER_PORT']
|
13
|
+
scheme = env['rack.url_scheme']
|
14
|
+
"#{scheme}://#{env['SERVER_NAME']}#{":#{port}" unless SCHEME_MAP[scheme] == port}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initialize a new RelativeRedirect object with the given arguments. Arguments:
|
18
|
+
# * app : The next middleware in the chain. This is always called.
|
19
|
+
# * &block : If provided, it is called with the environment and the response
|
20
|
+
# from the next middleware. It should return a string representing the scheme
|
21
|
+
# and server name (such as 'http://example.org').
|
22
|
+
def initialize(app, &block)
|
23
|
+
@app = app
|
24
|
+
@absolute_proc = block || DEFAULT_ABSOLUTE_PROC
|
25
|
+
end
|
26
|
+
|
27
|
+
# Call the next middleware with the environment. If the request was a
|
28
|
+
# redirect (response status 301, 302, or 303), and the location header does
|
29
|
+
# not start with an http or https url scheme, call the block provided by new
|
30
|
+
# and use that to make the Location header an absolute url. If the Location
|
31
|
+
# does not start with a slash, make location relative to the path requested.
|
32
|
+
def call(env)
|
33
|
+
res = @app.call(env)
|
34
|
+
if [301,302,303].include?(res[0]) and loc = res[1]['Location'] and !%r{\Ahttps?://}o.match(loc)
|
35
|
+
absolute = @absolute_proc.call(env, res)
|
36
|
+
res[1]['Location'] = if %r{\A/}.match(loc)
|
37
|
+
"#{absolute}#{loc}"
|
38
|
+
else
|
39
|
+
"#{absolute}#{File.dirname(Rack::Utils.unescape(env['PATH_INFO']))}/#{loc}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
res
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
# Rack::ResponseCache is a Rack middleware that caches responses for successful
|
5
|
+
# GET requests with no query string to disk or any ruby object that has an
|
6
|
+
# []= method (so it works with memcached). When caching to disk, it works similar to
|
7
|
+
# Rails' page caching, allowing you to cache dynamic pages to static files that can
|
8
|
+
# be served directly by a front end webserver.
|
9
|
+
class Rack::ResponseCache
|
10
|
+
# The default proc used if a block is not provided to .new
|
11
|
+
# It unescapes the PATH_INFO of the environment, and makes sure that it doesn't
|
12
|
+
# include '..'. If the Content-Type of the response is text/(html|css|xml),
|
13
|
+
# return a path with the appropriate extension (.html, .css, or .xml).
|
14
|
+
# If the path ends with a / and the Content-Type is text/html, change the basename
|
15
|
+
# of the path to index.html.
|
16
|
+
DEFAULT_PATH_PROC = proc do |env, res|
|
17
|
+
path = Rack::Utils.unescape(env['PATH_INFO'])
|
18
|
+
if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(res[1]['Content-Type'])
|
19
|
+
type = match[1]
|
20
|
+
path = "#{path}.#{type}" unless /\.#{type}\z/.match(path)
|
21
|
+
path = File.join(File.dirname(path), 'index.html') if type == 'html' and File.basename(path) == '.html'
|
22
|
+
path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Initialize a new ReponseCache object with the given arguments. Arguments:
|
27
|
+
# * app : The next middleware in the chain. This is always called.
|
28
|
+
# * cache : The place to cache responses. If a string is provided, a disk
|
29
|
+
# cache is used, and all cached files will use this directory as the root directory.
|
30
|
+
# If anything other than a string is provided, it should respond to []=, which will
|
31
|
+
# be called with a path string and a body value (the 3rd element of the response).
|
32
|
+
# * &block : If provided, it is called with the environment and the response from the next middleware.
|
33
|
+
# It should return nil or false if the path should not be cached, and should return
|
34
|
+
# the pathname to use as a string if the result should be cached.
|
35
|
+
# If not provided, the DEFAULT_PATH_PROC is used.
|
36
|
+
def initialize(app, cache, &block)
|
37
|
+
@app = app
|
38
|
+
@cache = cache
|
39
|
+
@path_proc = block || DEFAULT_PATH_PROC
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call the next middleware with the environment. If the request was successful (response status 200),
|
43
|
+
# was a GET request, and had an empty query string, call the block set up in initialize to get the path.
|
44
|
+
# If the cache is a string, create any necessary middle directories, and cache the file in the appropriate
|
45
|
+
# subdirectory of cache. Otherwise, cache the body of the reponse as the value with the path as the key.
|
46
|
+
def call(env)
|
47
|
+
res = @app.call(env)
|
48
|
+
if env['REQUEST_METHOD'] == 'GET' and env['QUERY_STRING'] == '' and res[0] == 200 and path = @path_proc.call(env, res)
|
49
|
+
if @cache.is_a?(String)
|
50
|
+
path = File.join(@cache, path) if @cache
|
51
|
+
FileUtils.mkdir_p(File.dirname(path))
|
52
|
+
File.open(path, 'wb'){|f| res[2].each{|c| f.write(c)}}
|
53
|
+
else
|
54
|
+
@cache[path] = res[2]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
res
|
58
|
+
end
|
59
|
+
end
|
@@ -4,45 +4,46 @@ module Rack
|
|
4
4
|
[Exception, '/error/internal']
|
5
5
|
]
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
PATH_INFO = 'rack.route_exceptions.path_info'.freeze
|
8
|
+
EXCEPTION = 'rack.route_exceptions.exception'.freeze
|
9
|
+
RETURNED = 'rack.route_exceptions.returned'.freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def route(exception, to)
|
13
|
+
ROUTES.delete_if{|k,v| k == exception }
|
14
|
+
ROUTES << [exception, to]
|
15
|
+
end
|
16
|
+
|
17
|
+
alias []= route
|
18
|
+
end
|
10
19
|
|
11
20
|
def initialize(app)
|
12
21
|
@app = app
|
13
22
|
end
|
14
23
|
|
15
24
|
def call(env, try_again = true)
|
16
|
-
|
17
|
-
|
18
|
-
response
|
25
|
+
returned = @app.call(env)
|
19
26
|
rescue Exception => exception
|
20
27
|
raise(exception) unless try_again
|
21
28
|
|
22
29
|
ROUTES.each do |klass, to|
|
23
30
|
next unless klass === exception
|
24
|
-
return route(to, env,
|
31
|
+
return route(to, env, returned, exception)
|
25
32
|
end
|
26
33
|
|
27
34
|
raise(exception)
|
28
35
|
end
|
29
36
|
|
30
|
-
def route(to, env,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
env.merge!(hash)
|
37
|
+
def route(to, env, returned, exception)
|
38
|
+
env.merge!(
|
39
|
+
PATH_INFO => env['PATH_INFO'],
|
40
|
+
EXCEPTION => exception,
|
41
|
+
RETURNED => returned
|
42
|
+
)
|
37
43
|
|
38
44
|
env['PATH_INFO'] = to
|
39
45
|
|
40
46
|
call(env, try_again = false)
|
41
47
|
end
|
42
|
-
|
43
|
-
def self.route(exception, to)
|
44
|
-
ROUTES.delete_if{|k,v| k == exception }
|
45
|
-
ROUTES << [exception, to]
|
46
|
-
end
|
47
48
|
end
|
48
49
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rack
|
2
|
+
# Installs signal handlers that are safely processed after a request
|
3
|
+
#
|
4
|
+
# NOTE: This middleware should not be used in a threaded environment
|
5
|
+
#
|
6
|
+
# use Rack::Signals.new do
|
7
|
+
# trap 'INT', lambda {
|
8
|
+
# puts "Exiting now"
|
9
|
+
# exit
|
10
|
+
# }
|
11
|
+
#
|
12
|
+
# trap_when_ready 'USR1', lambda {
|
13
|
+
# puts "Exiting when ready"
|
14
|
+
# exit
|
15
|
+
# }
|
16
|
+
# end
|
17
|
+
class Signals
|
18
|
+
class BodyWithCallback
|
19
|
+
def initialize(body, callback)
|
20
|
+
@body, @callback = body, callback
|
21
|
+
end
|
22
|
+
|
23
|
+
def each(&block)
|
24
|
+
@body.each(&block)
|
25
|
+
@callback.call
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(app, &block)
|
30
|
+
@app = app
|
31
|
+
@processing = false
|
32
|
+
@when_ready = nil
|
33
|
+
instance_eval(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
begin
|
38
|
+
@processing, @when_ready = true, nil
|
39
|
+
status, headers, body = @app.call(env)
|
40
|
+
|
41
|
+
if handler = @when_ready
|
42
|
+
body = BodyWithCallback.new(body, handler)
|
43
|
+
@when_ready = nil
|
44
|
+
end
|
45
|
+
ensure
|
46
|
+
@processing = false
|
47
|
+
end
|
48
|
+
|
49
|
+
[status, headers, body]
|
50
|
+
end
|
51
|
+
|
52
|
+
def trap_when_ready(signal, handler)
|
53
|
+
when_ready_handler = lambda { |signal|
|
54
|
+
if @processing
|
55
|
+
@when_ready = lambda { handler.call(signal) }
|
56
|
+
else
|
57
|
+
handler.call(signal)
|
58
|
+
end
|
59
|
+
}
|
60
|
+
trap(signal, when_ready_handler)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/rack-contrib.gemspec
CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
|
|
3
3
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
4
|
|
5
5
|
s.name = 'rack-contrib'
|
6
|
-
s.version = '0.9.
|
7
|
-
s.date = '2009-
|
6
|
+
s.version = '0.9.2'
|
7
|
+
s.date = '2009-03-07'
|
8
8
|
|
9
9
|
s.description = "Contributed Rack Middleware and Utilities"
|
10
10
|
s.summary = "Contributed Rack Middleware and Utilities"
|
@@ -18,8 +18,13 @@ Gem::Specification.new do |s|
|
|
18
18
|
README.rdoc
|
19
19
|
Rakefile
|
20
20
|
lib/rack/contrib.rb
|
21
|
+
lib/rack/contrib/accept_format.rb
|
22
|
+
lib/rack/contrib/backstage.rb
|
21
23
|
lib/rack/contrib/bounce_favicon.rb
|
22
24
|
lib/rack/contrib/callbacks.rb
|
25
|
+
lib/rack/contrib/config.rb
|
26
|
+
lib/rack/contrib/csshttprequest.rb
|
27
|
+
lib/rack/contrib/deflect.rb
|
23
28
|
lib/rack/contrib/etag.rb
|
24
29
|
lib/rack/contrib/evil.rb
|
25
30
|
lib/rack/contrib/garbagecollector.rb
|
@@ -28,16 +33,27 @@ Gem::Specification.new do |s|
|
|
28
33
|
lib/rack/contrib/locale.rb
|
29
34
|
lib/rack/contrib/mailexceptions.rb
|
30
35
|
lib/rack/contrib/nested_params.rb
|
36
|
+
lib/rack/contrib/not_found.rb
|
31
37
|
lib/rack/contrib/post_body_content_type_parser.rb
|
32
38
|
lib/rack/contrib/proctitle.rb
|
33
39
|
lib/rack/contrib/profiler.rb
|
40
|
+
lib/rack/contrib/relative_redirect.rb
|
41
|
+
lib/rack/contrib/response_cache.rb
|
34
42
|
lib/rack/contrib/route_exceptions.rb
|
35
43
|
lib/rack/contrib/sendfile.rb
|
44
|
+
lib/rack/contrib/signals.rb
|
36
45
|
lib/rack/contrib/time_zone.rb
|
37
46
|
rack-contrib.gemspec
|
47
|
+
test/404.html
|
48
|
+
test/Maintenance.html
|
38
49
|
test/mail_settings.rb
|
50
|
+
test/spec_rack_accept_format.rb
|
51
|
+
test/spec_rack_backstage.rb
|
39
52
|
test/spec_rack_callbacks.rb
|
53
|
+
test/spec_rack_config.rb
|
40
54
|
test/spec_rack_contrib.rb
|
55
|
+
test/spec_rack_csshttprequest.rb
|
56
|
+
test/spec_rack_deflect.rb
|
41
57
|
test/spec_rack_etag.rb
|
42
58
|
test/spec_rack_evil.rb
|
43
59
|
test/spec_rack_garbagecollector.rb
|
@@ -45,9 +61,12 @@ Gem::Specification.new do |s|
|
|
45
61
|
test/spec_rack_lighttpd_script_name_fix.rb
|
46
62
|
test/spec_rack_mailexceptions.rb
|
47
63
|
test/spec_rack_nested_params.rb
|
64
|
+
test/spec_rack_not_found.rb
|
48
65
|
test/spec_rack_post_body_content_type_parser.rb
|
49
66
|
test/spec_rack_proctitle.rb
|
50
67
|
test/spec_rack_profiler.rb
|
68
|
+
test/spec_rack_relative_redirect.rb
|
69
|
+
test/spec_rack_response_cache.rb
|
51
70
|
test/spec_rack_sendfile.rb
|
52
71
|
]
|
53
72
|
# = MANIFEST =
|
@@ -55,7 +74,7 @@ Gem::Specification.new do |s|
|
|
55
74
|
s.test_files = s.files.select {|path| path =~ /^test\/spec_.*\.rb/}
|
56
75
|
|
57
76
|
s.extra_rdoc_files = %w[README.rdoc COPYING]
|
58
|
-
s.add_dependency 'rack', '
|
77
|
+
s.add_dependency 'rack', '>= 0.9.1'
|
59
78
|
s.add_dependency 'test-spec', '~> 0.9.0'
|
60
79
|
s.add_development_dependency 'tmail', '>= 1.2'
|
61
80
|
s.add_development_dependency 'json', '>= 1.1'
|
data/test/404.html
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Not Found
|
@@ -0,0 +1 @@
|
|
1
|
+
Under maintenance.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'rack/mock'
|
3
|
+
require 'rack/contrib/accept_format'
|
4
|
+
require 'rack/mime'
|
5
|
+
|
6
|
+
context "Rack::AcceptFormat" do
|
7
|
+
app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, env['PATH_INFO']] }
|
8
|
+
|
9
|
+
specify "should do nothing when a format extension is already provided" do
|
10
|
+
request = Rack::MockRequest.env_for("/resource.json")
|
11
|
+
body = Rack::AcceptFormat.new(app).call(request).last
|
12
|
+
body.should == "/resource.json"
|
13
|
+
end
|
14
|
+
|
15
|
+
context "default extention" do
|
16
|
+
specify "should allow custom default" do
|
17
|
+
request = Rack::MockRequest.env_for("/resource")
|
18
|
+
body = Rack::AcceptFormat.new(app, '.xml').call(request).last
|
19
|
+
body.should == "/resource.xml"
|
20
|
+
end
|
21
|
+
|
22
|
+
specify "should default to html" do
|
23
|
+
request = Rack::MockRequest.env_for("/resource")
|
24
|
+
body = Rack::AcceptFormat.new(app).call(request).last
|
25
|
+
body.should == "/resource.html"
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "should notmalize custom extention" do
|
29
|
+
request = Rack::MockRequest.env_for("/resource")
|
30
|
+
|
31
|
+
body = Rack::AcceptFormat.new(app,'xml').call(request).last #no dot prefix
|
32
|
+
body.should == "/resource.xml"
|
33
|
+
|
34
|
+
body = Rack::AcceptFormat.new(app, :xml).call(request).last
|
35
|
+
body.should == "/resource.xml"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "there is no format extension" do
|
40
|
+
Rack::Mime::MIME_TYPES.clear
|
41
|
+
|
42
|
+
def mime(ext, type)
|
43
|
+
ext = ".#{ext}" unless ext.to_s[0] == ?.
|
44
|
+
Rack::Mime::MIME_TYPES[ext.to_s] = type
|
45
|
+
end
|
46
|
+
|
47
|
+
specify "should add the default extension if no Accept header" do
|
48
|
+
request = Rack::MockRequest.env_for("/resource")
|
49
|
+
body = Rack::AcceptFormat.new(app).call(request).last
|
50
|
+
body.should == "/resource.html"
|
51
|
+
end
|
52
|
+
|
53
|
+
specify "should add the default extension if the Accept header is not registered in the Mime::Types" do
|
54
|
+
request = Rack::MockRequest.env_for("/resource", 'HTTP_ACCEPT' => 'application/json;q=1.0, text/html;q=0.8, */*;q=0.1')
|
55
|
+
body = Rack::AcceptFormat.new(app).call(request).last
|
56
|
+
body.should == "/resource.html"
|
57
|
+
end
|
58
|
+
|
59
|
+
specify "should add the correct extension if the Accept header is registered in the Mime::Types" do
|
60
|
+
mime :json, 'application/json'
|
61
|
+
request = Rack::MockRequest.env_for("/resource", 'HTTP_ACCEPT' => 'application/json;q=1.0, text/html;q=0.8, */*;q=0.1')
|
62
|
+
body = Rack::AcceptFormat.new(app).call(request).last
|
63
|
+
body.should == "/resource.json"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
specify "shouldn't confuse extention when there are dots in path" do
|
68
|
+
request = Rack::MockRequest.env_for("/parent.resource/resource")
|
69
|
+
body = Rack::AcceptFormat.new(app, '.html').call(request).last
|
70
|
+
body.should == "/parent.resource/resource.html"
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'rack/builder'
|
3
|
+
require 'rack/mock'
|
4
|
+
require 'rack/contrib/backstage'
|
5
|
+
|
6
|
+
context "Rack::Backstage" do
|
7
|
+
specify "shows maintenances page if present" do
|
8
|
+
app = Rack::Builder.new do
|
9
|
+
use Rack::Backstage, 'test/Maintenance.html'
|
10
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
|
11
|
+
end
|
12
|
+
response = Rack::MockRequest.new(app).get('/')
|
13
|
+
response.body.should.equal('Under maintenance.')
|
14
|
+
response.status.should.equal(503)
|
15
|
+
end
|
16
|
+
|
17
|
+
specify "passes on request if page is not present" do
|
18
|
+
app = Rack::Builder.new do
|
19
|
+
use Rack::Backstage, 'test/Nonsense.html'
|
20
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
|
21
|
+
end
|
22
|
+
response = Rack::MockRequest.new(app).get('/')
|
23
|
+
response.body.should.equal('Hello, World!')
|
24
|
+
response.status.should.equal(200)
|
25
|
+
end
|
26
|
+
end
|
data/test/spec_rack_callbacks.rb
CHANGED
@@ -43,7 +43,7 @@ context "Rack::Callbacks" do
|
|
43
43
|
before Flame
|
44
44
|
before Pacify, "with love"
|
45
45
|
|
46
|
-
run lambda {|env| [200, {}, env['flame']
|
46
|
+
run lambda {|env| [200, {}, [env['flame'], env['peace']]] }
|
47
47
|
|
48
48
|
after Finale
|
49
49
|
after TheEnd
|
@@ -62,4 +62,4 @@ context "Rack::Callbacks" do
|
|
62
62
|
|
63
63
|
response.headers['last'].should.equal 'TheEnd'
|
64
64
|
end
|
65
|
-
end
|
65
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'rack/mock'
|
3
|
+
require 'rack/contrib/config'
|
4
|
+
|
5
|
+
context "Rack::Config" do
|
6
|
+
|
7
|
+
specify "should accept a block that modifies the environment" do
|
8
|
+
app = Rack::Builder.new do
|
9
|
+
use Rack::Lint
|
10
|
+
use Rack::ContentLength
|
11
|
+
use Rack::Config do |env|
|
12
|
+
env['greeting'] = 'hello'
|
13
|
+
end
|
14
|
+
run lambda { |env|
|
15
|
+
[200, {'Content-Type' => 'text/plain'}, [env['greeting'] || '']]
|
16
|
+
}
|
17
|
+
end
|
18
|
+
response = Rack::MockRequest.new(app).get('/')
|
19
|
+
response.body.should.equal('hello')
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|