diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-09-30 22:27:02 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-09-30 22:27:02 +0100 |
commit | dd2779e1b83b4d867d47dd286ec0c919f5df12a9 (patch) | |
tree | 6e52ea0a329c24429f4d1d41b065e082f0ed6baa /actionpack/lib/action_dispatch | |
parent | 329b14aa8fdd291a00d17ba12c2e0ab4c3a157cc (diff) | |
parent | 420004e030e96f2ace6e27fd622c90ee9e986677 (diff) | |
download | rails-dd2779e1b83b4d867d47dd286ec0c919f5df12a9.tar.gz rails-dd2779e1b83b4d867d47dd286ec0c919f5df12a9.tar.bz2 rails-dd2779e1b83b4d867d47dd286ec0c919f5df12a9.zip |
Merge commit 'mainstream/master'
Diffstat (limited to 'actionpack/lib/action_dispatch')
8 files changed, 592 insertions, 15 deletions
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index e457450059..3e3b473178 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/module/delegation' module ActionDispatch # :nodoc: # Represents an HTTP response generated by a controller action. One can use - # an ActionController::Response object to retrieve the current state + # an ActionDispatch::Response object to retrieve the current state # of the response, or customize the response. An Response object can # either represent a "real" HTTP response (i.e. one that is meant to be sent # back to the web browser) or a test response (i.e. one that is generated @@ -18,14 +18,14 @@ module ActionDispatch # :nodoc: # Nevertheless, integration tests may want to inspect controller responses in # more detail, and that's when Response can be useful for application # developers. Integration test methods such as - # ActionController::Integration::Session#get and - # ActionController::Integration::Session#post return objects of type + # ActionDispatch::Integration::Session#get and + # ActionDispatch::Integration::Session#post return objects of type # TestResponse (which are of course also of type Response). # # For example, the following demo integration "test" prints the body of the # controller response to the console: # - # class DemoControllerTest < ActionController::IntegrationTest + # class DemoControllerTest < ActionDispatch::IntegrationTest # def test_print_root_path_to_console # get('/') # puts @response.body diff --git a/actionpack/lib/action_dispatch/http/utils.rb b/actionpack/lib/action_dispatch/http/utils.rb new file mode 100644 index 0000000000..e04a39935e --- /dev/null +++ b/actionpack/lib/action_dispatch/http/utils.rb @@ -0,0 +1,20 @@ +module ActionDispatch + module Utils + # TODO: Pull this into rack core + # http://github.com/halorgium/rack/commit/feaf071c1de743fbd10bc316830180a9af607278 + def parse_config(config) + if config =~ /\.ru$/ + cfgfile = ::File.read(config) + if cfgfile[/^#\\(.*)/] + opts.parse! $1.split(/\s+/) + end + inner_app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app", + nil, config + else + require config + inner_app = Object.const_get(::File.basename(config, '.rb').capitalize) + end + end + module_function :parse_config + end +end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 2f86a382c2..56d6da1706 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -2,7 +2,7 @@ module ActionDispatch class Callbacks include ActiveSupport::NewCallbacks - define_callbacks :call, :terminator => "result == false", :scope => :kind + define_callbacks :call, :terminator => "result == false", :rescuable => true define_callbacks :prepare, :scope => :name # Add a preparation callback. Preparation callbacks are run before every @@ -25,10 +25,6 @@ module ActionDispatch set_callback(:call, :before, *args, &block) end - def self.around(*args, &block) - set_callback(:call, :around, *args, &block) - end - def self.after(*args, &block) set_callback(:call, :after, *args, &block) end @@ -36,7 +32,6 @@ module ActionDispatch class << self # DEPRECATED alias_method :before_dispatch, :before - alias_method :around_dispatch, :around alias_method :after_dispatch, :after end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index a8768633cc..c5c06f74a2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -2,6 +2,9 @@ require 'rack/utils' module ActionDispatch module Session + class SessionRestoreError < StandardError #:nodoc: + end + class AbstractStore ENV_SESSION_KEY = 'rack.session'.freeze ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze @@ -19,7 +22,7 @@ module ActionDispatch def session_id ActiveSupport::Deprecation.warn( - "ActionController::Session::AbstractStore::SessionHash#session_id " + + "ActionDispatch::Session::AbstractStore::SessionHash#session_id " + "has been deprecated. Please use request.session_options[:id] instead.", caller) @env[ENV_SESSION_OPTIONS_KEY][:id] end @@ -62,7 +65,7 @@ module ActionDispatch def data ActiveSupport::Deprecation.warn( - "ActionController::Session::AbstractStore::SessionHash#data " + + "ActionDispatch::Session::AbstractStore::SessionHash#data " + "has been deprecated. Please use #to_hash instead.", caller) to_hash end @@ -98,7 +101,7 @@ module ActionDispatch # Note that the regexp does not allow $1 to end with a ':' $1.constantize rescue LoadError, NameError => const_error - raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" + raise ActionDispatch::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" end retry diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb new file mode 100644 index 0000000000..d7e88a54e4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -0,0 +1,44 @@ +require 'rack/utils' + +module ActionDispatch + class Static + FILE_METHODS = %w(GET HEAD).freeze + + def initialize(app, root) + @app = app + @file_server = ::Rack::File.new(root) + end + + def call(env) + path = env['PATH_INFO'].chomp('/') + method = env['REQUEST_METHOD'] + + if FILE_METHODS.include?(method) + if file_exist?(path) + return @file_server.call(env) + else + cached_path = directory_exist?(path) ? "#{path}/index" : path + cached_path += ::ActionController::Base.page_cache_extension + + if file_exist?(cached_path) + env['PATH_INFO'] = cached_path + return @file_server.call(env) + end + end + end + + @app.call(env) + end + + private + def file_exist?(path) + full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) + File.file?(full_path) && File.readable?(full_path) + end + + def directory_exist?(path) + full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) + File.directory?(full_path) && File.readable?(full_path) + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb new file mode 100644 index 0000000000..2c4a3a356d --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -0,0 +1,500 @@ +require 'stringio' +require 'uri' +require 'active_support/test_case' +require 'active_support/core_ext/object/metaclass' + +# TODO: Remove circular dependency on ActionController +require 'action_controller/testing/process' + +module ActionDispatch + module Integration #:nodoc: + module RequestHelpers + # Performs a GET request with the given parameters. + # + # - +path+: The URI (as a String) on which you want to perform a GET + # request. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, + # a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or + # <tt>multipart/form-data</tt>). + # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will + # automatically be upcased, with the prefix 'HTTP_' added if needed. + # + # This method returns an Response object, which one can use to + # inspect the details of the response. Furthermore, if this method was + # called from an ActionDispatch::IntegrationTest object, then that + # object's <tt>@response</tt> instance variable will point to the same + # response object. + # + # You can also perform POST, PUT, DELETE, and HEAD requests with +post+, + # +put+, +delete+, and +head+. + def get(path, parameters = nil, headers = nil) + process :get, path, parameters, headers + end + + # Performs a POST request with the given parameters. See get() for more + # details. + def post(path, parameters = nil, headers = nil) + process :post, path, parameters, headers + end + + # Performs a PUT request with the given parameters. See get() for more + # details. + def put(path, parameters = nil, headers = nil) + process :put, path, parameters, headers + end + + # Performs a DELETE request with the given parameters. See get() for + # more details. + def delete(path, parameters = nil, headers = nil) + process :delete, path, parameters, headers + end + + # Performs a HEAD request with the given parameters. See get() for more + # details. + def head(path, parameters = nil, headers = nil) + process :head, path, parameters, headers + end + + # Performs an XMLHttpRequest request with the given parameters, mirroring + # a request from the Prototype library. + # + # The request_method is :get, :post, :put, :delete or :head; the + # parameters are +nil+, a hash, or a url-encoded or multipart string; + # the headers are a hash. Keys are automatically upcased and prefixed + # with 'HTTP_' if not already. + def xml_http_request(request_method, path, parameters = nil, headers = nil) + headers ||= {} + headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers) + end + alias xhr :xml_http_request + + # Follow a single redirect response. If the last response was not a + # redirect, an exception will be raised. Otherwise, the redirect is + # performed on the location header. + def follow_redirect! + raise "not a redirect! #{status} #{status_message}" unless redirect? + get(response.location) + status + end + + # Performs a request using the specified method, following any subsequent + # redirect. Note that the redirects are followed until the response is + # not a redirect--this means you may run into an infinite loop if your + # redirect loops back to itself. + def request_via_redirect(http_method, path, parameters = nil, headers = nil) + process(http_method, path, parameters, headers) + follow_redirect! while redirect? + status + end + + # Performs a GET request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def get_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:get, path, parameters, headers) + end + + # Performs a POST request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def post_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:post, path, parameters, headers) + end + + # Performs a PUT request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def put_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:put, path, parameters, headers) + end + + # Performs a DELETE request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def delete_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:delete, path, parameters, headers) + end + end + + # An integration Session instance represents a set of requests and responses + # performed sequentially by some virtual user. Because you can instantiate + # multiple sessions and run them side-by-side, you can also mimic (to some + # limited extent) multiple simultaneous users interacting with your system. + # + # Typically, you will instantiate a new session using + # IntegrationTest#open_session, rather than instantiating + # Integration::Session directly. + class Session + DEFAULT_HOST = "www.example.com" + + include Test::Unit::Assertions + include ActionDispatch::Assertions + include ActionController::TestProcess + include RequestHelpers + + %w( status status_message headers body redirect? ).each do |method| + delegate method, :to => :response, :allow_nil => true + end + + %w( path ).each do |method| + delegate method, :to => :request, :allow_nil => true + end + + # The hostname used in the last request. + attr_accessor :host + + # The remote_addr used in the last request. + attr_accessor :remote_addr + + # The Accept header to send. + attr_accessor :accept + + # A map of the cookies returned by the last response, and which will be + # sent with the next request. + def cookies + @mock_session.cookie_jar + end + + # A reference to the controller instance used by the last request. + attr_reader :controller + + # A reference to the request instance used by the last request. + attr_reader :request + + # A reference to the response instance used by the last request. + attr_reader :response + + # A running counter of the number of requests processed. + attr_accessor :request_count + + # Create and initialize a new Session instance. + def initialize(app) + @app = app + reset! + end + + # Resets the instance. This can be used to reset the state information + # in an existing session instance, so it can be used from a clean-slate + # condition. + # + # session.reset! + def reset! + @https = false + @mock_session = Rack::MockSession.new(@app, DEFAULT_HOST) + @controller = @request = @response = nil + @request_count = 0 + + self.host = DEFAULT_HOST + self.remote_addr = "127.0.0.1" + self.accept = "text/xml,application/xml,application/xhtml+xml," + + "text/html;q=0.9,text/plain;q=0.8,image/png," + + "*/*;q=0.5" + + unless defined? @named_routes_configured + # install the named routes in this session instance. + klass = metaclass + ActionController::Routing::Routes.install_helpers(klass) + + # the helpers are made protected by default--we make them public for + # easier access during testing and troubleshooting. + klass.module_eval { public *ActionController::Routing::Routes.named_routes.helpers } + @named_routes_configured = true + end + end + + # Specify whether or not the session should mimic a secure HTTPS request. + # + # session.https! + # session.https!(false) + def https!(flag = true) + @https = flag + end + + # Return +true+ if the session is mimicking a secure HTTPS request. + # + # if session.https? + # ... + # end + def https? + @https + end + + # Set the host name to use in the next request. + # + # session.host! "www.example.com" + def host!(name) + @host = name + end + + # Returns the URL for the given options, according to the rules specified + # in the application's routes. + def url_for(options) + controller ? + controller.url_for(options) : + generic_url_rewriter.rewrite(options) + end + + private + + # Performs the actual request. + def process(method, path, parameters = nil, rack_environment = nil) + if path =~ %r{://} + location = URI.parse(path) + https! URI::HTTPS === location if location.scheme + host! location.host if location.host + path = location.query ? "#{location.path}?#{location.query}" : location.path + end + + [ControllerCapture, ActionController::Testing].each do |mod| + unless ActionController::Base < mod + ActionController::Base.class_eval { include mod } + end + end + + env = { + :method => method, + :params => parameters, + + "SERVER_NAME" => host, + "SERVER_PORT" => (https? ? "443" : "80"), + "HTTPS" => https? ? "on" : "off", + "rack.url_scheme" => https? ? "https" : "http", + + "REQUEST_URI" => path, + "HTTP_HOST" => host, + "REMOTE_ADDR" => remote_addr, + "CONTENT_TYPE" => "application/x-www-form-urlencoded", + "HTTP_ACCEPT" => accept + } + + (rack_environment || {}).each do |key, value| + env[key] = value + end + + session = Rack::Test::Session.new(@mock_session) + + @controller = ActionController::Base.capture_instantiation do + session.request(path, env) + end + + @request_count += 1 + @request = ActionDispatch::Request.new(session.last_request.env) + @response = ActionDispatch::TestResponse.from_response(@mock_session.last_response) + @html_document = nil + + return response.status + end + + # Get a temporary URL writer object + def generic_url_rewriter + env = { + 'REQUEST_METHOD' => "GET", + 'QUERY_STRING' => "", + "REQUEST_URI" => "/", + "HTTP_HOST" => host, + "SERVER_PORT" => https? ? "443" : "80", + "HTTPS" => https? ? "on" : "off" + } + ActionController::UrlRewriter.new(ActionDispatch::Request.new(env), {}) + end + end + + # A module used to extend ActionController::Base, so that integration tests + # can capture the controller used to satisfy a request. + module ControllerCapture #:nodoc: + extend ActiveSupport::Concern + + included do + alias_method_chain :initialize, :capture + end + + def initialize_with_capture(*args) + initialize_without_capture + self.class.last_instantiation ||= self + end + + module ClassMethods #:nodoc: + mattr_accessor :last_instantiation + + def capture_instantiation + self.last_instantiation = nil + yield + return last_instantiation + end + end + end + + module Runner + def app + @app + end + + # Reset the current session. This is useful for testing multiple sessions + # in a single test case. + def reset! + @integration_session = open_session + end + + %w(get post put head delete cookies assigns + xml_http_request xhr get_via_redirect post_via_redirect).each do |method| + define_method(method) do |*args| + reset! unless @integration_session + # reset the html_document variable, but only for new get/post calls + @html_document = nil unless %w(cookies assigns).include?(method) + returning @integration_session.__send__(method, *args) do + copy_session_variables! + end + end + end + + # Open a new session instance. If a block is given, the new session is + # yielded to the block before being returned. + # + # session = open_session do |sess| + # sess.extend(CustomAssertions) + # end + # + # 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(app = nil) + session = Integration::Session.new(app || self.app) + + # delegate the fixture accessors back to the test instance + extras = Module.new { attr_accessor :delegate, :test_result } + if self.class.respond_to?(:fixture_table_names) + self.class.fixture_table_names.each do |table_name| + name = table_name.tr(".", "_") + next unless respond_to?(name) + extras.__send__(:define_method, name) { |*args| + delegate.send(name, *args) + } + end + end + + # delegate add_assertion to the test case + extras.__send__(:define_method, :add_assertion) { + test_result.add_assertion + } + session.extend(extras) + session.delegate = self + session.test_result = @_result + + yield session if block_given? + session + end + + # Copy the instance variables from the current session instance into the + # test instance. + def copy_session_variables! #:nodoc: + return unless @integration_session + %w(controller response request).each do |var| + instance_variable_set("@#{var}", @integration_session.__send__(var)) + end + end + + # Delegate unhandled messages to the current session instance. + def method_missing(sym, *args, &block) + reset! unless @integration_session + returning @integration_session.__send__(sym, *args, &block) do + copy_session_variables! + end + end + end + end + + # An IntegrationTest is one that spans multiple controllers and actions, + # tying them all together to ensure they work together as expected. It tests + # more completely than either unit or functional tests do, exercising the + # entire stack, from the dispatcher to the database. + # + # At its simplest, you simply extend IntegrationTest and write your tests + # using the get/post methods: + # + # require "#{File.dirname(__FILE__)}/test_helper" + # + # class ExampleTest < ActionController::IntegrationTest + # fixtures :people + # + # def test_login + # # get the login page + # get "/login" + # assert_equal 200, status + # + # # post the login and follow through to the home page + # post "/login", :username => people(:jamis).username, + # :password => people(:jamis).password + # follow_redirect! + # assert_equal 200, status + # assert_equal "/home", path + # end + # end + # + # However, you can also have multiple session instances open per test, and + # even extend those instances with assertions and methods to create a very + # powerful testing DSL that is specific for your application. You can even + # reference any named routes you happen to have defined! + # + # require "#{File.dirname(__FILE__)}/test_helper" + # + # class AdvancedTest < ActionController::IntegrationTest + # fixtures :people, :rooms + # + # def test_login_and_speak + # jamis, david = login(:jamis), login(:david) + # room = rooms(:office) + # + # jamis.enter(room) + # jamis.speak(room, "anybody home?") + # + # david.enter(room) + # david.speak(room, "hello!") + # end + # + # private + # + # module CustomAssertions + # def enter(room) + # # reference a named route, for maximum internal consistency! + # get(room_url(:id => room.id)) + # assert(...) + # ... + # end + # + # def speak(room, message) + # xml_http_request "/say/#{room.id}", :message => message + # assert(...) + # ... + # end + # end + # + # def login(who) + # open_session do |sess| + # sess.extend(CustomAssertions) + # who = people(who) + # sess.post "/login", :username => who.username, + # :password => who.password + # assert(...) + # end + # end + # end + class IntegrationTest < ActiveSupport::TestCase + include Integration::Runner + + @@app = nil + + def self.app + # DEPRECATE Rails application fallback + # This should be set by the initializer + @@app || (defined?(Rails.application) && Rails.application.new) || nil + end + + def self.app=(app) + @@app = app + end + + def app + super || self.class.app + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb new file mode 100644 index 0000000000..b1ed9d31f4 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/performance_test.rb @@ -0,0 +1,15 @@ +require 'active_support/testing/performance' +require 'active_support/testing/default' + +module ActionDispatch + # An integration test that runs a code profiler on your test methods. + # Profiling output for combinations of each test method, measurement, and + # output format are written to your tmp/performance directory. + # + # By default, process_time is measured and both flat and graph_html output + # formats are written, so you'll have two output files per test method. + class PerformanceTest < ActionDispatch::IntegrationTest + include ActiveSupport::Testing::Performance + include ActiveSupport::Testing::Default + end +end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index c35982e075..6d019023ce 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -1,6 +1,6 @@ module ActionDispatch - # Integration test methods such as ActionController::Integration::Session#get - # and ActionController::Integration::Session#post return objects of class + # Integration test methods such as ActionDispatch::Integration::Session#get + # and ActionDispatch::Integration::Session#post return objects of class # TestResponse, which represent the HTTP response results of the requested # controller actions. # |