From 5ec35fdfddda59afe3422991093088a605b2aa11 Mon Sep 17 00:00:00 2001 From: David Chelimsky Date: Sun, 25 Apr 2010 16:28:11 -0500 Subject: [PATCH] include ActionController::TestCase::Behavior - this includes patches sumbitted to rails, supporting new behavior with rails-3.0.0.beta.3 and future releases as well - https://rails.lighthouseapp.com/projects/8994/tickets/4433 (merged) - http://rails.lighthouseapp.com/projects/8994/tickets/4474 (pending) --- lib/rspec/rails.rb | 1 + .../rails/example/controller_example_group.rb | 82 ++------ lib/rspec/rails/monkey.rb | 2 + .../monkey/action_controller/test_case.rb | 198 ++++++++++++++++++ .../active_support/notifications/fanout.rb | 20 ++ 5 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 lib/rspec/rails/monkey.rb create mode 100644 lib/rspec/rails/monkey/action_controller/test_case.rb create mode 100644 lib/rspec/rails/monkey/active_support/notifications/fanout.rb diff --git a/lib/rspec/rails.rb b/lib/rspec/rails.rb index 0c1e789ee9..bf04508d30 100644 --- a/lib/rspec/rails.rb +++ b/lib/rspec/rails.rb @@ -1,3 +1,4 @@ +require 'rspec/rails/monkey' require 'rspec/rails/transactional_database_support' require 'rspec/rails/matchers' require 'rspec/rails/example' diff --git a/lib/rspec/rails/example/controller_example_group.rb b/lib/rspec/rails/example/controller_example_group.rb index d62ba46147..00e4d72c0b 100644 --- a/lib/rspec/rails/example/controller_example_group.rb +++ b/lib/rspec/rails/example/controller_example_group.rb @@ -2,92 +2,42 @@ require 'action_controller' require 'action_dispatch' require 'webrat' +require 'test/unit/assertions' -# BEGIN PATCH -# -# The following monkey patches to rails can be removed if/when -# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4433 -# is resolved. -require 'active_support/notifications/fanout' -class ActiveSupport::Notifications::Fanout - def unsubscribe(subscriber_or_name) - @listeners_for.clear - @subscribers.reject! do |s| - s.instance_eval do - case subscriber_or_name - when String - @pattern && @pattern =~ subscriber_or_name - when self - true - end - end - end +module Rspec::Rails::ActiveSupportConcernAdapter + def setup(*methods) + methods.each {|method| before { send method } } end -end -require 'action_controller/test_case' -module ActionController::TemplateAssertions - def teardown_subscriptions - ActiveSupport::Notifications.unsubscribe("action_view.render_template") - ActiveSupport::Notifications.unsubscribe("action_view.render_template!") + def teardown(*methods) + methods.each {|method| after { send method } } end end -# END PATCH - -# Preliminary documentation (more to come ....): -# -# allow_forgery_protection is set to false -# - you can set it to true in a before(:each) block -# if you have a specific example that needs it, but -# be sure to restore it to false (or supply tokens -# to all of your example requests) module ControllerExampleGroupBehaviour - include ActionDispatch::Assertions - include ActionDispatch::Integration::Runner + include Test::Unit::Assertions include Webrat::Matchers include Webrat::Methods include Rspec::Matchers - module ActiveSupportConcernAdapter - def setup(*methods) - methods.each {|method| before { send method } } - end + def self.included(mod) + mod.extend Rspec::Rails::ActiveSupportConcernAdapter + mod.__send__ :include, ActionController::TestCase::Behavior - def teardown(*methods) - methods.each {|method| after { send method } } + def mod.controller_class + describes end - end - - def self.included(mod) - mod.extend ActiveSupportConcernAdapter mod.before do + @routes = Rails.application.routes @_result = Struct.new(:add_assertion).new ActionController::Base.allow_forgery_protection = false end end - - def app - described_class.action(@_action).tap do |endpoint| - def endpoint.routes - Rails.application.routes - end - end - end - - %w[get post put delete head].map do |method| - eval <<-CODE - def #{method}(*args) - @_action = args.shift - super '/', *args - end - CODE - end end Rspec.configure do |c| - [ControllerExampleGroupBehaviour, ActionController::TemplateAssertions].each do |mod| - c.include mod, :example_group => { :file_path => /\bspec\/controllers\// } - end + c.include ControllerExampleGroupBehaviour, :example_group => { + :describes => lambda {|described| described < ActionController::Base } + } end diff --git a/lib/rspec/rails/monkey.rb b/lib/rspec/rails/monkey.rb new file mode 100644 index 0000000000..d48cbfe75c --- /dev/null +++ b/lib/rspec/rails/monkey.rb @@ -0,0 +1,2 @@ +require 'rspec/rails/monkey/action_controller/test_case' +require 'rspec/rails/monkey/active_support/notifications/fanout' diff --git a/lib/rspec/rails/monkey/action_controller/test_case.rb b/lib/rspec/rails/monkey/action_controller/test_case.rb new file mode 100644 index 0000000000..8ff1451c54 --- /dev/null +++ b/lib/rspec/rails/monkey/action_controller/test_case.rb @@ -0,0 +1,198 @@ +require 'action_controller/test_case' + +module ActionController + # This has been merged to rails HEAD after the 3.0.0.beta.3 release (see + # https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4433). + # Once 3.0.0.rc.1 comes out, we can remove it. + module TemplateAssertions + def teardown_subscriptions + ActiveSupport::Notifications.unsubscribe("action_view.render_template") + ActiveSupport::Notifications.unsubscribe("action_view.render_template!") + end + end + + # The remainder of this file has yet to be merged to rails HEAD and is + # therefore merely speculative and hopeful. + class TestCase < ActiveSupport::TestCase + module Behavior + extend ActiveSupport::Concern + include ActionDispatch::TestProcess + + attr_reader :response, :request + + module ClassMethods + + # Sets the controller class name. Useful if the name can't be inferred from test class. + # Expects +controller_class+ as a constant. Example: tests WidgetController. + 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, ActionController::TestCase::RaiseActionExceptions + end + + end + + # 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 + + 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 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(@routes @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! + @controller.response_body = nil + @controller.formats = nil + @controller.params = nil + + @html_document = nil + @request.env['REQUEST_METHOD'] = http_method + + parameters ||= {} + @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) + + @request.session = ActionController::TestSession.new(session) unless session.nil? + @request.session["flash"] = @request.flash.update(flash || {}) + @request.session["flash"].sweep + + @controller.request = @request + @controller.params.merge!(parameters) + build_request_uri(action, parameters) + Base.class_eval { include Testing } + @controller.process_with_new_base_test(@request, @response) + @request.session.delete('flash') if @request.session['flash'].blank? + @response + 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 + + @request.env.delete('PATH_INFO') + + if @controller + @controller.request = @request + @controller.params = {} + 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 + + included do + include ActionController::TemplateAssertions + include ActionDispatch::Assertions + setup :setup_controller_request_and_response + end + + private + + def build_request_uri(action, parameters) + unless @request.env["PATH_INFO"] + options = @controller.__send__(:url_options).merge(parameters) + options.update( + :only_path => true, + :action => action, + :relative_url_root => nil, + :_path_segments => @request.symbolized_path_parameters) + + url, query_string = @routes.url_for(options).split("?", 2) + + @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root + @request.env["PATH_INFO"] = url + @request.env["QUERY_STRING"] = query_string || "" + end + end + end + + # 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 + def self.included(base) + base.class_eval do + attr_accessor :exception + protected :exception, :exception= + end + end + + protected + 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 + end +end diff --git a/lib/rspec/rails/monkey/active_support/notifications/fanout.rb b/lib/rspec/rails/monkey/active_support/notifications/fanout.rb new file mode 100644 index 0000000000..a89a02c9d5 --- /dev/null +++ b/lib/rspec/rails/monkey/active_support/notifications/fanout.rb @@ -0,0 +1,20 @@ +require 'active_support/notifications/fanout' + +# This has been merged to rails HEAD after the 3.0.0.beta.3 release (see +# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4433). +# Once 3.0.0.rc.1 comes out, we can remove it. +class ActiveSupport::Notifications::Fanout + def unsubscribe(subscriber_or_name) + @listeners_for.clear + @subscribers.reject! do |s| + s.instance_eval do + case subscriber_or_name + when String + @pattern && @pattern =~ subscriber_or_name + when self + true + end + end + end + end +end