lack 2.0.0

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