diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb index d51b9b63ff58c1d28b3b8c920be659eec1d5617e..8bba6dfd592cc0c53419438990e8c8904ba4997d 100644 --- a/actionpack/lib/action_controller/testing/integration.rb +++ b/actionpack/lib/action_controller/testing/integration.rb @@ -17,9 +17,6 @@ class Session include ActionController::TestCase::Assertions include ActionController::TestProcess - # Rack application to use - attr_accessor :application - # The integer HTTP status code of the last request. attr_reader :status @@ -60,12 +57,9 @@ class Session # A running counter of the number of requests processed. attr_accessor :request_count - class MultiPartNeededException < Exception - end - # Create and initialize a new Session instance. def initialize(app = nil) - @application = app || ActionController::Dispatcher.new + @app = app || ActionController::Dispatcher.new reset! end @@ -255,103 +249,65 @@ def interpret_uri(path) # Performs the actual request. def process(method, path, parameters = nil, headers = nil) - data = requestify(parameters) path = interpret_uri(path) if path =~ %r{://} - path = "/#{path}" unless path[0] == ?/ @path = path - env = {} - if method == :get - env["QUERY_STRING"] = data - data = nil + [ControllerCapture, ActionController::ProcessWithTest].each do |mod| + unless ActionController::Base < mod + ActionController::Base.class_eval { include mod } + end end - env["QUERY_STRING"] ||= "" + ActionController::Base.clear_last_instantiation! - data = data.is_a?(IO) ? data : StringIO.new(data || '') + opts = { + :method => method.to_s.upcase, + :params => parameters, + :headers => headers, - env.update( - "REQUEST_METHOD" => method.to_s.upcase, "SERVER_NAME" => host, "SERVER_PORT" => (https? ? "443" : "80"), "HTTPS" => https? ? "on" : "off", "rack.url_scheme" => https? ? "https" : "http", - "SCRIPT_NAME" => "", "REQUEST_URI" => path, "PATH_INFO" => path, "HTTP_HOST" => host, "REMOTE_ADDR" => remote_addr, "CONTENT_TYPE" => "application/x-www-form-urlencoded", - "CONTENT_LENGTH" => data ? data.length.to_s : nil, - "HTTP_COOKIE" => encode_cookies, "HTTP_ACCEPT" => accept, + "HTTP_COOKIE" => cookies.inject("") { |string, (name, value)| + string << "#{name}=#{value}; " + }, - "rack.version" => [0,1], - "rack.input" => data, - "rack.errors" => StringIO.new, - "rack.multithread" => true, - "rack.multiprocess" => true, - "rack.run_once" => false, - - "rack.test" => true - ) - - (headers || {}).each do |key, value| - key = key.to_s.upcase.gsub(/-/, "_") - key = "HTTP_#{key}" unless env.has_key?(key) || key =~ /^HTTP_/ - env[key] = value - end - - [ControllerCapture, ActionController::ProcessWithTest].each do |mod| - unless ActionController::Base < mod - ActionController::Base.class_eval { include mod } - end - end - - ActionController::Base.clear_last_instantiation! - - app = @application - # Rack::Lint doesn't accept String headers or bodies in Ruby 1.9 - unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0' - app = Rack::Lint.new(app) - end + "rack.test" => true + } + env = ActionDispatch::Test::MockRequest.env_for(@path, opts) + app = Rack::Lint.new(@app) status, headers, body = app.call(env) + response = ::Rack::MockResponse.new(status, headers, body) @request_count += 1 @html_document = nil - @status = status.to_i + @status = response.status @status_message = ActionDispatch::StatusCodes::STATUS_CODES[@status] - - @headers = Rack::Utils::HeaderHash.new(headers) + @headers = response.headers + @body = response.body (@headers['Set-Cookie'] || "").split("\n").each do |cookie| name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2] @cookies[name] = value end - if body.is_a?(String) - @body_parts = [body] - @body = body - else - @body_parts = [] - body.each { |part| @body_parts << part.to_s } - @body = @body_parts.join - end - if @controller = ActionController::Base.last_instantiation @request = @controller.request @response = @controller.response @controller.send(:set_test_assigns) else - # Decorate responses from Rack Middleware and Rails Metal - # as an Response for the purposes of integration testing - @response = ActionDispatch::Response.new - @response.status = status.to_s - @response.headers.replace(@headers) - @response.body = @body_parts + @request = ::Rack::Request.new(env) + @response = response end # Decorate the response with the standard behavior of the @@ -360,21 +316,6 @@ def process(method, path, parameters = nil, headers = nil) @response.extend(TestResponseBehavior) return @status - rescue MultiPartNeededException - boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1" - status = process(method, path, - multipart_body(parameters, boundary), - (headers || {}).merge( - {"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"})) - return status - end - - # Encode the cookies hash in a format suitable for passing to a - # request. - def encode_cookies - cookies.inject("") do |string, (name, value)| - string << "#{name}=#{value}; " - end end # Get a temporary URL writer object @@ -389,72 +330,6 @@ def generic_url_rewriter } UrlRewriter.new(ActionDispatch::Request.new(env), {}) end - - def name_with_prefix(prefix, name) - prefix ? "#{prefix}[#{name}]" : name.to_s - end - - # Convert the given parameters to a request string. The parameters may - # be a string, +nil+, or a Hash. - def requestify(parameters, prefix=nil) - if TestUploadedFile === parameters - raise MultiPartNeededException - elsif Hash === parameters - return nil if parameters.empty? - parameters.map { |k,v| - requestify(v, name_with_prefix(prefix, k)) - }.join("&") - elsif Array === parameters - parameters.map { |v| - requestify(v, name_with_prefix(prefix, "")) - }.join("&") - elsif prefix.nil? - parameters - else - "#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}" - end - end - - def multipart_requestify(params, first=true) - returning Hash.new do |p| - params.each do |key, value| - k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]" - if Hash === value - multipart_requestify(value, false).each do |subkey, subvalue| - p[k + subkey] = subvalue - end - else - p[k] = value - end - end - end - end - - def multipart_body(params, boundary) - 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 ---#{boundary}\r -Content-Disposition: form-data; name="#{key}"; filename="#{CGI.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 ---#{boundary}\r -Content-Disposition: form-data; name="#{key}"\r -\r -#{value}\r -EOF - end - end.join("")+"--#{boundary}--\r" - end end # A module used to extend ActionController::Base, so that integration tests @@ -513,8 +388,8 @@ def reset! # By default, a single session is automatically created for you, but you # can use this method to open multiple sessions that ought to be tested # simultaneously. - def open_session(application = nil) - session = Integration::Session.new(application) + def open_session(app = nil) + session = Integration::Session.new(app) # delegate the fixture accessors back to the test instance extras = Module.new { attr_accessor :delegate, :test_result } diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb index 7e2857614ccf90563b9f3736f79548600629f723..f5742af4727d00f6c35e9a8a487e48a05fd6d31c 100644 --- a/actionpack/lib/action_controller/testing/process.rb +++ b/actionpack/lib/action_controller/testing/process.rb @@ -363,34 +363,7 @@ def replace_attributes(attributes = nil) # # 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) - require 'tempfile' - class TestUploadedFile - # 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 = Mime::TEXT, binary = false) - raise "#{path} file does not exist" unless File.exist?(path) - @content_type = content_type - @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 } - @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 #:nodoc: - @tempfile.path - end - - alias local_path path - - def method_missing(method_name, *args, &block) #:nodoc: - @tempfile.__send__(method_name, *args, &block) - end - end + TestUploadedFile = ActionDispatch::Test::UploadedFile module TestProcess def self.included(base) diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 5feb8a486394f42a67747385e46596ebbbf985b3..13ce38e928cb716788b901098c1fa097bc5f2bb5 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -60,6 +60,11 @@ module Session 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/test/mock.rb b/actionpack/lib/action_dispatch/test/mock.rb new file mode 100644 index 0000000000000000000000000000000000000000..86269fad0164df0748f3eff9da45c4f9943fb7ac --- /dev/null +++ b/actionpack/lib/action_dispatch/test/mock.rb @@ -0,0 +1,127 @@ +module ActionDispatch + module Test + class MockRequest < Rack::MockRequest + MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1" + + class << self + def env_for(path, opts) + headers = opts.delete(:headers) + + 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] || {}).any? do |k, v| + UploadedFile === v + end + + 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.update(::Rack::Utils.parse_query(uri.query)) + uri.query = requestify(params) + end + + env = ::Rack::MockRequest.env_for(uri.to_s, opts) + + (headers || {}).each do |key, value| + key = key.to_s.upcase.gsub(/-/, "_") + key = "HTTP_#{key}" unless env.has_key?(key) || key =~ /^HTTP_/ + env[key] = value + end + + env + 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 new file mode 100644 index 0000000000000000000000000000000000000000..0ac7db4863b9bef181856354bc4fe607755cac03 --- /dev/null +++ b/actionpack/lib/action_dispatch/test/uploaded_file.rb @@ -0,0 +1,33 @@ +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 diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index e39a934c24ee979ee8bcc3e0ed0dfb9d5720f697..9eeaa7b4e18a7272c09ca4a930840ccfdc9ba743 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -337,7 +337,7 @@ def test_get_with_query_string get '/get_with_params?foo=bar' assert_equal '/get_with_params?foo=bar', request.env["REQUEST_URI"] assert_equal '/get_with_params?foo=bar', request.request_uri - assert_equal "", request.env["QUERY_STRING"] + assert_equal "foo=bar", request.env["QUERY_STRING"] assert_equal 'foo=bar', request.query_string assert_equal 'bar', request.parameters['foo']