rack 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (79) hide show
  1. data/COPYING +1 -1
  2. data/RDOX +115 -16
  3. data/README +54 -7
  4. data/Rakefile +61 -85
  5. data/SPEC +50 -17
  6. data/bin/rackup +9 -5
  7. data/example/protectedlobster.ru +1 -1
  8. data/lib/rack.rb +7 -3
  9. data/lib/rack/auth/abstract/handler.rb +13 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -1
  11. data/lib/rack/auth/digest/request.rb +2 -2
  12. data/lib/rack/auth/openid.rb +344 -302
  13. data/lib/rack/builder.rb +1 -5
  14. data/lib/rack/chunked.rb +49 -0
  15. data/lib/rack/conditionalget.rb +4 -0
  16. data/lib/rack/content_length.rb +7 -3
  17. data/lib/rack/content_type.rb +23 -0
  18. data/lib/rack/deflater.rb +83 -74
  19. data/lib/rack/directory.rb +5 -2
  20. data/lib/rack/file.rb +4 -1
  21. data/lib/rack/handler.rb +22 -1
  22. data/lib/rack/handler/cgi.rb +7 -3
  23. data/lib/rack/handler/fastcgi.rb +26 -24
  24. data/lib/rack/handler/lsws.rb +7 -4
  25. data/lib/rack/handler/mongrel.rb +5 -3
  26. data/lib/rack/handler/scgi.rb +5 -3
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +11 -5
  29. data/lib/rack/lint.rb +138 -66
  30. data/lib/rack/lock.rb +16 -0
  31. data/lib/rack/mime.rb +4 -4
  32. data/lib/rack/mock.rb +3 -3
  33. data/lib/rack/reloader.rb +88 -46
  34. data/lib/rack/request.rb +46 -10
  35. data/lib/rack/response.rb +15 -3
  36. data/lib/rack/rewindable_input.rb +98 -0
  37. data/lib/rack/session/abstract/id.rb +71 -82
  38. data/lib/rack/session/cookie.rb +2 -0
  39. data/lib/rack/session/memcache.rb +59 -47
  40. data/lib/rack/session/pool.rb +56 -29
  41. data/lib/rack/showexceptions.rb +2 -1
  42. data/lib/rack/showstatus.rb +1 -1
  43. data/lib/rack/urlmap.rb +12 -5
  44. data/lib/rack/utils.rb +115 -65
  45. data/rack.gemspec +54 -0
  46. data/test/multipart/binary +0 -0
  47. data/test/multipart/empty +10 -0
  48. data/test/multipart/ie +6 -0
  49. data/test/multipart/nested +10 -0
  50. data/test/multipart/none +9 -0
  51. data/test/multipart/text +10 -0
  52. data/test/spec_rack_auth_basic.rb +5 -1
  53. data/test/spec_rack_auth_digest.rb +93 -36
  54. data/test/spec_rack_auth_openid.rb +47 -100
  55. data/test/spec_rack_builder.rb +2 -2
  56. data/test/spec_rack_chunked.rb +62 -0
  57. data/test/spec_rack_conditionalget.rb +7 -7
  58. data/test/spec_rack_content_type.rb +30 -0
  59. data/test/spec_rack_deflater.rb +36 -14
  60. data/test/spec_rack_directory.rb +1 -1
  61. data/test/spec_rack_file.rb +11 -0
  62. data/test/spec_rack_handler.rb +21 -2
  63. data/test/spec_rack_lint.rb +163 -44
  64. data/test/spec_rack_lock.rb +38 -0
  65. data/test/spec_rack_mock.rb +6 -1
  66. data/test/spec_rack_request.rb +81 -12
  67. data/test/spec_rack_response.rb +46 -2
  68. data/test/spec_rack_rewindable_input.rb +118 -0
  69. data/test/spec_rack_session_memcache.rb +170 -62
  70. data/test/spec_rack_session_pool.rb +129 -41
  71. data/test/spec_rack_static.rb +2 -2
  72. data/test/spec_rack_thin.rb +3 -2
  73. data/test/spec_rack_urlmap.rb +10 -0
  74. data/test/spec_rack_utils.rb +214 -49
  75. data/test/spec_rack_webrick.rb +7 -0
  76. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  77. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  78. metadata +95 -6
  79. data/AUTHORS +0 -8
@@ -8,34 +8,34 @@ context "Rack::ConditionalGet" do
8
8
  specify "should set a 304 status and truncate body when If-Modified-Since hits" do
9
9
  timestamp = Time.now.httpdate
10
10
  app = Rack::ConditionalGet.new(lambda { |env|
11
- [200, {'Last-Modified'=>timestamp}, 'TEST'] })
11
+ [200, {'Last-Modified'=>timestamp}, ['TEST']] })
12
12
 
13
13
  response = Rack::MockRequest.new(app).
14
14
  get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
15
15
 
16
- response.status.should.be == 304
16
+ response.status.should.equal 304
17
17
  response.body.should.be.empty
18
18
  end
19
19
 
20
20
  specify "should set a 304 status and truncate body when If-None-Match hits" do
21
21
  app = Rack::ConditionalGet.new(lambda { |env|
22
- [200, {'Etag'=>'1234'}, 'TEST'] })
22
+ [200, {'Etag'=>'1234'}, ['TEST']] })
23
23
 
24
24
  response = Rack::MockRequest.new(app).
25
25
  get("/", 'HTTP_IF_NONE_MATCH' => '1234')
26
26
 
27
- response.status.should.be == 304
27
+ response.status.should.equal 304
28
28
  response.body.should.be.empty
29
29
  end
30
30
 
31
31
  specify "should not affect non-GET/HEAD requests" do
32
32
  app = Rack::ConditionalGet.new(lambda { |env|
33
- [200, {'Etag'=>'1234'}, 'TEST'] })
33
+ [200, {'Etag'=>'1234'}, ['TEST']] })
34
34
 
35
35
  response = Rack::MockRequest.new(app).
36
36
  post("/", 'HTTP_IF_NONE_MATCH' => '1234')
37
37
 
38
- response.status.should.be == 200
39
- response.body.should.be == 'TEST'
38
+ response.status.should.equal 200
39
+ response.body.should.equal 'TEST'
40
40
  end
41
41
  end
@@ -0,0 +1,30 @@
1
+ require 'rack/mock'
2
+ require 'rack/content_type'
3
+
4
+ context "Rack::ContentType" do
5
+ specify "sets Content-Type to default text/html if none is set" do
6
+ app = lambda { |env| [200, {}, "Hello, World!"] }
7
+ status, headers, body = Rack::ContentType.new(app).call({})
8
+ headers['Content-Type'].should.equal 'text/html'
9
+ end
10
+
11
+ specify "sets Content-Type to chosen default if none is set" do
12
+ app = lambda { |env| [200, {}, "Hello, World!"] }
13
+ status, headers, body =
14
+ Rack::ContentType.new(app, 'application/octet-stream').call({})
15
+ headers['Content-Type'].should.equal 'application/octet-stream'
16
+ end
17
+
18
+ specify "does not change Content-Type if it is already set" do
19
+ app = lambda { |env| [200, {'Content-Type' => 'foo/bar'}, "Hello, World!"] }
20
+ status, headers, body = Rack::ContentType.new(app).call({})
21
+ headers['Content-Type'].should.equal 'foo/bar'
22
+ end
23
+
24
+ specify "case insensitive detection of Content-Type" do
25
+ app = lambda { |env| [200, {'CONTENT-Type' => 'foo/bar'}, "Hello, World!"] }
26
+ status, headers, body = Rack::ContentType.new(app).call({})
27
+ headers.to_a.select { |k,v| k.downcase == "content-type" }.
28
+ should.equal [["CONTENT-Type","foo/bar"]]
29
+ end
30
+ end
@@ -7,6 +7,7 @@ require 'time' # for Time#httpdate
7
7
 
8
8
  context "Rack::Deflater" do
9
9
  def build_response(status, body, accept_encoding, headers = {})
10
+ body = [body] if body.respond_to? :to_str
10
11
  app = lambda { |env| [status, {}, body] }
11
12
  request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding))
12
13
  response = Rack::Deflater.new(app).call(request)
@@ -21,8 +22,13 @@ context "Rack::Deflater" do
21
22
  response = build_response(200, body, "deflate")
22
23
 
23
24
  response[0].should.equal(200)
24
- response[1].should.equal({ "Content-Encoding" => "deflate", "Vary" => "Accept-Encoding" })
25
- response[2].to_s.should.equal("K\313\317OJ,\002\000")
25
+ response[1].should.equal({
26
+ "Content-Encoding" => "deflate",
27
+ "Vary" => "Accept-Encoding"
28
+ })
29
+ buf = ''
30
+ response[2].each { |part| buf << part }
31
+ buf.should.equal("K\313\317OJ,\002\000")
26
32
  end
27
33
 
28
34
  # TODO: This is really just a special case of the above...
@@ -30,8 +36,13 @@ context "Rack::Deflater" do
30
36
  response = build_response(200, "Hello world!", "deflate")
31
37
 
32
38
  response[0].should.equal(200)
33
- response[1].should.equal({ "Content-Encoding" => "deflate", "Vary" => "Accept-Encoding" })
34
- response[2].to_s.should.equal("\363H\315\311\311W(\317/\312IQ\004\000")
39
+ response[1].should.equal({
40
+ "Content-Encoding" => "deflate",
41
+ "Vary" => "Accept-Encoding"
42
+ })
43
+ buf = ''
44
+ response[2].each { |part| buf << part }
45
+ buf.should.equal("\363H\315\311\311W(\317/\312IQ\004\000")
35
46
  end
36
47
 
37
48
  specify "should be able to gzip bodies that respond to each" do
@@ -41,9 +52,14 @@ context "Rack::Deflater" do
41
52
  response = build_response(200, body, "gzip")
42
53
 
43
54
  response[0].should.equal(200)
44
- response[1].should.equal({ "Content-Encoding" => "gzip", "Vary" => "Accept-Encoding" })
45
-
46
- io = StringIO.new(response[2].to_s)
55
+ response[1].should.equal({
56
+ "Content-Encoding" => "gzip",
57
+ "Vary" => "Accept-Encoding",
58
+ })
59
+
60
+ buf = ''
61
+ response[2].each { |part| buf << part }
62
+ io = StringIO.new(buf)
47
63
  gz = Zlib::GzipReader.new(io)
48
64
  gz.read.should.equal("foobar")
49
65
  gz.close
@@ -54,7 +70,7 @@ context "Rack::Deflater" do
54
70
 
55
71
  response[0].should.equal(200)
56
72
  response[1].should.equal({ "Vary" => "Accept-Encoding" })
57
- response[2].should.equal("Hello world!")
73
+ response[2].should.equal(["Hello world!"])
58
74
  end
59
75
 
60
76
  specify "should be able to skip when there is no response entity body" do
@@ -68,26 +84,32 @@ context "Rack::Deflater" do
68
84
  specify "should handle the lack of an acceptable encoding" do
69
85
  response1 = build_response(200, "Hello world!", "identity;q=0", "PATH_INFO" => "/")
70
86
  response1[0].should.equal(406)
71
- response1[1].should.equal({"Content-Type" => "text/plain"})
87
+ response1[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "71"})
72
88
  response1[2].should.equal(["An acceptable encoding for the requested resource / could not be found."])
73
89
 
74
90
  response2 = build_response(200, "Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar")
75
91
  response2[0].should.equal(406)
76
- response2[1].should.equal({"Content-Type" => "text/plain"})
92
+ response2[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "78"})
77
93
  response2[2].should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."])
78
94
  end
79
95
 
80
96
  specify "should handle gzip response with Last-Modified header" do
81
97
  last_modified = Time.now.httpdate
82
98
 
83
- app = lambda { |env| [200, { "Last-Modified" => last_modified }, "Hello World!"] }
99
+ app = lambda { |env| [200, { "Last-Modified" => last_modified }, ["Hello World!"]] }
84
100
  request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
85
101
  response = Rack::Deflater.new(app).call(request)
86
102
 
87
103
  response[0].should.equal(200)
88
- response[1].should.equal({ "Content-Encoding" => "gzip", "Vary" => "Accept-Encoding", "Last-Modified" => last_modified })
89
-
90
- io = StringIO.new(response[2].to_s)
104
+ response[1].should.equal({
105
+ "Content-Encoding" => "gzip",
106
+ "Vary" => "Accept-Encoding",
107
+ "Last-Modified" => last_modified
108
+ })
109
+
110
+ buf = ''
111
+ response[2].each { |part| buf << part }
112
+ io = StringIO.new(buf)
91
113
  gz = Zlib::GzipReader.new(io)
92
114
  gz.read.should.equal("Hello World!")
93
115
  gz.close
@@ -7,7 +7,7 @@ require 'rack/mock'
7
7
 
8
8
  context "Rack::Directory" do
9
9
  DOCROOT = File.expand_path(File.dirname(__FILE__))
10
- FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, 'passed!'] }
10
+ FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, ['passed!']] }
11
11
  app = Rack::Directory.new DOCROOT, FILE_CATCH
12
12
 
13
13
  specify "serves directory indices" do
@@ -61,4 +61,15 @@ context "Rack::File" do
61
61
 
62
62
  res.should.be.not_found
63
63
  end
64
+
65
+ specify "returns bodies that respond to #to_path" do
66
+ env = Rack::MockRequest.env_for("/cgi/test")
67
+ status, headers, body = Rack::File.new(DOCROOT).call(env)
68
+
69
+ path = File.join(DOCROOT, "/cgi/test")
70
+
71
+ status.should.equal 200
72
+ body.should.respond_to :to_path
73
+ body.to_path.should.equal path
74
+ end
64
75
  end
@@ -12,13 +12,32 @@ context "Rack::Handler" do
12
12
  Rack::Handler.get('mongrel').should.equal Rack::Handler::Mongrel
13
13
  Rack::Handler.get('webrick').should.equal Rack::Handler::WEBrick
14
14
  end
15
+
16
+ specify "handler that doesn't exist should raise a NameError" do
17
+ lambda {
18
+ Rack::Handler.get('boom')
19
+ }.should.raise(NameError)
20
+ end
15
21
 
16
- specify "should get unregistered handler by name" do
17
- Rack::Handler.get('lobster').should.equal Rack::Handler::Lobster
22
+ specify "should get unregistered, but already required, handler by name" do
23
+ Rack::Handler.get('Lobster').should.equal Rack::Handler::Lobster
18
24
  end
19
25
 
20
26
  specify "should register custom handler" do
21
27
  Rack::Handler.register('rock_lobster', 'RockLobster')
22
28
  Rack::Handler.get('rock_lobster').should.equal RockLobster
23
29
  end
30
+
31
+ specify "should not need registration for properly coded handlers even if not already required" do
32
+ begin
33
+ $:.push "test/unregistered_handler"
34
+ Rack::Handler.get('Unregistered').should.equal Rack::Handler::Unregistered
35
+ lambda {
36
+ Rack::Handler.get('UnRegistered')
37
+ }.should.raise(NameError)
38
+ Rack::Handler.get('UnregisteredLongOne').should.equal Rack::Handler::UnregisteredLongOne
39
+ ensure
40
+ $:.delete "test/unregistered_handler"
41
+ end
42
+ end
24
43
  end
@@ -12,7 +12,7 @@ context "Rack::Lint" do
12
12
  specify "passes valid request" do
13
13
  lambda {
14
14
  Rack::Lint.new(lambda { |env|
15
- [200, {"Content-type" => "test/plain", "Content-length" => "3"}, "foo"]
15
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
16
16
  }).call(env({}))
17
17
  }.should.not.raise
18
18
  end
@@ -66,6 +66,11 @@ context "Rack::Lint" do
66
66
  }.should.raise(Rack::Lint::LintError).
67
67
  message.should.match(/url_scheme unknown/)
68
68
 
69
+ lambda {
70
+ Rack::Lint.new(nil).call(env("rack.session" => []))
71
+ }.should.raise(Rack::Lint::LintError).
72
+ message.should.equal("session [] must respond to store and []=")
73
+
69
74
  lambda {
70
75
  Rack::Lint.new(nil).call(env("REQUEST_METHOD" => "FUCKUP?"))
71
76
  }.should.raise(Rack::Lint::LintError).
@@ -133,73 +138,80 @@ context "Rack::Lint" do
133
138
  specify "notices header errors" do
134
139
  lambda {
135
140
  Rack::Lint.new(lambda { |env|
136
- [200, Object.new, ""]
141
+ [200, Object.new, []]
137
142
  }).call(env({}))
138
143
  }.should.raise(Rack::Lint::LintError).
139
144
  message.should.equal("headers object should respond to #each, but doesn't (got Object as headers)")
140
145
 
141
146
  lambda {
142
147
  Rack::Lint.new(lambda { |env|
143
- [200, {true=>false}, ""]
148
+ [200, {true=>false}, []]
144
149
  }).call(env({}))
145
150
  }.should.raise(Rack::Lint::LintError).
146
151
  message.should.equal("header key must be a string, was TrueClass")
147
152
 
148
153
  lambda {
149
154
  Rack::Lint.new(lambda { |env|
150
- [200, {"Status" => "404"}, ""]
155
+ [200, {"Status" => "404"}, []]
151
156
  }).call(env({}))
152
157
  }.should.raise(Rack::Lint::LintError).
153
158
  message.should.match(/must not contain Status/)
154
159
 
155
160
  lambda {
156
161
  Rack::Lint.new(lambda { |env|
157
- [200, {"Content-Type:" => "text/plain"}, ""]
162
+ [200, {"Content-Type:" => "text/plain"}, []]
158
163
  }).call(env({}))
159
164
  }.should.raise(Rack::Lint::LintError).
160
165
  message.should.match(/must not contain :/)
161
166
 
162
167
  lambda {
163
168
  Rack::Lint.new(lambda { |env|
164
- [200, {"Content-" => "text/plain"}, ""]
169
+ [200, {"Content-" => "text/plain"}, []]
165
170
  }).call(env({}))
166
171
  }.should.raise(Rack::Lint::LintError).
167
172
  message.should.match(/must not end/)
168
173
 
169
174
  lambda {
170
175
  Rack::Lint.new(lambda { |env|
171
- [200, {"..%%quark%%.." => "text/plain"}, ""]
176
+ [200, {"..%%quark%%.." => "text/plain"}, []]
172
177
  }).call(env({}))
173
178
  }.should.raise(Rack::Lint::LintError).
174
179
  message.should.equal("invalid header name: ..%%quark%%..")
175
180
 
176
181
  lambda {
177
182
  Rack::Lint.new(lambda { |env|
178
- [200, {"Foo" => Object.new}, ""]
183
+ [200, {"Foo" => Object.new}, []]
179
184
  }).call(env({}))
180
185
  }.should.raise(Rack::Lint::LintError).
181
- message.should.equal("header values must respond to #each, but the value of 'Foo' doesn't (is Object)")
186
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Object")
182
187
 
183
188
  lambda {
184
189
  Rack::Lint.new(lambda { |env|
185
- [200, {"Foo" => [1,2,3]}, ""]
190
+ [200, {"Foo" => [1, 2, 3]}, []]
186
191
  }).call(env({}))
187
192
  }.should.raise(Rack::Lint::LintError).
188
- message.should.equal("header values must consist of Strings, but 'Foo' also contains a Fixnum")
193
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Array")
189
194
 
190
195
 
191
196
  lambda {
192
197
  Rack::Lint.new(lambda { |env|
193
- [200, {"Foo-Bar" => "text\000plain"}, ""]
198
+ [200, {"Foo-Bar" => "text\000plain"}, []]
194
199
  }).call(env({}))
195
200
  }.should.raise(Rack::Lint::LintError).
196
201
  message.should.match(/invalid header/)
202
+
203
+ # line ends (010) should be allowed in header values.
204
+ lambda {
205
+ Rack::Lint.new(lambda { |env|
206
+ [200, {"Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []]
207
+ }).call(env({}))
208
+ }.should.not.raise(Rack::Lint::LintError)
197
209
  end
198
210
 
199
211
  specify "notices content-type errors" do
200
212
  lambda {
201
213
  Rack::Lint.new(lambda { |env|
202
- [200, {"Content-length" => "0"}, ""]
214
+ [200, {"Content-length" => "0"}, []]
203
215
  }).call(env({}))
204
216
  }.should.raise(Rack::Lint::LintError).
205
217
  message.should.match(/No Content-Type/)
@@ -207,7 +219,7 @@ context "Rack::Lint" do
207
219
  [100, 101, 204, 304].each do |status|
208
220
  lambda {
209
221
  Rack::Lint.new(lambda { |env|
210
- [status, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
222
+ [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
211
223
  }).call(env({}))
212
224
  }.should.raise(Rack::Lint::LintError).
213
225
  message.should.match(/Content-Type header found/)
@@ -215,17 +227,10 @@ context "Rack::Lint" do
215
227
  end
216
228
 
217
229
  specify "notices content-length errors" do
218
- lambda {
219
- Rack::Lint.new(lambda { |env|
220
- [200, {"Content-type" => "text/plain"}, ""]
221
- }).call(env({}))
222
- }.should.raise(Rack::Lint::LintError).
223
- message.should.match(/No Content-Length/)
224
-
225
230
  [100, 101, 204, 304].each do |status|
226
231
  lambda {
227
232
  Rack::Lint.new(lambda { |env|
228
- [status, {"Content-length" => "0"}, ""]
233
+ [status, {"Content-length" => "0"}, []]
229
234
  }).call(env({}))
230
235
  }.should.raise(Rack::Lint::LintError).
231
236
  message.should.match(/Content-Length header found/)
@@ -233,14 +238,7 @@ context "Rack::Lint" do
233
238
 
234
239
  lambda {
235
240
  Rack::Lint.new(lambda { |env|
236
- [200, {"Content-type" => "text/plain", "Content-Length" => "0", "Transfer-Encoding" => "chunked"}, ""]
237
- }).call(env({}))
238
- }.should.raise(Rack::Lint::LintError).
239
- message.should.match(/Content-Length header should not be used/)
240
-
241
- lambda {
242
- Rack::Lint.new(lambda { |env|
243
- [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, ""]
241
+ [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []]
244
242
  }).call(env({}))
245
243
  }.should.raise(Rack::Lint::LintError).
246
244
  message.should.match(/Content-Length header was 1, but should be 0/)
@@ -260,18 +258,58 @@ context "Rack::Lint" do
260
258
  lambda {
261
259
  Rack::Lint.new(lambda { |env|
262
260
  env["rack.input"].gets("\r\n")
263
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
261
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
264
262
  }).call(env({}))
265
263
  }.should.raise(Rack::Lint::LintError).
266
264
  message.should.match(/gets called with arguments/)
267
265
 
266
+ lambda {
267
+ Rack::Lint.new(lambda { |env|
268
+ env["rack.input"].read(1, 2, 3)
269
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
270
+ }).call(env({}))
271
+ }.should.raise(Rack::Lint::LintError).
272
+ message.should.match(/read called with too many arguments/)
273
+
268
274
  lambda {
269
275
  Rack::Lint.new(lambda { |env|
270
276
  env["rack.input"].read("foo")
271
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
277
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
278
+ }).call(env({}))
279
+ }.should.raise(Rack::Lint::LintError).
280
+ message.should.match(/read called with non-integer and non-nil length/)
281
+
282
+ lambda {
283
+ Rack::Lint.new(lambda { |env|
284
+ env["rack.input"].read(-1)
285
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
286
+ }).call(env({}))
287
+ }.should.raise(Rack::Lint::LintError).
288
+ message.should.match(/read called with a negative length/)
289
+
290
+ lambda {
291
+ Rack::Lint.new(lambda { |env|
292
+ env["rack.input"].read(nil, nil)
293
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
294
+ }).call(env({}))
295
+ }.should.raise(Rack::Lint::LintError).
296
+ message.should.match(/read called with non-String buffer/)
297
+
298
+ lambda {
299
+ Rack::Lint.new(lambda { |env|
300
+ env["rack.input"].read(nil, 1)
301
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
272
302
  }).call(env({}))
273
303
  }.should.raise(Rack::Lint::LintError).
274
- message.should.match(/read called with non-integer argument/)
304
+ message.should.match(/read called with non-String buffer/)
305
+
306
+ lambda {
307
+ Rack::Lint.new(lambda { |env|
308
+ env["rack.input"].rewind(0)
309
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
310
+ }).call(env({}))
311
+ }.should.raise(Rack::Lint::LintError).
312
+ message.should.match(/rewind called with arguments/)
275
313
 
276
314
  weirdio = Object.new
277
315
  class << weirdio
@@ -287,12 +325,33 @@ context "Rack::Lint" do
287
325
  yield 23
288
326
  yield 42
289
327
  end
328
+
329
+ def rewind
330
+ raise Errno::ESPIPE, "Errno::ESPIPE"
331
+ end
332
+ end
333
+
334
+ eof_weirdio = Object.new
335
+ class << eof_weirdio
336
+ def gets
337
+ nil
338
+ end
339
+
340
+ def read(*args)
341
+ nil
342
+ end
343
+
344
+ def each
345
+ end
346
+
347
+ def rewind
348
+ end
290
349
  end
291
350
 
292
351
  lambda {
293
352
  Rack::Lint.new(lambda { |env|
294
353
  env["rack.input"].gets
295
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
354
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
296
355
  }).call(env("rack.input" => weirdio))
297
356
  }.should.raise(Rack::Lint::LintError).
298
357
  message.should.match(/gets didn't return a String/)
@@ -300,7 +359,7 @@ context "Rack::Lint" do
300
359
  lambda {
301
360
  Rack::Lint.new(lambda { |env|
302
361
  env["rack.input"].each { |x| }
303
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
362
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
304
363
  }).call(env("rack.input" => weirdio))
305
364
  }.should.raise(Rack::Lint::LintError).
306
365
  message.should.match(/each didn't yield a String/)
@@ -308,16 +367,32 @@ context "Rack::Lint" do
308
367
  lambda {
309
368
  Rack::Lint.new(lambda { |env|
310
369
  env["rack.input"].read
311
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
370
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
312
371
  }).call(env("rack.input" => weirdio))
313
372
  }.should.raise(Rack::Lint::LintError).
314
- message.should.match(/read didn't return a String/)
373
+ message.should.match(/read didn't return nil or a String/)
374
+
375
+ lambda {
376
+ Rack::Lint.new(lambda { |env|
377
+ env["rack.input"].read
378
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
379
+ }).call(env("rack.input" => eof_weirdio))
380
+ }.should.raise(Rack::Lint::LintError).
381
+ message.should.match(/read\(nil\) returned nil on EOF/)
382
+
383
+ lambda {
384
+ Rack::Lint.new(lambda { |env|
385
+ env["rack.input"].rewind
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(/rewind raised Errno::ESPIPE/)
315
390
 
316
391
 
317
392
  lambda {
318
393
  Rack::Lint.new(lambda { |env|
319
394
  env["rack.input"].close
320
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
395
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
321
396
  }).call(env({}))
322
397
  }.should.raise(Rack::Lint::LintError).
323
398
  message.should.match(/close must not be called/)
@@ -327,7 +402,7 @@ context "Rack::Lint" do
327
402
  lambda {
328
403
  Rack::Lint.new(lambda { |env|
329
404
  env["rack.errors"].write(42)
330
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
405
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
331
406
  }).call(env({}))
332
407
  }.should.raise(Rack::Lint::LintError).
333
408
  message.should.match(/write not called with a String/)
@@ -335,7 +410,7 @@ context "Rack::Lint" do
335
410
  lambda {
336
411
  Rack::Lint.new(lambda { |env|
337
412
  env["rack.errors"].close
338
- [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
413
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
339
414
  }).call(env({}))
340
415
  }.should.raise(Rack::Lint::LintError).
341
416
  message.should.match(/close must not be called/)
@@ -350,11 +425,55 @@ context "Rack::Lint" do
350
425
 
351
426
  lambda {
352
427
  Rack::Lint.new(lambda { |env|
353
- [200, {"Content-type" => "test/plain", "Content-length" => "3"}, "foo"]
428
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
354
429
  }).call(env({"REQUEST_METHOD" => "HEAD"}))
355
430
  }.should.raise(Rack::Lint::LintError).
356
431
  message.should.match(/body was given for HEAD/)
357
432
  end
433
+
434
+ specify "passes valid read calls" do
435
+ lambda {
436
+ Rack::Lint.new(lambda { |env|
437
+ env["rack.input"].read
438
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
439
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
440
+ }.should.not.raise(Rack::Lint::LintError)
441
+
442
+ lambda {
443
+ Rack::Lint.new(lambda { |env|
444
+ env["rack.input"].read(0)
445
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
446
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
447
+ }.should.not.raise(Rack::Lint::LintError)
448
+
449
+ lambda {
450
+ Rack::Lint.new(lambda { |env|
451
+ env["rack.input"].read(1)
452
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
453
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
454
+ }.should.not.raise(Rack::Lint::LintError)
455
+
456
+ lambda {
457
+ Rack::Lint.new(lambda { |env|
458
+ env["rack.input"].read(nil)
459
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
460
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
461
+ }.should.not.raise(Rack::Lint::LintError)
462
+
463
+ lambda {
464
+ Rack::Lint.new(lambda { |env|
465
+ env["rack.input"].read(nil, '')
466
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
467
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
468
+ }.should.not.raise(Rack::Lint::LintError)
469
+
470
+ lambda {
471
+ Rack::Lint.new(lambda { |env|
472
+ env["rack.input"].read(1, '')
473
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
474
+ }).call(env({"rack.input" => StringIO.new("hello world")}))
475
+ }.should.not.raise(Rack::Lint::LintError)
476
+ end
358
477
  end
359
478
 
360
479
  context "Rack::Lint::InputWrapper" do
@@ -372,9 +491,9 @@ context "Rack::Lint::InputWrapper" do
372
491
  specify "delegates :rewind to underlying IO object" do
373
492
  io = StringIO.new("123")
374
493
  wrapper = Rack::Lint::InputWrapper.new(io)
375
- wrapper.read.should == "123"
376
- wrapper.read.should == ""
494
+ wrapper.read.should.equal "123"
495
+ wrapper.read.should.equal ""
377
496
  wrapper.rewind
378
- wrapper.read.should == "123"
497
+ wrapper.read.should.equal "123"
379
498
  end
380
499
  end