Files
EventHubBack/test/api/api_test_runner.erl
Алексей Сабилин ecf68ee300 Fix /v1/admin/stats всегда пустые данные
Добавлено поле с датой последнего логина пользователям и админам  #20
2026-05-08 20:21:04 +03:00

277 lines
10 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-module(api_test_runner).
-export([run_all/0, run/1]).
-export([http_post/2, http_post/3, http_get/1, http_get/2, http_put/3, http_delete/2]).
-export([extract_json/2, extract_json/3, assert_status/2]).
-export([unique_email/1, register_and_login/2, create_calendar/2, create_event/3]).
-export([get_admin_token/0, get_admin_id/0, get_user_token/0, get_user_id/0, get_admin_url/0, get_base_url/0, get_admin_ws_url/0, get_base_ws_url/0, login_admin/2, login_custom_admin/2]).
-export([wait_for_server/0]).
-export([format_datetime/1]).
-define(BASE_URL, base_url()).
-define(ADMIN_URL, admin_base_url()).
%% Учётные данные по умолчанию (используются в локальном режиме, если словарь пуст)
-define(FALLBACK_ADMIN_EMAIL, <<"admin@eventhub.local">>).
-define(FALLBACK_ADMIN_PASSWORD, <<"123456">>).
-define(USER_EMAIL, <<"global_user@test.com">>).
-define(USER_PASSWORD, <<"user123">>).
%% ------------------------------------------------------------------
%% Выбор базовых URL в зависимости от режима запуска
%% ------------------------------------------------------------------
base_url() ->
case os:getenv("CT_MODE", "local") of
"remote" -> os:getenv("API_HOST", "http://localhost:8080");
_ -> "http://localhost:8080"
end.
base_ws_url() ->
case os:getenv("CT_MODE", "local") of
"remote" -> os:getenv("WS_HOST", "ws://localhost:8081");
_ -> "ws://localhost:8081"
end.
admin_base_url() ->
case os:getenv("CT_MODE", "local") of
"remote" -> os:getenv("ADMIN_API_HOST", "http://localhost:8445");
_ -> "http://localhost:8445"
end.
admin_ws_url() ->
case os:getenv("CT_MODE", "local") of
"remote" -> os:getenv("ADMIN_WS_HOST", "ws://localhost:8446");
_ -> "ws://localhost:8446"
end.
%% ------------------------------------------------------------------
%% Инициализация глобальных тестовых пользователей
%% ------------------------------------------------------------------
init_global_urls() ->
put(admin_url, admin_base_url()),
put(admin_ws_url, admin_ws_url()),
put(base_url, base_url()),
put(base_ws_url, base_ws_url()).
init_global_users() ->
case get(admin_token) of
undefined ->
ct:pal("~n=== Initializing global test users ===~n"),
%% 1. Администратор
AdminEmail = get(admin_super_email),
AdminPassword = get(admin_super_password),
AdminToken =
if
AdminEmail =/= undefined, AdminPassword =/= undefined ->
%% Учётные данные переданы из api_SUITE (remoteрежим) просто логинимся
login_admin(AdminEmail, AdminPassword);
true ->
%% Локальный режим: админы уже есть, логинимся под суперадмином
login_admin(?FALLBACK_ADMIN_EMAIL, ?FALLBACK_ADMIN_PASSWORD)
end,
%% Получаем ID администратора через /v1/admin/me
MeUrl = ?ADMIN_URL ++ "/v1/admin/me",
{ok, {{_, 200, _}, _, MeBody}} = httpc:request(get,
{MeUrl, [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, ssl_opts(), []),
#{<<"id">> := AdminId} = jsx:decode(list_to_binary(MeBody), [return_maps]),
put(admin_token, AdminToken),
put(admin_id, AdminId),
%% 2. Обычный пользователь
UserToken = register_and_login(?USER_EMAIL, ?USER_PASSWORD),
{ok, {{_, 200, _}, _, UserMeBody}} = http_get("/v1/user/me", UserToken),
#{<<"id">> := UserId} = jsx:decode(list_to_binary(UserMeBody), [return_maps]),
put(user_token, UserToken),
put(user_id, UserId),
ct:pal("Admin ID: ~s, User ID: ~s~n", [AdminId, UserId]),
ct:pal("=== Global users initialized ===~n~n"),
ok;
_ ->
ct:pal("Global users already initialized.~n"),
ok
end.
%% ------------------------------------------------------------------
%% Вход администратора (используется, когда учётки уже известны)
%% ------------------------------------------------------------------
login_admin(Email, Password) ->
ct:pal("Admin url: ~s~n", [?ADMIN_URL]),
ct:pal("Admin: ~s, password: ~s~n", [Email, Password]),
LoginBody = jsx:encode(#{<<"email">> => Email, <<"password">> => Password}),
ct:pal("url: ~s, body: ~s~n", [?ADMIN_URL ++ "/v1/admin/login", LoginBody]),
{ok, {{_, _, _}, _, LoginResp}} = httpc:request(post,
{?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, ssl_opts(), []),
ct:pal("LoginResp: ~s~n", [LoginResp]),
#{<<"token">> := Token} = jsx:decode(list_to_binary(LoginResp), [return_maps]),
Token.
%% ------------------------------------------------------------------
%% Остальные функции (без изменений, только используют ?BASE_URL / ?ADMIN_URL)
%% ------------------------------------------------------------------
get_admin_url() ->
init_global_urls(),
get(admin_url).
get_admin_ws_url() ->
init_global_urls(),
get(admin_ws_url).
get_base_url() ->
init_global_urls(),
get(base_url).
get_base_ws_url() ->
init_global_urls(),
get(base_ws_url).
get_admin_token() ->
init_global_users(),
get(admin_token).
get_admin_id() ->
init_global_users(),
get(admin_id).
get_user_token() ->
init_global_users(),
get(user_token).
get_user_id() ->
init_global_users(),
get(user_id).
run_all() ->
inets:start(),
ssl:start(),
case wait_for_server() of
ok -> ok;
{error, _} -> ct:pal("❌ Server is not running!~n"), exit(server_not_running)
end,
init_global_users(),
ct:pal("Starting API tests...~n"),
Modules = [
api_auth_tests,
api_calendar_tests,
api_event_tests,
api_booking_tests,
api_search_tests,
api_reviews_tests,
api_moderation_tests,
api_tickets_tests,
api_subscription_tests,
api_admin_tests
],
lists:foreach(fun(M) -> M:test() end, Modules).
run(Module) ->
inets:start(),
ssl:start(),
init_global_users(),
Module:test().
%% ── HTTPзапросы ─────────────────────────────────────────
ssl_opts() ->
[{ssl, [{verify, verify_none}]}].
http_post(Url, Body) -> http_post(Url, Body, undefined).
http_post(Url, Body, Token) ->
Headers = case Token of
undefined -> [{"Content-Type", "application/json"}];
_ -> [{"Content-Type", "application/json"}, {"Authorization", "Bearer " ++ binary_to_list(Token)}]
end,
httpc:request(post, {?BASE_URL ++ Url, Headers, "application/json", jsx:encode(Body)}, ssl_opts(), []).
http_get(Url) -> http_get(Url, undefined).
http_get(Url, Token) ->
Headers = case Token of
undefined -> [];
_ -> [{"Authorization", "Bearer " ++ binary_to_list(Token)}]
end,
httpc:request(get, {?BASE_URL ++ Url, Headers}, ssl_opts(), []).
http_put(Url, Body, Token) ->
Headers = [{"Content-Type", "application/json"}, {"Authorization", "Bearer " ++ binary_to_list(Token)}],
httpc:request(put, {?BASE_URL ++ Url, Headers, "application/json", jsx:encode(Body)}, ssl_opts(), []).
http_delete(Url, Token) ->
Headers = [{"Authorization", "Bearer " ++ binary_to_list(Token)}],
httpc:request(delete, {?BASE_URL ++ Url, Headers}, ssl_opts(), []).
%% ── Вспомогательные функции ──────────────────────────────
extract_json({ok, {{_, 200, _}, _, Body}}, Field) ->
Map = jsx:decode(list_to_binary(Body), [return_maps]),
maps:get(Field, Map);
extract_json({ok, {{_, 201, _}, _, Body}}, Field) ->
Map = jsx:decode(list_to_binary(Body), [return_maps]),
maps:get(Field, Map);
extract_json(Response, _Field) ->
error({unexpected_response, Response}).
extract_json(Response, Field, ExpectedStatus) ->
case Response of
{ok, {{_, ExpectedStatus, _}, _, Body}} ->
Map = jsx:decode(list_to_binary(Body), [return_maps]),
maps:get(Field, Map);
_ ->
error({unexpected_response, Response})
end.
assert_status(Status, {ok, {{_, Status, _}, _, _}}) -> ok;
assert_status(Expected, {ok, {{_, Got, _}, _, _}}) ->
error({expected_status, Expected, got, Got}).
unique_email(Prefix) ->
list_to_binary([Prefix, "_", integer_to_binary(os:system_time(millisecond)), "@test.com"]).
register_and_login(Email, Password) ->
RegBody = #{email => Email, password => Password},
case http_post("/v1/register", RegBody) of
{ok, {{_, 201, _}, _, RegResp}} ->
Map = jsx:decode(list_to_binary(RegResp), [return_maps]),
maps:get(<<"token">>, Map);
{ok, {{_, 409, _}, _, _}} ->
LoginBody = #{email => Email, password => Password},
{ok, {{_, 200, _}, _, LoginResp}} = http_post("/v1/login", LoginBody),
Map = jsx:decode(list_to_binary(LoginResp), [return_maps]),
maps:get(<<"token">>, Map)
end.
login_custom_admin(Email, Password) ->
%% LoginBody = #{email => Email, password => Password},
LoginBody = jsx:encode(#{<<"email">> => Email, <<"password">> => Password}),
{ok, {{_, _, _}, _, LoginResp}} = httpc:request(post,
{?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, ssl_opts(), []),
Map = jsx:decode(list_to_binary(LoginResp), [return_maps]),
maps:get(<<"token">>, Map).
create_calendar(Token, Params) ->
Response = http_post("/v1/calendars", Params, Token),
ct:pal(" create_calendar Response: ~p~n", [Response]),
Id = extract_json(Response, <<"id">>),
Id.
create_event(Token, CalId, Params) ->
Url = "/v1/calendars/" ++ binary_to_list(CalId) ++ "/events",
Id = extract_json(http_post(Url, Params, Token), <<"id">>),
Id.
wait_for_server() -> wait_for_server(30).
wait_for_server(0) -> {error, timeout};
wait_for_server(Attempts) ->
case httpc:request(get, {?BASE_URL ++ "/health", []}, ssl_opts(), [{timeout, 1000}]) of
{ok, {{_, 200, _}, _, _}} -> ok;
_ -> timer:sleep(1000), wait_for_server(Attempts - 1)
end.
format_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
iolist_to_binary(
io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
[Year, Month, Day, Hour, Minute, Second])
).