Files
EventHubBack/test/api/api_test_runner.erl

302 lines
12 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.
%%%-------------------------------------------------------------------
%%% @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()),
<<Prefix/binary, "_", Unique/binary, "@test.local">>.
-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.