diff options
author | Scytrin dai Kinthra <scytrin@gmail.com> | 2008-08-07 03:01:31 -0700 |
---|---|---|
committer | Scytrin dai Kinthra <scytrin@gmail.com> | 2008-08-07 03:01:31 -0700 |
commit | 90c0938c76c77353e480960bdcfeefc26ac24dbe (patch) | |
tree | 13c93041aebd385c4592b13b36b529c9f05739e7 | |
parent | 835cfd6860864d0d0d559b3e997544aeb374dc00 (diff) | |
parent | f58c3a4cdf6192dcce880e722b3398049066a571 (diff) | |
download | rack-90c0938c76c77353e480960bdcfeefc26ac24dbe.tar.gz |
Merge commit 'core/master'
-rw-r--r-- | lib/rack.rb | 1 | ||||
-rw-r--r-- | lib/rack/auth/digest/params.rb | 4 | ||||
-rw-r--r-- | lib/rack/deflater.rb | 63 | ||||
-rw-r--r-- | lib/rack/directory.rb | 11 | ||||
-rw-r--r-- | lib/rack/file.rb | 9 | ||||
-rw-r--r-- | lib/rack/lint.rb | 93 | ||||
-rw-r--r-- | lib/rack/request.rb | 12 | ||||
-rw-r--r-- | lib/rack/showstatus.rb | 4 | ||||
-rw-r--r-- | lib/rack/utils.rb | 30 | ||||
-rw-r--r-- | test/spec_rack_deflater.rb | 70 | ||||
-rw-r--r-- | test/spec_rack_directory.rb | 2 | ||||
-rw-r--r-- | test/spec_rack_handler.rb | 2 | ||||
-rw-r--r-- | test/spec_rack_lint.rb | 73 | ||||
-rw-r--r-- | test/spec_rack_request.rb | 19 | ||||
-rw-r--r-- | test/spec_rack_showstatus.rb | 9 | ||||
-rw-r--r-- | test/spec_rack_utils.rb | 29 | ||||
-rw-r--r-- | test/testrequest.rb | 4 |
17 files changed, 376 insertions, 59 deletions
diff --git a/lib/rack.rb b/lib/rack.rb index 607d0f5c..933f5dcb 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -30,6 +30,7 @@ module Rack autoload :Cascade, "rack/cascade" autoload :CommonLogger, "rack/commonlogger" autoload :File, "rack/file" + autoload :Deflater, "rack/deflater" autoload :Directory, "rack/directory" autoload :ForwardRequest, "rack/recursive" autoload :Handler, "rack/handler" diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb index b4dcffd0..730e2efd 100644 --- a/lib/rack/auth/digest/params.rb +++ b/lib/rack/auth/digest/params.rb @@ -17,8 +17,8 @@ module Rack ret end - def self.split_header_value(str) # From WEBrick::HTTPUtils - str.scan(/((?:"(?:\\.|[^"])+?"|[^",]+)+)(?:,\s*|\Z)/n).collect{ |v| v[0] } + def self.split_header_value(str) + str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] } end def initialize diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb new file mode 100644 index 00000000..1341eece --- /dev/null +++ b/lib/rack/deflater.rb @@ -0,0 +1,63 @@ +require "zlib" +require "stringio" + +module Rack + +class Deflater + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + + request = Request.new(env) + + encoding = Utils.select_best_encoding(%w(gzip deflate identity), request.accept_encoding) + + case encoding + when "gzip" + mtime = headers["Last-Modified"] || Time.now + [status, headers.merge("Content-Encoding" => "gzip"), self.class.gzip(body, mtime)] + when "deflate" + [status, headers.merge("Content-Encoding" => "deflate"), self.class.deflate(body)] + when "identity" + [status, headers, body] + when nil + message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found." + [406, {"Content-Type" => "text/plain"}, message] + end + end + + def self.gzip(body, mtime) + io = StringIO.new + gzip = Zlib::GzipWriter.new(io) + gzip.mtime = mtime + + # TODO: Add streaming + body.each { |part| gzip << part } + + gzip.close + return io.string + end + + DEFLATE_ARGS = [ + Zlib::DEFAULT_COMPRESSION, + # drop the zlib header which causes both Safari and IE to choke + -Zlib::MAX_WBITS, + Zlib::DEF_MEM_LEVEL, + Zlib::DEFAULT_STRATEGY + ] + + # Loosely based on Mongrel's Deflate handler + def self.deflate(body) + deflater = Zlib::Deflate.new(*DEFLATE_ARGS) + + # TODO: Add streaming + body.each { |part| deflater << part } + + return deflater.finish + end +end + +end diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index 972b4bc6..31e0db84 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -1,3 +1,5 @@ +require 'time' + module Rack # Rack::Directory serves entries below the +root+ given, according to the # path info of the Rack request. If a directory is found, the file's contents @@ -51,7 +53,9 @@ table { width:100%%; } def _call(env) if env["PATH_INFO"].include? ".." - return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]] + body = "Forbidden\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]] end @path = F.join(@root, Utils.unescape(env['PATH_INFO'])) @@ -77,8 +81,9 @@ table { width:100%%; } end end - return [404, {"Content-Type" => "text/plain"}, - ["Entity not found: #{env["PATH_INFO"]}\n"]] + body = "Entity not found: #{env["PATH_INFO"]}\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]] end def each diff --git a/lib/rack/file.rb b/lib/rack/file.rb index 7538fd69..afb21383 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -23,7 +23,9 @@ module Rack def _call(env) if env["PATH_INFO"].include? ".." - return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]] + body = "Forbidden\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]] end @path = F.join(@root, Utils.unescape(env["PATH_INFO"])) @@ -36,8 +38,9 @@ module Rack "Content-Length" => F.size(@path).to_s }, self] else - return [404, {"Content-Type" => "text/plain"}, - ["File not found: #{env["PATH_INFO"]}\n"]] + body = "File not found: #{env["PATH_INFO"]}\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]] end end diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index b5232e00..2b81f10d 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -3,6 +3,8 @@ module Rack # responses according to the Rack spec. class Lint + STATUS_WITH_NO_ENTITY_BODY = (100..199).to_a << 204 << 304 + def initialize(app) @app = app end @@ -29,7 +31,11 @@ module Rack ## A Rack application is an Ruby object (not a class) that ## responds to +call+. - def call(env=nil) + def call(env=nil) + dup._call(env) + end + + def _call(env) ## It takes exactly one argument, the *environment* assert("No env given") { env } check_env env @@ -45,6 +51,7 @@ module Rack check_headers headers ## and the *body*. check_content_type status, headers + check_content_length status, headers [status, headers, self] end @@ -57,7 +64,7 @@ module Rack env.instance_of? Hash } - ## + ## ## The environment is required to include these variables ## (adopted from PEP333), except when they'd be empty, but see ## below. @@ -115,7 +122,7 @@ module Rack ## and should be prefixed uniquely. The prefix <tt>rack.</tt> ## is reserved for use with the Rack core distribution and must ## not be used otherwise. - ## + ## %w[REQUEST_METHOD SERVER_NAME SERVER_PORT QUERY_STRING @@ -141,7 +148,7 @@ module Rack } } - ## + ## ## There are the following restrictions: ## * <tt>rack.version</tt> must be an array of Integers. @@ -301,14 +308,16 @@ module Rack ## === The Status def check_status(status) - ## The status, if parsed as integer (+to_i+), must be bigger than 100. - assert("Status must be >100 seen as integer") { status.to_i > 100 } + ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100. + assert("Status must be >=100 seen as integer") { status.to_i >= 100 } end ## === The Headers def check_headers(header) ## The header must respond to each, and yield values of key and value. - assert("header should respond to #each") { header.respond_to? :each } + assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { + header.respond_to? :each + } header.each { |key, value| ## The header keys must be Strings. assert("header key must be a string, was #{key.class}") { @@ -323,12 +332,13 @@ module Rack ## but only contain keys that consist of ## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter. assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ } - ## + ## ## The values of the header must respond to #each. - assert("header values must respond to #each") { value.respond_to? :each } + assert("header values must respond to #each, but the value of " + + "'#{key}' doesn't (is #{value.class})") { value.respond_to? :each } value.each { |item| ## The values passed on #each must be Strings - assert("header values must consist of Strings") { + assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") { item.instance_of?(String) } ## and not contain characters below 037. @@ -343,18 +353,69 @@ module Rack def check_content_type(status, headers) headers.each { |key, value| ## There must be a <tt>Content-Type</tt>, except when the - ## +Status+ is 204 or 304, in which case there must be none + ## +Status+ is 1xx, 204 or 304, in which case there must be none ## given. if key.downcase == "content-type" - assert("Content-Type header found in #{status} response, not allowed"){ - not [204, 304].include? status.to_i + assert("Content-Type header found in #{status} response, not allowed") { + not STATUS_WITH_NO_ENTITY_BODY.include? status.to_i } return end } assert("No Content-Type header found") { - [201, 204, 304].include? status.to_i + STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + end + + ## === The Content-Length + def check_content_length(status, headers) + chunked_response = false + headers.each { |key, value| + if key.downcase == 'transfer-encoding' + chunked_response = value.downcase != 'identity' + end + } + + headers.each { |key, value| + if key.downcase == 'content-length' + ## There must be a <tt>Content-Length</tt>, except when the + ## +Status+ is 1xx, 204 or 304, in which case there must be none + ## given. + assert("Content-Length header found in #{status} response, not allowed") { + not STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + + assert('Content-Length header should not be used if body is chunked') { + not chunked_response + } + + bytes = 0 + string_body = true + + @body.each { |part| + unless part.kind_of?(String) + string_body = false + break + end + + bytes += (part.respond_to?(:bytesize) ? part.bytesize : part.size) + } + + if string_body + assert("Content-Length header was #{value}, but should be #{bytes}") { + value == bytes.to_s + } + end + + return + end } + + if [ String, Array ].include?(@body.class) && !chunked_response + assert('No Content-Length header found') { + STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + end end ## === The Body @@ -368,11 +429,11 @@ module Rack } yield part } - ## + ## ## If the Body responds to #close, it will be called after iteration. # XXX howto: assert("Body has not been closed") { @closed } - ## + ## ## The Body commonly is an Array of Strings, the application ## instance itself, or a File-like object. end diff --git a/lib/rack/request.rb b/lib/rack/request.rb index 68c11198..2a9bcc15 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -193,5 +193,17 @@ module Rack path << "?" << query_string unless query_string.empty? path end + + def accept_encoding + @env["HTTP_ACCEPT_ENCODING"].to_s.split(/,\s*/).map do |part| + m = /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) # From WEBrick + + if m + [m[1], (m[2] || 1.0).to_f] + else + raise "Invalid value for Accept-Encoding: #{part.inspect}" + end + end + end end end diff --git a/lib/rack/showstatus.rb b/lib/rack/showstatus.rb index 8aa1d441..ca81f7d8 100644 --- a/lib/rack/showstatus.rb +++ b/lib/rack/showstatus.rb @@ -25,7 +25,9 @@ module Rack req = Rack::Request.new(env) message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s detail = env["rack.showstatus.detail"] || message - [status, headers.merge("Content-Type" => "text/html"), [@template.result(binding)]] + body = @template.result(binding) + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]] else [status, headers, body] end diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 4329b988..25254bbd 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -71,6 +71,36 @@ module Rack end module_function :escape_html + def select_best_encoding(available_encodings, accept_encoding) + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + + expanded_accept_encoding = + accept_encoding.map { |m, q| + if m == "*" + (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] } + else + [[m, q]] + end + }.inject([]) { |mem, list| + mem + list + } + + encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m } + + unless encoding_candidates.include?("identity") + encoding_candidates.push("identity") + end + + expanded_accept_encoding.find_all { |m, q| + q == 0.0 + }.each { |m, _| + encoding_candidates.delete(m) + } + + return (encoding_candidates & available_encodings)[0] + end + module_function :select_best_encoding + # The recommended manner in which to implement a contexting application # is to define a method #context in which a new Context is instantiated. # diff --git a/test/spec_rack_deflater.rb b/test/spec_rack_deflater.rb new file mode 100644 index 00000000..db75d392 --- /dev/null +++ b/test/spec_rack_deflater.rb @@ -0,0 +1,70 @@ +require 'test/spec' + +require 'rack/mock' +require 'rack/deflater' +require 'stringio' + +context "Rack::Deflater" do + def build_response(body, accept_encoding, headers = {}) + app = lambda { |env| [200, {}, body] } + request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding)) + response = Rack::Deflater.new(app).call(request) + + return response + end + + specify "should be able to deflate bodies that respond to each" do + body = Object.new + class << body; def each; yield("foo"); yield("bar"); end; end + + response = build_response(body, "deflate") + + response[0].should.equal(200) + response[1].should.equal({ "Content-Encoding" => "deflate" }) + response[2].to_s.should.equal("K\313\317OJ,\002\000") + end + + # TODO: This is really just a special case of the above... + specify "should be able to deflate String bodies" do + response = build_response("Hello world!", "deflate") + + response[0].should.equal(200) + response[1].should.equal({ "Content-Encoding" => "deflate" }) + response[2].to_s.should.equal("\363H\315\311\311W(\317/\312IQ\004\000") + end + + specify "should be able to gzip bodies that respond to each" do + body = Object.new + class << body; def each; yield("foo"); yield("bar"); end; end + + response = build_response(body, "gzip") + + response[0].should.equal(200) + response[1].should.equal({ "Content-Encoding" => "gzip" }) + + io = StringIO.new(response[2].to_s) + gz = Zlib::GzipReader.new(io) + gz.read.should.equal("foobar") + gz.close + end + + specify "should be able to fallback to no deflation" do + response = build_response("Hello world!", "superzip") + + response[0].should.equal(200) + response[1].should.equal({}) + response[2].should.equal("Hello world!") + end + + specify "should handle the lack of an acceptable encoding" do + response1 = build_response("Hello world!", "identity;q=0", "PATH_INFO" => "/") + response1[0].should.equal(406) + response1[1].should.equal({"Content-Type" => "text/plain"}) + response1[2].should.equal("An acceptable encoding for the requested resource / could not be found.") + + response2 = build_response("Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar") + response2[0].should.equal(406) + response2[1].should.equal({"Content-Type" => "text/plain"}) + response2[2].should.equal("An acceptable encoding for the requested resource /foo/bar could not be found.") + end +end diff --git a/test/spec_rack_directory.rb b/test/spec_rack_directory.rb index c791b197..bf0b8794 100644 --- a/test/spec_rack_directory.rb +++ b/test/spec_rack_directory.rb @@ -7,7 +7,7 @@ require 'rack/mock' context "Rack::Directory" do DOCROOT = File.expand_path(File.dirname(__FILE__)) - FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain'}, 'passed!'] } + FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, 'passed!'] } app = Rack::Directory.new DOCROOT, FILE_CATCH specify "serves directory indices" do diff --git a/test/spec_rack_handler.rb b/test/spec_rack_handler.rb index be188afb..e961d2b7 100644 --- a/test/spec_rack_handler.rb +++ b/test/spec_rack_handler.rb @@ -13,7 +13,7 @@ context "Rack::Handler" do Rack::Handler.get('webrick').should.equal Rack::Handler::WEBrick end - specify "should get unregisted hanlder by name" do + specify "should get unregistered handler by name" do Rack::Handler.get('lobster').should.equal Rack::Handler::Lobster end diff --git a/test/spec_rack_lint.rb b/test/spec_rack_lint.rb index e863559c..a8112b85 100644 --- a/test/spec_rack_lint.rb +++ b/test/spec_rack_lint.rb @@ -8,11 +8,11 @@ context "Rack::Lint" do def env(*args) Rack::MockRequest.env_for("/", *args) end - + specify "passes valid request" do lambda { Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "test/plain"}, "foo"] + [200, {"Content-type" => "test/plain", "Content-length" => "3"}, "foo"] }).call(env({})) }.should.not.raise end @@ -120,14 +120,14 @@ context "Rack::Lint" do ["cc", {}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/must be >100 seen as integer/) + message.should.match(/must be >=100 seen as integer/) lambda { Rack::Lint.new(lambda { |env| [42, {}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/must be >100 seen as integer/) + message.should.match(/must be >=100 seen as integer/) end specify "notices header errors" do @@ -136,14 +136,14 @@ context "Rack::Lint" do [200, Object.new, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/should respond to #each/) + message.should.equal("headers object should respond to #each, but doesn't (got Object as headers)") lambda { Rack::Lint.new(lambda { |env| [200, {true=>false}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/header key must be a string/) + message.should.equal("header key must be a string, was TrueClass") lambda { Rack::Lint.new(lambda { |env| @@ -171,21 +171,21 @@ context "Rack::Lint" do [200, {"..%%quark%%.." => "text/plain"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/invalid header/) + message.should.equal("invalid header name: ..%%quark%%..") lambda { Rack::Lint.new(lambda { |env| [200, {"Foo" => Object.new}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/must respond to #each/) + message.should.equal("header values must respond to #each, but the value of 'Foo' doesn't (is Object)") lambda { Rack::Lint.new(lambda { |env| [200, {"Foo" => [1,2,3]}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/must consist of Strings/) + message.should.equal("header values must consist of Strings, but 'Foo' also contains a Fixnum") lambda { @@ -199,30 +199,57 @@ context "Rack::Lint" do specify "notices content-type errors" do lambda { Rack::Lint.new(lambda { |env| - [200, {}, ""] + [200, {"Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/No Content-Type/) + [100, 101, 204, 304].each do |status| + lambda { + Rack::Lint.new(lambda { |env| + [status, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] + }).call(env({})) + }.should.raise(Rack::Lint::LintError). + message.should.match(/Content-Type header found/) + end + end + + specify "notices content-length errors" do + lambda { + Rack::Lint.new(lambda { |env| + [200, {"Content-type" => "text/plain"}, ""] + }).call(env({})) + }.should.raise(Rack::Lint::LintError). + message.should.match(/No Content-Length/) + + [100, 101, 204, 304].each do |status| + lambda { + Rack::Lint.new(lambda { |env| + [status, {"Content-length" => "0"}, ""] + }).call(env({})) + }.should.raise(Rack::Lint::LintError). + message.should.match(/Content-Length header found/) + end + lambda { Rack::Lint.new(lambda { |env| - [204, {"Content-Type" => "text/plain"}, ""] + [200, {"Content-type" => "text/plain", "Content-Length" => "0", "Transfer-Encoding" => "chunked"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/Content-Type header found/) + message.should.match(/Content-Length header should not be used/) lambda { Rack::Lint.new(lambda { |env| - [204, {"Content-type" => "text/plain"}, ""] + [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). - message.should.match(/Content-Type header found/) + message.should.match(/Content-Length header was 1, but should be 0/) end specify "notices body errors" do lambda { status, header, body = Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "text/plain"}, [1,2,3]] + [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]] }).call(env({})) body.each { |part| } }.should.raise(Rack::Lint::LintError). @@ -233,7 +260,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].gets("\r\n") - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/gets called with arguments/) @@ -241,7 +268,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read("foo") - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/read called with non-integer argument/) @@ -265,7 +292,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].gets - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env("rack.input" => weirdio)) }.should.raise(Rack::Lint::LintError). message.should.match(/gets didn't return a String/) @@ -273,7 +300,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].each { |x| } - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env("rack.input" => weirdio)) }.should.raise(Rack::Lint::LintError). message.should.match(/each didn't yield a String/) @@ -281,7 +308,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env("rack.input" => weirdio)) }.should.raise(Rack::Lint::LintError). message.should.match(/read didn't return a String/) @@ -290,7 +317,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].close - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/close must not be called/) @@ -300,7 +327,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.errors"].write(42) - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/write not called with a String/) @@ -308,7 +335,7 @@ context "Rack::Lint" do lambda { Rack::Lint.new(lambda { |env| env["rack.errors"].close - [201, {"Content-type" => "text/plain"}, ""] + [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""] }).call(env({})) }.should.raise(Rack::Lint::LintError). message.should.match(/close must not be called/) diff --git a/test/spec_rack_request.rb b/test/spec_rack_request.rb index 4321516e..0b2c9fc6 100644 --- a/test/spec_rack_request.rb +++ b/test/spec_rack_request.rb @@ -359,7 +359,8 @@ EOF specify "does conform to the Rack spec" do app = lambda { |env| content = Rack::Request.new(env).POST["file"].inspect - [200, {"Content-Type" => "text/html"}, content] + size = content.respond_to?(:bytesize) ? content.bytesize : content.size + [200, {"Content-Type" => "text/html", "Content-Length" => size.to_s}, content] } input = <<EOF @@ -381,4 +382,20 @@ EOF res.should.be.ok end + + specify "should parse Accept-Encoding correctly" do + parser = lambda do |x| + Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => x)).accept_encoding + end + + parser.call(nil).should.equal([]) + + parser.call("compress, gzip").should.equal([["compress", 1.0], ["gzip", 1.0]]) + parser.call("").should.equal([]) + parser.call("*").should.equal([["*", 1.0]]) + parser.call("compress;q=0.5, gzip;q=1.0").should.equal([["compress", 0.5], ["gzip", 1.0]]) + parser.call("gzip;q=1.0, identity; q=0.5, *;q=0").should.equal([["gzip", 1.0], ["identity", 0.5], ["*", 0] ]) + + lambda { parser.call("gzip ; q=1.0") }.should.raise(RuntimeError) + end end diff --git a/test/spec_rack_showstatus.rb b/test/spec_rack_showstatus.rb index 03e57032..78700134 100644 --- a/test/spec_rack_showstatus.rb +++ b/test/spec_rack_showstatus.rb @@ -6,7 +6,7 @@ require 'rack/mock' context "Rack::ShowStatus" do specify "should provide a default status message" do req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env| - [404, {"Content-Type" => "text/plain"}, []] + [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] })) res = req.get("/", :lint => true) @@ -21,7 +21,7 @@ context "Rack::ShowStatus" do specify "should let the app provide additional information" do req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env| env["rack.showstatus.detail"] = "gone too meta." - [404, {"Content-Type" => "text/plain"}, []] + [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] })) res = req.get("/", :lint => true) @@ -36,7 +36,7 @@ context "Rack::ShowStatus" do specify "should not replace existing messages" do req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env| - [404, {"Content-Type" => "text/plain"}, ["foo!"]] + [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]] })) res = req.get("/", :lint => true) res.should.be.not_found @@ -56,7 +56,7 @@ context "Rack::ShowStatus" do specify "should replace existing messages if there is detail" do req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env| env["rack.showstatus.detail"] = "gone too meta." - [404, {"Content-Type" => "text/plain"}, ["foo!"]] + [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]] })) res = req.get("/", :lint => true) @@ -64,6 +64,7 @@ context "Rack::ShowStatus" do res.should.be.not.empty res["Content-Type"].should.equal("text/html") + res["Content-Length"].should.not.equal("4") res.should =~ /404/ res.should =~ /too meta/ res.body.should.not =~ /foo/ diff --git a/test/spec_rack_utils.rb b/test/spec_rack_utils.rb index 8256e12f..0ec0a39f 100644 --- a/test/spec_rack_utils.rb +++ b/test/spec_rack_utils.rb @@ -20,7 +20,7 @@ context "Rack::Utils" do should.equal "q1!2\"'w$5&7/z8)?\\" end - specify "should parse queries correctly" do + specify "should parse query strings correctly" do Rack::Utils.parse_query("foo=bar").should.equal "foo" => "bar" Rack::Utils.parse_query("foo=bar&foo=quux"). should.equal "foo" => ["bar", "quux"] @@ -30,7 +30,7 @@ context "Rack::Utils" do should.equal "my weird field" => "q1!2\"'w$5&7/z8)?" end - specify "should create queries correctly" do + specify "should build query strings correctly" do Rack::Utils.build_query("foo" => "bar").should.equal "foo=bar" Rack::Utils.build_query("foo" => ["bar", "quux"]). should.equal "foo=bar&foo=quux" @@ -39,6 +39,29 @@ context "Rack::Utils" do Rack::Utils.build_query("my weird field" => "q1!2\"'w$5&7/z8)?"). should.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F" end + + specify "should figure out which encodings are acceptable" do + helper = lambda do |a, b| + request = Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => a)) + Rack::Utils.select_best_encoding(a, b) + end + + helper.call(%w(), [["x", 1]]).should.equal(nil) + helper.call(%w(identity), [["identity", 0.0]]).should.equal(nil) + helper.call(%w(identity), [["*", 0.0]]).should.equal(nil) + + helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("identity") + + helper.call(%w(compress gzip identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("compress") + helper.call(%w(compress gzip identity), [["compress", 0.5], ["gzip", 1.0]]).should.equal("gzip") + + helper.call(%w(foo bar identity), []).should.equal("identity") + helper.call(%w(foo bar identity), [["*", 1.0]]).should.equal("foo") + helper.call(%w(foo bar identity), [["*", 1.0], ["foo", 0.9]]).should.equal("bar") + + helper.call(%w(foo bar identity), [["foo", 0], ["bar", 0]]).should.equal("identity") + helper.call(%w(foo bar baz identity), [["*", 0], ["identity", 0.1]]).should.equal("identity") + end end context "Rack::Utils::HeaderHash" do @@ -85,7 +108,7 @@ context "Rack::Utils::Context" do test_target1 = proc{|e| e.to_s+' world' } test_target2 = proc{|e| e.to_i+2 } test_target3 = proc{|e| nil } - test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain'},['']] } + test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain', 'Content-Length'=>'0'},['']] } test_target5 = Object.new specify "should perform checks on both arguments" do diff --git a/test/testrequest.rb b/test/testrequest.rb index 1b045ea7..348cd495 100644 --- a/test/testrequest.rb +++ b/test/testrequest.rb @@ -5,7 +5,9 @@ class TestRequest def call(env) status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200 env["test.postdata"] = env["rack.input"].read - [status, {"Content-Type" => "text/yaml"}, [env.to_yaml]] + body = env.to_yaml + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + [status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]] end module Helpers |