Files
EventHubBack/test/api/api_websocket_tests.erl

283 lines
9.9 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.
-module(api_websocket_tests).
-export([test/0]).
-define(BASE_URL, api_test_runner:get_base_url()).
-define(WS_URL, api_test_runner:get_base_ws_url() ++ "/ws").
-define(ADMIN_WS_URL, api_test_runner:get_admin_ws_url() ++ "/admin/ws").
test() ->
ct:pal("Testing WebSocket API..."),
% Запускаем gun
application:ensure_all_started(gun),
% Используем глобальных пользователей
AdminToken = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
ct:pal(" AdminToken: ~s...", [binary_part(AdminToken, 0, 30)]),
ct:pal(" UserToken: ~s...", [binary_part(UserToken, 0, 30)]),
% Создаём календарь и событие для тестов
CalId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/calendars",
#{title => <<"WS Test Calendar">>, type => <<"commercial">>},
UserToken), <<"id">>, 201),
ct:pal(" CalId: ~s", [CalId]),
EventId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events",
#{title => <<"WS Test Event">>,
start_time => <<"2026-06-01T10:00:00Z">>,
duration => 60},
UserToken), <<"id">>, 201),
ct:pal(" EventId: ~s", [EventId]),
% TEST 1: Connect to WebSocket with valid token
ct:pal(" TEST 1: Connect WebSocket with valid token..."),
ct:pal(" URL: ~s", [?WS_URL]),
ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]),
case test_ws_connect_debug(?WS_URL, UserToken) of
{ok, WS} ->
ct:pal(" OK - Connected"),
% TEST 2: Subscribe to calendar updates
ct:pal(" TEST 2: Subscribe to calendar..."),
SubMsg = #{action => <<"subscribe">>, calendar_id => CalId},
ct:pal(" Sending: ~p", [SubMsg]),
ok = test_ws_send(WS, SubMsg),
case test_ws_recv(WS) of
{ok, #{<<"status">> := <<"subscribed">>}} ->
ct:pal(" OK - Subscribed");
{ok, Other} ->
ct:pal(" ERROR: Unexpected response: ~p", [Other]),
error({unexpected_response, Other});
{error, timeout} ->
ct:pal(" ERROR: Timeout waiting for response"),
error(timeout)
end,
% Закрываем соединение
test_ws_close(WS);
{error, Reason} ->
ct:pal(" ERROR: ~p", [Reason]),
error({websocket_connect_failed, Reason})
end,
ct:pal("~n✅ WebSocket API tests passed!"),
% ============ ТЕСТЫ АДМИНСКОГО WEBSOCKET ============
ct:pal("~n=== ADMIN WEBSOCKET TESTS ==="),
% TEST 6: Admin WebSocket connection
ct:pal(" TEST 6: Admin WebSocket connect..."),
{ok, AdminWS} = test_ws_connect_debug(?ADMIN_WS_URL, AdminToken),
ct:pal(" OK - Admin connected"),
% TEST 7: Admin subscribe to reports channel
ct:pal(" TEST 7: Admin subscribe to reports channel..."),
ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"reports">>}),
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS),
ct:pal(" OK - Subscribed to reports"),
% TEST 8: Admin subscribe to tickets channel
ct:pal(" TEST 8: Admin subscribe to tickets channel..."),
ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"tickets">>}),
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS),
ct:pal(" OK - Subscribed to tickets"),
% TEST 9: Admin receives report notification
ct:pal(" TEST 9: Admin receives report notification..."),
% Создаём жалобу через HTTP
api_test_runner:http_post("/v1/reports",
#{target_type => <<"event">>, target_id => EventId, reason => <<"Test report">>},
UserToken),
{ok, #{<<"type">> := <<"report_created">>}} = test_ws_recv(AdminWS, 5000),
ct:pal(" OK - Received report notification"),
% TEST 10: Admin Ping/Pong
ct:pal(" TEST 10: Admin Ping/Pong..."),
ok = test_ws_send(AdminWS, #{action => <<"ping">>}),
{ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(AdminWS),
ct:pal(" OK - Admin Ping/Pong"),
% TEST 11: Admin unsubscribe
ct:pal(" TEST 11: Admin unsubscribe from reports..."),
ok = test_ws_send(AdminWS, #{action => <<"unsubscribe">>, channel => <<"reports">>}),
{ok, #{<<"status">> := <<"unsubscribed">>}} = test_ws_recv(AdminWS),
ct:pal(" OK - Unsubscribed"),
test_ws_close(AdminWS),
% TEST 12: Admin WebSocket with user token (should fail)
ct:pal(" TEST 12: Admin WS with user token..."),
{error, {403, _}} = test_ws_connect_debug(?ADMIN_WS_URL, UserToken),
ct:pal(" OK - Rejected"),
% TEST 13: Admin WebSocket with invalid token
ct:pal(" TEST 13: Admin WS with invalid token..."),
Chars = <<"abcdefghijklmnopqrstuvwxyz0123456789">>,
InvalidToken = << <<(binary:at(Chars, rand:uniform(byte_size(Chars)) - 1))>> || _ <- lists:seq(1, 30) >>,
{error, {401, _}} = test_ws_connect_debug(?ADMIN_WS_URL, InvalidToken),
ct:pal(" OK - Rejected"),
ct:pal("~n✅ Admin WebSocket API tests passed!"),
{?MODULE, ok}.
%% ============ 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.