diff --git a/test/test_authenticatable.rb b/test/test_authenticatable.rb index ed4f3e2..a386b78 100644 --- a/test/test_authenticatable.rb +++ b/test/test_authenticatable.rb @@ -159,3 +159,161 @@ class AuthCappedDashboardController < Prism::Controller 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 +