-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([wait_for_server/0]). -define(BASE_URL, "http://localhost:8080"). -define(ADMIN_URL, "http://localhost:8445"). %% ============ Глобальные переменные для тестов ============ -define(ADMIN_EMAIL, <<"admin@eventhub.local">>). -define(ADMIN_PASSWORD, <<"123456">>). -define(USER_EMAIL, <<"global_user@test.com">>). -define(USER_PASSWORD, <<"user123">>). %% ============ Инициализация ============ init_global_users() -> case get(admin_token) of undefined -> io:format("~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, % Логинимся через админский 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]), put(admin_token, AdminToken), put(admin_id, AdminId), % ---------- ПОЛЬЗОВАТЕЛЬ ---------- 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]), 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"), ok; _ -> io:format("Global users already initialized.~n"), ok end. 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, _} -> io:format("❌ Server is not running!~n"), exit(server_not_running) end, init_global_users(), io:format("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 запросы ============ 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)}, [], []). 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}, [], []). 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)}, [], []). http_delete(Url, Token) -> Headers = [{"Authorization", "Bearer " ++ binary_to_list(Token)}], httpc:request(delete, {?BASE_URL ++ Url, Headers}, [], []). %% ============ Утилиты ============ 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) -> Id = extract_json(http_post("/v1/calendars", Params, Token), <<"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", []}, [], [{timeout, 1000}]) of {ok, {{_, 200, _}, _, _}} -> ok; _ -> timer:sleep(1000), wait_for_server(Attempts - 1) end.