From 64e66cf161ee8db0bdbccb1be18fb760f5a9d24e Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Thu, 30 Apr 2009 14:40:46 -0500 Subject: Vendor new Rack::Mock changes --- .../lib/action_controller/testing/integration.rb | 2 +- .../lib/action_controller/testing/process.rb | 2 +- actionpack/lib/action_dispatch.rb | 6 +- actionpack/lib/action_dispatch/extensions/rack.rb | 5 + .../lib/action_dispatch/extensions/rack/mock.rb | 65 +++++ .../lib/action_dispatch/extensions/rack/utils.rb | 262 +++++++++++++++++++++ actionpack/lib/action_dispatch/test/mock.rb | 115 --------- .../lib/action_dispatch/test/uploaded_file.rb | 33 --- 8 files changed, 335 insertions(+), 155 deletions(-) create mode 100644 actionpack/lib/action_dispatch/extensions/rack.rb create mode 100644 actionpack/lib/action_dispatch/extensions/rack/mock.rb create mode 100644 actionpack/lib/action_dispatch/extensions/rack/utils.rb delete mode 100644 actionpack/lib/action_dispatch/test/mock.rb delete mode 100644 actionpack/lib/action_dispatch/test/uploaded_file.rb diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb index f088e91de4..ce161ef846 100644 --- a/actionpack/lib/action_controller/testing/integration.rb +++ b/actionpack/lib/action_controller/testing/integration.rb @@ -263,7 +263,7 @@ module ActionController string << "#{name}=#{value}; " } } - env = ActionDispatch::Test::MockRequest.env_for(path, opts) + env = Rack::MockRequest.env_for(path, opts) (headers || {}).each do |key, value| key = key.to_s.upcase.gsub(/-/, "_") diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb index a2bb9afaa5..3a7445f890 100644 --- a/actionpack/lib/action_controller/testing/process.rb +++ b/actionpack/lib/action_controller/testing/process.rb @@ -250,7 +250,7 @@ module ActionController #:nodoc: # # Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows): # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary) - TestUploadedFile = ActionDispatch::Test::UploadedFile + TestUploadedFile = Rack::Utils::Multipart::UploadedFile module TestProcess def self.included(base) diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 8b8a1774e4..078e631be4 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -34,6 +34,7 @@ require 'active_support/core/all' gem 'rack', '~> 1.0.0' require 'rack' +require 'action_dispatch/extensions/rack' module ActionDispatch autoload :Request, 'action_dispatch/http/request' @@ -56,11 +57,6 @@ module ActionDispatch autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' end - - module Test - autoload :UploadedFile, 'action_dispatch/test/uploaded_file' - autoload :MockRequest, 'action_dispatch/test/mock' - end end autoload :Mime, 'action_dispatch/http/mime_type' diff --git a/actionpack/lib/action_dispatch/extensions/rack.rb b/actionpack/lib/action_dispatch/extensions/rack.rb new file mode 100644 index 0000000000..8a543e2110 --- /dev/null +++ b/actionpack/lib/action_dispatch/extensions/rack.rb @@ -0,0 +1,5 @@ +# Monkey patch in new test helper methods +unless Rack::Utils.respond_to?(:build_nested_query) + require 'action_dispatch/extensions/rack/mock' + require 'action_dispatch/extensions/rack/utils' +end diff --git a/actionpack/lib/action_dispatch/extensions/rack/mock.rb b/actionpack/lib/action_dispatch/extensions/rack/mock.rb new file mode 100644 index 0000000000..d773a74244 --- /dev/null +++ b/actionpack/lib/action_dispatch/extensions/rack/mock.rb @@ -0,0 +1,65 @@ +require 'rack/mock' + +module Rack + class MockRequest + # Return the Rack environment used for a request to +uri+. + def self.env_for(uri="", opts={}) + uri = URI(uri) + uri.path = "/#{uri.path}" unless uri.path[0] == ?/ + + env = DEFAULT_ENV.dup + + env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET" + env["SERVER_NAME"] = uri.host || "example.org" + env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80" + env["QUERY_STRING"] = uri.query.to_s + env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path + env["rack.url_scheme"] = uri.scheme || "http" + env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off" + + env["SCRIPT_NAME"] = opts[:script_name] || "" + + if opts[:fatal] + env["rack.errors"] = FatalWarner.new + else + env["rack.errors"] = StringIO.new + end + + if params = opts[:params] + if env["REQUEST_METHOD"] == "GET" + params = Utils.parse_nested_query(params) if params.is_a?(String) + params.update(Utils.parse_nested_query(env["QUERY_STRING"])) + env["QUERY_STRING"] = Utils.build_nested_query(params) + elsif !opts.has_key?(:input) + opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + if params.is_a?(Hash) + if data = Utils::Multipart.build_multipart(params) + opts[:input] = data + opts["CONTENT_LENGTH"] ||= data.length.to_s + opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}" + else + opts[:input] = Utils.build_nested_query(params) + end + else + opts[:input] = params + end + end + end + + opts[:input] ||= "" + if String === opts[:input] + env["rack.input"] = StringIO.new(opts[:input]) + else + env["rack.input"] = opts[:input] + end + + env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s + + opts.each { |field, value| + env[field] = value if String === field + } + + env + end + end +end diff --git a/actionpack/lib/action_dispatch/extensions/rack/utils.rb b/actionpack/lib/action_dispatch/extensions/rack/utils.rb new file mode 100644 index 0000000000..1a46f8a4db --- /dev/null +++ b/actionpack/lib/action_dispatch/extensions/rack/utils.rb @@ -0,0 +1,262 @@ +# -*- encoding: binary -*- + +require 'rack/utils' + +module Rack + module Utils + def normalize_params(params, name, v = nil) + name =~ %r(\A[\[\]]*([^\[\]]+)\]*) + k = $1 || '' + after = $' || '' + + return if k.empty? + + if after == "" + params[k] = v + elsif after == "[]" + params[k] ||= [] + raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) + params[k] << v + elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) + child_key = $1 + params[k] ||= [] + raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) + if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key) + normalize_params(params[k].last, child_key, v) + else + params[k] << normalize_params({}, child_key, v) + end + else + params[k] ||= {} + raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash) + params[k] = normalize_params(params[k], after, v) + end + + return params + end + module_function :normalize_params + + def build_nested_query(value, prefix = nil) + case value + when Array + value.map { |v| + build_nested_query(v, "#{prefix}[]") + }.join("&") + when Hash + value.map { |k, v| + build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) + }.join("&") + when String + raise ArgumentError, "value must be a Hash" if prefix.nil? + "#{prefix}=#{escape(value)}" + else + prefix + end + end + module_function :build_nested_query + + module Multipart + class UploadedFile + # The filename, *not* including the path, of the "uploaded" file + attr_reader :original_filename + + # The content type of the "uploaded" file + attr_accessor :content_type + + def initialize(path, content_type = "text/plain", binary = false) + raise "#{path} file does not exist" unless ::File.exist?(path) + @content_type = content_type + @original_filename = ::File.basename(path) + @tempfile = Tempfile.new(@original_filename) + @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) + @tempfile.binmode if binary + FileUtils.copy_file(path, @tempfile.path) + end + + def path + @tempfile.path + end + alias_method :local_path, :path + + def method_missing(method_name, *args, &block) #:nodoc: + @tempfile.__send__(method_name, *args, &block) + end + end + + EOL = "\r\n" + MULTIPART_BOUNDARY = "AaB03x" + + def self.parse_multipart(env) + unless env['CONTENT_TYPE'] =~ + %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n + nil + else + boundary = "--#{$1}" + + params = {} + buf = "" + content_length = env['CONTENT_LENGTH'].to_i + input = env['rack.input'] + input.rewind + + boundary_size = Utils.bytesize(boundary) + EOL.size + bufsize = 16384 + + content_length -= boundary_size + + read_buffer = '' + + status = input.read(boundary_size, read_buffer) + raise EOFError, "bad content body" unless status == boundary + EOL + + rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n + + loop { + head = nil + body = '' + filename = content_type = name = nil + + until head && buf =~ rx + if !head && i = buf.index(EOL+EOL) + head = buf.slice!(0, i+2) # First \r\n + buf.slice!(0, 2) # Second \r\n + + filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1] + content_type = head[/Content-Type: (.*)#{EOL}/ni, 1] + name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1] + + if content_type || filename + body = Tempfile.new("RackMultipart") + body.binmode if body.respond_to?(:binmode) + end + + next + end + + # Save the read body part. + if head && (boundary_size+4 < buf.size) + body << buf.slice!(0, buf.size - (boundary_size+4)) + end + + c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer) + raise EOFError, "bad content body" if c.nil? || c.empty? + buf << c + content_length -= c.size + end + + # Save the rest. + if i = buf.index(rx) + body << buf.slice!(0, i) + buf.slice!(0, boundary_size+2) + + content_length = -1 if $1 == "--" + end + + if filename == "" + # filename is blank which means no file has been selected + data = nil + elsif filename + body.rewind + + # Take the basename of the upload's original filename. + # This handles the full Windows paths given by Internet Explorer + # (and perhaps other broken user agents) without affecting + # those which give the lone filename. + filename =~ /^(?:.*[:\\\/])?(.*)/m + filename = $1 + + data = {:filename => filename, :type => content_type, + :name => name, :tempfile => body, :head => head} + elsif !filename && content_type + body.rewind + + # Generic multipart cases, not coming from a form + data = {:type => content_type, + :name => name, :tempfile => body, :head => head} + else + data = body + end + + Utils.normalize_params(params, name, data) unless data.nil? + + break if buf.empty? || content_length == -1 + } + + input.rewind + + params + end + end + + def self.build_multipart(params, first = true) + if first + unless params.is_a?(Hash) + raise ArgumentError, "value must be a Hash" + end + + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when UploadedFile + multipart = true + end + } + params.values.each(&query) + return nil unless multipart + end + + flattened_params = Hash.new + + params.each do |key, value| + k = first ? key.to_s : "[#{key}]" + + case value + when Array + value.map { |v| + build_multipart(v, false).each { |subkey, subvalue| + flattened_params["#{k}[]#{subkey}"] = subvalue + } + } + when Hash + build_multipart(value, false).each { |subkey, subvalue| + flattened_params[k + subkey] = subvalue + } + else + flattened_params[k] = value + end + end + + if first + flattened_params.map { |name, file| + if file.respond_to?(:original_filename) + ::File.open(file.path, "rb") do |f| + f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) +<<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r +Content-Type: #{file.content_type}\r +Content-Length: #{::File.stat(file.path).size}\r +\r +#{f.read}\r +EOF + end + else +<<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{name}"\r +\r +#{file}\r +EOF + end + }.join + "--#{MULTIPART_BOUNDARY}--\r" + else + flattened_params + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/test/mock.rb b/actionpack/lib/action_dispatch/test/mock.rb deleted file mode 100644 index 68e5b108b4..0000000000 --- a/actionpack/lib/action_dispatch/test/mock.rb +++ /dev/null @@ -1,115 +0,0 @@ -module ActionDispatch - module Test - class MockRequest < Rack::MockRequest - MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1" - - class << self - def env_for(path, opts) - method = (opts[:method] || opts["REQUEST_METHOD"]).to_s.upcase - opts[:method] = opts["REQUEST_METHOD"] = method - - path = "/#{path}" unless path[0] == ?/ - uri = URI.parse(path) - uri.host ||= "example.org" - - if URI::HTTPS === uri - opts.update("SERVER_PORT" => "443", "HTTPS" => "on") - end - - if method == "POST" && !opts.has_key?(:input) - opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded" - - multipart = opts[:params].respond_to?(:any?) && opts[:params].any? { |k, v| UploadedFile === v } - if multipart - opts[:input] = multipart_body(opts.delete(:params)) - opts["CONTENT_LENGTH"] ||= opts[:input].length.to_s - opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}" - else - params = opts.delete(:params) - opts[:input] = case params - when Hash then requestify(params) - when nil then "" - else params - end - end - end - - params = opts[:params] || {} - if params.is_a?(String) - if method == "GET" - uri.query = params - else - opts[:input] = params - end - else - params.stringify_keys! - params.update(::Rack::Utils.parse_query(uri.query)) - uri.query = requestify(params) - end - - ::Rack::MockRequest.env_for(uri.to_s, opts) - end - - private - def requestify(value, prefix = nil) - case value - when Array - value.map do |v| - requestify(v, "#{prefix}[]") - end.join("&") - when Hash - value.map do |k, v| - requestify(v, prefix ? "#{prefix}[#{::Rack::Utils.escape(k)}]" : ::Rack::Utils.escape(k)) - end.join("&") - else - "#{prefix}=#{::Rack::Utils.escape(value)}" - end - end - - def multipart_requestify(params, first=true) - p = Hash.new - - params.each do |key, value| - k = first ? key.to_s : "[#{key}]" - - if Hash === value - multipart_requestify(value, false).each do |subkey, subvalue| - p[k + subkey] = subvalue - end - else - p[k] = value - end - end - - return p - end - - def multipart_body(params) - multipart_requestify(params).map do |key, value| - if value.respond_to?(:original_filename) - ::File.open(value.path, "rb") do |f| - f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) - - <<-EOF ---#{MULTIPART_BOUNDARY}\r -Content-Disposition: form-data; name="#{key}"; filename="#{::Rack::Utils.escape(value.original_filename)}"\r -Content-Type: #{value.content_type}\r -Content-Length: #{::File.stat(value.path).size}\r -\r -#{f.read}\r -EOF - end - else -<<-EOF ---#{MULTIPART_BOUNDARY}\r -Content-Disposition: form-data; name="#{key}"\r -\r -#{value}\r -EOF - end - end.join("")+"--#{MULTIPART_BOUNDARY}--\r" - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/test/uploaded_file.rb b/actionpack/lib/action_dispatch/test/uploaded_file.rb deleted file mode 100644 index 0ac7db4863..0000000000 --- a/actionpack/lib/action_dispatch/test/uploaded_file.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "tempfile" - -module ActionDispatch - module Test - class UploadedFile - # The filename, *not* including the path, of the "uploaded" file - attr_reader :original_filename - - # The content type of the "uploaded" file - attr_accessor :content_type - - def initialize(path, content_type = "text/plain", binary = false) - raise "#{path} file does not exist" unless ::File.exist?(path) - @content_type = content_type - @original_filename = ::File.basename(path) - @tempfile = Tempfile.new(@original_filename) - @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) - @tempfile.binmode if binary - FileUtils.copy_file(path, @tempfile.path) - end - - def path - @tempfile.path - end - - alias_method :local_path, :path - - def method_missing(method_name, *args, &block) #:nodoc: - @tempfile.__send__(method_name, *args, &block) - end - end - end -end -- cgit v1.2.3