-module(logic_auth_session_tests). -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>). -define(USER_JWT_SECRET, <<"test-user-secret-key-32-by">>). setup() -> ok = meck:new(core_admin, [non_strict]), ok = meck:new(core_user, [non_strict]), mnesia:start(), catch ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]), mnesia:create_table(auth_session, [ {attributes, record_info(fields, auth_session)}, {ram_copies, [node()]} ]), mnesia:add_table_index(auth_session, #auth_session.family_id), mnesia:add_table_index(auth_session, #auth_session.subject_id), application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET), application:set_env(eventhub, jwt_secret, ?USER_JWT_SECRET), {ok, _} = application:ensure_all_started(jose), ok. cleanup(_) -> application:unset_env(eventhub, admin_jwt_secret), application:unset_env(eventhub, jwt_secret), application:stop(jose), meck:unload(core_admin), meck:unload(core_user), catch mnesia:delete_table(auth_session), catch ets:delete(eventhub_counters), mnesia:stop(), ok. logic_auth_session_test_() -> {foreach, fun setup/0, fun cleanup/1, [ {"Issue and refresh admin tokens", {timeout, 120, fun test_issue_and_refresh_admin/0}}, {"Refresh admin reuse revokes family", {timeout, 120, fun test_admin_reuse_revokes/0}}, {"Issue and refresh user tokens", {timeout, 120, fun test_issue_and_refresh_user/0}}, {"Refresh user reuse revokes family", {timeout, 120, fun test_user_reuse_revokes/0}}, {"Logout user then refresh fails", {timeout, 120, fun test_logout_user/0}}, {"Logout admin then refresh fails", {timeout, 120, fun test_logout_admin/0}}, {"Mobile device meta and list", {timeout, 120, fun test_mobile_device_and_list/0}}, {"Revoke others keeps current", {timeout, 120, fun test_revoke_others/0}} ]}. test_issue_and_refresh_admin() -> AdminId = <<"adm1">>, Admin = #admin{ id = AdminId, email = <<"a@test.com">>, role = superadmin, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_admin, get_by_id, fun(Id) -> ?assertEqual(AdminId, Id), {ok, Admin} end), {ok, Access1, Refresh1, Sid1} = logic_auth_session:issue_admin_tokens(AdminId, <<"superadmin">>), ?assert(is_binary(Access1)), ?assert(is_binary(Sid1)), {ok, _, _} = eventhub_auth:verify_admin_token(Access1), {ok, Access2, Refresh2, Sid2} = logic_auth_session:refresh_admin(Refresh1), ?assertEqual(Sid1, Sid2), ?assert(is_binary(Access2)), ?assertNotEqual(Refresh1, Refresh2), {ok, _, _} = eventhub_auth:verify_admin_token(Access2). test_admin_reuse_revokes() -> AdminId = <<"adm1">>, Admin = #admin{ id = AdminId, email = <<"a@test.com">>, role = admin, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_admin, get_by_id, fun(_) -> {ok, Admin} end), {ok, _, Refresh1, _} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>), {ok, _, Refresh2, _} = logic_auth_session:refresh_admin(Refresh1), ?assertMatch({error, reuse_detected}, logic_auth_session:refresh_admin(Refresh1)), ?assertMatch({error, revoked}, logic_auth_session:refresh_admin(Refresh2)). test_issue_and_refresh_user() -> UserId = <<"usr1">>, User = #user{ id = UserId, email = <<"u@test.com">>, role = user, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_user, get_by_id, fun(Id) -> ?assertEqual(UserId, Id), {ok, User} end), {ok, Access1, Refresh1, Sid1} = logic_auth_session:issue_user_tokens(UserId, <<"user">>), ?assert(is_binary(Access1)), {ok, _, _} = eventhub_auth:verify_user_token(Access1), {ok, Claims} = eventhub_auth:verify_user_refresh_token(Refresh1), ?assertEqual(<<"user">>, maps:get(<<"aud">>, Claims)), ?assertEqual(<<"web">>, maps:get(<<"client">>, Claims)), {ok, Access2, Refresh2, Sid2} = logic_auth_session:refresh_user(Refresh1), ?assertEqual(Sid1, Sid2), ?assert(is_binary(Access2)), ?assertNotEqual(Refresh1, Refresh2), {ok, _, _} = eventhub_auth:verify_user_token(Access2). test_user_reuse_revokes() -> UserId = <<"usr1">>, User = #user{ id = UserId, email = <<"u@test.com">>, role = user, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end), {ok, _, Refresh1, _} = logic_auth_session:issue_user_tokens(UserId, <<"user">>), {ok, _, Refresh2, _} = logic_auth_session:refresh_user(Refresh1), ?assertMatch({error, reuse_detected}, logic_auth_session:refresh_user(Refresh1)), ?assertMatch({error, revoked}, logic_auth_session:refresh_user(Refresh2)). test_logout_user() -> UserId = <<"usr_logout">>, User = #user{ id = UserId, email = <<"ul@test.com">>, role = user, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end), {ok, _, Refresh, _} = logic_auth_session:issue_user_tokens(UserId, <<"user">>), ?assertEqual(ok, logic_auth_session:logout_user(Refresh)), ?assertMatch({error, revoked}, logic_auth_session:refresh_user(Refresh)). test_logout_admin() -> AdminId = <<"adm_logout">>, Admin = #admin{ id = AdminId, email = <<"al@test.com">>, role = admin, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_admin, get_by_id, fun(_) -> {ok, Admin} end), {ok, _, Refresh, _} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>), ?assertEqual(ok, logic_auth_session:logout_admin(Refresh)), ?assertMatch({error, revoked}, logic_auth_session:refresh_admin(Refresh)). test_mobile_device_and_list() -> UserId = <<"usr_dev">>, User = #user{ id = UserId, email = <<"d@test.com">>, role = user, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end), LongName = list_to_binary(lists:duplicate(200, $x)), {ok, _, Refresh, Sid} = logic_auth_session:issue_user_tokens( UserId, <<"user">>, <<"mobile">>, LongName, <<"UA-Test">>), {ok, Claims} = eventhub_auth:verify_user_refresh_token(Refresh), ?assertEqual(<<"mobile">>, maps:get(<<"client">>, Claims)), [S] = logic_auth_session:list_user_sessions(UserId), ?assertEqual(Sid, S#auth_session.session_id), ?assertEqual(<<"mobile">>, S#auth_session.client_type), ?assertEqual(<<"UA-Test">>, S#auth_session.user_agent), ?assertEqual(120, byte_size(S#auth_session.device_name)), ?assertMatch({error, invalid_client_type}, logic_auth_session:issue_user_tokens(UserId, <<"user">>, <<"tablet">>, <<>>, <<>>)). test_revoke_others() -> UserId = <<"usr_rev">>, User = #user{ id = UserId, email = <<"r@test.com">>, role = user, password_hash = <<>>, status = active, nickname = <<>>, avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>, phone = <<>>, preferences = #{}, last_login = undefined, created_at = undefined, updated_at = undefined }, ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end), {ok, _, RefreshKeep, KeepId} = logic_auth_session:issue_user_tokens( UserId, <<"user">>, <<"web">>, <<"Keep">>, <<"ua1">>), {ok, _, _RefreshOther, OtherId} = logic_auth_session:issue_user_tokens( UserId, <<"user">>, <<"web">>, <<"Other">>, <<"ua2">>), {ok, 1} = logic_auth_session:revoke_other_user_sessions(UserId, RefreshKeep), ?assertEqual(ok, logic_auth_session:revoke_user_session(UserId, OtherId)), Active = logic_auth_session:list_user_sessions(UserId), ?assertEqual(1, length(Active)), ?assertEqual(KeepId, (hd(Active))#auth_session.session_id), ?assertMatch({error, not_found}, logic_auth_session:revoke_user_session(UserId, <<"nope">>)).