Рефакторинг обработчиков. Часть 3 #21

This commit is contained in:
2026-05-13 23:02:59 +03:00
parent 61bb44ab4a
commit 40806df62a
91 changed files with 6138 additions and 7150 deletions

View File

@@ -0,0 +1,255 @@
-module(user_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
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"WS Test Calendar">>, type => <<"commercial">>}),
ct:pal(" CalId: ~s", [CalId]),
EventId = api_test_runner:create_event(UserToken, CalId, #{
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 ==========
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.