Files
EventHubBack/test/api/admins/admin_websocket_tests.erl

267 lines
9.3 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(admin_websocket_tests).
-export([test/0]).
test() ->
ct:pal("Testing WebSocket API..."),
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)]),
% Создаём календарь и событие через новый api_test_runner
#{<<"id">> := CalId} = api_test_runner:client_post(
<<"/v1/calendars">>, UserToken,
#{title => <<"WS Test Calendar">>, type => <<"commercial">>}),
ct:pal(" CalId: ~s", [CalId]),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, UserToken,
#{title => <<"WS Test Event">>,
start_time => <<"2026-06-01T10:00:00Z">>,
duration => 60}),
ct:pal(" EventId: ~s", [EventId]),
WsUrl = api_test_runner:get_base_ws_url() ++ "/ws",
AdminWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws",
%% TEST 1: Connect to WebSocket with valid token
ct:pal(" TEST 1: Connect WebSocket with valid token..."),
ct:pal(" URL: ~s", [WsUrl]),
ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]),
case test_ws_connect_debug(WsUrl, 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(AdminWsUrl, 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..."),
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
#{target_type => <<"event">>,
target_id => EventId,
reason => <<"Test report">>}),
{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(AdminWsUrl, 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(AdminWsUrl, 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 ==========
extract_port(Url) ->
case string:split(Url, "://", trailing) of
[_, Rest] ->
HostPort = case string:split(Rest, "/", leading) of
[H, _] -> H;
[H] -> H
end,
case string:split(HostPort, ":", trailing) of
[_, PortStr] -> {ok, list_to_integer(PortStr)};
_ -> case string:split(Rest, "://", trailing) of
[_, R] -> extract_port("https://" ++ R);
_ -> {ok, default_port(Url)}
end
end;
_ -> {ok, default_port(Url)}
end.
default_port(Url) ->
case string:prefix(Url, "wss://") of
nomatch -> case string:prefix(Url, "ws://") of
nomatch -> 80;
_ -> 80
end;
_ -> 443
end.
extract_host(Url) ->
case string:split(Url, "://", trailing) of
[_, Rest] ->
HostPort = case string:split(Rest, "/", leading) of
[H, _] -> H;
[H] -> H
end,
case string:split(HostPort, ":", trailing) of
[Host, _] -> {ok, Host};
[Host] -> {ok, Host}
end;
_ -> {ok, "localhost"}
end.