Files
prism/test/test_authenticatable.rb
T

320 lines
10 KiB
Ruby

require 'minitest/autorun'
require 'prism'
require 'support'
require 'tmpdir'
class AuthSessionsController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser
def create
user = AuthUser.find_by(email: params['email'])
if user && user.authenticate(params['password'])
sign_in(user)
halt(200, after_authentication_url)
else
halt(401, 'invalid credentials')
end
end
def destroy
sign_out
halt(200, 'signed out')
end
end
class AuthDashboardController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in'
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
class AuthApiController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
class AuthRememberSessionsController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, remember_cookie: :remember_token
def create
user = AuthUser.find_by(email: params['email'])
if user && user.authenticate(params['password'])
sign_in(user, remember: params['remember'] == '1')
halt(200, 'signed in')
else
halt(401, 'invalid credentials')
end
end
def destroy
sign_out
halt(200, 'signed out')
end
end
class AuthRememberDashboardController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in', remember_cookie: :remember_token
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
# `authenticate_with` is declared only on the base class here, the way a
# real app declares it once on ApplicationController - proving the setting
# (and its unauthenticated_redirect_path) reaches a subclass that never
# calls `authenticate_with` itself.
class AuthBaseController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in'
end
class AuthInheritedController < AuthBaseController
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
class AuthMultiSessionsController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession
def create
user = AuthUser.find_by(email: params['email'])
if user && user.authenticate(params['password'])
sign_in(user, remember: params['remember'] == '1')
halt(200, 'signed in')
else
halt(401, 'invalid credentials')
end
end
def destroy
sign_out
halt(200, 'signed out')
end
end
class AuthMultiDashboardController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in', remember_sessions: :AuthLoginSession
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
def other_devices
halt(200, other_login_sessions.count.to_s)
end
def sign_out_others
sign_out_all_other_sessions
halt(200, other_login_sessions.count.to_s)
end
def revoke
# Deliberately the raw String params['id'] gives, not params['id'].to_i -
# a real controller (see examples/blog's LoginSessionsController) has no
# reason to convert it itself; revoke_login_session! must do that on its
# own or every real HTTP request would silently no-op (see its comment).
ok = revoke_login_session!(params['id'])
halt(ok ? 200 : 404, ok ? 'revoked' : 'not found')
end
end
class AuthCappedSessionsController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession, max_remember_sessions: 2
def create
user = AuthUser.find_by(email: params['email'])
if user && user.authenticate(params['password'])
sign_in(user, remember: true)
halt(200, 'signed in')
else
halt(401, 'invalid credentials')
end
end
end
class AuthCappedDashboardController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in',
remember_sessions: :AuthLoginSession, max_remember_sessions: 2
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
class AuthZeroCappedSessionsController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession, max_remember_sessions: 0
def create
user = AuthUser.find_by(email: params['email'])
if user && user.authenticate(params['password'])
sign_in(user, remember: true)
halt(200, 'signed in')
else
halt(401, 'invalid credentials')
end
end
end
class AuthZeroCappedDashboardController < Prism::Controller
include Prism::Authenticatable
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in',
remember_sessions: :AuthLoginSession, max_remember_sessions: 0
before_action :require_authentication
def index
halt(200, "hello #{current_user.email}")
end
end
class AuthenticatableApp < Prism::Application
enable_sessions(secret: 'auth-secret')
post '/sign_in', to: 'auth_sessions#create'
post '/sign_out', to: 'auth_sessions#destroy'
get '/dashboard', to: 'auth_dashboard#index'
get '/api/me', to: 'auth_api#index'
get '/inherited/me', to: 'auth_inherited#index'
post '/remember/sign_in', to: 'auth_remember_sessions#create'
post '/remember/sign_out', to: 'auth_remember_sessions#destroy'
get '/remember/dashboard', to: 'auth_remember_dashboard#index'
post '/multi/sign_in', to: 'auth_multi_sessions#create'
post '/multi/sign_out', to: 'auth_multi_sessions#destroy'
get '/multi/dashboard', to: 'auth_multi_dashboard#index'
get '/multi/other_devices', to: 'auth_multi_dashboard#other_devices'
post '/multi/sign_out_others', to: 'auth_multi_dashboard#sign_out_others'
post '/multi/revoke/:id', to: 'auth_multi_dashboard#revoke'
post '/capped/sign_in', to: 'auth_capped_sessions#create'
get '/capped/dashboard', to: 'auth_capped_dashboard#index'
post '/zero-capped/sign_in', to: 'auth_zero_capped_sessions#create'
get '/zero-capped/dashboard', to: 'auth_zero_capped_dashboard#index'
end
class AuthenticatableTest < Minitest::Test
include TestSupport
def setup
@dir = Dir.mktmpdir('prism-authenticatable')
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
@store.create_table(:auth_users, %w[id email password_digest remember_token])
@store.create_table(:auth_login_sessions, %w[id user_id token user_agent ip_address])
user_class = Class.new(Prism::Orm::Model) do
table_name :auth_users
has_secure_password
has_secure_token :remember_token
end
user_class.store = @store
Object.const_set(:AuthUser, user_class)
login_session_class = Class.new(Prism::Orm::Model) do
table_name :auth_login_sessions
has_secure_token :token
end
login_session_class.store = @store
Object.const_set(:AuthLoginSession, login_session_class)
AuthUser.create!(email: 'foksiu@example.com', password: 'hunter2')
end
def teardown
FileUtils.remove_entry(@dir)
Object.send(:remove_const, :AuthUser) if Object.const_defined?(:AuthUser)
Object.send(:remove_const, :AuthLoginSession) if Object.const_defined?(:AuthLoginSession)
end
def call(method, path, **opts)
AuthenticatableApp.call(env_for(method, path, **opts))
end
def session_cookie_from(headers)
cookie_named(headers, 'prism.session')
end
def cookie_named(headers, name)
line = headers['Set-Cookie'].split("\n").find { |c| c.start_with?("#{name}=") }
line&.split(';')&.first
end
def sign_in_cookie
_, headers, = call('POST', '/sign_in',
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
body: 'email=foksiu@example.com&password=hunter2')
session_cookie_from(headers)
end
def test_wrong_password_is_rejected
status, = call('POST', '/sign_in',
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
body: 'email=foksiu@example.com&password=wrong')
assert_equal 401, status
end
def test_dashboard_without_session_redirects_to_configured_path
status, headers, = call('GET', '/dashboard')
assert_equal 302, status
assert_equal '/sign_in', headers['Location']
end
def test_api_without_session_halts_with_401
status, = call('GET', '/api/me')
assert_equal 401, status
end
def test_sign_in_then_dashboard_sees_current_user
cookie = sign_in_cookie
status, _, body = call('GET', '/dashboard', cookies: cookie)
assert_equal 200, status
assert_equal 'hello foksiu@example.com', body.join
end
def test_sign_out_forgets_current_user
cookie = sign_in_cookie
_, sign_out_headers, = call('POST', '/sign_out', cookies: cookie)
fresh_cookie = session_cookie_from(sign_out_headers)
status, = call('GET', '/dashboard', cookies: fresh_cookie)
assert_equal 302, status
end
def test_return_to_url_is_remembered_across_the_redirect
_, headers, = call('GET', '/dashboard')
refute_nil headers['Location']
cookie = session_cookie_from(headers)
_, sign_in_headers, body = call('POST', '/sign_in',
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
cookies: cookie,
body: 'email=foksiu@example.com&password=hunter2')
assert_equal '/dashboard', body.join
refute_nil sign_in_headers['Set-Cookie']
end
def test_authenticate_with_declared_on_a_base_controller_redirects_a_subclass_without_a_session
status, headers, = call('GET', '/inherited/me')
assert_equal 302, status
assert_equal '/sign_in', headers['Location']
end