diff --git a/scripts/run-stand-api-tests.sh b/scripts/run-stand-api-tests.sh index 78c2ba7..51aff91 100644 --- a/scripts/run-stand-api-tests.sh +++ b/scripts/run-stand-api-tests.sh @@ -2,6 +2,11 @@ # Full API CT suite against live stand (remote) + smoke-core on stand host. # Usage: run-stand-api-tests.sh # 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 diff --git a/test/api/api_test_runner.erl b/test/api/api_test_runner.erl index a5b8d29..7394ece 100644 --- a/test/api/api_test_runner.erl +++ b/test/api/api_test_runner.erl @@ -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. diff --git a/test/api_admins_SUITE.erl b/test/api_admins_SUITE.erl index aa6a9ad..e41b063 100644 --- a/test/api_admins_SUITE.erl +++ b/test/api_admins_SUITE.erl @@ -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 -> diff --git a/test/api_users_SUITE.erl b/test/api_users_SUITE.erl index f8ebca1..8328c2f 100755 --- a/test/api_users_SUITE.erl +++ b/test/api_users_SUITE.erl @@ -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 ->