Files
EventHubBack/test/api/users/user_websocket_tests.erl
T

209 lines
7.5 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 Тесты пользовательского WebSocket API.
%%%
%%% Покрывает эндпоинты:
%%% ws://localhost:8081/ws
%%%
%%% Проверяет:
%%% - подключение с валидным пользовательским токеном
%%% - подписку на календарь и получение подтверждения
%%% @end
%%%-------------------------------------------------------------------
-module(user_websocket_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== User WebSocket Tests ==="),
application:ensure_all_started(gun),
UserToken = api_test_runner:get_user_token(),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"WS Test Calendar">>, type => <<"commercial">>}),
_EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"WS Test Event">>,
start_time => <<"2026-06-01T10:00:00Z">>,
duration => 60
}),
WsUrl = api_test_runner:get_base_ws_url() ++ "/ws",
% ── Пользовательские WebSocket тесты ──
{ok, UserWS} = test_user_ws_connect(WsUrl, UserToken),
test_user_ws_subscribe(UserWS, CalId),
test_ws_close(UserWS),
ct:pal("~n=== All user WebSocket tests passed! ==="),
{?MODULE, ok}.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
%% @doc Подключение пользовательского WebSocket с валидным токеном.
-spec test_user_ws_connect(string(), binary()) -> {ok, pid()}.
test_user_ws_connect(Url, Token) ->
ct:pal(" TEST: Connect user WebSocket with valid token"),
case test_ws_connect_debug(Url, Token) of
{ok, WS} -> {ok, WS};
Other -> error({unexpected, Other})
end.
%% @doc Подписка на календарь и получение подтверждения.
-spec test_user_ws_subscribe(pid(), binary()) -> ok.
test_user_ws_subscribe(WS, CalId) ->
ct:pal(" TEST: Subscribe to calendar"),
SubMsg = #{action => <<"subscribe">>,
calendar_id => CalId},
ok = test_ws_send(WS, SubMsg),
case test_ws_recv(WS) of
{ok, #{<<"status">> := <<"subscribed">>}} -> ok;
{ok, Other} -> error({unexpected_response, Other});
{error, timeout} -> error(timeout)
end.
%% ============ WebSocket хелперы с отладкой ============
test_ws_connect_debug(Url, Token) ->
Path = case string:split(Url, "://", trailing) of
[_, Rest] ->
case string:split(Rest, "/", leading) of
[_HostPort, WsPath] ->
"/" ++ WsPath ++ "?token=" ++ binary_to_list(Token);
_ ->
"/ws?token=" ++ binary_to_list(Token)
end;
_ ->
"/ws?token=" ++ binary_to_list(Token)
end,
{ok, Port} = extract_port(Url),
{ok, Host} = extract_host(Url),
Opts = case Port of
443 -> #{protocols => [http],
transport => tls,
tls_opts => [{verify, verify_none}]};
_ -> #{protocols => [http]}
end,
ct:pal(" Host: ~s", [Host]),
ct:pal(" Port: ~p", [Port]),
ct:pal(" Path: ~s", [Path]),
{ok, ConnPid} = gun:open(Host, Port, Opts),
{ok, http} = gun:await_up(ConnPid, 5000),
Headers = [{<<"host">>, list_to_binary(Host ++ ":" ++ integer_to_list(Port))}],
StreamRef = gun:ws_upgrade(ConnPid, Path, Headers),
receive
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
ct:pal(" WebSocket upgrade OK"),
{ok, ConnPid};
{gun_response, ConnPid, StreamRef, fin, 401, _} ->
ct:pal(" ERROR: HTTP 401 Unauthorized"),
gun:close(ConnPid),
{error, {401, <<"Invalid token">>}};
{gun_response, ConnPid, StreamRef, fin, 403, _} ->
ct:pal(" ERROR: HTTP 403 Forbidden"),
gun:close(ConnPid),
{error, {403, <<"Admin access required">>}};
{gun_response, ConnPid, StreamRef, nofin, 403, _} ->
ct:pal(" ERROR: HTTP 403 Forbidden (nofin)"),
gun:close(ConnPid),
{error, {403, <<"Admin access required">>}};
{gun_response, ConnPid, StreamRef, fin, Status, _} ->
ct:pal(" ERROR: HTTP ~p", [Status]),
gun:close(ConnPid),
{error, {Status, <<"WebSocket upgrade failed">>}};
{gun_response, ConnPid, StreamRef, nofin, Status, _} ->
ct:pal(" ERROR: HTTP ~p (nofin)", [Status]),
gun:close(ConnPid),
{error, {Status, <<"WebSocket upgrade failed">>}};
{gun_error, ConnPid, Reason} ->
ct:pal(" ERROR: ~p", [Reason]),
gun:close(ConnPid),
{error, Reason}
after 5000 ->
ct:pal(" ERROR: Timeout"),
gun:close(ConnPid),
{error, timeout}
end.
test_ws_send(ConnPid, Data) ->
Msg = jsx:encode(Data),
ct:pal(" Sending: ~s", [Msg]),
case catch gun:ws_send(ConnPid, {text, Msg}) of
ok -> ok;
{'EXIT', {undef, _}} ->
gun:ws_send(ConnPid, fin, {text, Msg});
Other ->
ct:pal(" ERROR sending: ~p", [Other]),
error({ws_send_failed, Other})
end.
test_ws_recv(ConnPid) ->
test_ws_recv(ConnPid, 3000).
test_ws_recv(ConnPid, Timeout) ->
receive
{gun_ws, ConnPid, _StreamRef, {text, Msg}} ->
ct:pal(" Received (with StreamRef): ~s", [Msg]),
{ok, jsx:decode(Msg, [return_maps])};
{gun_ws, ConnPid, {text, Msg}} ->
ct:pal(" Received: ~s", [Msg]),
{ok, jsx:decode(Msg, [return_maps])};
{gun_ws, ConnPid, _StreamRef, Frame} ->
ct:pal(" Received frame: ~p", [Frame]),
{ok, Frame};
{gun_ws, ConnPid, Frame} ->
ct:pal(" Received: ~p", [Frame]),
{ok, Frame};
{gun_error, ConnPid, Reason} ->
ct:pal(" ERROR: gun_error ~p", [Reason]),
{error, Reason};
Other ->
ct:pal(" Received unexpected: ~p", [Other]),
test_ws_recv(ConnPid, Timeout)
after Timeout ->
{error, timeout}
end.
test_ws_close(ConnPid) ->
gun:close(ConnPid).
%% ========== URL parsing helpers ==========
normalize_scheme(S) when is_binary(S) -> S;
normalize_scheme(S) when is_list(S) -> list_to_binary(S);
normalize_scheme(S) when is_atom(S) -> atom_to_binary(S, utf8);
normalize_scheme(_) -> <<"unknown">>.
extract_host(Url) ->
try
Parsed = uri_string:parse(Url),
#{scheme := SchemeRaw, host := Host} = Parsed,
Scheme = normalize_scheme(SchemeRaw),
if Scheme =:= <<"ws">>; Scheme =:= <<"wss">> -> ok;
true -> throw({invalid_scheme, SchemeRaw})
end,
HostStr = if is_binary(Host) -> binary_to_list(Host); true -> Host end,
{ok, HostStr}
catch
Class:Reason:Stacktrace ->
{error, {parse_error, {Class, Reason}, Stacktrace}}
end.
extract_port(Url) ->
try
Parsed = uri_string:parse(Url),
#{scheme := SchemeRaw} = Parsed,
Scheme = normalize_scheme(SchemeRaw),
DefaultPort = if Scheme =:= <<"ws">> -> 80; Scheme =:= <<"wss">> -> 443; true -> throw({invalid_scheme, SchemeRaw}) end,
case maps:find(port, Parsed) of
{ok, P} when is_integer(P) -> {ok, P};
{ok, P} -> {ok, try list_to_integer(binary_to_list(normalize_scheme(P))) catch _:_ -> DefaultPort end};
error -> {ok, DefaultPort}
end
catch
Class:Reason:Stacktrace ->
{error, {parse_error, {Class, Reason}, Stacktrace}}
end.