diff --git a/lib/devise.rb b/lib/devise.rb index 9c10fc1e..045b6177 100755 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -39,6 +39,11 @@ module Devise autoload :Authenticatable, 'devise/strategies/authenticatable' end + module Test + autoload :ControllerHelpers, 'devise/test/controller_helpers' + end + + # Constants which holds devise configuration for extensions. Those should # not be modified by the "end user" (this is why they are constants). ALL = [] diff --git a/lib/devise/test/controller_helpers.rb b/lib/devise/test/controller_helpers.rb new file mode 100644 index 00000000..328767a8 --- /dev/null +++ b/lib/devise/test/controller_helpers.rb @@ -0,0 +1,139 @@ +module Devise + module Test + # Devise::Test::ControllerHelpers provides a facility to test controllers in isolation + # when using ActionController::TestCase allowing you to quickly sign_in or + # sign_out a user. Do not use Devise::TestHelpers in integration tests. + # + # Notice you should not test Warden specific behavior (like Warden callbacks) + # using Devise::TestHelpers since it is a stub of the actual behavior. Such + # callbacks should be tested in your integration suite instead. + module ControllerHelpers + def self.included(base) + base.class_eval do + setup :setup_controller_for_warden, :warden if respond_to?(:setup) + end + end + + # Override process to consider warden. + def process(*) + # Make sure we always return @response, a la ActionController::TestCase::Behaviour#process, even if warden interrupts + _catch_warden { super } # || @response # _catch_warden will setup the @response object + + # process needs to return the ActionDispath::TestResponse object + @response + end + + # We need to set up the environment variables and the response in the controller. + def setup_controller_for_warden #:nodoc: + @request.env['action_controller.instance'] = @controller + end + + # Quick access to Warden::Proxy. + def warden #:nodoc: + @request.env['warden'] ||= begin + manager = Warden::Manager.new(nil) do |config| + config.merge! Devise.warden_config + end + Warden::Proxy.new(@request.env, manager) + end + end + + # sign_in a given resource by storing its keys in the session. + # This method bypass any warden authentication callback. + # + # Examples: + # + # sign_in :user, @user # sign_in(scope, resource) + # sign_in @user # sign_in(resource) + # + def sign_in(resource_or_scope, resource=nil) + scope ||= Devise::Mapping.find_scope!(resource_or_scope) + resource ||= resource_or_scope + warden.instance_variable_get(:@users).delete(scope) + warden.session_serializer.store(resource, scope) + end + + # Sign out a given resource or scope by calling logout on Warden. + # This method bypass any warden logout callback. + # + # Examples: + # + # sign_out :user # sign_out(scope) + # sign_out @user # sign_out(resource) + # + def sign_out(resource_or_scope) + scope = Devise::Mapping.find_scope!(resource_or_scope) + @controller.instance_variable_set(:"@current_#{scope}", nil) + user = warden.instance_variable_get(:@users).delete(scope) + warden.session_serializer.delete(scope, user) + end + + protected + + # Catch warden continuations and handle like the middleware would. + # Returns nil when interrupted, otherwise the normal result of the block. + def _catch_warden(&block) + result = catch(:warden, &block) + + env = @controller.request.env + + result ||= {} + + # Set the response. In production, the rack result is returned + # from Warden::Manager#call, which the following is modelled on. + case result + when Array + if result.first == 401 && intercept_401?(env) # does this happen during testing? + _process_unauthenticated(env) + else + result + end + when Hash + _process_unauthenticated(env, result) + else + result + end + end + + def _process_unauthenticated(env, options = {}) + options[:action] ||= :unauthenticated + proxy = env['warden'] + result = options[:result] || proxy.result + + ret = case result + when :redirect + body = proxy.message || "You are being redirected to #{proxy.headers['Location']}" + [proxy.status, proxy.headers, [body]] + when :custom + proxy.custom_response + else + env["PATH_INFO"] = "/#{options[:action]}" + env["warden.options"] = options + Warden::Manager._run_callbacks(:before_failure, env, options) + + status, headers, response = Devise.warden_config[:failure_app].call(env).to_a + @controller.response.headers.merge!(headers) + r_opts = { status: status, content_type: headers["Content-Type"], location: headers["Location"] } + r_opts[Rails.version.start_with?('5') ? :body : :text] = response.body + @controller.send :render, r_opts + nil # causes process return @response + end + + # ensure that the controller response is set up. In production, this is + # not necessary since warden returns the results to rack. However, at + # testing time, we want the response to be available to the testing + # framework to verify what would be returned to rack. + if ret.is_a?(Array) + # ensure the controller response is set to our response. + @controller.response ||= @response + @response.status = ret.first + @response.headers.clear + ret.second.each { |k,v| @response[k] = v } + @response.body = ret.third + end + + ret + end + end + end +end diff --git a/lib/devise/test_helpers.rb b/lib/devise/test_helpers.rb index 7d97147b..dc53be82 100644 --- a/lib/devise/test_helpers.rb +++ b/lib/devise/test_helpers.rb @@ -1,137 +1,13 @@ module Devise - # Devise::TestHelpers provides a facility to test controllers in isolation - # when using ActionController::TestCase allowing you to quickly sign_in or - # sign_out a user. Do not use Devise::TestHelpers in integration tests. - # - # Notice you should not test Warden specific behavior (like Warden callbacks) - # using Devise::TestHelpers since it is a stub of the actual behavior. Such - # callbacks should be tested in your integration suite instead. module TestHelpers def self.included(base) base.class_eval do - setup :setup_controller_for_warden, :warden if respond_to?(:setup) + ActiveSupport::Deprecation.warn <<-DEPRECATION + [Devise] including `Devise::TestHelpers` is deprecated and will be removed from Devise. + For controller tests, please include `Devise::Test::ControllerHelpers` instead. + DEPRECATION + include Devise::Test::ControllerHelpers end end - - # Override process to consider warden. - def process(*) - # Make sure we always return @response, a la ActionController::TestCase::Behaviour#process, even if warden interrupts - _catch_warden { super } # || @response # _catch_warden will setup the @response object - - # process needs to return the ActionDispath::TestResponse object - @response - end - - # We need to set up the environment variables and the response in the controller. - def setup_controller_for_warden #:nodoc: - @request.env['action_controller.instance'] = @controller - end - - # Quick access to Warden::Proxy. - def warden #:nodoc: - @request.env['warden'] ||= begin - manager = Warden::Manager.new(nil) do |config| - config.merge! Devise.warden_config - end - Warden::Proxy.new(@request.env, manager) - end - end - - # sign_in a given resource by storing its keys in the session. - # This method bypass any warden authentication callback. - # - # Examples: - # - # sign_in :user, @user # sign_in(scope, resource) - # sign_in @user # sign_in(resource) - # - def sign_in(resource_or_scope, resource=nil) - scope ||= Devise::Mapping.find_scope!(resource_or_scope) - resource ||= resource_or_scope - warden.instance_variable_get(:@users).delete(scope) - warden.session_serializer.store(resource, scope) - end - - # Sign out a given resource or scope by calling logout on Warden. - # This method bypass any warden logout callback. - # - # Examples: - # - # sign_out :user # sign_out(scope) - # sign_out @user # sign_out(resource) - # - def sign_out(resource_or_scope) - scope = Devise::Mapping.find_scope!(resource_or_scope) - @controller.instance_variable_set(:"@current_#{scope}", nil) - user = warden.instance_variable_get(:@users).delete(scope) - warden.session_serializer.delete(scope, user) - end - - protected - - # Catch warden continuations and handle like the middleware would. - # Returns nil when interrupted, otherwise the normal result of the block. - def _catch_warden(&block) - result = catch(:warden, &block) - - env = @controller.request.env - - result ||= {} - - # Set the response. In production, the rack result is returned - # from Warden::Manager#call, which the following is modelled on. - case result - when Array - if result.first == 401 && intercept_401?(env) # does this happen during testing? - _process_unauthenticated(env) - else - result - end - when Hash - _process_unauthenticated(env, result) - else - result - end - end - - def _process_unauthenticated(env, options = {}) - options[:action] ||= :unauthenticated - proxy = env['warden'] - result = options[:result] || proxy.result - - ret = case result - when :redirect - body = proxy.message || "You are being redirected to #{proxy.headers['Location']}" - [proxy.status, proxy.headers, [body]] - when :custom - proxy.custom_response - else - env["PATH_INFO"] = "/#{options[:action]}" - env["warden.options"] = options - Warden::Manager._run_callbacks(:before_failure, env, options) - - status, headers, response = Devise.warden_config[:failure_app].call(env).to_a - @controller.response.headers.merge!(headers) - r_opts = { status: status, content_type: headers["Content-Type"], location: headers["Location"] } - r_opts[Rails.version.start_with?('5') ? :body : :text] = response.body - @controller.send :render, r_opts - nil # causes process return @response - end - - # ensure that the controller response is set up. In production, this is - # not necessary since warden returns the results to rack. However, at - # testing time, we want the response to be available to the testing - # framework to verify what would be returned to rack. - if ret.is_a?(Array) - # ensure the controller response is set to our response. - @controller.response ||= @response - @response.status = ret.first - @response.headers.clear - ret.second.each { |k,v| @response[k] = v } - @response.body = ret.third - end - - ret - end end end diff --git a/test/controllers/custom_registrations_controller_test.rb b/test/controllers/custom_registrations_controller_test.rb index 60db1f49..13e4eef2 100644 --- a/test/controllers/custom_registrations_controller_test.rb +++ b/test/controllers/custom_registrations_controller_test.rb @@ -3,7 +3,7 @@ require 'test_helper' class CustomRegistrationsControllerTest < Devise::ControllerTestCase tests Custom::RegistrationsController - include Devise::TestHelpers + include Devise::Test::ControllerHelpers setup do request.env["devise.mapping"] = Devise.mappings[:user] diff --git a/test/controllers/custom_strategy_test.rb b/test/controllers/custom_strategy_test.rb index 165f5a2c..b63bead6 100644 --- a/test/controllers/custom_strategy_test.rb +++ b/test/controllers/custom_strategy_test.rb @@ -27,7 +27,7 @@ end class CustomStrategyTest < Devise::ControllerTestCase tests CustomStrategyController - include Devise::TestHelpers + include Devise::Test::ControllerHelpers setup do Warden::Strategies.add(:custom_strategy, CustomStrategy) diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb index f8a3f2c8..5c359eed 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/passwords_controller_test.rb @@ -2,7 +2,7 @@ require 'test_helper' class PasswordsControllerTest < Devise::ControllerTestCase tests Devise::PasswordsController - include Devise::TestHelpers + include Devise::Test::ControllerHelpers setup do request.env["devise.mapping"] = Devise.mappings[:user] diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 6d28c6d9..82d066b2 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -2,7 +2,7 @@ require 'test_helper' class SessionsControllerTest < Devise::ControllerTestCase tests Devise::SessionsController - include Devise::TestHelpers + include Devise::Test::ControllerHelpers test "#create doesn't raise unpermitted params when sign in fails" do begin diff --git a/test/test_helpers_test.rb b/test/test/controller_helpers_test.rb similarity index 97% rename from test/test_helpers_test.rb rename to test/test/controller_helpers_test.rb index 374c2dba..dc0c2a2a 100644 --- a/test/test_helpers_test.rb +++ b/test/test/controller_helpers_test.rb @@ -1,8 +1,8 @@ require 'test_helper' -class TestHelpersTest < Devise::ControllerTestCase +class TestControllerHelpersTest < Devise::ControllerTestCase tests UsersController - include Devise::TestHelpers + include Devise::Test::ControllerHelpers test "redirects if attempting to access a page unauthenticated" do get :index