mirror of
https://github.com/heartcombo/devise.git
synced 2026-01-10 08:08:00 -05:00
A common usage of I18n with different locales is to create some around callback in the application controller that sets the locale for the entire action, via params/url/user/etc., which ensure the locale is respected for the duration of that action, and resets at the end. Devise was not respecting the locale when the authenticate failed and triggered the failure app, because that happens in a warden middleware right up in the change, by that time the controller around callback had already reset the locale back to its default, and the failure app would just translate flash messages using the default locale. Now we are passing the current locale down to the failure app via warden options, and wrapping it with an around callback, which makes the failure app respect the set I18n locale by the controller at the time the authentication failure is triggered, working as expected. (much more like a normal controller would.) I chose to introduce a callback in the failure app so we could wrap the whole `respond` action processing rather than adding individual `locale` options to the `I18n.t` calls, because that should ensure other possible `I18n.t` calls from overridden failure apps would respect the set locale as well, and makes it more like one would implement in a controller. I don't recommend people using callbacks in their own failure apps though, as this is not going to be documented as a "feature" of failures apps, it's considered "internal" and could be refactored at any point. It is possible to override the locale with the new `i18n_locale` method, which simply defaults to the passed locale from the controller. Closes #5247 Closes #5246 Related to: #3052, #4823, and possible others already closed. Related to warden: (may be closed there afterwards) https://github.com/wardencommunity/warden/issues/180 https://github.com/wardencommunity/warden/issues/170
468 lines
18 KiB
Ruby
468 lines
18 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'test_helper'
|
|
require 'ostruct'
|
|
|
|
class FailureTest < ActiveSupport::TestCase
|
|
class RootFailureApp < Devise::FailureApp
|
|
def fake_app
|
|
Object.new
|
|
end
|
|
end
|
|
|
|
class FailureWithSubdomain < RootFailureApp
|
|
routes = ActionDispatch::Routing::RouteSet.new
|
|
|
|
routes.draw do
|
|
scope subdomain: 'sub' do
|
|
root to: 'foo#bar'
|
|
end
|
|
end
|
|
|
|
include routes.url_helpers
|
|
end
|
|
|
|
class FailureWithI18nOptions < Devise::FailureApp
|
|
def i18n_options(options)
|
|
options.merge(name: 'Steve')
|
|
end
|
|
end
|
|
|
|
class FailureWithoutRootPath < Devise::FailureApp
|
|
class FakeURLHelpers
|
|
end
|
|
|
|
class FakeRoutesWithoutRoot
|
|
def url_helpers
|
|
FakeURLHelpers.new
|
|
end
|
|
end
|
|
|
|
class FakeAppWithoutRootPath
|
|
def routes
|
|
FakeRoutesWithoutRoot.new
|
|
end
|
|
end
|
|
|
|
def main_app
|
|
FakeAppWithoutRootPath.new
|
|
end
|
|
end
|
|
|
|
class FakeEngineApp < Devise::FailureApp
|
|
class FakeEngine
|
|
def new_user_on_engine_session_url _
|
|
'/user_on_engines/sign_in'
|
|
end
|
|
end
|
|
|
|
def main_app
|
|
raise 'main_app router called instead of fake_engine'
|
|
end
|
|
|
|
def fake_engine
|
|
@fake_engine ||= FakeEngine.new
|
|
end
|
|
end
|
|
|
|
class RequestWithoutFlashSupport < ActionDispatch::Request
|
|
undef_method :flash
|
|
end
|
|
|
|
def self.context(name, &block)
|
|
instance_eval(&block)
|
|
end
|
|
|
|
def call_failure(env_params = {})
|
|
env = {
|
|
'REQUEST_URI' => 'http://test.host/',
|
|
'HTTP_HOST' => 'test.host',
|
|
'REQUEST_METHOD' => 'GET',
|
|
'warden.options' => { scope: :user },
|
|
'action_dispatch.request.formats' => Array(env_params.delete('formats') || Mime[:html]),
|
|
'rack.input' => "",
|
|
'warden' => OpenStruct.new(message: nil)
|
|
}.merge!(env_params)
|
|
|
|
# Passing nil for action_dispatch.request.formats prevents the default from being used in Rails 5, need to remove it
|
|
if env.has_key?('action_dispatch.request.formats') && env['action_dispatch.request.formats'].nil?
|
|
env.delete 'action_dispatch.request.formats' unless env['action_dispatch.request.formats']
|
|
end
|
|
|
|
@response = (env.delete(:app) || Devise::FailureApp).call(env).to_a
|
|
@request = (env.delete(:request_klass) || ActionDispatch::Request).new(env)
|
|
end
|
|
|
|
context 'When redirecting' do
|
|
test 'returns to the default redirect location' do
|
|
call_failure
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/users/sign_in', @response.second['Location']
|
|
end
|
|
|
|
test 'returns to the default redirect location considering subdomain' do
|
|
call_failure('warden.options' => { scope: :subdomain_user })
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://sub.test.host/subdomain_users/sign_in', @response.second['Location']
|
|
end
|
|
|
|
test 'returns to the default redirect location for wildcard requests' do
|
|
call_failure 'action_dispatch.request.formats' => nil, 'HTTP_ACCEPT' => '*/*'
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/users/sign_in', @response.second['Location']
|
|
end
|
|
|
|
test 'returns to the root path if no session path is available' do
|
|
swap Devise, router_name: :fake_app do
|
|
call_failure app: RootFailureApp
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/', @response.second['Location']
|
|
end
|
|
end
|
|
|
|
test 'returns to the root path even when it\'s not defined' do
|
|
call_failure app: FailureWithoutRootPath
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/', @response.second['Location']
|
|
end
|
|
|
|
test 'returns to the root path considering subdomain if no session path is available' do
|
|
swap Devise, router_name: :fake_app do
|
|
call_failure app: FailureWithSubdomain
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://sub.test.host/', @response.second['Location']
|
|
end
|
|
end
|
|
|
|
test 'returns to the default redirect location considering the router for supplied scope' do
|
|
call_failure app: FakeEngineApp, 'warden.options' => { scope: :user_on_engine }
|
|
assert_equal 302, @response.first
|
|
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/user_on_engines/sign_in', @response.second['Location']
|
|
end
|
|
|
|
if Rails.application.config.respond_to?(:relative_url_root)
|
|
test 'returns to the default redirect location considering the relative url root' do
|
|
swap Rails.application.config, relative_url_root: "/sample" do
|
|
call_failure
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/sample/users/sign_in', @response.second['Location']
|
|
end
|
|
end
|
|
|
|
test 'returns to the default redirect location considering the relative url root and subdomain' do
|
|
swap Rails.application.config, relative_url_root: "/sample" do
|
|
call_failure('warden.options' => { scope: :subdomain_user })
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://sub.test.host/sample/subdomain_users/sign_in', @response.second['Location']
|
|
end
|
|
end
|
|
end
|
|
|
|
if Rails.application.config.action_controller.respond_to?(:relative_url_root)
|
|
test "returns to the default redirect location considering action_controller's relative url root" do
|
|
swap Rails.application.config.action_controller, relative_url_root: "/sample" do
|
|
call_failure
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/sample/users/sign_in', @response.second['Location']
|
|
end
|
|
end
|
|
|
|
test "returns to the default redirect location considering action_controller's relative url root and subdomain" do
|
|
swap Rails.application.config.action_controller, relative_url_root: "/sample" do
|
|
call_failure('warden.options' => { scope: :subdomain_user })
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://sub.test.host/sample/subdomain_users/sign_in', @response.second['Location']
|
|
end
|
|
end
|
|
end
|
|
|
|
test 'uses the proxy failure message as symbol' do
|
|
call_failure('warden' => OpenStruct.new(message: :invalid))
|
|
assert_equal 'Invalid Email or password.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
|
|
end
|
|
|
|
test 'supports authentication_keys as a Hash for the flash message' do
|
|
swap Devise, authentication_keys: { email: true, login: true } do
|
|
call_failure('warden' => OpenStruct.new(message: :invalid))
|
|
assert_equal 'Invalid Email, Login or password.', @request.flash[:alert]
|
|
end
|
|
end
|
|
|
|
test 'uses custom i18n options' do
|
|
call_failure('warden' => OpenStruct.new(message: :does_not_exist), app: FailureWithI18nOptions)
|
|
assert_equal 'User Steve does not exist', @request.flash[:alert]
|
|
end
|
|
|
|
test 'respects the i18n locale passed via warden options when redirecting' do
|
|
call_failure('warden' => OpenStruct.new(message: :invalid), 'warden.options' => { locale: :"pt-BR" })
|
|
|
|
assert_equal 'Email ou senha inválidos.', @request.flash[:alert]
|
|
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
|
|
end
|
|
|
|
test 'uses the proxy failure message as string' do
|
|
call_failure('warden' => OpenStruct.new(message: 'Hello world'))
|
|
assert_equal 'Hello world', @request.flash[:alert]
|
|
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
|
|
end
|
|
|
|
test 'set content type to default text/html' do
|
|
call_failure
|
|
assert_equal 'text/html; charset=utf-8', @response.second['Content-Type']
|
|
end
|
|
|
|
test 'set up a default message' do
|
|
call_failure
|
|
if Devise::Test.rails71_and_up?
|
|
assert_empty @response.last.body
|
|
else
|
|
assert_match(/You are being/, @response.last.body)
|
|
assert_match(/redirected/, @response.last.body)
|
|
assert_match(/users\/sign_in/, @response.last.body)
|
|
end
|
|
end
|
|
|
|
test 'works for any navigational format' do
|
|
swap Devise, navigational_formats: [:json] do
|
|
call_failure('formats' => Mime[:json])
|
|
assert_equal 302, @response.first
|
|
end
|
|
end
|
|
|
|
test 'redirects the correct format if it is a non-html format request' do
|
|
swap Devise, navigational_formats: [:js] do
|
|
call_failure('formats' => Mime[:js])
|
|
assert_equal 'http://test.host/users/sign_in.js', @response.second["Location"]
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'For HTTP request' do
|
|
test 'return 401 status' do
|
|
call_failure('formats' => Mime[:json])
|
|
assert_equal 401, @response.first
|
|
end
|
|
|
|
test 'return appropriate body for xml' do
|
|
call_failure('formats' => Mime[:xml])
|
|
result = %(<?xml version="1.0" encoding="UTF-8"?>\n<errors>\n <error>You need to sign in or sign up before continuing.</error>\n</errors>\n)
|
|
assert_equal result, @response.last.body
|
|
end
|
|
|
|
test 'return appropriate body for json' do
|
|
call_failure('formats' => Mime[:json])
|
|
result = %({"error":"You need to sign in or sign up before continuing."})
|
|
assert_equal result, @response.last.body
|
|
end
|
|
|
|
test 'return 401 status for unknown formats' do
|
|
call_failure 'formats' => []
|
|
assert_equal 401, @response.first
|
|
end
|
|
|
|
test 'return WWW-authenticate headers if model allows' do
|
|
call_failure('formats' => Mime[:json])
|
|
assert_equal 'Basic realm="Application"', @response.second["WWW-Authenticate"]
|
|
end
|
|
|
|
test 'does not return WWW-authenticate headers if model does not allow' do
|
|
swap Devise, http_authenticatable: false do
|
|
call_failure('formats' => Mime[:json])
|
|
assert_nil @response.second["WWW-Authenticate"]
|
|
end
|
|
end
|
|
|
|
test 'works for any non navigational format' do
|
|
swap Devise, navigational_formats: [] do
|
|
call_failure('formats' => Mime[:html])
|
|
assert_equal 401, @response.first
|
|
end
|
|
end
|
|
|
|
test 'uses the failure message as response body' do
|
|
call_failure('formats' => Mime[:xml], 'warden' => OpenStruct.new(message: :invalid))
|
|
assert_match '<error>Invalid Email or password.</error>', @response.third.body
|
|
end
|
|
|
|
test 'respects the i18n locale passed via warden options when responding to HTTP request' do
|
|
call_failure('formats' => Mime[:json], 'warden' => OpenStruct.new(message: :invalid), 'warden.options' => { locale: :"pt-BR" })
|
|
|
|
assert_equal %({"error":"Email ou senha inválidos."}), @response.third.body
|
|
end
|
|
|
|
context 'on ajax call' do
|
|
context 'when http_authenticatable_on_xhr is false' do
|
|
test 'dont return 401 with navigational formats' do
|
|
swap Devise, http_authenticatable_on_xhr: false do
|
|
call_failure('formats' => Mime[:html], 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/users/sign_in', @response.second["Location"]
|
|
end
|
|
end
|
|
|
|
test 'dont return 401 with non navigational formats' do
|
|
swap Devise, http_authenticatable_on_xhr: false do
|
|
call_failure('formats' => Mime[:json], 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/users/sign_in.json', @response.second["Location"]
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when http_authenticatable_on_xhr is true' do
|
|
test 'return 401' do
|
|
swap Devise, http_authenticatable_on_xhr: true do
|
|
call_failure('formats' => Mime[:html], 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')
|
|
assert_equal 401, @response.first
|
|
end
|
|
end
|
|
|
|
test 'skip WWW-Authenticate header' do
|
|
swap Devise, http_authenticatable_on_xhr: true do
|
|
call_failure('formats' => Mime[:html], 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')
|
|
assert_nil @response.second['WWW-Authenticate']
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'With recall' do
|
|
test 'calls the original controller if invalid email or password' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
assert_includes @response.third.body, '<h2>Log in</h2>'
|
|
assert_includes @response.third.body, 'Invalid Email or password.'
|
|
end
|
|
|
|
test 'calls the original controller if not confirmed email' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in", message: :unconfirmed },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
assert_includes @response.third.body, '<h2>Log in</h2>'
|
|
assert_includes @response.third.body, 'You have to confirm your email address before continuing.'
|
|
end
|
|
|
|
test 'calls the original controller if inactive account' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in", message: :inactive },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
assert_includes @response.third.body, '<h2>Log in</h2>'
|
|
assert_includes @response.third.body, 'Your account is not activated yet.'
|
|
end
|
|
|
|
if Rails.application.config.respond_to?(:relative_url_root)
|
|
test 'calls the original controller with the proper environment considering the relative url root' do
|
|
swap Rails.application.config, relative_url_root: "/sample" do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/sample/users/sign_in"},
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
assert_includes @response.third.body, '<h2>Log in</h2>'
|
|
assert_includes @response.third.body, 'Invalid Email or password.'
|
|
assert_equal '/sample', @request.env["SCRIPT_NAME"]
|
|
assert_equal '/users/sign_in', @request.env["PATH_INFO"]
|
|
end
|
|
end
|
|
end
|
|
|
|
test 'respects the i18n locale passed via warden options when recalling original controller' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in", locale: :"pt-BR" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
|
|
assert_includes @response.third.body, '<h2>Log in</h2>'
|
|
assert_includes @response.third.body, 'Email ou senha inválidos.'
|
|
end
|
|
|
|
# TODO: remove conditional/else when supporting only responders 3.1+
|
|
if ActionController::Responder.respond_to?(:error_status=)
|
|
test 'respects the configured responder `error_status` for the status code' do
|
|
swap Devise.responder, error_status: :unprocessable_entity do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
|
|
assert_equal 422, @response.first
|
|
assert_includes @response.third.body, 'Invalid Email or password.'
|
|
end
|
|
end
|
|
|
|
test 'respects the configured responder `redirect_status` if the recall app returns a redirect status code' do
|
|
swap Devise.responder, redirect_status: :see_other do
|
|
env = {
|
|
"warden.options" => { recall: "devise/registrations#cancel", attempted_path: "/users/cancel" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
|
|
assert_equal 303, @response.first
|
|
end
|
|
end
|
|
else
|
|
test 'uses default hardcoded responder `error_status` for the status code since responders version does not support configuring it' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/sessions#new", attempted_path: "/users/sign_in" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
|
|
assert_equal 200, @response.first
|
|
assert_includes @response.third.body, 'Invalid Email or password.'
|
|
end
|
|
|
|
test 'users default hardcoded responder `redirect_status` for the status code since responders version does not support configuring it' do
|
|
env = {
|
|
"warden.options" => { recall: "devise/registrations#cancel", attempted_path: "/users/cancel" },
|
|
"devise.mapping" => Devise.mappings[:user],
|
|
"warden" => stub_everything
|
|
}
|
|
call_failure(env)
|
|
|
|
assert_equal 302, @response.first
|
|
end
|
|
end
|
|
end
|
|
|
|
context "Lazy loading" do
|
|
test "loads" do
|
|
assert_equal "yes it does", Devise::FailureApp.new.lazy_loading_works?
|
|
end
|
|
end
|
|
|
|
context "Without Flash Support" do
|
|
test "returns to the default redirect location without a flash message" do
|
|
call_failure request_klass: RequestWithoutFlashSupport
|
|
assert_equal 302, @response.first
|
|
assert_equal 'http://test.host/users/sign_in', @response.second['Location']
|
|
end
|
|
end
|
|
end
|