302 lines
12 KiB
Erlang
302 lines
12 KiB
Erlang
%%%-------------------------------------------------------------------
|
||
%%% @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, [{timeout, 15000}, {ssl, [{verify, verify_none}]}], []),
|
||
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. |