fix(ct): teardown litter users/calendars after API suites
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
Reference in New Issue
Block a user