edgar-rack 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. data/COPYING +18 -0
  2. data/KNOWN-ISSUES +21 -0
  3. data/README +401 -0
  4. data/Rakefile +101 -0
  5. data/SPEC +171 -0
  6. data/bin/rackup +4 -0
  7. data/contrib/rack_logo.svg +111 -0
  8. data/example/lobster.ru +4 -0
  9. data/example/protectedlobster.rb +14 -0
  10. data/example/protectedlobster.ru +8 -0
  11. data/lib/rack.rb +81 -0
  12. data/lib/rack/auth/abstract/handler.rb +37 -0
  13. data/lib/rack/auth/abstract/request.rb +43 -0
  14. data/lib/rack/auth/basic.rb +58 -0
  15. data/lib/rack/auth/digest/md5.rb +124 -0
  16. data/lib/rack/auth/digest/nonce.rb +51 -0
  17. data/lib/rack/auth/digest/params.rb +53 -0
  18. data/lib/rack/auth/digest/request.rb +40 -0
  19. data/lib/rack/builder.rb +80 -0
  20. data/lib/rack/cascade.rb +41 -0
  21. data/lib/rack/chunked.rb +52 -0
  22. data/lib/rack/commonlogger.rb +49 -0
  23. data/lib/rack/conditionalget.rb +63 -0
  24. data/lib/rack/config.rb +15 -0
  25. data/lib/rack/content_length.rb +29 -0
  26. data/lib/rack/content_type.rb +23 -0
  27. data/lib/rack/deflater.rb +96 -0
  28. data/lib/rack/directory.rb +157 -0
  29. data/lib/rack/etag.rb +59 -0
  30. data/lib/rack/file.rb +118 -0
  31. data/lib/rack/handler.rb +88 -0
  32. data/lib/rack/handler/cgi.rb +61 -0
  33. data/lib/rack/handler/evented_mongrel.rb +8 -0
  34. data/lib/rack/handler/fastcgi.rb +90 -0
  35. data/lib/rack/handler/lsws.rb +61 -0
  36. data/lib/rack/handler/mongrel.rb +90 -0
  37. data/lib/rack/handler/scgi.rb +59 -0
  38. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  39. data/lib/rack/handler/thin.rb +17 -0
  40. data/lib/rack/handler/webrick.rb +73 -0
  41. data/lib/rack/head.rb +19 -0
  42. data/lib/rack/lint.rb +567 -0
  43. data/lib/rack/lobster.rb +65 -0
  44. data/lib/rack/lock.rb +44 -0
  45. data/lib/rack/logger.rb +18 -0
  46. data/lib/rack/methodoverride.rb +27 -0
  47. data/lib/rack/mime.rb +210 -0
  48. data/lib/rack/mock.rb +185 -0
  49. data/lib/rack/nulllogger.rb +18 -0
  50. data/lib/rack/recursive.rb +61 -0
  51. data/lib/rack/reloader.rb +109 -0
  52. data/lib/rack/request.rb +307 -0
  53. data/lib/rack/response.rb +151 -0
  54. data/lib/rack/rewindable_input.rb +104 -0
  55. data/lib/rack/runtime.rb +27 -0
  56. data/lib/rack/sendfile.rb +139 -0
  57. data/lib/rack/server.rb +289 -0
  58. data/lib/rack/session/abstract/id.rb +348 -0
  59. data/lib/rack/session/cookie.rb +152 -0
  60. data/lib/rack/session/memcache.rb +93 -0
  61. data/lib/rack/session/pool.rb +79 -0
  62. data/lib/rack/showexceptions.rb +378 -0
  63. data/lib/rack/showstatus.rb +113 -0
  64. data/lib/rack/static.rb +53 -0
  65. data/lib/rack/urlmap.rb +55 -0
  66. data/lib/rack/utils.rb +698 -0
  67. data/rack.gemspec +39 -0
  68. data/test/cgi/lighttpd.conf +25 -0
  69. data/test/cgi/rackup_stub.rb +6 -0
  70. data/test/cgi/sample_rackup.ru +5 -0
  71. data/test/cgi/test +9 -0
  72. data/test/cgi/test.fcgi +8 -0
  73. data/test/cgi/test.ru +5 -0
  74. data/test/gemloader.rb +6 -0
  75. data/test/multipart/bad_robots +259 -0
  76. data/test/multipart/binary +0 -0
  77. data/test/multipart/empty +10 -0
  78. data/test/multipart/fail_16384_nofile +814 -0
  79. data/test/multipart/file1.txt +1 -0
  80. data/test/multipart/filename_and_modification_param +7 -0
  81. data/test/multipart/filename_with_escaped_quotes +6 -0
  82. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  83. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  84. data/test/multipart/filename_with_unescaped_quotes +6 -0
  85. data/test/multipart/ie +6 -0
  86. data/test/multipart/nested +10 -0
  87. data/test/multipart/none +9 -0
  88. data/test/multipart/semicolon +6 -0
  89. data/test/multipart/text +15 -0
  90. data/test/rackup/config.ru +31 -0
  91. data/test/spec_auth_basic.rb +70 -0
  92. data/test/spec_auth_digest.rb +241 -0
  93. data/test/spec_builder.rb +123 -0
  94. data/test/spec_cascade.rb +45 -0
  95. data/test/spec_cgi.rb +102 -0
  96. data/test/spec_chunked.rb +60 -0
  97. data/test/spec_commonlogger.rb +56 -0
  98. data/test/spec_conditionalget.rb +86 -0
  99. data/test/spec_config.rb +23 -0
  100. data/test/spec_content_length.rb +36 -0
  101. data/test/spec_content_type.rb +29 -0
  102. data/test/spec_deflater.rb +125 -0
  103. data/test/spec_directory.rb +57 -0
  104. data/test/spec_etag.rb +75 -0
  105. data/test/spec_fastcgi.rb +107 -0
  106. data/test/spec_file.rb +92 -0
  107. data/test/spec_handler.rb +49 -0
  108. data/test/spec_head.rb +30 -0
  109. data/test/spec_lint.rb +515 -0
  110. data/test/spec_lobster.rb +43 -0
  111. data/test/spec_lock.rb +142 -0
  112. data/test/spec_logger.rb +28 -0
  113. data/test/spec_methodoverride.rb +58 -0
  114. data/test/spec_mock.rb +241 -0
  115. data/test/spec_mongrel.rb +182 -0
  116. data/test/spec_nulllogger.rb +12 -0
  117. data/test/spec_recursive.rb +69 -0
  118. data/test/spec_request.rb +774 -0
  119. data/test/spec_response.rb +245 -0
  120. data/test/spec_rewindable_input.rb +118 -0
  121. data/test/spec_runtime.rb +39 -0
  122. data/test/spec_sendfile.rb +83 -0
  123. data/test/spec_server.rb +8 -0
  124. data/test/spec_session_abstract_id.rb +43 -0
  125. data/test/spec_session_cookie.rb +171 -0
  126. data/test/spec_session_memcache.rb +289 -0
  127. data/test/spec_session_pool.rb +200 -0
  128. data/test/spec_showexceptions.rb +87 -0
  129. data/test/spec_showstatus.rb +79 -0
  130. data/test/spec_static.rb +48 -0
  131. data/test/spec_thin.rb +86 -0
  132. data/test/spec_urlmap.rb +213 -0
  133. data/test/spec_utils.rb +678 -0
  134. data/test/spec_webrick.rb +141 -0
  135. data/test/testrequest.rb +78 -0
  136. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  137. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  138. metadata +329 -0
@@ -0,0 +1,57 @@
1
+ require 'rack/directory'
2
+ require 'rack/mock'
3
+
4
+ describe Rack::Directory do
5
+ DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
6
+ FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, ['passed!']] }
7
+ app = Rack::Directory.new DOCROOT, FILE_CATCH
8
+
9
+ should "serve directory indices" do
10
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
11
+ get("/cgi/")
12
+
13
+ res.should.be.ok
14
+ res.should =~ /<html><head>/
15
+ end
16
+
17
+ should "pass to app if file found" do
18
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
19
+ get("/cgi/test")
20
+
21
+ res.should.be.ok
22
+ res.should =~ /passed!/
23
+ end
24
+
25
+ should "serve uri with URL encoded filenames" do
26
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
27
+ get("/%63%67%69/") # "/cgi/test"
28
+
29
+ res.should.be.ok
30
+ res.should =~ /<html><head>/
31
+
32
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
33
+ get("/cgi/%74%65%73%74") # "/cgi/test"
34
+
35
+ res.should.be.ok
36
+ res.should =~ /passed!/
37
+ end
38
+
39
+ should "not allow directory traversal" do
40
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
41
+ get("/cgi/../test")
42
+
43
+ res.should.be.forbidden
44
+
45
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
46
+ get("/cgi/%2E%2E/test")
47
+
48
+ res.should.be.forbidden
49
+ end
50
+
51
+ should "404 if it can't find the file" do
52
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
53
+ get("/cgi/blubb")
54
+
55
+ res.should.be.not_found
56
+ end
57
+ end
@@ -0,0 +1,75 @@
1
+ require 'rack/etag'
2
+
3
+ describe Rack::ETag do
4
+ def sendfile_body
5
+ res = ['Hello World']
6
+ def res.to_path ; "/tmp/hello.txt" ; end
7
+ res
8
+ end
9
+
10
+ should "set ETag if none is set if status is 200" do
11
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
12
+ response = Rack::ETag.new(app).call({})
13
+ response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
14
+ end
15
+
16
+ should "set ETag if none is set if status is 201" do
17
+ app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
18
+ response = Rack::ETag.new(app).call({})
19
+ response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
20
+ end
21
+
22
+ should "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do
23
+ app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
24
+ response = Rack::ETag.new(app).call({})
25
+ response[1]['Cache-Control'].should.equal 'max-age=0, private, must-revalidate'
26
+ end
27
+
28
+ should "set Cache-Control to chosen one if none is set" do
29
+ app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
30
+ response = Rack::ETag.new(app, nil, 'public').call({})
31
+ response[1]['Cache-Control'].should.equal 'public'
32
+ end
33
+
34
+ should "set a given Cache-Control even if digest could not be calculated" do
35
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, []] }
36
+ response = Rack::ETag.new(app, 'no-cache').call({})
37
+ response[1]['Cache-Control'].should.equal 'no-cache'
38
+ end
39
+
40
+ should "not set Cache-Control if it is already set" do
41
+ app = lambda { |env| [201, {'Content-Type' => 'text/plain', 'Cache-Control' => 'public'}, ["Hello, World!"]] }
42
+ response = Rack::ETag.new(app).call({})
43
+ response[1]['Cache-Control'].should.equal 'public'
44
+ end
45
+
46
+ should "not change ETag if it is already set" do
47
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, ["Hello, World!"]] }
48
+ response = Rack::ETag.new(app).call({})
49
+ response[1]['ETag'].should.equal "\"abc\""
50
+ end
51
+
52
+ should "not set ETag if body is empty" do
53
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, []] }
54
+ response = Rack::ETag.new(app).call({})
55
+ response[1]['ETag'].should.be.nil
56
+ end
57
+
58
+ should "not set ETag if Last-Modified is set" do
59
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, ["Hello, World!"]] }
60
+ response = Rack::ETag.new(app).call({})
61
+ response[1]['ETag'].should.be.nil
62
+ end
63
+
64
+ should "not set ETag if a sendfile_body is given" do
65
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, sendfile_body] }
66
+ response = Rack::ETag.new(app).call({})
67
+ response[1]['ETag'].should.be.nil
68
+ end
69
+
70
+ should "not set ETag if a status is not 200 or 201" do
71
+ app = lambda { |env| [401, {'Content-Type' => 'text/plain'}, ['Access denied.']] }
72
+ response = Rack::ETag.new(app).call({})
73
+ response[1]['ETag'].should.be.nil
74
+ end
75
+ end
@@ -0,0 +1,107 @@
1
+ begin
2
+ require File.expand_path('../testrequest', __FILE__)
3
+ require 'rack/handler/fastcgi'
4
+
5
+ describe Rack::Handler::FastCGI do
6
+ extend TestRequest::Helpers
7
+
8
+ @host = '0.0.0.0'
9
+ @port = 9203
10
+
11
+ if `which lighttpd` && !$?.success?
12
+ raise "lighttpd not found"
13
+ end
14
+
15
+ # Keep this first.
16
+ $pid = fork {
17
+ ENV['RACK_ENV'] = 'deployment'
18
+ ENV['RUBYLIB'] = [
19
+ File.expand_path('../../lib', __FILE__),
20
+ ENV['RUBYLIB'],
21
+ ].compact.join(':')
22
+
23
+ Dir.chdir(File.expand_path("../cgi", __FILE__)) do
24
+ exec "lighttpd -D -f lighttpd.conf"
25
+ end
26
+ }
27
+
28
+ should "respond" do
29
+ sleep 1
30
+ GET("/test")
31
+ response.should.not.be.nil
32
+ end
33
+
34
+ should "respond via rackup server" do
35
+ GET("/sample_rackup.ru")
36
+ status.should.equal 200
37
+ end
38
+
39
+ should "be a lighttpd" do
40
+ GET("/test.fcgi")
41
+ status.should.equal 200
42
+ response["SERVER_SOFTWARE"].should =~ /lighttpd/
43
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
44
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
45
+ response["SERVER_PORT"].should.equal @port.to_s
46
+ response["SERVER_NAME"].should.equal @host
47
+ end
48
+
49
+ should "have rack headers" do
50
+ GET("/test.fcgi")
51
+ response["rack.version"].should.equal [1,1]
52
+ response["rack.multithread"].should.be.false
53
+ response["rack.multiprocess"].should.be.true
54
+ response["rack.run_once"].should.be.false
55
+ end
56
+
57
+ should "have CGI headers on GET" do
58
+ GET("/test.fcgi")
59
+ response["REQUEST_METHOD"].should.equal "GET"
60
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
61
+ response["REQUEST_PATH"].should.equal "/"
62
+ response["PATH_INFO"].should.equal ""
63
+ response["QUERY_STRING"].should.equal ""
64
+ response["test.postdata"].should.equal ""
65
+
66
+ GET("/test.fcgi/foo?quux=1")
67
+ response["REQUEST_METHOD"].should.equal "GET"
68
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
69
+ response["REQUEST_PATH"].should.equal "/"
70
+ response["PATH_INFO"].should.equal "/foo"
71
+ response["QUERY_STRING"].should.equal "quux=1"
72
+ end
73
+
74
+ should "have CGI headers on POST" do
75
+ POST("/test.fcgi", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
76
+ status.should.equal 200
77
+ response["REQUEST_METHOD"].should.equal "POST"
78
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
79
+ response["REQUEST_PATH"].should.equal "/"
80
+ response["QUERY_STRING"].should.equal ""
81
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
82
+ response["test.postdata"].should.equal "rack-form-data=23"
83
+ end
84
+
85
+ should "support HTTP auth" do
86
+ GET("/test.fcgi", {:user => "ruth", :passwd => "secret"})
87
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
88
+ end
89
+
90
+ should "set status" do
91
+ GET("/test.fcgi?secret")
92
+ status.should.equal 403
93
+ response["rack.url_scheme"].should.equal "http"
94
+ end
95
+
96
+ # Keep this last.
97
+ should "shutdown" do
98
+ Process.kill 15, $pid
99
+ Process.wait($pid).should.equal $pid
100
+ end
101
+ end
102
+
103
+ rescue RuntimeError
104
+ $stderr.puts "Skipping Rack::Handler::FastCGI tests (lighttpd is required). Install lighttpd and try again."
105
+ rescue LoadError
106
+ $stderr.puts "Skipping Rack::Handler::FastCGI tests (FCGI is required). `gem install fcgi` and try again."
107
+ end
@@ -0,0 +1,92 @@
1
+ require 'rack/file'
2
+ require 'rack/mock'
3
+
4
+ describe Rack::File do
5
+ DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
6
+
7
+ should "serve files" do
8
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
9
+ get("/cgi/test")
10
+
11
+ res.should.be.ok
12
+ res.should =~ /ruby/
13
+ end
14
+
15
+ should "set Last-Modified header" do
16
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
17
+ get("/cgi/test")
18
+
19
+ path = File.join(DOCROOT, "/cgi/test")
20
+
21
+ res.should.be.ok
22
+ res["Last-Modified"].should.equal File.mtime(path).httpdate
23
+ end
24
+
25
+ should "serve files with URL encoded filenames" do
26
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
27
+ get("/cgi/%74%65%73%74") # "/cgi/test"
28
+
29
+ res.should.be.ok
30
+ res.should =~ /ruby/
31
+ end
32
+
33
+ should "not allow directory traversal" do
34
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
35
+ get("/cgi/../test")
36
+
37
+ res.should.be.forbidden
38
+ end
39
+
40
+ should "not allow directory traversal with encoded periods" do
41
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
42
+ get("/%2E%2E/README")
43
+
44
+ res.should.be.forbidden
45
+ end
46
+
47
+ should "404 if it can't find the file" do
48
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
49
+ get("/cgi/blubb")
50
+
51
+ res.should.be.not_found
52
+ end
53
+
54
+ should "detect SystemCallErrors" do
55
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
56
+ get("/cgi")
57
+
58
+ res.should.be.not_found
59
+ end
60
+
61
+ should "return bodies that respond to #to_path" do
62
+ env = Rack::MockRequest.env_for("/cgi/test")
63
+ status, _, body = Rack::File.new(DOCROOT).call(env)
64
+
65
+ path = File.join(DOCROOT, "/cgi/test")
66
+
67
+ status.should.equal 200
68
+ body.should.respond_to :to_path
69
+ body.to_path.should.equal path
70
+ end
71
+
72
+ should "return correct byte range in body" do
73
+ env = Rack::MockRequest.env_for("/cgi/test")
74
+ env["HTTP_RANGE"] = "bytes=22-33"
75
+ res = Rack::MockResponse.new(*Rack::File.new(DOCROOT).call(env))
76
+
77
+ res.status.should.equal 206
78
+ res["Content-Length"].should.equal "12"
79
+ res["Content-Range"].should.equal "bytes 22-33/193"
80
+ res.body.should.equal "-*- ruby -*-"
81
+ end
82
+
83
+ should "return error for unsatisfiable byte range" do
84
+ env = Rack::MockRequest.env_for("/cgi/test")
85
+ env["HTTP_RANGE"] = "bytes=1234-5678"
86
+ res = Rack::MockResponse.new(*Rack::File.new(DOCROOT).call(env))
87
+
88
+ res.status.should.equal 416
89
+ res["Content-Range"].should.equal "bytes */193"
90
+ end
91
+
92
+ end
@@ -0,0 +1,49 @@
1
+ require 'rack/handler'
2
+
3
+ class Rack::Handler::Lobster; end
4
+ class RockLobster; end
5
+
6
+ describe Rack::Handler do
7
+ it "has registered default handlers" do
8
+ Rack::Handler.get('cgi').should.equal Rack::Handler::CGI
9
+ Rack::Handler.get('webrick').should.equal Rack::Handler::WEBrick
10
+
11
+ begin
12
+ Rack::Handler.get('fastcgi').should.equal Rack::Handler::FastCGI
13
+ rescue LoadError
14
+ end
15
+
16
+ begin
17
+ Rack::Handler.get('mongrel').should.equal Rack::Handler::Mongrel
18
+ rescue LoadError
19
+ end
20
+ end
21
+
22
+ should "raise NameError if handler doesn't exist" do
23
+ lambda {
24
+ Rack::Handler.get('boom')
25
+ }.should.raise(NameError)
26
+ end
27
+
28
+ should "get unregistered, but already required, handler by name" do
29
+ Rack::Handler.get('Lobster').should.equal Rack::Handler::Lobster
30
+ end
31
+
32
+ should "register custom handler" do
33
+ Rack::Handler.register('rock_lobster', 'RockLobster')
34
+ Rack::Handler.get('rock_lobster').should.equal RockLobster
35
+ end
36
+
37
+ should "not need registration for properly coded handlers even if not already required" do
38
+ begin
39
+ $LOAD_PATH.push File.expand_path('../unregistered_handler', __FILE__)
40
+ Rack::Handler.get('Unregistered').should.equal Rack::Handler::Unregistered
41
+ lambda {
42
+ Rack::Handler.get('UnRegistered')
43
+ }.should.raise(NameError)
44
+ Rack::Handler.get('UnregisteredLongOne').should.equal Rack::Handler::UnregisteredLongOne
45
+ ensure
46
+ $LOAD_PATH.delete File.expand_path('../unregistered_handler', __FILE__)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ require 'rack/head'
2
+ require 'rack/mock'
3
+
4
+ describe Rack::Head do
5
+ def test_response(headers = {})
6
+ app = lambda { |env| [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]] }
7
+ request = Rack::MockRequest.env_for("/", headers)
8
+ response = Rack::Head.new(app).call(request)
9
+
10
+ return response
11
+ end
12
+
13
+ should "pass GET, POST, PUT, DELETE, OPTIONS, TRACE requests" do
14
+ %w[GET POST PUT DELETE OPTIONS TRACE].each do |type|
15
+ resp = test_response("REQUEST_METHOD" => type)
16
+
17
+ resp[0].should.equal(200)
18
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
19
+ resp[2].should.equal(["foo"])
20
+ end
21
+ end
22
+
23
+ should "remove body from HEAD requests" do
24
+ resp = test_response("REQUEST_METHOD" => "HEAD")
25
+
26
+ resp[0].should.equal(200)
27
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
28
+ resp[2].should.equal([])
29
+ end
30
+ end
@@ -0,0 +1,515 @@
1
+ require 'stringio'
2
+ require 'rack/lint'
3
+ require 'rack/mock'
4
+
5
+ describe Rack::Lint do
6
+ def env(*args)
7
+ Rack::MockRequest.env_for("/", *args)
8
+ end
9
+
10
+ should "pass valid request" do
11
+ lambda {
12
+ Rack::Lint.new(lambda { |env|
13
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
14
+ }).call(env({}))
15
+ }.should.not.raise
16
+ end
17
+
18
+ should "notice fatal errors" do
19
+ lambda { Rack::Lint.new(nil).call }.should.raise(Rack::Lint::LintError).
20
+ message.should.match(/No env given/)
21
+ end
22
+
23
+ should "notice environment errors" do
24
+ lambda { Rack::Lint.new(nil).call 5 }.should.raise(Rack::Lint::LintError).
25
+ message.should.match(/not a Hash/)
26
+
27
+ lambda {
28
+ e = env
29
+ e.delete("REQUEST_METHOD")
30
+ Rack::Lint.new(nil).call(e)
31
+ }.should.raise(Rack::Lint::LintError).
32
+ message.should.match(/missing required key REQUEST_METHOD/)
33
+
34
+ lambda {
35
+ e = env
36
+ e.delete("SERVER_NAME")
37
+ Rack::Lint.new(nil).call(e)
38
+ }.should.raise(Rack::Lint::LintError).
39
+ message.should.match(/missing required key SERVER_NAME/)
40
+
41
+
42
+ lambda {
43
+ Rack::Lint.new(nil).call(env("HTTP_CONTENT_TYPE" => "text/plain"))
44
+ }.should.raise(Rack::Lint::LintError).
45
+ message.should.match(/contains HTTP_CONTENT_TYPE/)
46
+
47
+ lambda {
48
+ Rack::Lint.new(nil).call(env("HTTP_CONTENT_LENGTH" => "42"))
49
+ }.should.raise(Rack::Lint::LintError).
50
+ message.should.match(/contains HTTP_CONTENT_LENGTH/)
51
+
52
+ lambda {
53
+ Rack::Lint.new(nil).call(env("FOO" => Object.new))
54
+ }.should.raise(Rack::Lint::LintError).
55
+ message.should.match(/non-string value/)
56
+
57
+ lambda {
58
+ Rack::Lint.new(nil).call(env("rack.version" => "0.2"))
59
+ }.should.raise(Rack::Lint::LintError).
60
+ message.should.match(/must be an Array/)
61
+
62
+ lambda {
63
+ Rack::Lint.new(nil).call(env("rack.url_scheme" => "gopher"))
64
+ }.should.raise(Rack::Lint::LintError).
65
+ message.should.match(/url_scheme unknown/)
66
+
67
+ lambda {
68
+ Rack::Lint.new(nil).call(env("rack.session" => []))
69
+ }.should.raise(Rack::Lint::LintError).
70
+ message.should.equal("session [] must respond to store and []=")
71
+
72
+ lambda {
73
+ Rack::Lint.new(nil).call(env("rack.logger" => []))
74
+ }.should.raise(Rack::Lint::LintError).
75
+ message.should.equal("logger [] must respond to info")
76
+
77
+ lambda {
78
+ Rack::Lint.new(nil).call(env("REQUEST_METHOD" => "FUCKUP?"))
79
+ }.should.raise(Rack::Lint::LintError).
80
+ message.should.match(/REQUEST_METHOD/)
81
+
82
+ lambda {
83
+ Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "howdy"))
84
+ }.should.raise(Rack::Lint::LintError).
85
+ message.should.match(/must start with/)
86
+
87
+ lambda {
88
+ Rack::Lint.new(nil).call(env("PATH_INFO" => "../foo"))
89
+ }.should.raise(Rack::Lint::LintError).
90
+ message.should.match(/must start with/)
91
+
92
+ lambda {
93
+ Rack::Lint.new(nil).call(env("CONTENT_LENGTH" => "xcii"))
94
+ }.should.raise(Rack::Lint::LintError).
95
+ message.should.match(/Invalid CONTENT_LENGTH/)
96
+
97
+ lambda {
98
+ e = env
99
+ e.delete("PATH_INFO")
100
+ e.delete("SCRIPT_NAME")
101
+ Rack::Lint.new(nil).call(e)
102
+ }.should.raise(Rack::Lint::LintError).
103
+ message.should.match(/One of .* must be set/)
104
+
105
+ lambda {
106
+ Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "/"))
107
+ }.should.raise(Rack::Lint::LintError).
108
+ message.should.match(/cannot be .* make it ''/)
109
+ end
110
+
111
+ should "notice input errors" do
112
+ lambda {
113
+ Rack::Lint.new(nil).call(env("rack.input" => ""))
114
+ }.should.raise(Rack::Lint::LintError).
115
+ message.should.match(/does not respond to #gets/)
116
+
117
+ lambda {
118
+ input = Object.new
119
+ def input.binmode?
120
+ false
121
+ end
122
+ Rack::Lint.new(nil).call(env("rack.input" => input))
123
+ }.should.raise(Rack::Lint::LintError).
124
+ message.should.match(/is not opened in binary mode/)
125
+
126
+ lambda {
127
+ input = Object.new
128
+ def input.external_encoding
129
+ result = Object.new
130
+ def result.name
131
+ "US-ASCII"
132
+ end
133
+ result
134
+ end
135
+ Rack::Lint.new(nil).call(env("rack.input" => input))
136
+ }.should.raise(Rack::Lint::LintError).
137
+ message.should.match(/does not have ASCII-8BIT as its external encoding/)
138
+ end
139
+
140
+ should "notice error errors" do
141
+ lambda {
142
+ Rack::Lint.new(nil).call(env("rack.errors" => ""))
143
+ }.should.raise(Rack::Lint::LintError).
144
+ message.should.match(/does not respond to #puts/)
145
+ end
146
+
147
+ should "notice status errors" do
148
+ lambda {
149
+ Rack::Lint.new(lambda { |env|
150
+ ["cc", {}, ""]
151
+ }).call(env({}))
152
+ }.should.raise(Rack::Lint::LintError).
153
+ message.should.match(/must be >=100 seen as integer/)
154
+
155
+ lambda {
156
+ Rack::Lint.new(lambda { |env|
157
+ [42, {}, ""]
158
+ }).call(env({}))
159
+ }.should.raise(Rack::Lint::LintError).
160
+ message.should.match(/must be >=100 seen as integer/)
161
+ end
162
+
163
+ should "notice header errors" do
164
+ lambda {
165
+ Rack::Lint.new(lambda { |env|
166
+ [200, Object.new, []]
167
+ }).call(env({}))
168
+ }.should.raise(Rack::Lint::LintError).
169
+ message.should.equal("headers object should respond to #each, but doesn't (got Object as headers)")
170
+
171
+ lambda {
172
+ Rack::Lint.new(lambda { |env|
173
+ [200, {true=>false}, []]
174
+ }).call(env({}))
175
+ }.should.raise(Rack::Lint::LintError).
176
+ message.should.equal("header key must be a string, was TrueClass")
177
+
178
+ lambda {
179
+ Rack::Lint.new(lambda { |env|
180
+ [200, {"Status" => "404"}, []]
181
+ }).call(env({}))
182
+ }.should.raise(Rack::Lint::LintError).
183
+ message.should.match(/must not contain Status/)
184
+
185
+ lambda {
186
+ Rack::Lint.new(lambda { |env|
187
+ [200, {"Content-Type:" => "text/plain"}, []]
188
+ }).call(env({}))
189
+ }.should.raise(Rack::Lint::LintError).
190
+ message.should.match(/must not contain :/)
191
+
192
+ lambda {
193
+ Rack::Lint.new(lambda { |env|
194
+ [200, {"Content-" => "text/plain"}, []]
195
+ }).call(env({}))
196
+ }.should.raise(Rack::Lint::LintError).
197
+ message.should.match(/must not end/)
198
+
199
+ lambda {
200
+ Rack::Lint.new(lambda { |env|
201
+ [200, {"..%%quark%%.." => "text/plain"}, []]
202
+ }).call(env({}))
203
+ }.should.raise(Rack::Lint::LintError).
204
+ message.should.equal("invalid header name: ..%%quark%%..")
205
+
206
+ lambda {
207
+ Rack::Lint.new(lambda { |env|
208
+ [200, {"Foo" => Object.new}, []]
209
+ }).call(env({}))
210
+ }.should.raise(Rack::Lint::LintError).
211
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Object")
212
+
213
+ lambda {
214
+ Rack::Lint.new(lambda { |env|
215
+ [200, {"Foo" => [1, 2, 3]}, []]
216
+ }).call(env({}))
217
+ }.should.raise(Rack::Lint::LintError).
218
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Array")
219
+
220
+
221
+ lambda {
222
+ Rack::Lint.new(lambda { |env|
223
+ [200, {"Foo-Bar" => "text\000plain"}, []]
224
+ }).call(env({}))
225
+ }.should.raise(Rack::Lint::LintError).
226
+ message.should.match(/invalid header/)
227
+
228
+ # line ends (010) should be allowed in header values.
229
+ lambda {
230
+ Rack::Lint.new(lambda { |env|
231
+ [200, {"Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []]
232
+ }).call(env({}))
233
+ }.should.not.raise(Rack::Lint::LintError)
234
+ end
235
+
236
+ should "notice content-type errors" do
237
+ lambda {
238
+ Rack::Lint.new(lambda { |env|
239
+ [200, {"Content-length" => "0"}, []]
240
+ }).call(env({}))
241
+ }.should.raise(Rack::Lint::LintError).
242
+ message.should.match(/No Content-Type/)
243
+
244
+ [100, 101, 204, 304].each do |status|
245
+ lambda {
246
+ Rack::Lint.new(lambda { |env|
247
+ [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
248
+ }).call(env({}))
249
+ }.should.raise(Rack::Lint::LintError).
250
+ message.should.match(/Content-Type header found/)
251
+ end
252
+ end
253
+
254
+ should "notice content-length errors" do
255
+ [100, 101, 204, 304].each do |status|
256
+ lambda {
257
+ Rack::Lint.new(lambda { |env|
258
+ [status, {"Content-length" => "0"}, []]
259
+ }).call(env({}))
260
+ }.should.raise(Rack::Lint::LintError).
261
+ message.should.match(/Content-Length header found/)
262
+ end
263
+
264
+ lambda {
265
+ Rack::Lint.new(lambda { |env|
266
+ [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []]
267
+ }).call(env({}))[2].each { }
268
+ }.should.raise(Rack::Lint::LintError).
269
+ message.should.match(/Content-Length header was 1, but should be 0/)
270
+ end
271
+
272
+ should "notice body errors" do
273
+ lambda {
274
+ body = Rack::Lint.new(lambda { |env|
275
+ [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]]
276
+ }).call(env({}))[2]
277
+ body.each { |part| }
278
+ }.should.raise(Rack::Lint::LintError).
279
+ message.should.match(/yielded non-string/)
280
+ end
281
+
282
+ should "notice input handling errors" do
283
+ lambda {
284
+ Rack::Lint.new(lambda { |env|
285
+ env["rack.input"].gets("\r\n")
286
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
287
+ }).call(env({}))
288
+ }.should.raise(Rack::Lint::LintError).
289
+ message.should.match(/gets called with arguments/)
290
+
291
+ lambda {
292
+ Rack::Lint.new(lambda { |env|
293
+ env["rack.input"].read(1, 2, 3)
294
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
295
+ }).call(env({}))
296
+ }.should.raise(Rack::Lint::LintError).
297
+ message.should.match(/read called with too many arguments/)
298
+
299
+ lambda {
300
+ Rack::Lint.new(lambda { |env|
301
+ env["rack.input"].read("foo")
302
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
303
+ }).call(env({}))
304
+ }.should.raise(Rack::Lint::LintError).
305
+ message.should.match(/read called with non-integer and non-nil length/)
306
+
307
+ lambda {
308
+ Rack::Lint.new(lambda { |env|
309
+ env["rack.input"].read(-1)
310
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
311
+ }).call(env({}))
312
+ }.should.raise(Rack::Lint::LintError).
313
+ message.should.match(/read called with a negative length/)
314
+
315
+ lambda {
316
+ Rack::Lint.new(lambda { |env|
317
+ env["rack.input"].read(nil, nil)
318
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
319
+ }).call(env({}))
320
+ }.should.raise(Rack::Lint::LintError).
321
+ message.should.match(/read called with non-String buffer/)
322
+
323
+ lambda {
324
+ Rack::Lint.new(lambda { |env|
325
+ env["rack.input"].read(nil, 1)
326
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
327
+ }).call(env({}))
328
+ }.should.raise(Rack::Lint::LintError).
329
+ message.should.match(/read called with non-String buffer/)
330
+
331
+ lambda {
332
+ Rack::Lint.new(lambda { |env|
333
+ env["rack.input"].rewind(0)
334
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
335
+ }).call(env({}))
336
+ }.should.raise(Rack::Lint::LintError).
337
+ message.should.match(/rewind called with arguments/)
338
+
339
+ weirdio = Object.new
340
+ class << weirdio
341
+ def gets
342
+ 42
343
+ end
344
+
345
+ def read
346
+ 23
347
+ end
348
+
349
+ def each
350
+ yield 23
351
+ yield 42
352
+ end
353
+
354
+ def rewind
355
+ raise Errno::ESPIPE, "Errno::ESPIPE"
356
+ end
357
+ end
358
+
359
+ eof_weirdio = Object.new
360
+ class << eof_weirdio
361
+ def gets
362
+ nil
363
+ end
364
+
365
+ def read(*args)
366
+ nil
367
+ end
368
+
369
+ def each
370
+ end
371
+
372
+ def rewind
373
+ end
374
+ end
375
+
376
+ lambda {
377
+ Rack::Lint.new(lambda { |env|
378
+ env["rack.input"].gets
379
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
380
+ }).call(env("rack.input" => weirdio))
381
+ }.should.raise(Rack::Lint::LintError).
382
+ message.should.match(/gets didn't return a String/)
383
+
384
+ lambda {
385
+ Rack::Lint.new(lambda { |env|
386
+ env["rack.input"].each { |x| }
387
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
388
+ }).call(env("rack.input" => weirdio))
389
+ }.should.raise(Rack::Lint::LintError).
390
+ message.should.match(/each didn't yield a String/)
391
+
392
+ lambda {
393
+ Rack::Lint.new(lambda { |env|
394
+ env["rack.input"].read
395
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
396
+ }).call(env("rack.input" => weirdio))
397
+ }.should.raise(Rack::Lint::LintError).
398
+ message.should.match(/read didn't return nil or a String/)
399
+
400
+ lambda {
401
+ Rack::Lint.new(lambda { |env|
402
+ env["rack.input"].read
403
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
404
+ }).call(env("rack.input" => eof_weirdio))
405
+ }.should.raise(Rack::Lint::LintError).
406
+ message.should.match(/read\(nil\) returned nil on EOF/)
407
+
408
+ lambda {
409
+ Rack::Lint.new(lambda { |env|
410
+ env["rack.input"].rewind
411
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
412
+ }).call(env("rack.input" => weirdio))
413
+ }.should.raise(Rack::Lint::LintError).
414
+ message.should.match(/rewind raised Errno::ESPIPE/)
415
+
416
+
417
+ lambda {
418
+ Rack::Lint.new(lambda { |env|
419
+ env["rack.input"].close
420
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
421
+ }).call(env({}))
422
+ }.should.raise(Rack::Lint::LintError).
423
+ message.should.match(/close must not be called/)
424
+ end
425
+
426
+ should "notice error handling errors" do
427
+ lambda {
428
+ Rack::Lint.new(lambda { |env|
429
+ env["rack.errors"].write(42)
430
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
431
+ }).call(env({}))
432
+ }.should.raise(Rack::Lint::LintError).
433
+ message.should.match(/write not called with a String/)
434
+
435
+ lambda {
436
+ Rack::Lint.new(lambda { |env|
437
+ env["rack.errors"].close
438
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
439
+ }).call(env({}))
440
+ }.should.raise(Rack::Lint::LintError).
441
+ message.should.match(/close must not be called/)
442
+ end
443
+
444
+ should "notice HEAD errors" do
445
+ lambda {
446
+ Rack::Lint.new(lambda { |env|
447
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, []]
448
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))
449
+ }.should.not.raise
450
+
451
+ lambda {
452
+ Rack::Lint.new(lambda { |env|
453
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
454
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))[2].each { }
455
+ }.should.raise(Rack::Lint::LintError).
456
+ message.should.match(/body was given for HEAD/)
457
+ end
458
+
459
+ should "pass valid read calls" do
460
+ hello_str = "hello world"
461
+ hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding
462
+ lambda {
463
+ Rack::Lint.new(lambda { |env|
464
+ env["rack.input"].read
465
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
466
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
467
+ }.should.not.raise(Rack::Lint::LintError)
468
+
469
+ lambda {
470
+ Rack::Lint.new(lambda { |env|
471
+ env["rack.input"].read(0)
472
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
473
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
474
+ }.should.not.raise(Rack::Lint::LintError)
475
+
476
+ lambda {
477
+ Rack::Lint.new(lambda { |env|
478
+ env["rack.input"].read(1)
479
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
480
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
481
+ }.should.not.raise(Rack::Lint::LintError)
482
+
483
+ lambda {
484
+ Rack::Lint.new(lambda { |env|
485
+ env["rack.input"].read(nil)
486
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
487
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
488
+ }.should.not.raise(Rack::Lint::LintError)
489
+
490
+ lambda {
491
+ Rack::Lint.new(lambda { |env|
492
+ env["rack.input"].read(nil, '')
493
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
494
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
495
+ }.should.not.raise(Rack::Lint::LintError)
496
+
497
+ lambda {
498
+ Rack::Lint.new(lambda { |env|
499
+ env["rack.input"].read(1, '')
500
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
501
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
502
+ }.should.not.raise(Rack::Lint::LintError)
503
+ end
504
+ end
505
+
506
+ describe "Rack::Lint::InputWrapper" do
507
+ should "delegate :rewind to underlying IO object" do
508
+ io = StringIO.new("123")
509
+ wrapper = Rack::Lint::InputWrapper.new(io)
510
+ wrapper.read.should.equal "123"
511
+ wrapper.read.should.equal ""
512
+ wrapper.rewind
513
+ wrapper.read.should.equal "123"
514
+ end
515
+ end