diff options
Diffstat (limited to 'actionpack/lib/action_controller/test_case.rb')
| -rw-r--r-- | actionpack/lib/action_controller/test_case.rb | 665 |
1 files changed, 665 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb new file mode 100644 index 0000000000..c55720859e --- /dev/null +++ b/actionpack/lib/action_controller/test_case.rb @@ -0,0 +1,665 @@ +require 'rack/session/abstract/id' +require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/hash/keys' +require 'action_controller/template_assertions' +require 'rails-dom-testing' + +module ActionController + # :stopdoc: + # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # Please use ActionDispatch::IntegrationTest going forward. + class TestRequest < ActionDispatch::TestRequest #:nodoc: + DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup + DEFAULT_ENV.delete 'PATH_INFO' + + def self.new_session + TestSession.new + end + + # Create a new test request with default `env` values + def self.create + env = {} + env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + env["rack.request.cookie_hash"] = {}.with_indifferent_access + new(default_env.merge(env), new_session) + end + + def self.default_env + DEFAULT_ENV + end + private_class_method :default_env + + def initialize(env, session) + super(env) + + self.session = session + self.session_options = TestSession::DEFAULT_OPTIONS + @custom_param_parsers = { + Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + } + end + + def query_string=(string) + set_header Rack::QUERY_STRING, string + end + + def content_type=(type) + set_header 'CONTENT_TYPE', type + end + + def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) + non_path_parameters = {} + path_parameters = {} + + parameters.each do |key, value| + if query_string_keys.include?(key) + non_path_parameters[key] = value + else + if value.is_a?(Array) + value = value.map(&:to_param) + else + value = value.to_param + end + + path_parameters[key] = value + end + end + + if get? + if self.query_string.blank? + self.query_string = non_path_parameters.to_query + end + else + if ENCODER.should_multipart?(non_path_parameters) + self.content_type = ENCODER.content_type + data = ENCODER.build_multipart non_path_parameters + else + fetch_header('CONTENT_TYPE') do |k| + set_header k, 'application/x-www-form-urlencoded' + end + + case content_mime_type.to_sym + when nil + raise "Unknown Content-Type: #{content_type}" + when :json + data = ActiveSupport::JSON.encode(non_path_parameters) + when :xml + data = non_path_parameters.to_xml + when :url_encoded_form + data = non_path_parameters.to_query + else + @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } + data = non_path_parameters.to_query + end + end + + set_header 'CONTENT_LENGTH', data.length.to_s + set_header 'rack.input', StringIO.new(data) + end + + fetch_header("PATH_INFO") do |k| + set_header k, generated_path + end + path_parameters[:controller] = controller_path + path_parameters[:action] = action + + self.path_parameters = path_parameters + end + + ENCODER = Class.new do + include Rack::Test::Utils + + def should_multipart?(params) + # FIXME: lifted from Rack-Test. We should push this separation upstream + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when Rack::Test::UploadedFile + multipart = true + end + } + params.values.each(&query) + multipart + end + + public :build_multipart + + def content_type + "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" + end + end.new + + private + + def params_parsers + super.merge @custom_param_parsers + end + end + + class LiveTestResponse < Live::Response + # Was the response successful? + alias_method :success?, :successful? + + # Was the URL not found? + alias_method :missing?, :not_found? + + # Was there a server-side error? + alias_method :error?, :server_error? + end + + # Methods #destroy and #load! are overridden to avoid calling methods on the + # @store object, which does not exist for the TestSession class. + class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: + DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS + + def initialize(session = {}) + super(nil, nil) + @id = SecureRandom.hex(16) + @data = stringify_keys(session) + @loaded = true + end + + def exists? + true + end + + def keys + @data.keys + end + + def values + @data.values + end + + def destroy + clear + end + + def fetch(key, *args, &block) + @data.fetch(key.to_s, *args, &block) + end + + private + + def load! + @id + end + end + + # Superclass for ActionController functional tests. Functional tests allow you to + # test a single controller action per test method. This should not be confused with + # integration tests (see ActionDispatch::IntegrationTest), which are more like + # "stories" that can involve multiple controllers and multiple actions (i.e. multiple + # different HTTP requests). + # + # == Basic example + # + # Functional tests are written as follows: + # 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate + # an HTTP request. + # 2. Then, one asserts whether the current state is as expected. "State" can be anything: + # the controller's HTTP response, the database contents, etc. + # + # For example: + # + # class BooksControllerTest < ActionController::TestCase + # def test_create + # # Simulate a POST response with the given HTTP parameters. + # post(:create, params: { book: { title: "Love Hina" }}) + # + # # Asserts that the controller tried to redirect us to + # # the created book's URI. + # assert_response :found + # + # # Asserts that the controller really put the book in the database. + # assert_not_nil Book.find_by(title: "Love Hina") + # end + # end + # + # You can also send a real document in the simulated HTTP request. + # + # def test_create + # json = {book: { title: "Love Hina" }}.to_json + # post :create, json + # end + # + # == Special instance variables + # + # ActionController::TestCase will also automatically provide the following instance + # variables for use in the tests: + # + # <b>@controller</b>:: + # The controller instance that will be tested. + # <b>@request</b>:: + # An ActionController::TestRequest, representing the current HTTP + # request. You can modify this object before sending the HTTP request. For example, + # you might want to set some session properties before sending a GET request. + # <b>@response</b>:: + # An ActionDispatch::TestResponse object, representing the response + # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid + # after calling +post+. If the various assert methods are not sufficient, then you + # may use this object to inspect the HTTP response in detail. + # + # (Earlier versions of \Rails required each functional test to subclass + # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) + # + # == Controller is automatically inferred + # + # ActionController::TestCase will automatically infer the controller under test + # from the test class name. If the controller cannot be inferred from the test + # class name, you can explicitly set it with +tests+. + # + # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase + # tests WidgetController + # end + # + # == \Testing controller internals + # + # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions + # can be used against. These collections are: + # + # * session: Objects being saved in the session. + # * flash: The flash objects currently in the session. + # * cookies: \Cookies being sent to the user on this request. + # + # These collections can be used just like any other hash: + # + # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" + # assert flash.empty? # makes sure that there's nothing in the flash + # + # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. + # + # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another + # action call which can then be asserted against. + # + # == Manipulating session and cookie variables + # + # Sometimes you need to set up the session and cookie variables for a test. + # To do this just assign a value to the session or cookie collection: + # + # session[:key] = "value" + # cookies[:key] = "value" + # + # To clear the cookies for a test just clear the cookie collection: + # + # cookies.clear + # + # == \Testing named routes + # + # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case. + # + # assert_redirected_to page_url(title: 'foo') + class TestCase < ActiveSupport::TestCase + module Behavior + extend ActiveSupport::Concern + include ActionDispatch::TestProcess + include ActiveSupport::Testing::ConstantLookup + include Rails::Dom::Testing::Assertions + + attr_reader :response, :request + + module ClassMethods + + # Sets the controller class name. Useful if the name can't be inferred from test class. + # Normalizes +controller_class+ before using. + # + # tests WidgetController + # tests :widget + # tests 'widget' + def tests(controller_class) + case controller_class + when String, Symbol + self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize + when Class + self.controller_class = controller_class + else + raise ArgumentError, "controller class must be a String, Symbol, or Class" + end + end + + def controller_class=(new_class) + self._controller_class = new_class + end + + def controller_class + if current_controller_class = self._controller_class + current_controller_class + else + self.controller_class = determine_default_controller_class(name) + end + end + + def determine_default_controller_class(name) + determine_constant_from_test_name(name) do |constant| + Class === constant && constant < ActionController::Metal + end + end + end + + # Simulate a GET request with the given parameters. + # + # - +action+: The controller action to call. + # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. + # - +body+: The request body with a string that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +post+, +patch+, +put+, +delete+, and +head+. + # Example sending parameters, session and setting a flash message: + # + # get :show, + # params: { id: 7 }, + # session: { user_id: 1 }, + # flash: { notice: 'This is flash message' } + # + # Note that the request method is not verified. The different methods are + # available to make the tests more expressive. + def get(action, *args) + res = process_with_kwargs("GET", action, *args) + cookies.update res.cookies + res + end + + # Simulate a POST request with the given parameters and set/volley the response. + # See +get+ for more details. + def post(action, *args) + process_with_kwargs("POST", action, *args) + end + + # Simulate a PATCH request with the given parameters and set/volley the response. + # See +get+ for more details. + def patch(action, *args) + process_with_kwargs("PATCH", action, *args) + end + + # Simulate a PUT request with the given parameters and set/volley the response. + # See +get+ for more details. + def put(action, *args) + process_with_kwargs("PUT", action, *args) + end + + # Simulate a DELETE request with the given parameters and set/volley the response. + # See +get+ for more details. + def delete(action, *args) + process_with_kwargs("DELETE", action, *args) + end + + # Simulate a HEAD request with the given parameters and set/volley the response. + # See +get+ for more details. + def head(action, *args) + process_with_kwargs("HEAD", action, *args) + end + + def xml_http_request(*args) + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + xhr and xml_http_request methods are deprecated in favor of + `get :index, xhr: true` and `post :create, xhr: true` + MSG + + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + @request.env['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') + __send__(*args).tap do + @request.env.delete 'HTTP_X_REQUESTED_WITH' + @request.env.delete 'HTTP_ACCEPT' + end + end + alias xhr :xml_http_request + + # Simulate a HTTP request to +action+ by specifying request method, + # parameters and set/volley the response. + # + # - +action+: The controller action to call. + # - +method+: Request method used to send the HTTP request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol. + # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. + # - +body+: The request body with a string that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # - +format+: Request format. Defaults to +nil+. Can be string or symbol. + # + # Example calling +create+ action and sending two params: + # + # process :create, + # method: 'POST', + # params: { + # user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # }, + # session: { user_id: 1 }, + # flash: { notice: 'This is flash message' } + # + # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests + # prefer using #get, #post, #patch, #put, #delete and #head methods + # respectively which will make tests more expressive. + # + # Note that the request method is not verified. + def process(action, *args) + check_required_ivars + + if kwarg_request?(args) + parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr) + else + http_method, parameters, session, flash = args + format = nil + + if parameters.is_a?(String) && http_method != 'HEAD' + body = parameters + parameters = nil + end + + if parameters.present? || session.present? || flash.present? + non_kwarg_request_warning + end + end + + if body.present? + @request.set_header 'RAW_POST_DATA', body + end + + if http_method.present? + http_method = http_method.to_s.upcase + else + http_method = "GET" + end + + parameters ||= {} + + if format.present? + parameters[:format] = format + end + + @html_document = nil + + unless @controller.respond_to?(:recycle!) + @controller.extend(Testing::Functional) + end + + self.cookies.update @request.cookies + self.cookies.update_cookies_from_jar + @request.set_header 'HTTP_COOKIE', cookies.to_header + @request.delete_header 'action_dispatch.cookies' + + @request = TestRequest.new scrub_env!(@request.env), @request.session + @response = build_response @response_klass + @response.request = @request + @controller.recycle! + + @request.set_header 'REQUEST_METHOD', http_method + + parameters = parameters.symbolize_keys + + generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) + generated_path = generated_path(generated_extras) + query_string_keys = query_parameter_names(generated_extras) + + @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys) + + @request.session.update(session) if session + @request.flash.update(flash || {}) + + if xhr + @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' + @request.fetch_header('HTTP_ACCEPT') do |k| + @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') + end + end + + @request.fetch_header("SCRIPT_NAME") do |k| + @request.set_header k, @controller.config.relative_url_root + end + + @controller.recycle! + @controller.dispatch(action, @request, @response) + @request = @controller.request + @response = @controller.response + + @request.delete_header 'HTTP_COOKIE' + + if @request.have_cookie_jar? + unless @request.cookie_jar.committed? + @request.cookie_jar.write(@response) + self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) + end + end + @response.prepare! + + if flash_value = @request.flash.to_session_value + @request.session['flash'] = flash_value + else + @request.session.delete('flash') + end + + if xhr + @request.delete_header 'HTTP_X_REQUESTED_WITH' + @request.delete_header 'HTTP_ACCEPT' + end + @request.query_string = '' + + @response + end + + def controller_class_name + @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path + end + + def generated_path(generated_extras) + generated_extras[0] + end + + def query_parameter_names(generated_extras) + generated_extras[1] + [:controller, :action] + end + + def setup_controller_request_and_response + @controller = nil unless defined? @controller + + @response_klass = ActionDispatch::TestResponse + + if klass = self.class.controller_class + if klass < ActionController::Live + @response_klass = LiveTestResponse + end + unless @controller + begin + @controller = klass.new + rescue + warn "could not construct controller #{klass}" if $VERBOSE + end + end + end + + @request = TestRequest.create + @response = build_response @response_klass + @response.request = @request + + if @controller + @controller.request = @request + @controller.params = {} + end + end + + def build_response(klass) + klass.create + end + + included do + include ActionController::TemplateAssertions + include ActionDispatch::Assertions + class_attribute :_controller_class + setup :setup_controller_request_and_response + end + + private + + def scrub_env!(env) + env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + env.delete 'action_dispatch.request.query_parameters' + env.delete 'action_dispatch.request.request_parameters' + env + end + + def process_with_kwargs(http_method, action, *args) + if kwarg_request?(args) + args.first.merge!(method: http_method) + process(action, *args) + else + non_kwarg_request_warning if args.any? + + args = args.unshift(http_method) + process(action, *args) + end + end + + REQUEST_KWARGS = %i(params session flash method body xhr) + def kwarg_request?(args) + args[0].respond_to?(:keys) && ( + (args[0].key?(:format) && args[0].keys.size == 1) || + args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } + ) + end + + def non_kwarg_request_warning + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + ActionController::TestCase HTTP request methods will accept only + keyword arguments in future Rails versions. + + Examples: + + get :show, params: { id: 1 }, session: { user_id: 1 } + process :update, method: :post, params: { id: 1 } + MSG + end + + def document_root_element + html_document.root + end + + def check_required_ivars + # Sanity check for required instance variables so we can give an + # understandable error message. + [:@routes, :@controller, :@request, :@response].each do |iv_name| + if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." + end + end + end + + def html_format?(parameters) + return true unless parameters.key?(:format) + Mime.fetch(parameters[:format]) { Mime['html'] }.html? + end + end + + include Behavior + end + # :startdoc: +end |
