aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/testing
diff options
context:
space:
mode:
authorJoshua Peek <josh@joshpeek.com>2009-01-27 18:17:39 -0600
committerJoshua Peek <josh@joshpeek.com>2009-01-27 18:17:39 -0600
commita0f2b1d95d3785de92ae271fd7ea23e91c0cadc6 (patch)
treee125027e317889e6402dac147e03fc112c129aec /actionpack/lib/action_controller/testing
parenteb9af20b7cc0e374277cf330bdd404f9daab28ec (diff)
downloadrails-a0f2b1d95d3785de92ae271fd7ea23e91c0cadc6.tar.gz
rails-a0f2b1d95d3785de92ae271fd7ea23e91c0cadc6.tar.bz2
rails-a0f2b1d95d3785de92ae271fd7ea23e91c0cadc6.zip
Reorganize ActionController folder structure
Diffstat (limited to 'actionpack/lib/action_controller/testing')
-rw-r--r--actionpack/lib/action_controller/testing/assertions/dom.rb39
-rw-r--r--actionpack/lib/action_controller/testing/assertions/model.rb21
-rw-r--r--actionpack/lib/action_controller/testing/assertions/response.rb150
-rw-r--r--actionpack/lib/action_controller/testing/assertions/routing.rb146
-rw-r--r--actionpack/lib/action_controller/testing/assertions/selector.rb632
-rw-r--r--actionpack/lib/action_controller/testing/assertions/tag.rb127
-rw-r--r--actionpack/lib/action_controller/testing/integration.rb676
-rw-r--r--actionpack/lib/action_controller/testing/performance.rb15
-rw-r--r--actionpack/lib/action_controller/testing/process.rb543
-rw-r--r--actionpack/lib/action_controller/testing/test_case.rb199
10 files changed, 2548 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/testing/assertions/dom.rb b/actionpack/lib/action_controller/testing/assertions/dom.rb
new file mode 100644
index 0000000000..5ffe5f1883
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/dom.rb
@@ -0,0 +1,39 @@
+module ActionController
+ module Assertions
+ module DomAssertions
+ # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
+ #
+ # ==== Examples
+ #
+ # # assert that the referenced method generates the appropriate HTML string
+ # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
+ #
+ def assert_dom_equal(expected, actual, message = "")
+ clean_backtrace do
+ expected_dom = HTML::Document.new(expected).root
+ actual_dom = HTML::Document.new(actual).root
+ full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s)
+
+ assert_block(full_message) { expected_dom == actual_dom }
+ end
+ end
+
+ # The negated form of +assert_dom_equivalent+.
+ #
+ # ==== Examples
+ #
+ # # assert that the referenced method does not generate the specified HTML string
+ # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
+ #
+ def assert_dom_not_equal(expected, actual, message = "")
+ clean_backtrace do
+ expected_dom = HTML::Document.new(expected).root
+ actual_dom = HTML::Document.new(actual).root
+ full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
+
+ assert_block(full_message) { expected_dom != actual_dom }
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/assertions/model.rb b/actionpack/lib/action_controller/testing/assertions/model.rb
new file mode 100644
index 0000000000..3a7b39b106
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/model.rb
@@ -0,0 +1,21 @@
+module ActionController
+ module Assertions
+ module ModelAssertions
+ # Ensures that the passed record is valid by Active Record standards and
+ # returns any error messages if it is not.
+ #
+ # ==== Examples
+ #
+ # # assert that a newly created record is valid
+ # model = Model.new
+ # assert_valid(model)
+ #
+ def assert_valid(record)
+ ::ActiveSupport::Deprecation.warn("assert_valid is deprecated. Use assert record.valid? instead", caller)
+ clean_backtrace do
+ assert record.valid?, record.errors.full_messages.join("\n")
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/assertions/response.rb b/actionpack/lib/action_controller/testing/assertions/response.rb
new file mode 100644
index 0000000000..5976090273
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/response.rb
@@ -0,0 +1,150 @@
+module ActionController
+ module Assertions
+ # A small suite of assertions that test responses from Rails applications.
+ module ResponseAssertions
+ # Asserts that the response is one of the following types:
+ #
+ # * <tt>:success</tt> - Status code was 200
+ # * <tt>:redirect</tt> - Status code was in the 300-399 range
+ # * <tt>:missing</tt> - Status code was 404
+ # * <tt>:error</tt> - Status code was in the 500-599 range
+ #
+ # You can also pass an explicit status number like assert_response(501)
+ # or its symbolic equivalent assert_response(:not_implemented).
+ # See ActionController::StatusCodes for a full list.
+ #
+ # ==== Examples
+ #
+ # # assert that the response was a redirection
+ # assert_response :redirect
+ #
+ # # assert that the response code was status code 401 (unauthorized)
+ # assert_response 401
+ #
+ def assert_response(type, message = nil)
+ clean_backtrace do
+ if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
+ assert_block("") { true } # to count the assertion
+ elsif type.is_a?(Fixnum) && @response.response_code == type
+ assert_block("") { true } # to count the assertion
+ elsif type.is_a?(Symbol) && @response.response_code == ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[type]
+ assert_block("") { true } # to count the assertion
+ else
+ if @response.error?
+ exception = @response.template.instance_variable_get(:@exception)
+ exception_message = exception && exception.message
+ assert_block(build_message(message, "Expected response to be a <?>, but was <?>\n<?>", type, @response.response_code, exception_message.to_s)) { false }
+ else
+ assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false }
+ end
+ end
+ end
+ end
+
+ # Assert that the redirection options passed in match those of the redirect called in the latest action.
+ # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also
+ # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on.
+ #
+ # ==== Examples
+ #
+ # # assert that the redirection was to the "index" action on the WeblogController
+ # assert_redirected_to :controller => "weblog", :action => "index"
+ #
+ # # assert that the redirection was to the named route login_url
+ # assert_redirected_to login_url
+ #
+ # # assert that the redirection was to the url for @customer
+ # assert_redirected_to @customer
+ #
+ def assert_redirected_to(options = {}, message=nil)
+ clean_backtrace do
+ assert_response(:redirect, message)
+ return true if options == @response.redirected_to
+
+ # Support partial arguments for hash redirections
+ if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
+ return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
+ end
+
+ redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
+ options_after_normalisation = normalize_argument_to_redirection(options)
+
+ if redirected_to_after_normalisation != options_after_normalisation
+ flunk "Expected response to be a redirect to <#{options_after_normalisation}> but was a redirect to <#{redirected_to_after_normalisation}>"
+ end
+ end
+ end
+
+ # Asserts that the request was rendered with the appropriate template file or partials
+ #
+ # ==== Examples
+ #
+ # # assert that the "new" view template was rendered
+ # assert_template "new"
+ #
+ # # assert that the "_customer" partial was rendered twice
+ # assert_template :partial => '_customer', :count => 2
+ #
+ # # assert that no partials were rendered
+ # assert_template :partial => false
+ #
+ def assert_template(options = {}, message = nil)
+ clean_backtrace do
+ case options
+ when NilClass, String
+ rendered = @response.rendered[:template].to_s
+ msg = build_message(message,
+ "expecting <?> but rendering with <?>",
+ options, rendered)
+ assert_block(msg) do
+ if options.nil?
+ @response.rendered[:template].blank?
+ else
+ rendered.to_s.match(options)
+ end
+ end
+ when Hash
+ if expected_partial = options[:partial]
+ partials = @response.rendered[:partials]
+ if expected_count = options[:count]
+ found = partials.detect { |p, _| p.to_s.match(expected_partial) }
+ actual_count = found.nil? ? 0 : found.second
+ msg = build_message(message,
+ "expecting ? to be rendered ? time(s) but rendered ? time(s)",
+ expected_partial, expected_count, actual_count)
+ assert(actual_count == expected_count.to_i, msg)
+ else
+ msg = build_message(message,
+ "expecting partial <?> but action rendered <?>",
+ options[:partial], partials.keys)
+ assert(partials.keys.any? { |p| p.to_s.match(expected_partial) }, msg)
+ end
+ else
+ assert @response.rendered[:partials].empty?,
+ "Expected no partials to be rendered"
+ end
+ end
+ end
+ end
+
+ private
+ # Proxy to to_param if the object will respond to it.
+ def parameterize(value)
+ value.respond_to?(:to_param) ? value.to_param : value
+ end
+
+ def normalize_argument_to_redirection(fragment)
+ after_routing = @controller.url_for(fragment)
+ if after_routing =~ %r{^\w+://.*}
+ after_routing
+ else
+ # FIXME - this should probably get removed.
+ if after_routing.first != '/'
+ after_routing = '/' + after_routing
+ end
+ @request.protocol + @request.host_with_port + after_routing
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/assertions/routing.rb b/actionpack/lib/action_controller/testing/assertions/routing.rb
new file mode 100644
index 0000000000..5101751cea
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/routing.rb
@@ -0,0 +1,146 @@
+module ActionController
+ module Assertions
+ # Suite of assertions to test routes generated by Rails and the handling of requests made to them.
+ module RoutingAssertions
+ # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
+ # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+.
+ #
+ # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
+ # requiring a specific HTTP method. The hash should contain a :path with the incoming request path
+ # and a :method containing the required HTTP verb.
+ #
+ # # assert that POSTing to /items will call the create action on ItemsController
+ # assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}
+ #
+ # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
+ # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
+ # extras argument, appending the query string on the path directly will not work. For example:
+ #
+ # # assert that a path of '/items/list/1?view=print' returns the correct options
+ # assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }
+ #
+ # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
+ #
+ # ==== Examples
+ # # Check the default route (i.e., the index action)
+ # assert_recognizes {:controller => 'items', :action => 'index'}, 'items'
+ #
+ # # Test a specific action
+ # assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list'
+ #
+ # # Test an action with a parameter
+ # assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1'
+ #
+ # # Test a custom route
+ # assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1'
+ #
+ # # Check a Simply RESTful generated route
+ # assert_recognizes list_items_url, 'items/list'
+ def assert_recognizes(expected_options, path, extras={}, message=nil)
+ if path.is_a? Hash
+ request_method = path[:method]
+ path = path[:path]
+ else
+ request_method = nil
+ end
+
+ clean_backtrace do
+ ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
+ request = recognized_request_for(path, request_method)
+
+ expected_options = expected_options.clone
+ extras.each_key { |key| expected_options.delete key } unless extras.nil?
+
+ expected_options.stringify_keys!
+ routing_diff = expected_options.diff(request.path_parameters)
+ msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>",
+ request.path_parameters, expected_options, expected_options.diff(request.path_parameters))
+ assert_block(msg) { request.path_parameters == expected_options }
+ end
+ end
+
+ # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
+ # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
+ # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
+ #
+ # The +defaults+ parameter is unused.
+ #
+ # ==== Examples
+ # # Asserts that the default action is generated for a route with no action
+ # assert_generates "/items", :controller => "items", :action => "index"
+ #
+ # # Tests that the list action is properly routed
+ # assert_generates "/items/list", :controller => "items", :action => "list"
+ #
+ # # Tests the generation of a route with a parameter
+ # assert_generates "/items/list/1", { :controller => "items", :action => "list", :id => "1" }
+ #
+ # # Asserts that the generated route gives us our custom route
+ # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" }
+ def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
+ clean_backtrace do
+ expected_path = "/#{expected_path}" unless expected_path[0] == ?/
+ # Load routes.rb if it hasn't been loaded.
+ ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
+
+ generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults)
+ found_extras = options.reject {|k, v| ! extra_keys.include? k}
+
+ msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
+ assert_block(msg) { found_extras == extras }
+
+ msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
+ expected_path)
+ assert_block(msg) { expected_path == generated_path }
+ end
+ end
+
+ # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
+ # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
+ # and +assert_generates+ into one step.
+ #
+ # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
+ # +message+ parameter allows you to specify a custom error message to display upon failure.
+ #
+ # ==== Examples
+ # # Assert a basic route: a controller with the default action (index)
+ # assert_routing '/home', :controller => 'home', :action => 'index'
+ #
+ # # Test a route generated with a specific controller, action, and parameter (id)
+ # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23
+ #
+ # # Assert a basic route (controller + default action), with an error message if it fails
+ # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly'
+ #
+ # # Tests a route, providing a defaults hash
+ # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"}
+ #
+ # # Tests a route with a HTTP method
+ # assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }
+ def assert_routing(path, options, defaults={}, extras={}, message=nil)
+ assert_recognizes(options, path, extras, message)
+
+ controller, default_controller = options[:controller], defaults[:controller]
+ if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
+ options[:controller] = "/#{controller}"
+ end
+
+ assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message)
+ end
+
+ private
+ # Recognizes the route for a given path.
+ def recognized_request_for(path, request_method = nil)
+ path = "/#{path}" unless path.first == '/'
+
+ # Assume given controller
+ request = ActionController::TestRequest.new
+ request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
+ request.path = path
+
+ ActionController::Routing::Routes.recognize(request)
+ request
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/assertions/selector.rb b/actionpack/lib/action_controller/testing/assertions/selector.rb
new file mode 100644
index 0000000000..0d56ea5ef7
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/selector.rb
@@ -0,0 +1,632 @@
+#--
+# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
+# Under MIT and/or CC By license.
+#++
+
+module ActionController
+ module Assertions
+ unless const_defined?(:NO_STRIP)
+ NO_STRIP = %w{pre script style textarea}
+ end
+
+ # Adds the +assert_select+ method for use in Rails functional
+ # test cases, which can be used to make assertions on the response HTML of a controller
+ # action. You can also call +assert_select+ within another +assert_select+ to
+ # make assertions on elements selected by the enclosing assertion.
+ #
+ # Use +css_select+ to select elements without making an assertions, either
+ # from the response HTML or elements selected by the enclosing assertion.
+ #
+ # In addition to HTML responses, you can make the following assertions:
+ # * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations.
+ # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
+ # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
+ #
+ # Also see HTML::Selector to learn how to use selectors.
+ module SelectorAssertions
+ # :call-seq:
+ # css_select(selector) => array
+ # css_select(element, selector) => array
+ #
+ # Select and return all matching elements.
+ #
+ # If called with a single argument, uses that argument as a selector
+ # to match all elements of the current page. Returns an empty array
+ # if no match is found.
+ #
+ # If called with two arguments, uses the first argument as the base
+ # element and the second argument as the selector. Attempts to match the
+ # base element and any of its children. Returns an empty array if no
+ # match is found.
+ #
+ # The selector may be a CSS selector expression (String), an expression
+ # with substitution values (Array) or an HTML::Selector object.
+ #
+ # ==== Examples
+ # # Selects all div tags
+ # divs = css_select("div")
+ #
+ # # Selects all paragraph tags and does something interesting
+ # pars = css_select("p")
+ # pars.each do |par|
+ # # Do something fun with paragraphs here...
+ # end
+ #
+ # # Selects all list items in unordered lists
+ # items = css_select("ul>li")
+ #
+ # # Selects all form tags and then all inputs inside the form
+ # forms = css_select("form")
+ # forms.each do |form|
+ # inputs = css_select(form, "input")
+ # ...
+ # end
+ #
+ def css_select(*args)
+ # See assert_select to understand what's going on here.
+ arg = args.shift
+
+ if arg.is_a?(HTML::Node)
+ root = arg
+ arg = args.shift
+ elsif arg == nil
+ raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
+ elsif @selected
+ matches = []
+
+ @selected.each do |selected|
+ subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
+ subset.each do |match|
+ matches << match unless matches.any? { |m| m.equal?(match) }
+ end
+ end
+
+ return matches
+ else
+ root = response_from_page_or_rjs
+ end
+
+ case arg
+ when String
+ selector = HTML::Selector.new(arg, args)
+ when Array
+ selector = HTML::Selector.new(*arg)
+ when HTML::Selector
+ selector = arg
+ else raise ArgumentError, "Expecting a selector as the first argument"
+ end
+
+ selector.select(root)
+ end
+
+ # :call-seq:
+ # assert_select(selector, equality?, message?)
+ # assert_select(element, selector, equality?, message?)
+ #
+ # An assertion that selects elements and makes one or more equality tests.
+ #
+ # If the first argument is an element, selects all matching elements
+ # starting from (and including) that element and all its children in
+ # depth-first order.
+ #
+ # If no element if specified, calling +assert_select+ selects from the
+ # response HTML unless +assert_select+ is called from within an +assert_select+ block.
+ #
+ # When called with a block +assert_select+ passes an array of selected elements
+ # to the block. Calling +assert_select+ from the block, with no element specified,
+ # runs the assertion on the complete set of elements selected by the enclosing assertion.
+ # Alternatively the array may be iterated through so that +assert_select+ can be called
+ # separately for each element.
+ #
+ #
+ # ==== Example
+ # If the response contains two ordered lists, each with four list elements then:
+ # assert_select "ol" do |elements|
+ # elements.each do |element|
+ # assert_select element, "li", 4
+ # end
+ # end
+ #
+ # will pass, as will:
+ # assert_select "ol" do
+ # assert_select "li", 8
+ # end
+ #
+ # The selector may be a CSS selector expression (String), an expression
+ # with substitution values, or an HTML::Selector object.
+ #
+ # === Equality Tests
+ #
+ # The equality test may be one of the following:
+ # * <tt>true</tt> - Assertion is true if at least one element selected.
+ # * <tt>false</tt> - Assertion is true if no element selected.
+ # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
+ # one element matches the string or regular expression.
+ # * <tt>Integer</tt> - Assertion is true if exactly that number of
+ # elements are selected.
+ # * <tt>Range</tt> - Assertion is true if the number of selected
+ # elements fit the range.
+ # If no equality test specified, the assertion is true if at least one
+ # element selected.
+ #
+ # To perform more than one equality tests, use a hash with the following keys:
+ # * <tt>:text</tt> - Narrow the selection to elements that have this text
+ # value (string or regexp).
+ # * <tt>:html</tt> - Narrow the selection to elements that have this HTML
+ # content (string or regexp).
+ # * <tt>:count</tt> - Assertion is true if the number of selected elements
+ # is equal to this value.
+ # * <tt>:minimum</tt> - Assertion is true if the number of selected
+ # elements is at least this value.
+ # * <tt>:maximum</tt> - Assertion is true if the number of selected
+ # elements is at most this value.
+ #
+ # If the method is called with a block, once all equality tests are
+ # evaluated the block is called with an array of all matched elements.
+ #
+ # ==== Examples
+ #
+ # # At least one form element
+ # assert_select "form"
+ #
+ # # Form element includes four input fields
+ # assert_select "form input", 4
+ #
+ # # Page title is "Welcome"
+ # assert_select "title", "Welcome"
+ #
+ # # Page title is "Welcome" and there is only one title element
+ # assert_select "title", {:count=>1, :text=>"Welcome"},
+ # "Wrong title or more than one title element"
+ #
+ # # Page contains no forms
+ # assert_select "form", false, "This page must contain no forms"
+ #
+ # # Test the content and style
+ # assert_select "body div.header ul.menu"
+ #
+ # # Use substitution values
+ # assert_select "ol>li#?", /item-\d+/
+ #
+ # # All input fields in the form have a name
+ # assert_select "form input" do
+ # assert_select "[name=?]", /.+/ # Not empty
+ # end
+ def assert_select(*args, &block)
+ # Start with optional element followed by mandatory selector.
+ arg = args.shift
+
+ if arg.is_a?(HTML::Node)
+ # First argument is a node (tag or text, but also HTML root),
+ # so we know what we're selecting from.
+ root = arg
+ arg = args.shift
+ elsif arg == nil
+ # This usually happens when passing a node/element that
+ # happens to be nil.
+ raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
+ elsif @selected
+ root = HTML::Node.new(nil)
+ root.children.concat @selected
+ else
+ # Otherwise just operate on the response document.
+ root = response_from_page_or_rjs
+ end
+
+ # First or second argument is the selector: string and we pass
+ # all remaining arguments. Array and we pass the argument. Also
+ # accepts selector itself.
+ case arg
+ when String
+ selector = HTML::Selector.new(arg, args)
+ when Array
+ selector = HTML::Selector.new(*arg)
+ when HTML::Selector
+ selector = arg
+ else raise ArgumentError, "Expecting a selector as the first argument"
+ end
+
+ # Next argument is used for equality tests.
+ equals = {}
+ case arg = args.shift
+ when Hash
+ equals = arg
+ when String, Regexp
+ equals[:text] = arg
+ when Integer
+ equals[:count] = arg
+ when Range
+ equals[:minimum] = arg.begin
+ equals[:maximum] = arg.end
+ when FalseClass
+ equals[:count] = 0
+ when NilClass, TrueClass
+ equals[:minimum] = 1
+ else raise ArgumentError, "I don't understand what you're trying to match"
+ end
+
+ # By default we're looking for at least one match.
+ if equals[:count]
+ equals[:minimum] = equals[:maximum] = equals[:count]
+ else
+ equals[:minimum] = 1 unless equals[:minimum]
+ end
+
+ # Last argument is the message we use if the assertion fails.
+ message = args.shift
+ #- message = "No match made with selector #{selector.inspect}" unless message
+ if args.shift
+ raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
+ end
+
+ matches = selector.select(root)
+ # If text/html, narrow down to those elements that match it.
+ content_mismatch = nil
+ if match_with = equals[:text]
+ matches.delete_if do |match|
+ text = ""
+ text.force_encoding(match_with.encoding) if text.respond_to?(:force_encoding)
+ stack = match.children.reverse
+ while node = stack.pop
+ if node.tag?
+ stack.concat node.children.reverse
+ else
+ content = node.content
+ content.force_encoding(match_with.encoding) if content.respond_to?(:force_encoding)
+ text << content
+ end
+ end
+ text.strip! unless NO_STRIP.include?(match.name)
+ unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
+ content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text)
+ true
+ end
+ end
+ elsif match_with = equals[:html]
+ matches.delete_if do |match|
+ html = match.children.map(&:to_s).join
+ html.strip! unless NO_STRIP.include?(match.name)
+ unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
+ content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html)
+ true
+ end
+ end
+ end
+ # Expecting foo found bar element only if found zero, not if
+ # found one but expecting two.
+ message ||= content_mismatch if matches.empty?
+ # Test minimum/maximum occurrence.
+ min, max = equals[:minimum], equals[:maximum]
+ message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.)
+ assert matches.size >= min, message if min
+ assert matches.size <= max, message if max
+
+ # If a block is given call that block. Set @selected to allow
+ # nested assert_select, which can be nested several levels deep.
+ if block_given? && !matches.empty?
+ begin
+ in_scope, @selected = @selected, matches
+ yield matches
+ ensure
+ @selected = in_scope
+ end
+ end
+
+ # Returns all matches elements.
+ matches
+ end
+
+ def count_description(min, max) #:nodoc:
+ pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
+
+ if min && max && (max != min)
+ "between #{min} and #{max} elements"
+ elsif min && !(min == 1 && max == 1)
+ "at least #{min} #{pluralize['element', min]}"
+ elsif max
+ "at most #{max} #{pluralize['element', max]}"
+ end
+ end
+
+ # :call-seq:
+ # assert_select_rjs(id?) { |elements| ... }
+ # assert_select_rjs(statement, id?) { |elements| ... }
+ # assert_select_rjs(:insert, position, id?) { |elements| ... }
+ #
+ # Selects content from the RJS response.
+ #
+ # === Narrowing down
+ #
+ # With no arguments, asserts that one or more elements are updated or
+ # inserted by RJS statements.
+ #
+ # Use the +id+ argument to narrow down the assertion to only statements
+ # that update or insert an element with that identifier.
+ #
+ # Use the first argument to narrow down assertions to only statements
+ # of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>,
+ # <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and
+ # <tt>:insert_html</tt>.
+ #
+ # Use the argument <tt>:insert</tt> followed by an insertion position to narrow
+ # down the assertion to only statements that insert elements in that
+ # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt>
+ # and <tt>:after</tt>.
+ #
+ # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will
+ # be ignored as there is no HTML passed for this statement.
+ #
+ # === Using blocks
+ #
+ # Without a block, +assert_select_rjs+ merely asserts that the response
+ # contains one or more RJS statements that replace or update content.
+ #
+ # With a block, +assert_select_rjs+ also selects all elements used in
+ # these statements and passes them to the block. Nested assertions are
+ # supported.
+ #
+ # Calling +assert_select_rjs+ with no arguments and using nested asserts
+ # asserts that the HTML content is returned by one or more RJS statements.
+ # Using +assert_select+ directly makes the same assertion on the content,
+ # but without distinguishing whether the content is returned in an HTML
+ # or JavaScript.
+ #
+ # ==== Examples
+ #
+ # # Replacing the element foo.
+ # # page.replace 'foo', ...
+ # assert_select_rjs :replace, "foo"
+ #
+ # # Replacing with the chained RJS proxy.
+ # # page[:foo].replace ...
+ # assert_select_rjs :chained_replace, 'foo'
+ #
+ # # Inserting into the element bar, top position.
+ # assert_select_rjs :insert, :top, "bar"
+ #
+ # # Remove the element bar
+ # assert_select_rjs :remove, "bar"
+ #
+ # # Changing the element foo, with an image.
+ # assert_select_rjs "foo" do
+ # assert_select "img[src=/images/logo.gif""
+ # end
+ #
+ # # RJS inserts or updates a list with four items.
+ # assert_select_rjs do
+ # assert_select "ol>li", 4
+ # end
+ #
+ # # The same, but shorter.
+ # assert_select "ol>li", 4
+ def assert_select_rjs(*args, &block)
+ rjs_type = args.first.is_a?(Symbol) ? args.shift : nil
+ id = args.first.is_a?(String) ? args.shift : nil
+
+ # If the first argument is a symbol, it's the type of RJS statement we're looking
+ # for (update, replace, insertion, etc). Otherwise, we're looking for just about
+ # any RJS statement.
+ if rjs_type
+ if rjs_type == :insert
+ position = args.shift
+ id = args.shift
+ insertion = "insert_#{position}".to_sym
+ raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion]
+ statement = "(#{RJS_STATEMENTS[insertion]})"
+ else
+ raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type]
+ statement = "(#{RJS_STATEMENTS[rjs_type]})"
+ end
+ else
+ statement = "#{RJS_STATEMENTS[:any]}"
+ end
+
+ # Next argument we're looking for is the element identifier. If missing, we pick
+ # any element, otherwise we replace it in the statement.
+ pattern = Regexp.new(
+ id ? statement.gsub(RJS_ANY_ID, "\"#{id}\"") : statement
+ )
+
+ # Duplicate the body since the next step involves destroying it.
+ matches = nil
+ case rjs_type
+ when :remove, :show, :hide, :toggle
+ matches = @response.body.match(pattern)
+ else
+ @response.body.gsub(pattern) do |match|
+ html = unescape_rjs(match)
+ matches ||= []
+ matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
+ ""
+ end
+ end
+
+ if matches
+ assert_block("") { true } # to count the assertion
+ if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type)
+ begin
+ in_scope, @selected = @selected, matches
+ yield matches
+ ensure
+ @selected = in_scope
+ end
+ end
+ matches
+ else
+ # RJS statement not found.
+ case rjs_type
+ when :remove, :show, :hide, :toggle
+ flunk_message = "No RJS statement that #{rjs_type.to_s}s '#{id}' was rendered."
+ else
+ flunk_message = "No RJS statement that replaces or inserts HTML content."
+ end
+ flunk args.shift || flunk_message
+ end
+ end
+
+ # :call-seq:
+ # assert_select_encoded(element?) { |elements| ... }
+ #
+ # Extracts the content of an element, treats it as encoded HTML and runs
+ # nested assertion on it.
+ #
+ # You typically call this method within another assertion to operate on
+ # all currently selected elements. You can also pass an element or array
+ # of elements.
+ #
+ # The content of each element is un-encoded, and wrapped in the root
+ # element +encoded+. It then calls the block with all un-encoded elements.
+ #
+ # ==== Examples
+ # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix)
+ # assert_select_feed :atom, 1.0 do
+ # # Select each entry item and then the title item
+ # assert_select "entry>title" do
+ # # Run assertions on the encoded title elements
+ # assert_select_encoded do
+ # assert_select "b"
+ # end
+ # end
+ # end
+ #
+ #
+ # # Selects all paragraph tags from within the description of an RSS feed
+ # assert_select_feed :rss, 2.0 do
+ # # Select description element of each feed item.
+ # assert_select "channel>item>description" do
+ # # Run assertions on the encoded elements.
+ # assert_select_encoded do
+ # assert_select "p"
+ # end
+ # end
+ # end
+ def assert_select_encoded(element = nil, &block)
+ case element
+ when Array
+ elements = element
+ when HTML::Node
+ elements = [element]
+ when nil
+ unless elements = @selected
+ raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
+ end
+ else
+ raise ArgumentError, "Argument is optional, and may be node or array of nodes"
+ end
+
+ fix_content = lambda do |node|
+ # Gets around a bug in the Rails 1.1 HTML parser.
+ node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) }
+ end
+
+ selected = elements.map do |element|
+ text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
+ root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
+ css_select(root, "encoded:root", &block)[0]
+ end
+
+ begin
+ old_selected, @selected = @selected, selected
+ assert_select ":root", &block
+ ensure
+ @selected = old_selected
+ end
+ end
+
+ # :call-seq:
+ # assert_select_email { }
+ #
+ # Extracts the body of an email and runs nested assertions on it.
+ #
+ # You must enable deliveries for this assertion to work, use:
+ # ActionMailer::Base.perform_deliveries = true
+ #
+ # ==== Examples
+ #
+ # assert_select_email do
+ # assert_select "h1", "Email alert"
+ # end
+ #
+ # assert_select_email do
+ # items = assert_select "ol>li"
+ # items.each do
+ # # Work with items here...
+ # end
+ # end
+ #
+ def assert_select_email(&block)
+ deliveries = ActionMailer::Base.deliveries
+ assert !deliveries.empty?, "No e-mail in delivery list"
+
+ for delivery in deliveries
+ for part in delivery.parts
+ if part["Content-Type"].to_s =~ /^text\/html\W/
+ root = HTML::Document.new(part.body).root
+ assert_select root, ":root", &block
+ end
+ end
+ end
+ end
+
+ protected
+ unless const_defined?(:RJS_STATEMENTS)
+ RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\""
+ RJS_ANY_ID = "\"([^\"])*\""
+ RJS_STATEMENTS = {
+ :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)",
+ :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)",
+ :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)",
+ :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)"
+ }
+ [:remove, :show, :hide, :toggle].each do |action|
+ RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)"
+ end
+ RJS_INSERTIONS = ["top", "bottom", "before", "after"]
+ RJS_INSERTIONS.each do |insertion|
+ RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)"
+ end
+ RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)"
+ RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})")
+ RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/
+ end
+
+ # +assert_select+ and +css_select+ call this to obtain the content in the HTML
+ # page, or from all the RJS statements, depending on the type of response.
+ def response_from_page_or_rjs()
+ content_type = @response.content_type
+
+ if content_type && Mime::JS =~ content_type
+ body = @response.body.dup
+ root = HTML::Node.new(nil)
+
+ while true
+ next if body.sub!(RJS_STATEMENTS[:any]) do |match|
+ html = unescape_rjs(match)
+ matches = HTML::Document.new(html).root.children.select { |n| n.tag? }
+ root.children.concat matches
+ ""
+ end
+ break
+ end
+
+ root
+ else
+ html_document.root
+ end
+ end
+
+ # Unescapes a RJS string.
+ def unescape_rjs(rjs_string)
+ # RJS encodes double quotes and line breaks.
+ unescaped= rjs_string.gsub('\"', '"')
+ unescaped.gsub!(/\\\//, '/')
+ unescaped.gsub!('\n', "\n")
+ unescaped.gsub!('\076', '>')
+ unescaped.gsub!('\074', '<')
+ # RJS encodes non-ascii characters.
+ unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')}
+ unescaped
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/assertions/tag.rb b/actionpack/lib/action_controller/testing/assertions/tag.rb
new file mode 100644
index 0000000000..80249e0e83
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/assertions/tag.rb
@@ -0,0 +1,127 @@
+module ActionController
+ module Assertions
+ # Pair of assertions to testing elements in the HTML output of the response.
+ module TagAssertions
+ # Asserts that there is a tag/node/element in the body of the response
+ # that meets all of the given conditions. The +conditions+ parameter must
+ # be a hash of any of the following keys (all are optional):
+ #
+ # * <tt>:tag</tt>: the node type must match the corresponding value
+ # * <tt>:attributes</tt>: a hash. The node's attributes must match the
+ # corresponding values in the hash.
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
+ # corresponding hash.
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
+ # must meet the criteria described by the hash.
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
+ # meet the criteria described by the hash.
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
+ # must meet the criteria described by the hash.
+ # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
+ # meet the criteria described by the hash.
+ # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
+ # the keys:
+ # * <tt>:count</tt>: either a number or a range which must equal (or
+ # include) the number of children that match.
+ # * <tt>:less_than</tt>: the number of matching children must be less
+ # than this number.
+ # * <tt>:greater_than</tt>: the number of matching children must be
+ # greater than this number.
+ # * <tt>:only</tt>: another hash consisting of the keys to use
+ # to match on the children, and only matching children will be
+ # counted.
+ # * <tt>:content</tt>: the textual content of the node must match the
+ # given value. This will not match HTML tags in the body of a
+ # tag--only text.
+ #
+ # Conditions are matched using the following algorithm:
+ #
+ # * if the condition is a string, it must be a substring of the value.
+ # * if the condition is a regexp, it must match the value.
+ # * if the condition is a number, the value must match number.to_s.
+ # * if the condition is +true+, the value must not be +nil+.
+ # * if the condition is +false+ or +nil+, the value must be +nil+.
+ #
+ # === Examples
+ #
+ # # Assert that there is a "span" tag
+ # assert_tag :tag => "span"
+ #
+ # # Assert that there is a "span" tag with id="x"
+ # assert_tag :tag => "span", :attributes => { :id => "x" }
+ #
+ # # Assert that there is a "span" tag using the short-hand
+ # assert_tag :span
+ #
+ # # Assert that there is a "span" tag with id="x" using the short-hand
+ # assert_tag :span, :attributes => { :id => "x" }
+ #
+ # # Assert that there is a "span" inside of a "div"
+ # assert_tag :tag => "span", :parent => { :tag => "div" }
+ #
+ # # Assert that there is a "span" somewhere inside a table
+ # assert_tag :tag => "span", :ancestor => { :tag => "table" }
+ #
+ # # Assert that there is a "span" with at least one "em" child
+ # assert_tag :tag => "span", :child => { :tag => "em" }
+ #
+ # # Assert that there is a "span" containing a (possibly nested)
+ # # "strong" tag.
+ # assert_tag :tag => "span", :descendant => { :tag => "strong" }
+ #
+ # # Assert that there is a "span" containing between 2 and 4 "em" tags
+ # # as immediate children
+ # assert_tag :tag => "span",
+ # :children => { :count => 2..4, :only => { :tag => "em" } }
+ #
+ # # Get funky: assert that there is a "div", with an "ul" ancestor
+ # # and an "li" parent (with "class" = "enum"), and containing a
+ # # "span" descendant that contains text matching /hello world/
+ # assert_tag :tag => "div",
+ # :ancestor => { :tag => "ul" },
+ # :parent => { :tag => "li",
+ # :attributes => { :class => "enum" } },
+ # :descendant => { :tag => "span",
+ # :child => /hello world/ }
+ #
+ # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
+ # with well-formed XHTML. They recognize a few tags as implicitly self-closing
+ # (like br and hr and such) but will not work correctly with tags
+ # that allow optional closing tags (p, li, td). <em>You must explicitly
+ # close all of your tags to use these assertions.</em>
+ def assert_tag(*opts)
+ clean_backtrace do
+ opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
+ tag = find_tag(opts)
+ assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
+ end
+ end
+
+ # Identical to +assert_tag+, but asserts that a matching tag does _not_
+ # exist. (See +assert_tag+ for a full discussion of the syntax.)
+ #
+ # === Examples
+ # # Assert that there is not a "div" containing a "p"
+ # assert_no_tag :tag => "div", :descendant => { :tag => "p" }
+ #
+ # # Assert that an unordered list is empty
+ # assert_no_tag :tag => "ul", :descendant => { :tag => "li" }
+ #
+ # # Assert that there is not a "p" tag with between 1 to 3 "img" tags
+ # # as immediate children
+ # assert_no_tag :tag => "p",
+ # :children => { :count => 1..3, :only => { :tag => "img" } }
+ def assert_no_tag(*opts)
+ clean_backtrace do
+ opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
+ tag = find_tag(opts)
+ assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb
new file mode 100644
index 0000000000..163ba84a3e
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/integration.rb
@@ -0,0 +1,676 @@
+require 'stringio'
+require 'uri'
+require 'active_support/test_case'
+
+module ActionController
+ module Integration #:nodoc:
+ # An integration Session instance represents a set of requests and responses
+ # performed sequentially by some virtual user. Becase 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
+ include Test::Unit::Assertions
+ 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
+
+ # The status message that accompanied the status code of the last request.
+ attr_reader :status_message
+
+ # The URI of the last request.
+ attr_reader :path
+
+ # 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.
+ attr_reader :cookies
+
+ # A map of the headers returned by the last response.
+ attr_reader :headers
+
+ # 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
+
+ class MultiPartNeededException < Exception
+ end
+
+ # Create and initialize a new Session instance.
+ def initialize(app = nil)
+ @application = app || ActionController::Dispatcher.new
+ 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!
+ @status = @path = @headers = nil
+ @result = @status_message = nil
+ @https = false
+ @cookies = {}
+ @controller = @request = @response = nil
+ @request_count = 0
+
+ self.host = "www.example.com"
+ 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 = class << self; self; end
+ 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 *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
+
+ # 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(interpret_uri(headers['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)
+ send(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
+
+ # Returns +true+ if the last response was a redirect.
+ def redirect?
+ status/100 == 3
+ end
+
+ # 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 ActionController::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['X-Requested-With'] = 'XMLHttpRequest'
+ headers['Accept'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ process(request_method, path, parameters, headers)
+ end
+ alias xhr :xml_http_request
+
+ # 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
+ # Tailors the session based on the given URI, setting the HTTPS value
+ # and the hostname.
+ def interpret_uri(path)
+ location = URI.parse(path)
+ https! URI::HTTPS === location if location.scheme
+ host! location.host if location.host
+ location.query ? "#{location.path}?#{location.query}" : location.path
+ end
+
+ # 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
+ end
+
+ env["QUERY_STRING"] ||= ""
+
+ data = data.is_a?(IO) ? data : StringIO.new(data || '')
+
+ 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,
+
+ "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 = Rack::Lint.new(@application)
+
+ status, headers, body = app.call(env)
+ @request_count += 1
+
+ @html_document = nil
+
+ @status = status.to_i
+ @status_message = StatusCodes::STATUS_CODES[@status]
+
+ @headers = Rack::Utils::HeaderHash.new(headers)
+
+ (@headers['Set-Cookie'] || []).each do |cookie|
+ name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
+ @cookies[name] = value
+ end
+
+ @body = ""
+ body.each { |part| @body << part }
+
+ 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 = Response.new
+ @response.status = status.to_s
+ @response.headers.replace(@headers)
+ @response.body = @body
+ end
+
+ # Decorate the response with the standard behavior of the
+ # TestResponse so that things like assert_response can be
+ # used in integration tests.
+ @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
+ def generic_url_rewriter
+ env = {
+ 'REQUEST_METHOD' => "GET",
+ 'QUERY_STRING' => "",
+ "REQUEST_URI" => "/",
+ "HTTP_HOST" => host,
+ "SERVER_PORT" => https? ? "443" : "80",
+ "HTTPS" => https? ? "on" : "off"
+ }
+ UrlRewriter.new(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
+ # can capture the controller used to satisfy a request.
+ module ControllerCapture #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ base.class_eval do
+ class << self
+ alias_method_chain :new, :capture
+ end
+ end
+ end
+
+ module ClassMethods #:nodoc:
+ mattr_accessor :last_instantiation
+
+ def clear_last_instantiation!
+ self.last_instantiation = nil
+ end
+
+ def new_with_capture(*args)
+ controller = new_without_capture(*args)
+ self.last_instantiation ||= controller
+ controller
+ end
+ end
+ end
+
+ module Runner
+ # 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(application = nil)
+ session = Integration::Session.new(application)
+
+ # 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
+
+ # Work around a bug in test/unit caused by the default test being named
+ # as a symbol (:default_test), which causes regex test filters
+ # (like "ruby test.rb -n /foo/") to fail because =~ doesn't work on
+ # symbols.
+ def initialize(name) #:nodoc:
+ super(name.to_s)
+ end
+
+ # Work around test/unit's requirement that every subclass of TestCase have
+ # at least one test method. Note that this implementation extends to all
+ # subclasses, as well, so subclasses of IntegrationTest may also exist
+ # without any test methods.
+ def run(*args) #:nodoc:
+ return if @method_name == "default_test"
+ super
+ end
+
+ # Because of how use_instantiated_fixtures and use_transactional_fixtures
+ # are defined, we need to treat them as special cases. Otherwise, users
+ # would potentially have to set their values for both Test::Unit::TestCase
+ # ActionController::IntegrationTest, since by the time the value is set on
+ # TestCase, IntegrationTest has already been defined and cannot inherit
+ # changes to those variables. So, we make those two attributes
+ # copy-on-write.
+
+ class << self
+ def use_transactional_fixtures=(flag) #:nodoc:
+ @_use_transactional_fixtures = true
+ @use_transactional_fixtures = flag
+ end
+
+ def use_instantiated_fixtures=(flag) #:nodoc:
+ @_use_instantiated_fixtures = true
+ @use_instantiated_fixtures = flag
+ end
+
+ def use_transactional_fixtures #:nodoc:
+ @_use_transactional_fixtures ?
+ @use_transactional_fixtures :
+ superclass.use_transactional_fixtures
+ end
+
+ def use_instantiated_fixtures #:nodoc:
+ @_use_instantiated_fixtures ?
+ @use_instantiated_fixtures :
+ superclass.use_instantiated_fixtures
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/performance.rb b/actionpack/lib/action_controller/testing/performance.rb
new file mode 100644
index 0000000000..d88180087d
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/performance.rb
@@ -0,0 +1,15 @@
+require 'active_support/testing/performance'
+require 'active_support/testing/default'
+
+module ActionController
+ # 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 < ActionController::IntegrationTest
+ include ActiveSupport::Testing::Performance
+ include ActiveSupport::Testing::Default
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb
new file mode 100644
index 0000000000..22b97fc157
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/process.rb
@@ -0,0 +1,543 @@
+module ActionController #:nodoc:
+ class TestRequest < Request #:nodoc:
+ attr_accessor :cookies, :session_options
+ attr_accessor :query_parameters, :path, :session
+ attr_accessor :host
+
+ def initialize
+ super(Rack::MockRequest.env_for("/"))
+
+ @query_parameters = {}
+ @session = TestSession.new
+
+ initialize_default_values
+ initialize_containers
+ end
+
+ def reset_session
+ @session = TestSession.new
+ end
+
+ # Wraps raw_post in a StringIO.
+ def body_stream #:nodoc:
+ StringIO.new(raw_post)
+ end
+
+ # Either the RAW_POST_DATA environment variable or the URL-encoded request
+ # parameters.
+ def raw_post
+ @env['RAW_POST_DATA'] ||= begin
+ data = url_encoded_request_parameters
+ data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding)
+ data
+ end
+ end
+
+ def port=(number)
+ @env["SERVER_PORT"] = number.to_i
+ port(true)
+ end
+
+ def action=(action_name)
+ @query_parameters.update({ "action" => action_name })
+ @parameters = nil
+ end
+
+ # Used to check AbstractRequest's request_uri functionality.
+ # Disables the use of @path and @request_uri so superclass can handle those.
+ def set_REQUEST_URI(value)
+ @env["REQUEST_URI"] = value
+ @request_uri = nil
+ @path = nil
+ request_uri(true)
+ path(true)
+ end
+
+ def request_uri=(uri)
+ @request_uri = uri
+ @path = uri.split("?").first
+ end
+
+ def accept=(mime_types)
+ @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
+ accepts(true)
+ end
+
+ def if_modified_since=(last_modified)
+ @env["HTTP_IF_MODIFIED_SINCE"] = last_modified
+ end
+
+ def if_none_match=(etag)
+ @env["HTTP_IF_NONE_MATCH"] = etag
+ end
+
+ def remote_addr=(addr)
+ @env['REMOTE_ADDR'] = addr
+ end
+
+ def request_uri(*args)
+ @request_uri || super
+ end
+
+ def path(*args)
+ @path || super
+ end
+
+ def assign_parameters(controller_path, action, parameters)
+ parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
+ extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
+ non_path_parameters = get? ? query_parameters : request_parameters
+ parameters.each do |key, value|
+ if value.is_a? Fixnum
+ value = value.to_s
+ elsif value.is_a? Array
+ value = ActionController::Routing::PathSegment::Result.new(value)
+ end
+
+ if extra_keys.include?(key.to_sym)
+ non_path_parameters[key] = value
+ else
+ path_parameters[key.to_s] = value
+ end
+ end
+ raw_post # populate env['RAW_POST_DATA']
+ @parameters = nil # reset TestRequest#parameters to use the new path_parameters
+ end
+
+ def recycle!
+ self.query_parameters = {}
+ self.path_parameters = {}
+ unmemoize_all
+ end
+
+ def user_agent=(user_agent)
+ @env['HTTP_USER_AGENT'] = user_agent
+ end
+
+ private
+ def initialize_containers
+ @cookies = {}
+ end
+
+ def initialize_default_values
+ @host = "test.host"
+ @request_uri = "/"
+ @env['HTTP_USER_AGENT'] = "Rails Testing"
+ @env['REMOTE_ADDR'] = "0.0.0.0"
+ @env["SERVER_PORT"] = 80
+ @env['REQUEST_METHOD'] = "GET"
+ end
+
+ def url_encoded_request_parameters
+ params = self.request_parameters.dup
+
+ %w(controller action only_path).each do |k|
+ params.delete(k)
+ params.delete(k.to_sym)
+ end
+
+ params.to_query
+ end
+ end
+
+ # A refactoring of TestResponse to allow the same behavior to be applied
+ # to the "real" CgiResponse class in integration tests.
+ module TestResponseBehavior #:nodoc:
+ # The response code of the request
+ def response_code
+ status.to_s[0,3].to_i rescue 0
+ end
+
+ # Returns a String to ensure compatibility with Net::HTTPResponse
+ def code
+ status.to_s.split(' ')[0]
+ end
+
+ def message
+ status.to_s.split(' ',2)[1]
+ end
+
+ # Was the response successful?
+ def success?
+ (200..299).include?(response_code)
+ end
+
+ # Was the URL not found?
+ def missing?
+ response_code == 404
+ end
+
+ # Were we redirected?
+ def redirect?
+ (300..399).include?(response_code)
+ end
+
+ # Was there a server-side error?
+ def error?
+ (500..599).include?(response_code)
+ end
+
+ alias_method :server_error?, :error?
+
+ # Was there a client client?
+ def client_error?
+ (400..499).include?(response_code)
+ end
+
+ # Returns the redirection location or nil
+ def redirect_url
+ headers['Location']
+ end
+
+ # Does the redirect location match this regexp pattern?
+ def redirect_url_match?( pattern )
+ return false if redirect_url.nil?
+ p = Regexp.new(pattern) if pattern.class == String
+ p = pattern if pattern.class == Regexp
+ return false if p.nil?
+ p.match(redirect_url) != nil
+ end
+
+ # Returns the template of the file which was used to
+ # render this response (or nil)
+ def rendered
+ template.instance_variable_get(:@_rendered)
+ end
+
+ # A shortcut to the flash. Returns an empty hash if no session flash exists.
+ def flash
+ session['flash'] || {}
+ end
+
+ # Do we have a flash?
+ def has_flash?
+ !flash.empty?
+ end
+
+ # Do we have a flash that has contents?
+ def has_flash_with_contents?
+ !flash.empty?
+ end
+
+ # Does the specified flash object exist?
+ def has_flash_object?(name=nil)
+ !flash[name].nil?
+ end
+
+ # Does the specified object exist in the session?
+ def has_session_object?(name=nil)
+ !session[name].nil?
+ end
+
+ # A shortcut to the template.assigns
+ def template_objects
+ template.assigns || {}
+ end
+
+ # Does the specified template object exist?
+ def has_template_object?(name=nil)
+ !template_objects[name].nil?
+ end
+
+ # Returns the response cookies, converted to a Hash of (name => value) pairs
+ #
+ # assert_equal 'AuthorOfNewPage', r.cookies['author']
+ def cookies
+ cookies = {}
+ Array(headers['Set-Cookie']).each do |cookie|
+ key, value = cookie.split(";").first.split("=")
+ cookies[key] = value
+ end
+ cookies
+ end
+
+ # Returns binary content (downloadable file), converted to a String
+ def binary_content
+ raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc)
+ require 'stringio'
+
+ sio = StringIO.new
+ body.call(self, sio)
+
+ sio.rewind
+ sio.read
+ end
+ end
+
+ # Integration test methods such as ActionController::Integration::Session#get
+ # and ActionController::Integration::Session#post return objects of class
+ # TestResponse, which represent the HTTP response results of the requested
+ # controller actions.
+ #
+ # See Response for more information on controller response objects.
+ class TestResponse < Response
+ include TestResponseBehavior
+
+ def recycle!
+ headers.delete('ETag')
+ headers.delete('Last-Modified')
+ end
+ end
+
+ class TestSession #:nodoc:
+ attr_accessor :session_id
+
+ def initialize(attributes = nil)
+ @session_id = ''
+ @attributes = attributes.nil? ? nil : attributes.stringify_keys
+ @saved_attributes = nil
+ end
+
+ def data
+ @attributes ||= @saved_attributes || {}
+ end
+
+ def [](key)
+ data[key.to_s]
+ end
+
+ def []=(key, value)
+ data[key.to_s] = value
+ end
+
+ def update
+ @saved_attributes = @attributes
+ end
+
+ def delete
+ @attributes = nil
+ end
+
+ def close
+ update
+ delete
+ end
+ end
+
+ # Essentially generates a modified Tempfile object similar to the object
+ # you'd get from the standard library CGI module in a multipart
+ # request. This means you can use an ActionController::TestUploadedFile
+ # object in the params of a test request in order to simulate
+ # a file upload.
+ #
+ # Usage example, within a functional test:
+ # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
+ #
+ # 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
+
+ module TestProcess
+ def self.included(base)
+ # Executes a request simulating GET HTTP method and set/volley the response
+ def get(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "GET")
+ end
+
+ # Executes a request simulating POST HTTP method and set/volley the response
+ def post(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "POST")
+ end
+
+ # Executes a request simulating PUT HTTP method and set/volley the response
+ def put(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "PUT")
+ end
+
+ # Executes a request simulating DELETE HTTP method and set/volley the response
+ def delete(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "DELETE")
+ end
+
+ # Executes a request simulating HEAD HTTP method and set/volley the response
+ def head(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "HEAD")
+ end
+ end
+
+ def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
+ # Sanity check for required instance variables so we can give an
+ # understandable error message.
+ %w(@controller @request @response).each do |iv_name|
+ if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil?
+ raise "#{iv_name} is nil: make sure you set it in your test's setup method."
+ end
+ end
+
+ @request.recycle!
+ @response.recycle!
+
+ @html_document = nil
+ @request.env['REQUEST_METHOD'] = http_method
+
+ @request.action = action.to_s
+
+ parameters ||= {}
+ @request.assign_parameters(@controller.class.controller_path, action.to_s, parameters)
+
+ @request.session = ActionController::TestSession.new(session) unless session.nil?
+ @request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
+ build_request_uri(action, parameters)
+
+ Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest
+ @controller.process_with_test(@request, @response)
+ end
+
+ def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
+ @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ @request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ returning __send__(request_method, action, parameters, session, flash) do
+ @request.env.delete 'HTTP_X_REQUESTED_WITH'
+ @request.env.delete 'HTTP_ACCEPT'
+ end
+ end
+ alias xhr :xml_http_request
+
+ def assigns(key = nil)
+ if key.nil?
+ @response.template.assigns
+ else
+ @response.template.assigns[key.to_s]
+ end
+ end
+
+ def session
+ @request.session
+ end
+
+ def flash
+ @response.flash
+ end
+
+ def cookies
+ @response.cookies
+ end
+
+ def redirect_to_url
+ @response.redirect_url
+ end
+
+ def build_request_uri(action, parameters)
+ unless @request.env['REQUEST_URI']
+ options = @controller.__send__(:rewrite_options, parameters)
+ options.update(:only_path => true, :action => action)
+
+ url = ActionController::UrlRewriter.new(@request, parameters)
+ @request.set_REQUEST_URI(url.rewrite(options))
+ end
+ end
+
+ def html_document
+ xml = @response.content_type =~ /xml$/
+ @html_document ||= HTML::Document.new(@response.body, false, xml)
+ end
+
+ def find_tag(conditions)
+ html_document.find(conditions)
+ end
+
+ def find_all_tag(conditions)
+ html_document.find_all(conditions)
+ end
+
+ def method_missing(selector, *args, &block)
+ if @controller && ActionController::Routing::Routes.named_routes.helpers.include?(selector)
+ @controller.send(selector, *args, &block)
+ else
+ super
+ end
+ end
+
+ # Shortcut for <tt>ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>:
+ #
+ # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
+ #
+ # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
+ # This will not affect other platforms:
+ #
+ # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
+ def fixture_file_upload(path, mime_type = nil, binary = false)
+ fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
+ ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
+ end
+
+ # A helper to make it easier to test different route configurations.
+ # This method temporarily replaces ActionController::Routing::Routes
+ # with a new RouteSet instance.
+ #
+ # The new instance is yielded to the passed block. Typically the block
+ # will create some routes using <tt>map.draw { map.connect ... }</tt>:
+ #
+ # with_routing do |set|
+ # set.draw do |map|
+ # map.connect ':controller/:action/:id'
+ # assert_equal(
+ # ['/content/10/show', {}],
+ # map.generate(:controller => 'content', :id => 10, :action => 'show')
+ # end
+ # end
+ # end
+ #
+ def with_routing
+ real_routes = ActionController::Routing::Routes
+ ActionController::Routing.module_eval { remove_const :Routes }
+
+ temporary_routes = ActionController::Routing::RouteSet.new
+ ActionController::Routing.module_eval { const_set :Routes, temporary_routes }
+
+ yield temporary_routes
+ ensure
+ if ActionController::Routing.const_defined? :Routes
+ ActionController::Routing.module_eval { remove_const :Routes }
+ end
+ ActionController::Routing.const_set(:Routes, real_routes) if real_routes
+ end
+ end
+
+ module ProcessWithTest #:nodoc:
+ def self.included(base)
+ base.class_eval { attr_reader :assigns }
+ end
+
+ def process_with_test(*args)
+ process(*args).tap { set_test_assigns }
+ end
+
+ private
+ def set_test_assigns
+ @assigns = {}
+ (instance_variable_names - self.class.protected_instance_variables).each do |var|
+ name, value = var[1..-1], instance_variable_get(var)
+ @assigns[name] = value
+ response.template.assigns[name] = value if response
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/testing/test_case.rb b/actionpack/lib/action_controller/testing/test_case.rb
new file mode 100644
index 0000000000..c14785ba83
--- /dev/null
+++ b/actionpack/lib/action_controller/testing/test_case.rb
@@ -0,0 +1,199 @@
+require 'active_support/test_case'
+require 'action_controller/testing/process'
+
+module ActionController
+ # 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 ActionController::IntegrationTest), which are more like
+ # "stories" that can involve multiple controllers and mutliple actions (i.e. multiple
+ # different HTTP requests).
+ #
+ # == Basic example
+ #
+ # Functional tests are written as follows:
+ # 1. First, one uses the +get+, +post+, +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, :book => { :title => "Love Hina" })
+ #
+ # # 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.
+ # assert_not_nil Book.find_by_title("Love Hina")
+ # end
+ # 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 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.
+ #
+ # (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 explicity 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:
+ #
+ # * assigns: Instance variables assigned in the action that are available for the view.
+ # * 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_not_nil assigns(:person) # makes sure that a @person instance variable was set
+ # 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
+ #
+ # For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
+ # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
+ # So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
+ #
+ # On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
+ #
+ # 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 the request collections
+ #
+ # The collections described above link to the response, so you can test if what the actions were expected to do happened. But
+ # sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
+ # and cookies, though. For sessions, you just do:
+ #
+ # @request.session[:key] = "value"
+ # @request.cookies["key"] = "value"
+ #
+ # == 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.
+ # Example:
+ #
+ # assert_redirected_to page_url(:title => 'foo')
+ class TestCase < ActiveSupport::TestCase
+ include TestProcess
+
+ module Assertions
+ %w(response selector tag dom routing model).each do |kind|
+ include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
+ end
+
+ def clean_backtrace(&block)
+ yield
+ rescue ActiveSupport::TestCase::Assertion => error
+ framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions"))
+ error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path }
+ raise
+ end
+ end
+ include Assertions
+
+ # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
+ # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
+ # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
+ # than 0.0.0.0.
+ #
+ # The exception is stored in the exception accessor for further inspection.
+ module RaiseActionExceptions
+ protected
+ attr_accessor :exception
+
+ def rescue_action_without_handler(e)
+ self.exception = e
+
+ if request.remote_addr == "0.0.0.0"
+ raise(e)
+ else
+ super(e)
+ end
+ end
+ end
+
+ setup :setup_controller_request_and_response
+
+ @@controller_class = nil
+
+ class << self
+ # Sets the controller class name. Useful if the name can't be inferred from test class.
+ # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>.
+ def tests(controller_class)
+ self.controller_class = controller_class
+ end
+
+ def controller_class=(new_class)
+ prepare_controller_class(new_class) if new_class
+ write_inheritable_attribute(:controller_class, new_class)
+ end
+
+ def controller_class
+ if current_controller_class = read_inheritable_attribute(:controller_class)
+ current_controller_class
+ else
+ self.controller_class = determine_default_controller_class(name)
+ end
+ end
+
+ def determine_default_controller_class(name)
+ name.sub(/Test$/, '').constantize
+ rescue NameError
+ nil
+ end
+
+ def prepare_controller_class(new_class)
+ new_class.send :include, RaiseActionExceptions
+ end
+ end
+
+ def setup_controller_request_and_response
+ @request = TestRequest.new
+ @response = TestResponse.new
+
+ if klass = self.class.controller_class
+ @controller ||= klass.new rescue nil
+ end
+
+ if @controller
+ @controller.request = @request
+ @controller.params = {}
+ @controller.send(:initialize_current_url)
+ end
+ end
+
+ # Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local
+ def rescue_action_in_public!
+ @request.remote_addr = '208.77.188.166' # example.com
+ end
+ end
+end