Переделать связь нод в кластере на автоматическое обнаружение #9

This commit is contained in:
2026-05-01 22:30:40 +03:00
parent 1787b0f8a3
commit f36dd3bbc1
25 changed files with 870 additions and 332 deletions

View File

@@ -1,67 +1,132 @@
-module(api_test_runner).
-include("records.hrl").
-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]).
-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]).
-export([wait_for_server/0]).
-define(BASE_URL, "http://localhost:8080").
-define(ADMIN_URL, "http://localhost:8445").
-define(BASE_URL, base_url()).
-define(ADMIN_URL, admin_base_url()).
%% ============ Глобальные переменные для тестов ============
-define(ADMIN_EMAIL, <<"admin@eventhub.local">>).
-define(ADMIN_PASSWORD, <<"123456">>).
-define(USER_EMAIL, <<"global_user@test.com">>).
-define(USER_PASSWORD, <<"user123">>).
%% Учётные данные по умолчанию (используются в локальном режиме, если словарь пуст)
-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 ->
io:format("~n=== Initializing global test users ===~n"),
ct:pal("~n=== Initializing global test users ===~n"),
% ---------- АДМИНИСТРАТОР ----------
% Проверяем, существует ли админ в таблице admin
case core_admin:get_by_email(?ADMIN_EMAIL) of
{ok, Admin} ->
io:format("Admin already exists: ~s~n", [Admin#admin.id]),
ok;
{error, not_found} ->
% Создаём суперадмина напрямую
{ok, Admin} = core_admin:create(?ADMIN_EMAIL, ?ADMIN_PASSWORD, superadmin),
io:format("Admin created: ~s~n", [Admin#admin.id])
end,
%% 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,
% Логинимся через админский API
LoginBody = jsx:encode(#{<<"email">> => ?ADMIN_EMAIL, <<"password">> => ?ADMIN_PASSWORD}),
{ok, {{_, 200, _}, _, LoginResp}} = httpc:request(post,
{?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []),
#{<<"token">> := AdminToken, <<"user">> := #{<<"id">> := AdminId}} =
jsx:decode(list_to_binary(LoginResp), [return_maps]),
%% Получаем 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, _}, _, UserMeResp}} = http_get("/v1/user/me", UserToken),
#{<<"id">> := UserId} = jsx:decode(list_to_binary(UserMeResp), [return_maps]),
{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),
io:format("Admin ID: ~s, User ID: ~s~n", [AdminId, UserId]),
io:format("=== Global users initialized ===~n~n"),
ct:pal("Admin ID: ~s, User ID: ~s~n", [AdminId, UserId]),
ct:pal("=== Global users initialized ===~n~n"),
ok;
_ ->
io:format("Global users already initialized.~n"),
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).
@@ -78,19 +143,18 @@ get_user_id() ->
init_global_users(),
get(user_id).
%% ============ Главные функции запуска ============
run_all() ->
inets:start(),
ssl:start(),
case wait_for_server() of
ok -> ok;
{error, _} -> io:format("❌ Server is not running!~n"), exit(server_not_running)
{error, _} -> ct:pal("❌ Server is not running!~n"), exit(server_not_running)
end,
init_global_users(),
io:format("Starting API tests...~n"),
ct:pal("Starting API tests...~n"),
Modules = [
api_auth_tests,
api_calendar_tests,
@@ -111,14 +175,17 @@ run(Module) ->
init_global_users(),
Module:test().
%% ============ HTTP запросы ============
%% ── 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)}, [], []).
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) ->
@@ -126,18 +193,17 @@ http_get(Url, Token) ->
undefined -> [];
_ -> [{"Authorization", "Bearer " ++ binary_to_list(Token)}]
end,
httpc:request(get, {?BASE_URL ++ Url, Headers}, [], []).
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)}, [], []).
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}, [], []).
%% ============ Утилиты ============
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);
@@ -170,7 +236,6 @@ register_and_login(Email, Password) ->
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]),
@@ -189,7 +254,7 @@ create_event(Token, CalId, Params) ->
wait_for_server() -> wait_for_server(30).
wait_for_server(0) -> {error, timeout};
wait_for_server(Attempts) ->
case httpc:request(get, {?BASE_URL ++ "/health", []}, [], [{timeout, 1000}]) of
case httpc:request(get, {?BASE_URL ++ "/health", []}, ssl_opts(), [{timeout, 1000}]) of
{ok, {{_, 200, _}, _, _}} -> ok;
_ -> timer:sleep(1000), wait_for_server(Attempts - 1)
end.