fix(ct): teardown litter users/calendars after API suites
CI / test (push) Successful in 7m3s
CI / deploy-ift (push) Successful in 3m53s
CI / e2e-ift (push) Successful in 3m34s
CI / deploy-stage (push) Successful in 2m19s
CI / e2e-stage (push) Successful in 1m19s

Track users/calendars created via register_and_login and create_calendar;
soft-delete via admin API in end_per_suite. Skip with CT_KEEP_DATA=1.
Safety-net soft-deletes remaining @test.local users.

Fixes EventHub/EventHubBack#65
This commit is contained in:
2026-07-31 12:21:15 +03:00
parent 7d39c004a1
commit ba1649f344
4 changed files with 151 additions and 2 deletions
+6
View File
@@ -2,6 +2,11 @@
# Full API CT suite against live stand (remote) + smoke-core on stand host.
# Usage: run-stand-api-tests.sh <ift|stage>
# Requires: ADMIN_* from load-stand-api-env.sh; SSH_* for smoke-core on stand.
#
# Litter cleanup: after each CT suite, api_test_runner:cleanup_tracked/0
# soft-deletes tracked calendars/users via admin API (and @test.local safety-net).
# Skip teardown: CT_KEEP_DATA=1 (data remains on the stand).
# Soft-delete ≠ hard wipe; full clean slate = ops Mnesia volume wipe.
set -euo pipefail
STAND="${1:?stand: ift|stage}"
@@ -134,6 +139,7 @@ docker run --rm --network=host \
-e "ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-}" \
-e "ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-}" \
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
-e "CT_KEEP_DATA=${CT_KEEP_DATA:-}" \
"${IMAGE}"
CT_RC=$?
set -e
+135
View File
@@ -52,6 +52,13 @@
-export([verify_user/2]).
-export([future_date_iso8601/0, iso8601_utc/1, future_month_range_iso8601/0,
next_month_start_iso8601/0, next_month_range_iso8601/0]).
-export([
track_user/1,
track_calendar/1,
cleanup_tracked/0,
keep_ct_data/0,
reset_litter_tracker/0
]).
%%%===================================================================
%%% Конфигурация окружения (CT_MODE, ...)
@@ -125,6 +132,127 @@ clear_cached_admin_tokens() ->
[admin, superadmin, moderator, support]
).
%%%===================================================================
%%% Litter tracker (users/calendars created during CT)
%%% persistent_term — survives CT testcase process exits (unlike ETS owner).
%%%===================================================================
-define(LITTER_USERS, {?MODULE, tracked_users}).
-define(LITTER_CALS, {?MODULE, tracked_calendars}).
%% @doc CT_KEEP_DATA=1 — skip suite teardown soft-delete (default: clean up).
-spec keep_ct_data() -> boolean().
keep_ct_data() ->
case os:getenv("CT_KEEP_DATA") of
"1" -> true;
"true" -> true;
"yes" -> true;
_ -> false
end.
-spec reset_litter_tracker() -> ok.
reset_litter_tracker() ->
persistent_term:put(?LITTER_USERS, []),
persistent_term:put(?LITTER_CALS, []),
ok.
-spec track_user(binary()) -> ok.
track_user(UserId) when is_binary(UserId), UserId =/= <<>> ->
track_id(?LITTER_USERS, UserId);
track_user(_) ->
ok.
-spec track_calendar(binary()) -> ok.
track_calendar(CalId) when is_binary(CalId), CalId =/= <<>> ->
track_id(?LITTER_CALS, CalId);
track_calendar(_) ->
ok.
track_id(Key, Id) ->
Old = persistent_term:get(Key, []),
case lists:member(Id, Old) of
true -> ok;
false -> persistent_term:put(Key, [Id | Old]), ok
end.
%% @doc Soft-delete tracked calendars then users via admin API.
%% Best-effort: never fails the suite. No-op when CT_KEEP_DATA=1.
-spec cleanup_tracked() -> ok.
cleanup_tracked() ->
case keep_ct_data() of
true ->
ct:pal("CT_KEEP_DATA set — skip API CT litter cleanup"),
ok;
false ->
do_cleanup_tracked()
end.
do_cleanup_tracked() ->
CalIds = lists:usort(persistent_term:get(?LITTER_CALS, [])),
UserIds = lists:usort(persistent_term:get(?LITTER_USERS, [])),
ct:pal("API CT litter cleanup: ~p calendars, ~p users", [length(CalIds), length(UserIds)]),
AdminToken =
try get_admin_token()
catch
_:Reason ->
ct:pal("API CT litter cleanup: no admin token (~p) — skip", [Reason]),
undefined
end,
case AdminToken of
undefined ->
ok;
Token ->
lists:foreach(
fun(Id) ->
admin_delete_best_effort(<<"/v1/admin/calendars/", Id/binary>>, Token)
end,
CalIds
),
lists:foreach(
fun(Id) ->
admin_delete_best_effort(<<"/v1/admin/users/", Id/binary>>, Token)
end,
UserIds
),
cleanup_test_local_users(Token),
reset_litter_tracker(),
ok
end.
%% Safety-net: soft-delete users with @test.local (missed by tracker).
cleanup_test_local_users(AdminToken) ->
case admin_request(get, <<"/v1/admin/users?q=test.local&limit=100">>, AdminToken) of
{ok, 200, _, Body} ->
Users = jsx:decode(list_to_binary(Body), [return_maps]),
lists:foreach(
fun
(#{<<"id">> := Id, <<"email">> := Email}) when is_binary(Email) ->
case binary:match(Email, <<"@test.local">>) of
nomatch -> ok;
_ -> admin_delete_best_effort(<<"/v1/admin/users/", Id/binary>>, AdminToken)
end;
(_) ->
ok
end,
case Users of
L when is_list(L) -> L;
_ -> []
end
);
Other ->
ct:pal("API CT litter cleanup: list users skip (~p)", [Other]),
ok
end.
admin_delete_best_effort(Path, Token) ->
case admin_request(delete, Path, Token) of
{ok, Status, _, _} when Status >= 200, Status < 300 -> ok;
{ok, 404, _, _} -> ok;
Other ->
ct:pal("API CT litter cleanup: DELETE ~s -> ~p", [Path, Other]),
ok
end.
%%%===================================================================
%%% Учётные данные администраторов (из переменных окружения)
%%%===================================================================
@@ -395,6 +523,7 @@ register_and_login(Email, Password) ->
jsx:encode(#{email => Email, password => Password})),
{ok, 201, _, Body} = Resp,
#{<<"user">> := #{<<"id">> := UserId}} = jsx:decode(list_to_binary(Body), [return_maps]),
track_user(UserId),
% 2. Получаем админский токен (используется кеширование)
AdminToken = get_admin_token(),
@@ -429,6 +558,12 @@ create_calendar(Token, Params) ->
case client_request(post, <<"/v1/calendars">>, Token, Body) of
{ok, 201, _, RespBody} ->
#{<<"id">> := CalId} = jsx:decode(list_to_binary(RespBody), [return_maps]),
%% Personal Default is sole — do not track for delete (403 default_calendar).
case Type of
<<"personal">> -> ok;
personal -> ok;
_ -> track_calendar(CalId)
end,
CalId;
{ok, 409, _, _} when Type =:= <<"personal">>; Type =:= personal ->
%% Sole Default personal already exists — reuse it.
+5 -1
View File
@@ -46,6 +46,7 @@ all() ->
%% @doc Инициализация сьюта.
%% В локальном режиме запускает приложение EventHub.
init_per_suite(Config) ->
ok = api_test_runner:reset_litter_tracker(),
case os:getenv("CT_MODE", "local") of
"remote" ->
ct:pal("Remote mode: assuming application is already running"),
@@ -72,8 +73,11 @@ init_per_suite(Config) ->
end
end.
%% @doc Завершение сьюта. В локальном режиме останавливает приложение.
%% @doc Завершение сьюта: litter cleanup, затем stop app в local.
end_per_suite(Config) ->
%% Soft-delete tracked @test.local users/calendars (remote and local).
%% Skip with CT_KEEP_DATA=1. Must run before application:stop.
catch api_test_runner:cleanup_tracked(),
case proplists:get_value(started_by_us, Config, false) andalso
os:getenv("CT_MODE", "local") =/= "remote" of
true ->
+5 -1
View File
@@ -55,6 +55,7 @@ all() ->
%% @doc Инициализация сьюта.
%% В локальном режиме запускает приложение EventHub.
init_per_suite(Config) ->
ok = api_test_runner:reset_litter_tracker(),
case os:getenv("CT_MODE", "local") of
"remote" ->
ct:pal("Remote mode: assuming application is already running"),
@@ -81,8 +82,11 @@ init_per_suite(Config) ->
end
end.
%% @doc Завершение сьюта. В локальном режиме останавливает приложение.
%% @doc Завершение сьюта: litter cleanup, затем stop app в local.
end_per_suite(Config) ->
%% Soft-delete tracked @test.local users/calendars (remote and local).
%% Skip with CT_KEEP_DATA=1. Must run before application:stop.
catch api_test_runner:cleanup_tracked(),
case proplists:get_value(started_by_us, Config, false) andalso
os:getenv("CT_MODE", "local") =/= "remote" of
true ->