edgar-rack 1.2.1

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.
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