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.
|
# Full API CT suite against live stand (remote) + smoke-core on stand host.
|
||||||
# Usage: run-stand-api-tests.sh <ift|stage>
|
# Usage: run-stand-api-tests.sh <ift|stage>
|
||||||
# Requires: ADMIN_* from load-stand-api-env.sh; SSH_* for smoke-core on stand.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
STAND="${1:?stand: ift|stage}"
|
STAND="${1:?stand: ift|stage}"
|
||||||
@@ -134,6 +139,7 @@ docker run --rm --network=host \
|
|||||||
-e "ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-}" \
|
-e "ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-}" \
|
||||||
-e "ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-}" \
|
-e "ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-}" \
|
||||||
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
|
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
|
||||||
|
-e "CT_KEEP_DATA=${CT_KEEP_DATA:-}" \
|
||||||
"${IMAGE}"
|
"${IMAGE}"
|
||||||
CT_RC=$?
|
CT_RC=$?
|
||||||
set -e
|
set -e
|
||||||
|
|||||||
@@ -52,6 +52,13 @@
|
|||||||
-export([verify_user/2]).
|
-export([verify_user/2]).
|
||||||
-export([future_date_iso8601/0, iso8601_utc/1, future_month_range_iso8601/0,
|
-export([future_date_iso8601/0, iso8601_utc/1, future_month_range_iso8601/0,
|
||||||
next_month_start_iso8601/0, next_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, ...)
|
%%% Конфигурация окружения (CT_MODE, ...)
|
||||||
@@ -125,6 +132,127 @@ clear_cached_admin_tokens() ->
|
|||||||
[admin, superadmin, moderator, support]
|
[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})),
|
jsx:encode(#{email => Email, password => Password})),
|
||||||
{ok, 201, _, Body} = Resp,
|
{ok, 201, _, Body} = Resp,
|
||||||
#{<<"user">> := #{<<"id">> := UserId}} = jsx:decode(list_to_binary(Body), [return_maps]),
|
#{<<"user">> := #{<<"id">> := UserId}} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||||
|
track_user(UserId),
|
||||||
|
|
||||||
% 2. Получаем админский токен (используется кеширование)
|
% 2. Получаем админский токен (используется кеширование)
|
||||||
AdminToken = get_admin_token(),
|
AdminToken = get_admin_token(),
|
||||||
@@ -429,6 +558,12 @@ create_calendar(Token, Params) ->
|
|||||||
case client_request(post, <<"/v1/calendars">>, Token, Body) of
|
case client_request(post, <<"/v1/calendars">>, Token, Body) of
|
||||||
{ok, 201, _, RespBody} ->
|
{ok, 201, _, RespBody} ->
|
||||||
#{<<"id">> := CalId} = jsx:decode(list_to_binary(RespBody), [return_maps]),
|
#{<<"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;
|
CalId;
|
||||||
{ok, 409, _, _} when Type =:= <<"personal">>; Type =:= personal ->
|
{ok, 409, _, _} when Type =:= <<"personal">>; Type =:= personal ->
|
||||||
%% Sole Default personal already exists — reuse it.
|
%% Sole Default personal already exists — reuse it.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ all() ->
|
|||||||
%% @doc Инициализация сьюта.
|
%% @doc Инициализация сьюта.
|
||||||
%% В локальном режиме запускает приложение EventHub.
|
%% В локальном режиме запускает приложение EventHub.
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
ok = api_test_runner:reset_litter_tracker(),
|
||||||
case os:getenv("CT_MODE", "local") of
|
case os:getenv("CT_MODE", "local") of
|
||||||
"remote" ->
|
"remote" ->
|
||||||
ct:pal("Remote mode: assuming application is already running"),
|
ct:pal("Remote mode: assuming application is already running"),
|
||||||
@@ -72,8 +73,11 @@ init_per_suite(Config) ->
|
|||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Завершение сьюта. В локальном режиме останавливает приложение.
|
%% @doc Завершение сьюта: litter cleanup, затем stop app в local.
|
||||||
end_per_suite(Config) ->
|
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
|
case proplists:get_value(started_by_us, Config, false) andalso
|
||||||
os:getenv("CT_MODE", "local") =/= "remote" of
|
os:getenv("CT_MODE", "local") =/= "remote" of
|
||||||
true ->
|
true ->
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ all() ->
|
|||||||
%% @doc Инициализация сьюта.
|
%% @doc Инициализация сьюта.
|
||||||
%% В локальном режиме запускает приложение EventHub.
|
%% В локальном режиме запускает приложение EventHub.
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
ok = api_test_runner:reset_litter_tracker(),
|
||||||
case os:getenv("CT_MODE", "local") of
|
case os:getenv("CT_MODE", "local") of
|
||||||
"remote" ->
|
"remote" ->
|
||||||
ct:pal("Remote mode: assuming application is already running"),
|
ct:pal("Remote mode: assuming application is already running"),
|
||||||
@@ -81,8 +82,11 @@ init_per_suite(Config) ->
|
|||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Завершение сьюта. В локальном режиме останавливает приложение.
|
%% @doc Завершение сьюта: litter cleanup, затем stop app в local.
|
||||||
end_per_suite(Config) ->
|
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
|
case proplists:get_value(started_by_us, Config, false) andalso
|
||||||
os:getenv("CT_MODE", "local") =/= "remote" of
|
os:getenv("CT_MODE", "local") =/= "remote" of
|
||||||
true ->
|
true ->
|
||||||
|
|||||||
Reference in New Issue
Block a user