-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]). -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. 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]) ).