test_case.rb 21.2 KB
Newer Older
1
require 'rack/session/abstract/id'
2
require 'active_support/core_ext/object/to_query'
3
require 'active_support/core_ext/module/anonymous'
A
Akira Matsuda 已提交
4
require 'active_support/core_ext/hash/keys'
5
require 'action_controller/template_assertions'
6 7
require 'rails-dom-testing'

8
module ActionController
9
  class TestRequest < ActionDispatch::TestRequest #:nodoc:
10 11 12
    DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
    DEFAULT_ENV.delete 'PATH_INFO'

13 14 15 16
    def self.new_session
      TestSession.new
    end

17 18 19 20
    # 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
21
      env["rack.request.cookie_hash"] = {}.with_indifferent_access
22 23 24 25 26 27 28 29
      new(default_env.merge(env), new_session)
    end

    def self.default_env
      DEFAULT_ENV
    end
    private_class_method :default_env

30 31
    def initialize(env, session)
      super(env)
32

33
      self.session = session
34
      self.session_options = TestSession::DEFAULT_OPTIONS
35 36
    end

37 38 39 40 41 42 43 44
    def query_parameters=(params)
      @env["action_dispatch.request.query_parameters"] = params
    end

    def request_parameters=(params)
      @env["action_dispatch.request.request_parameters"] = params
    end

J
Joshua Peek 已提交
45
    def assign_parameters(routes, controller_path, action, parameters = {})
46 47
      parameters = parameters.symbolize_keys
      extra_keys = routes.extra_keys(parameters.merge(:controller => controller_path, :action => action))
48
      non_path_parameters = {}.with_indifferent_access
49

50
      parameters.each do |key, value|
51 52 53 54 55
        if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?))
          value = value.map{ |v| v.duplicable? ? v.dup : v }
        elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? })
          value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }]
        elsif value.frozen? && value.duplicable?
56
          value = value.dup
57 58
        end

59
        if extra_keys.include?(key) || key == :action || key == :controller
60 61
          non_path_parameters[key] = value
        else
62
          if value.is_a?(Array)
63
            value = value.map(&:to_param)
64 65 66 67
          else
            value = value.to_param
          end

68
          path_parameters[key] = value
69 70 71
        end
      end

72 73 74 75 76 77
      if get?
        self.query_parameters = non_path_parameters
      else
        self.request_parameters = non_path_parameters
      end

78 79 80
      path_parameters[:controller] = controller_path
      path_parameters[:action] = action

81 82
      # Clear the combined params hash in case it was already referenced.
      @env.delete("action_dispatch.request.parameters")
83

84 85 86
      # Clear the filter cache variables so they're not stale
      @filtered_parameters = @filtered_env = @filtered_path = nil

87
      data = request_parameters.to_query
88 89 90 91 92 93 94 95
      @env['CONTENT_LENGTH'] = data.length.to_s
      @env['rack.input'] = StringIO.new(data)
    end
  end

  class TestResponse < ActionDispatch::TestResponse
  end

96 97 98 99
  class LiveTestResponse < Live::Response
    def body
      @body ||= super
    end
100 101 102 103 104 105 106 107 108 109 110 111

    # Was the response successful?
    alias_method :success?, :successful?

    # Was the URL not found?
    alias_method :missing?, :not_found?

    # Were we redirected?
    alias_method :redirect?, :redirection?

    # Was there a server-side error?
    alias_method :error?, :server_error?
112 113
  end

114 115
  # Methods #destroy and #load! are overridden to avoid calling methods on the
  # @store object, which does not exist for the TestSession class.
116 117
  class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
    DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
118 119

    def initialize(session = {})
120
      super(nil, nil)
121 122
      @id = SecureRandom.hex(16)
      @data = stringify_keys(session)
123 124
      @loaded = true
    end
125

126 127 128
    def exists?
      true
    end
129

130 131 132 133 134 135 136 137
    def keys
      @data.keys
    end

    def values
      @data.values
    end

138 139 140 141 142 143 144 145 146
    def destroy
      clear
    end

    private

      def load!
        @id
      end
147 148
  end

P
Pratik Naik 已提交
149 150
  # Superclass for ActionController functional tests. Functional tests allow you to
  # test a single controller action per test method. This should not be confused with
151
  # integration tests (see ActionDispatch::IntegrationTest), which are more like
152
  # "stories" that can involve multiple controllers and multiple actions (i.e. multiple
P
Pratik Naik 已提交
153
  # different HTTP requests).
P
Pratik Naik 已提交
154
  #
P
Pratik Naik 已提交
155 156 157
  # == Basic example
  #
  # Functional tests are written as follows:
158
  # 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
P
Pratik Naik 已提交
159 160 161 162 163 164 165 166 167
  #    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.
168
  #       post(:create, params: { book: { title: "Love Hina" }})
P
Pratik Naik 已提交
169 170 171 172 173 174
  #
  #       # Assert that the controller tried to redirect us to
  #       # the created book's URI.
  #       assert_response :found
  #
  #       # Assert that the controller really put the book in the database.
175
  #       assert_not_nil Book.find_by(title: "Love Hina")
P
Pratik Naik 已提交
176 177 178
  #     end
  #   end
  #
179 180 181
  # You can also send a real document in the simulated HTTP request.
  #
  #   def test_create
A
AvnerCohen 已提交
182
  #     json = {book: { title: "Love Hina" }}.to_json
183
  #     post :create, json
R
Rafael Mendonça França 已提交
184
  #   end
185
  #
P
Pratik Naik 已提交
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
  # == 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 ActionController::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.
  #
J
Joost Baaij 已提交
203
  # (Earlier versions of \Rails required each functional test to subclass
P
Pratik Naik 已提交
204
  # Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
P
Pratik Naik 已提交
205
  #
P
Pratik Naik 已提交
206
  # == Controller is automatically inferred
P
Pratik Naik 已提交
207
  #
P
Pratik Naik 已提交
208 209
  # ActionController::TestCase will automatically infer the controller under test
  # from the test class name. If the controller cannot be inferred from the test
P
Pratik Naik 已提交
210
  # class name, you can explicitly set it with +tests+.
P
Pratik Naik 已提交
211 212 213 214
  #
  #   class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
  #     tests WidgetController
  #   end
215
  #
J
Joost Baaij 已提交
216
  # == \Testing controller internals
217 218 219 220 221 222
  #
  # 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.
J
Joost Baaij 已提交
223
  # * cookies: \Cookies being sent to the user on this request.
224 225 226 227 228 229
  #
  # 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
  #
230
  # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>.
231 232 233 234
  #
  # 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.
  #
235
  # == Manipulating session and cookie variables
236
  #
237 238
  # 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:
239
  #
240 241
  #   session[:key] = "value"
  #   cookies[:key] = "value"
242
  #
243
  # To clear the cookies for a test just clear the cookie collection:
244
  #
245
  #   cookies.clear
246
  #
J
Joost Baaij 已提交
247
  # == \Testing named routes
248 249 250
  #
  # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
  #
A
AvnerCohen 已提交
251
  #  assert_redirected_to page_url(title: 'foo')
252
  class TestCase < ActiveSupport::TestCase
253 254 255
    module Behavior
      extend ActiveSupport::Concern
      include ActionDispatch::TestProcess
256
      include ActiveSupport::Testing::ConstantLookup
257
      include Rails::Dom::Testing::Assertions
258

259
      attr_reader :response, :request
260

261
      module ClassMethods
262

263
        # Sets the controller class name. Useful if the name can't be inferred from test class.
264
        # Normalizes +controller_class+ before using.
265 266 267 268
        #
        #   tests WidgetController
        #   tests :widget
        #   tests 'widget'
269
        def tests(controller_class)
270 271
          case controller_class
          when String, Symbol
272
            self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
273 274 275 276 277
          when Class
            self.controller_class = controller_class
          else
            raise ArgumentError, "controller class must be a String, Symbol, or Class"
          end
278
        end
279

280
        def controller_class=(new_class)
281
          self._controller_class = new_class
282
        end
283

284
        def controller_class
285
          if current_controller_class = self._controller_class
286 287 288 289 290
            current_controller_class
          else
            self.controller_class = determine_default_controller_class(name)
          end
        end
291

292
        def determine_default_controller_class(name)
293 294 295
          determine_constant_from_test_name(name) do |constant|
            Class === constant && constant < ActionController::Metal
          end
296
        end
297
      end
298

299 300
      # Simulate a GET request with the given parameters.
      #
301
      # - +action+: The controller action to call.
302 303
      # - +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
304
      #   (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
305 306
      # - +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+.
X
Xavier Noria 已提交
307
      #
308 309
      # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
      # +post+, +patch+, +put+, +delete+, and +head+.
310 311 312 313 314 315
      # Example sending parameters, session and setting a flash message:
      #
      #   get :show,
      #     params: { id: 7 },
      #     session: { user_id: 1 },
      #     flash: { notice: 'This is flash message' }
316 317 318
      #
      # Note that the request method is not verified. The different methods are
      # available to make the tests more expressive.
319
      def get(action, *args)
E
eileencodes 已提交
320 321 322
        res = process_with_kwargs("GET", action, *args)
        cookies.update res.cookies
        res
323 324
      end

325
      # Simulate a POST request with the given parameters and set/volley the response.
X
Xavier Noria 已提交
326
      # See +get+ for more details.
327
      def post(action, *args)
328
        process_with_kwargs("POST", action, *args)
329
      end
330

331
      # Simulate a PATCH request with the given parameters and set/volley the response.
X
Xavier Noria 已提交
332
      # See +get+ for more details.
333
      def patch(action, *args)
334
        process_with_kwargs("PATCH", action, *args)
335 336
      end

337
      # Simulate a PUT request with the given parameters and set/volley the response.
X
Xavier Noria 已提交
338
      # See +get+ for more details.
339
      def put(action, *args)
340
        process_with_kwargs("PUT", action, *args)
341
      end
342

343
      # Simulate a DELETE request with the given parameters and set/volley the response.
X
Xavier Noria 已提交
344
      # See +get+ for more details.
345
      def delete(action, *args)
346
        process_with_kwargs("DELETE", action, *args)
347
      end
348

349
      # Simulate a HEAD request with the given parameters and set/volley the response.
X
Xavier Noria 已提交
350
      # See +get+ for more details.
351
      def head(action, *args)
352
        process_with_kwargs("HEAD", action, *args)
353 354
      end

355
      def xml_http_request(*args)
356 357 358 359 360
        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

361
        @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
362
        @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
363
        __send__(*args).tap do
364 365 366 367 368 369
          @request.env.delete 'HTTP_X_REQUESTED_WITH'
          @request.env.delete 'HTTP_ACCEPT'
        end
      end
      alias xhr :xml_http_request

370
      def paramify_values(hash_or_array_or_value)
371 372
        case hash_or_array_or_value
        when Hash
373
          Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }]
374
        when Array
375
          hash_or_array_or_value.map {|i| paramify_values(i)}
376
        when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile
377
          hash_or_array_or_value
378 379
        else
          hash_or_array_or_value.to_param
380 381 382
        end
      end

383 384 385 386
      # Simulate a HTTP request to +action+ by specifying request method,
      # parameters and set/volley the response.
      #
      # - +action+: The controller action to call.
387 388
      # - +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.
389 390
      # - +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
391
      #   (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
392 393
      # - +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+.
394
      # - +format+: Request format. Defaults to +nil+. Can be string or symbol.
395
      #
396
      # Example calling +create+ action and sending two params:
397
      #
398 399 400 401 402 403 404
      #   process :create,
      #     method: 'POST',
      #     params: {
      #       user: { name: 'Gaurish Sharma', email: 'user@example.com' }
      #     },
      #     session: { user_id: 1 },
      #     flash: { notice: 'This is flash message' }
405
      #
406 407 408
      # 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.
409 410
      #
      # Note that the request method is not verified.
411
      def process(action, *args)
412
        check_required_ivars
A
Aaron Patterson 已提交
413

414
        if kwarg_request?(args)
415
          parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr)
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
        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.env['RAW_POST_DATA'] = body
        end

        if http_method.present?
          http_method = http_method.to_s.upcase
        else
          http_method = "GET"
438
        end
A
Aaron Patterson 已提交
439

440
        parameters ||= {}
A
Aaron Patterson 已提交
441

442
        # Ensure that numbers and symbols passed as params are converted to
443
        # proper params, as is the case when engaging rack.
444
        parameters = paramify_values(parameters) if html_format?(parameters)
445

446 447 448 449
        if format.present?
          parameters[:format] = format
        end

450 451
        @html_document = nil

A
Aaron Patterson 已提交
452 453 454 455
        unless @controller.respond_to?(:recycle!)
          @controller.extend(Testing::Functional)
        end

456 457 458 459
        self.cookies.update @request.cookies
        @request.env['HTTP_COOKIE'] = cookies.to_header
        @request.env['action_dispatch.cookies'] = nil

460
        @request          = TestRequest.new scrub_env!(@request.env), @request.session
461 462
        @response         = build_response @response_klass
        @response.request = @request
A
Aaron Patterson 已提交
463
        @controller.recycle!
464

465
        @request.env['REQUEST_METHOD'] = http_method
466

467
        controller_class_name = @controller.class.anonymous? ?
468
          "anonymous" :
469
          @controller.class.controller_path
470 471

        @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters)
472

473
        @request.session.update(session) if session
474
        @request.flash.update(flash || {})
475

476 477 478 479 480
        if xhr
          @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
          @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
        end

481 482 483
        @controller.request  = @request
        @controller.response = @response

484
        build_request_uri(controller_class_name, action, parameters)
485

486
        @controller.recycle!
487
        @controller.process(action)
488

489 490
        @request.env.delete 'HTTP_COOKIE'

491
        if cookies = @request.env['action_dispatch.cookies']
492
          unless @response.committed?
493
            cookies.write(@response)
E
eileencodes 已提交
494
            self.cookies.update(cookies.instance_variable_get(:@cookies))
495
          end
496 497 498
        end
        @response.prepare!

499
        if flash_value = @request.flash.to_session_value
500
          @request.session['flash'] = flash_value
501 502
        else
          @request.session.delete('flash')
503 504
        end

505 506 507 508 509
        if xhr
          @request.env.delete 'HTTP_X_REQUESTED_WITH'
          @request.env.delete 'HTTP_ACCEPT'
        end

510
        @response
511 512
      end

513
      def setup_controller_request_and_response
514 515
        @controller = nil unless defined? @controller

516
        @response_klass = TestResponse
517

518
        if klass = self.class.controller_class
519
          if klass < ActionController::Live
520
            @response_klass = LiveTestResponse
521
          end
522 523 524 525 526 527 528
          unless @controller
            begin
              @controller = klass.new
            rescue
              warn "could not construct controller #{klass}" if $VERBOSE
            end
          end
529
        end
530

531
        @request          = TestRequest.create
532
        @response         = build_response @response_klass
533 534
        @response.request = @request

535
        if @controller
536 537 538
          @controller.request = @request
          @controller.params = {}
        end
539 540
      end

541 542
      def build_response(klass)
        klass.new
543 544
      end

545 546 547
      included do
        include ActionController::TemplateAssertions
        include ActionDispatch::Assertions
548
        class_attribute :_controller_class
549 550
        setup :setup_controller_request_and_response
      end
551

A
Aaron Patterson 已提交
552
      private
553

554 555 556 557 558 559 560
      def scrub_env!(env)
        env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
        env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
        env['action_dispatch.request.query_parameters'] = {}
        env
      end

561
      def process_with_kwargs(http_method, action, *args)
562
        if kwarg_request?(args)
563 564 565
          args.first.merge!(method: http_method)
          process(action, *args)
        else
566
          non_kwarg_request_warning if args.any?
567 568 569 570 571 572

          args = args.unshift(http_method)
          process(action, *args)
        end
      end

573
      REQUEST_KWARGS = %i(params session flash method body xhr)
574
      def kwarg_request?(args)
575 576 577 578 579 580 581 582 583 584 585 586 587 588
        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 }
589
          process :update, method: :post, params: { id: 1 }
590 591 592
        MSG
      end

593 594 595 596
      def document_root_element
        html_document.root
      end

597 598 599
      def check_required_ivars
        # Sanity check for required instance variables so we can give an
        # understandable error message.
600 601
        [:@routes, :@controller, :@request, :@response].each do |iv_name|
          if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
602 603 604 605
            raise "#{iv_name} is nil: make sure you set it in your test's setup method."
          end
        end
      end
A
Aaron Patterson 已提交
606

607
      def build_request_uri(controller_class_name, action, parameters)
608
        unless @request.env["PATH_INFO"]
609
          options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
610
          options.update(
611
            :controller => controller_class_name,
612 613
            :action => action,
            :relative_url_root => nil,
614
            :_recall => @request.path_parameters)
615

616
          url, query_string = @routes.path_for(options).split("?", 2)
617 618 619 620 621

          @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root
          @request.env["PATH_INFO"] = url
          @request.env["QUERY_STRING"] = query_string || ""
        end
622
      end
623 624

      def html_format?(parameters)
625
        return true unless parameters.key?(:format)
626
        Mime.fetch(parameters[:format]) { Mime['html'] }.html?
627
      end
628
    end
629 630 631

    include Behavior
  end
P
Pratik Naik 已提交
632
end