feat(infra): add TTL cleanup worker for append-only tables
Periodically prune old read notifications, revoked/expired auth sessions, and admin_audit in chunked transactions; wire under eventhub config.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
-module(infra_cleanup_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
-define(TABLES, [notification, auth_session, admin_audit]).
|
||||
|
||||
setup() ->
|
||||
eh_test_support:start_mnesia(),
|
||||
eh_test_support:ensure_tables(?TABLES),
|
||||
ok.
|
||||
|
||||
cleanup(_) ->
|
||||
eh_test_support:clear_tables(?TABLES),
|
||||
eh_test_support:delete_tables(?TABLES),
|
||||
eh_test_support:stop_mnesia(),
|
||||
ok.
|
||||
|
||||
infra_cleanup_test_() ->
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"cutoff_datetime is in the past", fun test_cutoff_datetime/0},
|
||||
{"keeps unread notifications", fun test_keeps_unread_notifications/0},
|
||||
{"deletes old read notifications", fun test_deletes_old_read_notifications/0},
|
||||
{"keeps active non-expired sessions", fun test_keeps_active_sessions/0},
|
||||
{"deletes old revoked sessions", fun test_deletes_old_revoked_sessions/0},
|
||||
{"deletes old admin_audit", fun test_deletes_old_admin_audit/0}
|
||||
]}.
|
||||
|
||||
test_cutoff_datetime() ->
|
||||
Cutoff = infra_cleanup:cutoff_datetime(30),
|
||||
NowSecs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
||||
CutoffSecs = calendar:datetime_to_gregorian_seconds(Cutoff),
|
||||
?assert(CutoffSecs < NowSecs),
|
||||
%% ~30 days (± a few seconds for test runtime)
|
||||
DiffDays = (NowSecs - CutoffSecs) / 86400,
|
||||
?assert(DiffDays > 29.9),
|
||||
?assert(DiffDays < 30.1).
|
||||
|
||||
test_keeps_unread_notifications() ->
|
||||
Old = infra_cleanup:cutoff_datetime(60),
|
||||
write_notification(<<"n_unread">>, false, Old),
|
||||
write_notification(<<"n_read_recent">>, true, calendar:universal_time()),
|
||||
#{notifications := N} = infra_cleanup:run_once(),
|
||||
?assertEqual(0, N),
|
||||
?assertMatch([_], mnesia:dirty_read(notification, <<"n_unread">>)),
|
||||
?assertMatch([_], mnesia:dirty_read(notification, <<"n_read_recent">>)).
|
||||
|
||||
test_deletes_old_read_notifications() ->
|
||||
Old = infra_cleanup:cutoff_datetime(60),
|
||||
write_notification(<<"n_old_read">>, true, Old),
|
||||
write_notification(<<"n_unread">>, false, Old),
|
||||
#{notifications := N} = infra_cleanup:run_once(),
|
||||
?assertEqual(1, N),
|
||||
?assertEqual([], mnesia:dirty_read(notification, <<"n_old_read">>)),
|
||||
?assertMatch([_], mnesia:dirty_read(notification, <<"n_unread">>)).
|
||||
|
||||
test_keeps_active_sessions() ->
|
||||
Now = calendar:universal_time(),
|
||||
Future = days_offset(Now, 7),
|
||||
Recent = days_offset(Now, -1),
|
||||
write_auth_session(<<"s_active">>, false, Future, Recent),
|
||||
#{auth_sessions := N} = infra_cleanup:run_once(),
|
||||
?assertEqual(0, N),
|
||||
?assertMatch([_], mnesia:dirty_read(auth_session, <<"s_active">>)).
|
||||
|
||||
test_deletes_old_revoked_sessions() ->
|
||||
Now = calendar:universal_time(),
|
||||
Old = days_offset(Now, -14),
|
||||
Future = days_offset(Now, 7),
|
||||
write_auth_session(<<"s_revoked_old">>, true, Future, Old),
|
||||
write_auth_session(<<"s_active">>, false, Future, Old),
|
||||
#{auth_sessions := N} = infra_cleanup:run_once(),
|
||||
?assert(N >= 1),
|
||||
?assertEqual([], mnesia:dirty_read(auth_session, <<"s_revoked_old">>)),
|
||||
%% Active but old created_at with future expiry must stay
|
||||
?assertMatch([_], mnesia:dirty_read(auth_session, <<"s_active">>)).
|
||||
|
||||
test_deletes_old_admin_audit() ->
|
||||
Now = calendar:universal_time(),
|
||||
Old = days_offset(Now, -120),
|
||||
write_admin_audit(<<"a_old">>, Old),
|
||||
write_admin_audit(<<"a_new">>, Now),
|
||||
#{admin_audit := N} = infra_cleanup:run_once(),
|
||||
?assertEqual(1, N),
|
||||
?assertEqual([], mnesia:dirty_read(admin_audit, <<"a_old">>)),
|
||||
?assertMatch([_], mnesia:dirty_read(admin_audit, <<"a_new">>)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Helpers
|
||||
%%%===================================================================
|
||||
|
||||
write_notification(Id, IsRead, CreatedAt) ->
|
||||
mnesia:dirty_write(#notification{
|
||||
id = Id,
|
||||
user_id = <<"u1">>,
|
||||
type = custom,
|
||||
title = <<"t">>,
|
||||
body = <<"b">>,
|
||||
is_read = IsRead,
|
||||
created_at = CreatedAt
|
||||
}).
|
||||
|
||||
write_auth_session(Id, Revoked, ExpiresAt, CreatedAt) ->
|
||||
mnesia:dirty_write(#auth_session{
|
||||
session_id = Id,
|
||||
family_id = <<"fam">>,
|
||||
subject_id = <<"subj">>,
|
||||
subject_type = user,
|
||||
client_type = <<"web">>,
|
||||
current_jti = <<"jti">>,
|
||||
expires_at = ExpiresAt,
|
||||
revoked = Revoked,
|
||||
created_at = CreatedAt,
|
||||
updated_at = CreatedAt
|
||||
}).
|
||||
|
||||
write_admin_audit(Id, Timestamp) ->
|
||||
mnesia:dirty_write(#admin_audit{
|
||||
id = Id,
|
||||
admin_id = <<"adm">>,
|
||||
email = <<"a@test.local">>,
|
||||
role = admin,
|
||||
action = <<"test">>,
|
||||
entity_type = <<"user">>,
|
||||
entity_id = <<"u1">>,
|
||||
timestamp = Timestamp,
|
||||
ip = <<"127.0.0.1">>,
|
||||
reason = <<>>
|
||||
}).
|
||||
|
||||
days_offset(Datetime, Days) ->
|
||||
Secs = calendar:datetime_to_gregorian_seconds(Datetime),
|
||||
calendar:gregorian_seconds_to_datetime(Secs + Days * 86400).
|
||||
Reference in New Issue
Block a user