lack 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rackup +5 -0
  3. data/lib/rack.rb +26 -0
  4. data/lib/rack/body_proxy.rb +39 -0
  5. data/lib/rack/builder.rb +166 -0
  6. data/lib/rack/handler.rb +63 -0
  7. data/lib/rack/handler/webrick.rb +120 -0
  8. data/lib/rack/mime.rb +661 -0
  9. data/lib/rack/mock.rb +198 -0
  10. data/lib/rack/multipart.rb +31 -0
  11. data/lib/rack/multipart/generator.rb +93 -0
  12. data/lib/rack/multipart/parser.rb +239 -0
  13. data/lib/rack/multipart/uploaded_file.rb +34 -0
  14. data/lib/rack/request.rb +394 -0
  15. data/lib/rack/response.rb +160 -0
  16. data/lib/rack/server.rb +258 -0
  17. data/lib/rack/server/options.rb +121 -0
  18. data/lib/rack/utils.rb +653 -0
  19. data/lib/rack/version.rb +3 -0
  20. data/spec/spec_helper.rb +1 -0
  21. data/test/builder/anything.rb +5 -0
  22. data/test/builder/comment.ru +4 -0
  23. data/test/builder/end.ru +5 -0
  24. data/test/builder/line.ru +1 -0
  25. data/test/builder/options.ru +2 -0
  26. data/test/multipart/bad_robots +259 -0
  27. data/test/multipart/binary +0 -0
  28. data/test/multipart/content_type_and_no_filename +6 -0
  29. data/test/multipart/empty +10 -0
  30. data/test/multipart/fail_16384_nofile +814 -0
  31. data/test/multipart/file1.txt +1 -0
  32. data/test/multipart/filename_and_modification_param +7 -0
  33. data/test/multipart/filename_and_no_name +6 -0
  34. data/test/multipart/filename_with_escaped_quotes +6 -0
  35. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  36. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  37. data/test/multipart/filename_with_unescaped_percentages +6 -0
  38. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  39. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  40. data/test/multipart/filename_with_unescaped_quotes +6 -0
  41. data/test/multipart/ie +6 -0
  42. data/test/multipart/invalid_character +6 -0
  43. data/test/multipart/mixed_files +21 -0
  44. data/test/multipart/nested +10 -0
  45. data/test/multipart/none +9 -0
  46. data/test/multipart/semicolon +6 -0
  47. data/test/multipart/text +15 -0
  48. data/test/multipart/webkit +32 -0
  49. data/test/rackup/config.ru +31 -0
  50. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  51. data/test/spec_body_proxy.rb +69 -0
  52. data/test/spec_builder.rb +223 -0
  53. data/test/spec_chunked.rb +101 -0
  54. data/test/spec_file.rb +221 -0
  55. data/test/spec_handler.rb +59 -0
  56. data/test/spec_head.rb +45 -0
  57. data/test/spec_lint.rb +522 -0
  58. data/test/spec_mime.rb +51 -0
  59. data/test/spec_mock.rb +277 -0
  60. data/test/spec_multipart.rb +547 -0
  61. data/test/spec_recursive.rb +72 -0
  62. data/test/spec_request.rb +1199 -0
  63. data/test/spec_response.rb +343 -0
  64. data/test/spec_rewindable_input.rb +118 -0
  65. data/test/spec_sendfile.rb +130 -0
  66. data/test/spec_server.rb +167 -0
  67. data/test/spec_utils.rb +635 -0
  68. data/test/spec_webrick.rb +184 -0
  69. data/test/testrequest.rb +78 -0
  70. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  71. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  72. metadata +240 -0
@@ -0,0 +1,101 @@
1
+ require 'rack/chunked'
2
+ require 'rack/lint'
3
+ require 'rack/mock'
4
+
5
+ describe Rack::Chunked do
6
+ def chunked(app)
7
+ proc do |env|
8
+ app = Rack::Chunked.new(app)
9
+ response = Rack::Lint.new(app).call(env)
10
+ # we want to use body like an array, but it only has #each
11
+ response[2] = response[2].to_enum.to_a
12
+ response
13
+ end
14
+ end
15
+
16
+ before do
17
+ @env = Rack::MockRequest.
18
+ env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET')
19
+ end
20
+
21
+ should 'chunk responses with no Content-Length' do
22
+ app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
23
+ response = Rack::MockResponse.new(*chunked(app).call(@env))
24
+ response.headers.should.not.include 'Content-Length'
25
+ response.headers['Transfer-Encoding'].should.equal 'chunked'
26
+ response.body.should.equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\n\r\n"
27
+ end
28
+
29
+ should 'chunks empty bodies properly' do
30
+ app = lambda { |env| [200, {"Content-Type" => "text/plain"}, []] }
31
+ response = Rack::MockResponse.new(*chunked(app).call(@env))
32
+ response.headers.should.not.include 'Content-Length'
33
+ response.headers['Transfer-Encoding'].should.equal 'chunked'
34
+ response.body.should.equal "0\r\n\r\n"
35
+ end
36
+
37
+ should 'chunks encoded bodies properly' do
38
+ body = ["\uFFFEHello", " ", "World"].map {|t| t.encode("UTF-16LE") }
39
+ app = lambda { |env| [200, {"Content-Type" => "text/plain"}, body] }
40
+ response = Rack::MockResponse.new(*chunked(app).call(@env))
41
+ response.headers.should.not.include 'Content-Length'
42
+ response.headers['Transfer-Encoding'].should.equal 'chunked'
43
+ response.body.encoding.to_s.should.equal "ASCII-8BIT"
44
+ response.body.should.equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding("BINARY")
45
+ end if RUBY_VERSION >= "1.9"
46
+
47
+ should 'not modify response when Content-Length header present' do
48
+ app = lambda { |env|
49
+ [200, {"Content-Type" => "text/plain", 'Content-Length'=>'12'}, ['Hello', ' ', 'World!']]
50
+ }
51
+ status, headers, body = chunked(app).call(@env)
52
+ status.should.equal 200
53
+ headers.should.not.include 'Transfer-Encoding'
54
+ headers.should.include 'Content-Length'
55
+ body.join.should.equal 'Hello World!'
56
+ end
57
+
58
+ should 'not modify response when client is HTTP/1.0' do
59
+ app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
60
+ @env['HTTP_VERSION'] = 'HTTP/1.0'
61
+ status, headers, body = chunked(app).call(@env)
62
+ status.should.equal 200
63
+ headers.should.not.include 'Transfer-Encoding'
64
+ body.join.should.equal 'Hello World!'
65
+ end
66
+
67
+ should 'not modify response when client is ancient, pre-HTTP/1.0' do
68
+ app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
69
+ check = lambda do
70
+ status, headers, body = chunked(app).call(@env.dup)
71
+ status.should.equal 200
72
+ headers.should.not.include 'Transfer-Encoding'
73
+ body.join.should.equal 'Hello World!'
74
+ end
75
+
76
+ @env.delete('HTTP_VERSION') # unicorn will do this on pre-HTTP/1.0 requests
77
+ check.call
78
+
79
+ @env['HTTP_VERSION'] = 'HTTP/0.9' # not sure if this happens in practice
80
+ check.call
81
+ end
82
+
83
+ should 'not modify response when Transfer-Encoding header already present' do
84
+ app = lambda { |env|
85
+ [200, {"Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity'}, ['Hello', ' ', 'World!']]
86
+ }
87
+ status, headers, body = chunked(app).call(@env)
88
+ status.should.equal 200
89
+ headers['Transfer-Encoding'].should.equal 'identity'
90
+ body.join.should.equal 'Hello World!'
91
+ end
92
+
93
+ [100, 204, 205, 304].each do |status_code|
94
+ should "not modify response when status code is #{status_code}" do
95
+ app = lambda { |env| [status_code, {}, []] }
96
+ status, headers, _ = chunked(app).call(@env)
97
+ status.should.equal status_code
98
+ headers.should.not.include 'Transfer-Encoding'
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,221 @@
1
+ require 'rack/file'
2
+ require 'rack/lint'
3
+ require 'rack/mock'
4
+
5
+ describe Rack::File do
6
+ DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
7
+
8
+ def file(*args)
9
+ Rack::Lint.new Rack::File.new(*args)
10
+ end
11
+
12
+ should "serve files" do
13
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test")
14
+
15
+ res.should.be.ok
16
+ res.should =~ /ruby/
17
+ end
18
+
19
+ should "set Last-Modified header" do
20
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test")
21
+
22
+ path = File.join(DOCROOT, "/cgi/test")
23
+
24
+ res.should.be.ok
25
+ res["Last-Modified"].should.equal File.mtime(path).httpdate
26
+ end
27
+
28
+ should "return 304 if file isn't modified since last serve" do
29
+ path = File.join(DOCROOT, "/cgi/test")
30
+ res = Rack::MockRequest.new(file(DOCROOT)).
31
+ get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate)
32
+
33
+ res.status.should.equal 304
34
+ res.body.should.be.empty
35
+ end
36
+
37
+ should "return the file if it's modified since last serve" do
38
+ path = File.join(DOCROOT, "/cgi/test")
39
+ res = Rack::MockRequest.new(file(DOCROOT)).
40
+ get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => (File.mtime(path) - 100).httpdate)
41
+
42
+ res.should.be.ok
43
+ end
44
+
45
+ should "serve files with URL encoded filenames" do
46
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test"
47
+
48
+ res.should.be.ok
49
+ res.should =~ /ruby/
50
+ end
51
+
52
+ should "allow safe directory traversal" do
53
+ req = Rack::MockRequest.new(file(DOCROOT))
54
+
55
+ res = req.get('/cgi/../cgi/test')
56
+ res.should.be.successful
57
+
58
+ res = req.get('.')
59
+ res.should.be.not_found
60
+
61
+ res = req.get("test/..")
62
+ res.should.be.not_found
63
+ end
64
+
65
+ should "not allow unsafe directory traversal" do
66
+ req = Rack::MockRequest.new(file(DOCROOT))
67
+
68
+ res = req.get("/../README.rdoc")
69
+ res.should.be.client_error
70
+
71
+ res = req.get("../test/spec_file.rb")
72
+ res.should.be.client_error
73
+
74
+ res = req.get("../README.rdoc")
75
+ res.should.be.client_error
76
+
77
+ res.should.be.not_found
78
+ end
79
+
80
+ should "allow files with .. in their name" do
81
+ req = Rack::MockRequest.new(file(DOCROOT))
82
+ res = req.get("/cgi/..test")
83
+ res.should.be.not_found
84
+
85
+ res = req.get("/cgi/test..")
86
+ res.should.be.not_found
87
+
88
+ res = req.get("/cgi../test..")
89
+ res.should.be.not_found
90
+ end
91
+
92
+ should "not allow unsafe directory traversal with encoded periods" do
93
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/%2E%2E/README")
94
+
95
+ res.should.be.client_error?
96
+ res.should.be.not_found
97
+ end
98
+
99
+ should "allow safe directory traversal with encoded periods" do
100
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%2E%2E/cgi/test")
101
+
102
+ res.should.be.successful
103
+ end
104
+
105
+ should "404 if it can't find the file" do
106
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/blubb")
107
+
108
+ res.should.be.not_found
109
+ end
110
+
111
+ should "detect SystemCallErrors" do
112
+ res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi")
113
+
114
+ res.should.be.not_found
115
+ end
116
+
117
+ should "return bodies that respond to #to_path" do
118
+ env = Rack::MockRequest.env_for("/cgi/test")
119
+ status, _, body = Rack::File.new(DOCROOT).call(env)
120
+
121
+ path = File.join(DOCROOT, "/cgi/test")
122
+
123
+ status.should.equal 200
124
+ body.should.respond_to :to_path
125
+ body.to_path.should.equal path
126
+ end
127
+
128
+ should "return correct byte range in body" do
129
+ env = Rack::MockRequest.env_for("/cgi/test")
130
+ env["HTTP_RANGE"] = "bytes=22-33"
131
+ res = Rack::MockResponse.new(*file(DOCROOT).call(env))
132
+
133
+ res.status.should.equal 206
134
+ res["Content-Length"].should.equal "12"
135
+ res["Content-Range"].should.equal "bytes 22-33/193"
136
+ res.body.should.equal "-*- ruby -*-"
137
+ end
138
+
139
+ should "return error for unsatisfiable byte range" do
140
+ env = Rack::MockRequest.env_for("/cgi/test")
141
+ env["HTTP_RANGE"] = "bytes=1234-5678"
142
+ res = Rack::MockResponse.new(*file(DOCROOT).call(env))
143
+
144
+ res.status.should.equal 416
145
+ res["Content-Range"].should.equal "bytes */193"
146
+ end
147
+
148
+ should "support custom http headers" do
149
+ env = Rack::MockRequest.env_for("/cgi/test")
150
+ status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38',
151
+ 'Access-Control-Allow-Origin' => '*').call(env)
152
+
153
+ status.should.equal 200
154
+ heads['Cache-Control'].should.equal 'public, max-age=38'
155
+ heads['Access-Control-Allow-Origin'].should.equal '*'
156
+ end
157
+
158
+ should "support not add custom http headers if none are supplied" do
159
+ env = Rack::MockRequest.env_for("/cgi/test")
160
+ status, heads, _ = file(DOCROOT).call(env)
161
+
162
+ status.should.equal 200
163
+ heads['Cache-Control'].should.equal nil
164
+ heads['Access-Control-Allow-Origin'].should.equal nil
165
+ end
166
+
167
+ should "only support GET, HEAD, and OPTIONS requests" do
168
+ req = Rack::MockRequest.new(file(DOCROOT))
169
+
170
+ forbidden = %w[post put patch delete]
171
+ forbidden.each do |method|
172
+ res = req.send(method, "/cgi/test")
173
+ res.should.be.client_error
174
+ res.should.be.method_not_allowed
175
+ res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
176
+ end
177
+
178
+ allowed = %w[get head options]
179
+ allowed.each do |method|
180
+ res = req.send(method, "/cgi/test")
181
+ res.should.be.successful
182
+ end
183
+ end
184
+
185
+ should "set Allow correctly for OPTIONS requests" do
186
+ req = Rack::MockRequest.new(file(DOCROOT))
187
+ res = req.options('/cgi/test')
188
+ res.should.be.successful
189
+ res.headers['Allow'].should.not.equal nil
190
+ res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
191
+ end
192
+
193
+ should "set Content-Length correctly for HEAD requests" do
194
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
195
+ res = req.head "/cgi/test"
196
+ res.should.be.successful
197
+ res['Content-Length'].should.equal "193"
198
+ end
199
+
200
+ should "default to a mime type of text/plain" do
201
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
202
+ res = req.get "/cgi/test"
203
+ res.should.be.successful
204
+ res['Content-Type'].should.equal "text/plain"
205
+ end
206
+
207
+ should "allow the default mime type to be set" do
208
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream')))
209
+ res = req.get "/cgi/test"
210
+ res.should.be.successful
211
+ res['Content-Type'].should.equal "application/octet-stream"
212
+ end
213
+
214
+ should "not set Content-Type if the mime type is not set" do
215
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil)))
216
+ res = req.get "/cgi/test"
217
+ res.should.be.successful
218
+ res['Content-Type'].should.equal nil
219
+ end
220
+
221
+ end
@@ -0,0 +1,59 @@
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 LoadError if handler doesn't exist" do
23
+ lambda {
24
+ Rack::Handler.get('boom')
25
+ }.should.raise(LoadError)
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 LoadError
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
+
50
+ should "allow autoloaded handlers to be registered properly while being loaded" do
51
+ path = File.expand_path('../registering_handler', __FILE__)
52
+ begin
53
+ $LOAD_PATH.push path
54
+ Rack::Handler.get('registering_myself').should.equal Rack::Handler::RegisteringMyself
55
+ ensure
56
+ $LOAD_PATH.delete path
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ require 'rack/head'
2
+ require 'rack/lint'
3
+ require 'rack/mock'
4
+
5
+ describe Rack::Head do
6
+
7
+ def test_response(headers = {})
8
+ body = StringIO.new "foo"
9
+ app = lambda do |env|
10
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, body]
11
+ end
12
+ request = Rack::MockRequest.env_for("/", headers)
13
+ response = Rack::Lint.new(Rack::Head.new(app)).call(request)
14
+
15
+ return response, body
16
+ end
17
+
18
+ should "pass GET, POST, PUT, DELETE, OPTIONS, TRACE requests" do
19
+ %w[GET POST PUT DELETE OPTIONS TRACE].each do |type|
20
+ resp, _ = test_response("REQUEST_METHOD" => type)
21
+
22
+ resp[0].should.equal(200)
23
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
24
+ resp[2].to_enum.to_a.should.equal(["foo"])
25
+ end
26
+ end
27
+
28
+ should "remove body from HEAD requests" do
29
+ resp, _ = test_response("REQUEST_METHOD" => "HEAD")
30
+
31
+ resp[0].should.equal(200)
32
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
33
+ resp[2].to_enum.to_a.should.equal([])
34
+ end
35
+
36
+ should "close the body when it is removed" do
37
+ resp, body = test_response("REQUEST_METHOD" => "HEAD")
38
+ resp[0].should.equal(200)
39
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
40
+ resp[2].to_enum.to_a.should.equal([])
41
+ body.should.not.be.closed
42
+ resp[2].close
43
+ body.should.be.closed
44
+ end
45
+ end
@@ -0,0 +1,522 @@
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
+
235
+ # non-Hash header responses should be allowed
236
+ lambda {
237
+ Rack::Lint.new(lambda { |env|
238
+ [200, [%w(Content-Type text/plain), %w(Content-Length 0)], []]
239
+ }).call(env({}))
240
+ }.should.not.raise(TypeError)
241
+ end
242
+
243
+ should "notice content-type errors" do
244
+ # lambda {
245
+ # Rack::Lint.new(lambda { |env|
246
+ # [200, {"Content-length" => "0"}, []]
247
+ # }).call(env({}))
248
+ # }.should.raise(Rack::Lint::LintError).
249
+ # message.should.match(/No Content-Type/)
250
+
251
+ [100, 101, 204, 205, 304].each do |status|
252
+ lambda {
253
+ Rack::Lint.new(lambda { |env|
254
+ [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
255
+ }).call(env({}))
256
+ }.should.raise(Rack::Lint::LintError).
257
+ message.should.match(/Content-Type header found/)
258
+ end
259
+ end
260
+
261
+ should "notice content-length errors" do
262
+ [100, 101, 204, 205, 304].each do |status|
263
+ lambda {
264
+ Rack::Lint.new(lambda { |env|
265
+ [status, {"Content-length" => "0"}, []]
266
+ }).call(env({}))
267
+ }.should.raise(Rack::Lint::LintError).
268
+ message.should.match(/Content-Length header found/)
269
+ end
270
+
271
+ lambda {
272
+ Rack::Lint.new(lambda { |env|
273
+ [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []]
274
+ }).call(env({}))[2].each { }
275
+ }.should.raise(Rack::Lint::LintError).
276
+ message.should.match(/Content-Length header was 1, but should be 0/)
277
+ end
278
+
279
+ should "notice body errors" do
280
+ lambda {
281
+ body = Rack::Lint.new(lambda { |env|
282
+ [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]]
283
+ }).call(env({}))[2]
284
+ body.each { |part| }
285
+ }.should.raise(Rack::Lint::LintError).
286
+ message.should.match(/yielded non-string/)
287
+ end
288
+
289
+ should "notice input handling errors" do
290
+ lambda {
291
+ Rack::Lint.new(lambda { |env|
292
+ env["rack.input"].gets("\r\n")
293
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
294
+ }).call(env({}))
295
+ }.should.raise(Rack::Lint::LintError).
296
+ message.should.match(/gets called with arguments/)
297
+
298
+ lambda {
299
+ Rack::Lint.new(lambda { |env|
300
+ env["rack.input"].read(1, 2, 3)
301
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
302
+ }).call(env({}))
303
+ }.should.raise(Rack::Lint::LintError).
304
+ message.should.match(/read called with too many arguments/)
305
+
306
+ lambda {
307
+ Rack::Lint.new(lambda { |env|
308
+ env["rack.input"].read("foo")
309
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
310
+ }).call(env({}))
311
+ }.should.raise(Rack::Lint::LintError).
312
+ message.should.match(/read called with non-integer and non-nil length/)
313
+
314
+ lambda {
315
+ Rack::Lint.new(lambda { |env|
316
+ env["rack.input"].read(-1)
317
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
318
+ }).call(env({}))
319
+ }.should.raise(Rack::Lint::LintError).
320
+ message.should.match(/read called with a negative length/)
321
+
322
+ lambda {
323
+ Rack::Lint.new(lambda { |env|
324
+ env["rack.input"].read(nil, nil)
325
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
326
+ }).call(env({}))
327
+ }.should.raise(Rack::Lint::LintError).
328
+ message.should.match(/read called with non-String buffer/)
329
+
330
+ lambda {
331
+ Rack::Lint.new(lambda { |env|
332
+ env["rack.input"].read(nil, 1)
333
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
334
+ }).call(env({}))
335
+ }.should.raise(Rack::Lint::LintError).
336
+ message.should.match(/read called with non-String buffer/)
337
+
338
+ lambda {
339
+ Rack::Lint.new(lambda { |env|
340
+ env["rack.input"].rewind(0)
341
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
342
+ }).call(env({}))
343
+ }.should.raise(Rack::Lint::LintError).
344
+ message.should.match(/rewind called with arguments/)
345
+
346
+ weirdio = Object.new
347
+ class << weirdio
348
+ def gets
349
+ 42
350
+ end
351
+
352
+ def read
353
+ 23
354
+ end
355
+
356
+ def each
357
+ yield 23
358
+ yield 42
359
+ end
360
+
361
+ def rewind
362
+ raise Errno::ESPIPE, "Errno::ESPIPE"
363
+ end
364
+ end
365
+
366
+ eof_weirdio = Object.new
367
+ class << eof_weirdio
368
+ def gets
369
+ nil
370
+ end
371
+
372
+ def read(*args)
373
+ nil
374
+ end
375
+
376
+ def each
377
+ end
378
+
379
+ def rewind
380
+ end
381
+ end
382
+
383
+ lambda {
384
+ Rack::Lint.new(lambda { |env|
385
+ env["rack.input"].gets
386
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
387
+ }).call(env("rack.input" => weirdio))
388
+ }.should.raise(Rack::Lint::LintError).
389
+ message.should.match(/gets didn't return a String/)
390
+
391
+ lambda {
392
+ Rack::Lint.new(lambda { |env|
393
+ env["rack.input"].each { |x| }
394
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
395
+ }).call(env("rack.input" => weirdio))
396
+ }.should.raise(Rack::Lint::LintError).
397
+ message.should.match(/each didn't yield a String/)
398
+
399
+ lambda {
400
+ Rack::Lint.new(lambda { |env|
401
+ env["rack.input"].read
402
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
403
+ }).call(env("rack.input" => weirdio))
404
+ }.should.raise(Rack::Lint::LintError).
405
+ message.should.match(/read didn't return nil or a String/)
406
+
407
+ lambda {
408
+ Rack::Lint.new(lambda { |env|
409
+ env["rack.input"].read
410
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
411
+ }).call(env("rack.input" => eof_weirdio))
412
+ }.should.raise(Rack::Lint::LintError).
413
+ message.should.match(/read\(nil\) returned nil on EOF/)
414
+
415
+ lambda {
416
+ Rack::Lint.new(lambda { |env|
417
+ env["rack.input"].rewind
418
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
419
+ }).call(env("rack.input" => weirdio))
420
+ }.should.raise(Rack::Lint::LintError).
421
+ message.should.match(/rewind raised Errno::ESPIPE/)
422
+
423
+
424
+ lambda {
425
+ Rack::Lint.new(lambda { |env|
426
+ env["rack.input"].close
427
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
428
+ }).call(env({}))
429
+ }.should.raise(Rack::Lint::LintError).
430
+ message.should.match(/close must not be called/)
431
+ end
432
+
433
+ should "notice error handling errors" do
434
+ lambda {
435
+ Rack::Lint.new(lambda { |env|
436
+ env["rack.errors"].write(42)
437
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
438
+ }).call(env({}))
439
+ }.should.raise(Rack::Lint::LintError).
440
+ message.should.match(/write not called with a String/)
441
+
442
+ lambda {
443
+ Rack::Lint.new(lambda { |env|
444
+ env["rack.errors"].close
445
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
446
+ }).call(env({}))
447
+ }.should.raise(Rack::Lint::LintError).
448
+ message.should.match(/close must not be called/)
449
+ end
450
+
451
+ should "notice HEAD errors" do
452
+ lambda {
453
+ Rack::Lint.new(lambda { |env|
454
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, []]
455
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))
456
+ }.should.not.raise
457
+
458
+ lambda {
459
+ Rack::Lint.new(lambda { |env|
460
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
461
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))[2].each { }
462
+ }.should.raise(Rack::Lint::LintError).
463
+ message.should.match(/body was given for HEAD/)
464
+ end
465
+
466
+ should "pass valid read calls" do
467
+ hello_str = "hello world"
468
+ hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding
469
+ lambda {
470
+ Rack::Lint.new(lambda { |env|
471
+ env["rack.input"].read
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(0)
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(1)
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(nil, '')
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
+
504
+ lambda {
505
+ Rack::Lint.new(lambda { |env|
506
+ env["rack.input"].read(1, '')
507
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
508
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
509
+ }.should.not.raise(Rack::Lint::LintError)
510
+ end
511
+ end
512
+
513
+ describe "Rack::Lint::InputWrapper" do
514
+ should "delegate :rewind to underlying IO object" do
515
+ io = StringIO.new("123")
516
+ wrapper = Rack::Lint::InputWrapper.new(io)
517
+ wrapper.read.should.equal "123"
518
+ wrapper.read.should.equal ""
519
+ wrapper.rewind
520
+ wrapper.read.should.equal "123"
521
+ end
522
+ end