diff --git a/test/test_authenticatable.rb b/test/test_authenticatable.rb index 0816ee5..7a6ff04 100644 --- a/test/test_authenticatable.rb +++ b/test/test_authenticatable.rb @@ -476,3 +476,125 @@ class AuthenticatableTest < Minitest::Test assert_equal 200, status_a end + def test_revoke_login_session_by_id_signs_out_only_that_device + browser_a_remember = cookie_named(multi_sign_in, 'remember_session_token') + browser_b_remember = cookie_named(multi_sign_in, 'remember_session_token') + browser_b_id = AuthLoginSession.order(:id, :desc).first.id + + status, _, body = call('POST', "/multi/revoke/#{browser_b_id}", cookies: browser_a_remember) + assert_equal 200, status + assert_equal 'revoked', body.join + + status_b, = call('GET', '/multi/dashboard', cookies: browser_b_remember) + assert_equal 302, status_b + status_a, = call('GET', '/multi/dashboard', cookies: browser_a_remember) + assert_equal 200, status_a + end + + def test_revoke_login_session_refuses_a_session_belonging_to_another_user + AuthUser.create!(email: 'other@example.com', password: 'hunter2') + other_login = AuthLoginSession.create!(user_id: AuthUser.find_by(email: 'other@example.com').id) + browser_a_remember = cookie_named(multi_sign_in, 'remember_session_token') + + status, _, body = call('POST', "/multi/revoke/#{other_login.id}", cookies: browser_a_remember) + assert_equal 404, status + assert_equal 'not found', body.join + refute_nil AuthLoginSession.find_by(id: other_login.id) + end + + def test_revoking_the_current_login_session_signs_that_browser_out_too + browser_a_remember = cookie_named(multi_sign_in, 'remember_session_token') + current_id = AuthLoginSession.first.id + + call('POST', "/multi/revoke/#{current_id}", cookies: browser_a_remember) + + status, = call('GET', '/multi/dashboard', cookies: browser_a_remember) + assert_equal 302, status + end + + def test_signing_in_without_remember_does_not_set_a_remember_session_cookie + headers = multi_sign_in(remember: false) + assert_nil cookie_named(headers, 'remember_session_token') + assert_equal 0, AuthLoginSession.count + end + + def capped_sign_in + _, headers, = call('POST', '/capped/sign_in', + headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, + body: 'email=foksiu@example.com&password=hunter2') + headers + end + + def test_max_remember_sessions_requires_remember_sessions + error = assert_raises(ArgumentError) do + Class.new(Prism::Controller) do + include Prism::Authenticatable + authenticate_with model: :AuthUser, max_remember_sessions: 2 + end + end + assert_match(/max_remember_sessions: requires remember_sessions:/, error.message) + end + + def test_under_the_limit_keeps_every_browser + browser_a = cookie_named(capped_sign_in, 'remember_session_token') + browser_b = cookie_named(capped_sign_in, 'remember_session_token') + assert_equal 2, AuthLoginSession.count + + status_a, = call('GET', '/capped/dashboard', cookies: browser_a) + status_b, = call('GET', '/capped/dashboard', cookies: browser_b) + assert_equal 200, status_a + assert_equal 200, status_b + end + + def test_exceeding_the_limit_signs_out_the_oldest_browser_first + browser_a = cookie_named(capped_sign_in, 'remember_session_token') + browser_b = cookie_named(capped_sign_in, 'remember_session_token') + browser_c = cookie_named(capped_sign_in, 'remember_session_token') + + assert_equal 2, AuthLoginSession.count + + status_a, = call('GET', '/capped/dashboard', cookies: browser_a) + assert_equal 302, status_a, 'the oldest (first) browser should have been pruned' + + status_b, = call('GET', '/capped/dashboard', cookies: browser_b) + status_c, = call('GET', '/capped/dashboard', cookies: browser_c) + assert_equal 200, status_b + assert_equal 200, status_c + end + + def test_repeatedly_exceeding_the_limit_keeps_it_pinned_at_the_max + 5.times { capped_sign_in } + assert_equal 2, AuthLoginSession.count + end + + def zero_capped_sign_in + call('POST', '/zero-capped/sign_in', + headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, + body: 'email=foksiu@example.com&password=hunter2') + end + + # `max_remember_sessions: 0` is a deliberately-set "never actually + # remembered" limit (see the comment on prune_oldest_login_sessions) - + # unlike every positive limit above, it must prune the row `sign_in` + # itself just created, not just older ones. + def test_max_remember_sessions_of_zero_prunes_the_row_it_just_created + status, headers, = zero_capped_sign_in + assert_equal 200, status + assert_equal 0, AuthLoginSession.count, + 'a limit of 0 must prune the just-created row too, not only rows older than it' + + session_cookie = session_cookie_from(headers) + refute_nil session_cookie, 'sign_in always sets the plain session cookie regardless of the remember-row prune' + status, = call('GET', '/zero-capped/dashboard', cookies: session_cookie) + assert_equal 200, status, 'the session cookie set by this very sign_in must still authenticate the request' + end + + def test_max_remember_sessions_of_zero_makes_the_remember_cookie_dead_on_arrival + _, headers, = zero_capped_sign_in + remember_cookie = cookie_named(headers, 'remember_session_token') + refute_nil remember_cookie, 'sign_in(remember: true) sets the cookie unconditionally, before the prune runs' + + status, = call('GET', '/zero-capped/dashboard', cookies: remember_cookie) + assert_equal 302, status, 'the remember cookie token matches no row once the limit-0 prune destroyed it' + end +end