%%%------------------------------------------------------------------- %%% @doc Централизованный модуль для запуска API-тестов. %%% Предоставляет функции для выполнения HTTP-запросов %%% к административному и клиентскому API с автоматическим %%% логированием, проверкой статусов и конфигурацией %%% через стандартные переменные окружения. %%% @end %%%------------------------------------------------------------------- -module(api_test_runner). -export([ get_admin_url/0, get_base_url/0, get_base_ws_url/0, get_admin_ws_url/0, get_admin_token/0, get_superadmin_token/0, get_moderator_token/0, get_support_token/0, get_user_token/0, unique_email/1, future_date/0, register_and_login/2, create_calendar/2, create_event/3 ]). -export([ admin_request/3, admin_request/4, client_request/3, client_request/4 ]). -export([ admin_get/2, admin_post/3, admin_put/3, admin_delete/2, client_get/2, client_post/3, client_put/3, client_delete/2, admin_patch/3]). %%%=================================================================== %%% Конфигурация окружения (CT_MODE, ...) %%%=================================================================== -spec ct_mode() -> string(). ct_mode() -> os:getenv("CT_MODE", "local"). -spec get_base_url() -> string(). get_base_url() -> case ct_mode() of "remote" -> os:getenv("API_HOST", "http://localhost:8080"); _ -> "http://localhost:8080" end. -spec get_admin_url() -> string(). get_admin_url() -> case ct_mode() of "remote" -> os:getenv("ADMIN_API_HOST", "http://localhost:8445"); _ -> "http://localhost:8445" end. -spec get_base_ws_url() -> string(). get_base_ws_url() -> case ct_mode() of "remote" -> os:getenv("WS_HOST", "ws://localhost:8081"); _ -> "ws://localhost:8081" end. -spec get_admin_ws_url() -> string(). get_admin_ws_url() -> case ct_mode() of "remote" -> os:getenv("ADMIN_WS_HOST", "ws://localhost:8446"); _ -> "ws://localhost:8446" end. %%%=================================================================== %%% Учётные данные администраторов (из переменных окружения) %%%=================================================================== -spec admin_super_email() -> binary(). admin_super_email() -> list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")). -spec admin_super_password() -> binary(). admin_super_password() -> list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")). -spec admin_email() -> binary(). admin_email() -> list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")). -spec admin_password() -> binary(). admin_password() -> list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")). -spec admin_moder_email() -> binary(). admin_moder_email() -> list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")). -spec admin_moder_password() -> binary(). admin_moder_password() -> list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")). -spec admin_support_email() -> binary(). admin_support_email() -> list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")). -spec admin_support_password() -> binary(). admin_support_password() -> list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")). %%%=================================================================== %%% Получение токенов (с кешированием в persistent_term) %%%=================================================================== -spec get_admin_token() -> binary(). get_admin_token() -> get_or_login(admin, admin_email(), admin_password()). -spec get_superadmin_token() -> binary(). get_superadmin_token() -> get_or_login(superadmin, admin_super_email(), admin_super_password()). -spec get_moderator_token() -> binary(). get_moderator_token() -> get_or_login(moderator, admin_moder_email(), admin_moder_password()). -spec get_support_token() -> binary(). get_support_token() -> get_or_login(support, admin_support_email(), admin_support_password()). -spec get_or_login(atom(), binary(), binary()) -> binary(). get_or_login(Role, Email, Password) -> Key = {?MODULE, admin_token, Role}, case persistent_term:get(Key, undefined) of Token when is_binary(Token) -> Token; _ -> Token = login_admin(Email, Password), persistent_term:put(Key, Token), timer:apply_after(5 * 60 * 1000, fun() -> persistent_term:erase(Key) end), Token end. %% @doc Возвращает JWT-токен обычного пользователя. %% При каждом вызове создаёт нового уникального пользователя, %% чтобы избежать конфликтов состояния в тестах. -spec get_user_token() -> binary(). get_user_token() -> Email = unique_email(<<"testuser">>), register_and_login(Email, <<"testpass">>). %%%=================================================================== %%% HTTP-клиент (логирование, заголовки) %%%=================================================================== -spec admin_request(atom(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. admin_request(Method, Path, Token) -> admin_request(Method, Path, Token, <<>>). -spec admin_request(atom(), binary(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. admin_request(Method, Path, Token, Body) -> request(get_admin_url(), Method, Path, Token, Body, "ADMIN"). -spec client_request(atom(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. client_request(Method, Path, Token) -> client_request(Method, Path, Token, <<>>). -spec client_request(atom(), binary(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. client_request(Method, Path, Token, Body) -> request(get_base_url(), Method, Path, Token, Body, "CLIENT"). %%%=================================================================== %%% Внутренняя реализация HTTP-запроса %%%=================================================================== -spec request(string(), atom(), binary(), binary(), binary(), string()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. request(BaseUrl, Method, Path, Token, Body, Prefix) -> URL = BaseUrl ++ binary_to_list(Path), Headers0 = [], Headers = case Token of <<>> -> Headers0; % пустой токен – не добавляем Authorization _ -> [{"Authorization", "Bearer " ++ binary_to_list(Token)}] end, ct:pal("~s REQUEST: ~s ~s", [Prefix, Method, URL]), RequestArg = case Method of get -> {URL, Headers}; delete -> {URL, Headers}; _ -> {URL, Headers, "application/json", Body} end, Response = httpc:request(Method, RequestArg, [], []), case Response of {ok, {{_, Status, _}, RespHeaders, RespBody}} -> ct:pal("~s RESPONSE: ~p ~s", [Prefix, Status, RespBody]), {ok, Status, RespHeaders, RespBody}; _ -> ct:pal("~s REQUEST ERROR: ~p", [Prefix, Response]), {error, Response} end. %%%=================================================================== %%% Высокоуровневые обёртки (GET/POST/PUT/DELETE) %%%=================================================================== -spec admin_get(binary(), binary()) -> jsx:json_term(). admin_get(Path, Token) -> {ok, 200, _, Body} = admin_request(get, Path, Token), jsx:decode(list_to_binary(Body), [return_maps]). -spec admin_post(binary(), binary(), map()) -> jsx:json_term(). admin_post(Path, Token, BodyMap) -> Body = jsx:encode(BodyMap), {ok, 201, _, RespBody} = admin_request(post, Path, Token, Body), jsx:decode(list_to_binary(RespBody), [return_maps]). -spec admin_put(binary(), binary(), map()) -> jsx:json_term(). admin_put(Path, Token, BodyMap) -> Body = jsx:encode(BodyMap), {ok, 200, _, RespBody} = admin_request(put, Path, Token, Body), jsx:decode(list_to_binary(RespBody), [return_maps]). %% В api_test_runner.erl добавить в блок высокоуровневых обёрток: -spec admin_patch(binary(), binary(), [map()]) -> jsx:json_term(). admin_patch(Path, Token, BodyList) -> Body = jsx:encode(BodyList), {ok, 200, _, RespBody} = admin_request(patch, Path, Token, Body), jsx:decode(list_to_binary(RespBody), [return_maps]). -spec admin_delete(binary(), binary()) -> jsx:json_term(). admin_delete(Path, Token) -> {ok, 200, _, Body} = admin_request(delete, Path, Token), jsx:decode(list_to_binary(Body), [return_maps]). -spec client_get(binary(), binary()) -> jsx:json_term(). client_get(Path, Token) -> {ok, 200, _, Body} = client_request(get, Path, Token), jsx:decode(list_to_binary(Body), [return_maps]). -spec client_post(binary(), binary(), map()) -> jsx:json_term(). client_post(Path, Token, BodyMap) -> Body = jsx:encode(BodyMap), {ok, 201, _, RespBody} = client_request(post, Path, Token, Body), jsx:decode(list_to_binary(RespBody), [return_maps]). -spec client_put(binary(), binary(), map()) -> jsx:json_term(). client_put(Path, Token, BodyMap) -> Body = jsx:encode(BodyMap), {ok, 200, _, RespBody} = client_request(put, Path, Token, Body), jsx:decode(list_to_binary(RespBody), [return_maps]). -spec client_delete(binary(), binary()) -> jsx:json_term(). client_delete(Path, Token) -> {ok, 200, _, Body} = client_request(delete, Path, Token), jsx:decode(list_to_binary(Body), [return_maps]). %%%=================================================================== %%% Фикстуры (создание тестовых данных) %%%=================================================================== -spec unique_email(binary()) -> binary(). unique_email(Prefix) -> Unique = integer_to_binary(erlang:system_time()), <>. -spec future_date() -> calendar:datetime(). future_date() -> Seconds = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 86400, calendar:gregorian_seconds_to_datetime(Seconds). -spec register_and_login(binary(), binary()) -> binary(). register_and_login(Email, Password) -> Resp = client_request(post, <<"/v1/register">>, <<>>, jsx:encode(#{email => Email, password => Password})), {ok, 201, _, Body} = Resp, #{<<"token">> := Token} = jsx:decode(list_to_binary(Body), [return_maps]), Token. -spec create_calendar(binary(), map()) -> binary(). create_calendar(Token, Params) -> #{<<"id">> := CalId} = client_post(<<"/v1/calendars">>, Token, Params), CalId. -spec create_event(binary(), binary(), map()) -> binary(). create_event(Token, CalId, Params) -> Path = <<"/v1/calendars/", CalId/binary, "/events">>, #{<<"id">> := EventId} = client_post(Path, Token, Params), EventId. %%%=================================================================== %%% Внутренние функции %%%=================================================================== -spec login_admin(binary(), binary()) -> binary(). login_admin(Email, Password) -> BodyMap = #{<<"email">> => Email, <<"password">> => Password}, Body = jsx:encode(BodyMap), {ok, 200, _, RespBody} = admin_request(post, <<"/v1/admin/login">>, <<>>, Body), #{<<"token">> := Token} = jsx:decode(list_to_binary(RespBody), [return_maps]), Token.