Добавлена оперативная и историческая статистика нод и узлов mnesia #22

This commit is contained in:
2026-05-28 18:59:21 +03:00
parent 29fe0386be
commit f507d08e2c
16 changed files with 753 additions and 201 deletions
+159 -94
View File
@@ -1,125 +1,190 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты WebSocket API (пользовательские и административные).
%%%
%%% Покрывает эндпоинты:
%%% ws://localhost:8081/ws
%%% ws://localhost:8446/admin/ws
%%% ws://localhost:8446/admin/ws/metrics
%%%
%%% Проверяет:
%%% - подключение с валидным пользовательским токеном
%%% - подписку на календарь и получение подтверждения
%%% - подключение с валидным админским токеном
%%% - подписку на каналы отчётов и тикетов
%%% - получение уведомления о создании отчёта
%%% - Ping/Pong
%%% - отписку от канала
%%% - получение метрик узла через /admin/ws/metrics
%%% - отклонение при использовании пользовательского токена для админского сокета
%%% - отклонение при невалидном токене
%%% @end
%%%-------------------------------------------------------------------
-module(admin_websocket_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("Testing WebSocket API..."),
ct:pal("=== Admin WebSocket Tests ==="),
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",
WsUrl = api_test_runner:get_base_ws_url() ++ "/ws",
AdminWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws",
MetricsWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws/metrics",
%% 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"),
% ── Пользовательские WebSocket тесты ──
{ok, UserWS} = test_user_ws_connect(WsUrl, UserToken),
test_user_ws_subscribe(UserWS, CalId),
test_ws_close(UserWS),
%% 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"),
ct:pal("~n✅ User WebSocket tests passed!"),
% ── Административные WebSocket тесты ──
{ok, AdminWS} = test_admin_ws_connect(AdminWsUrl, AdminToken),
test_admin_ws_subscribe_reports(AdminWS),
test_admin_ws_subscribe_tickets(AdminWS),
test_admin_ws_report_notification(AdminWS, UserToken, EventId),
test_admin_ws_ping(AdminWS),
test_admin_ws_unsubscribe(AdminWS),
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_admin_ws_node_metrics(MetricsWsUrl, AdminToken),
%% TEST 13: Admin WebSocket with invalid token
ct:pal(" TEST 13: Admin WS with invalid token..."),
% ── Негативные тесты ──
test_admin_ws_user_token_rejected(AdminWsUrl, UserToken),
test_admin_ws_invalid_token_rejected(AdminWsUrl),
ct:pal("~n=== All admin 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.
%% @doc Подключение административного WebSocket.
-spec test_admin_ws_connect(string(), binary()) -> {ok, pid()}.
test_admin_ws_connect(Url, Token) ->
ct:pal(" TEST: Connect admin WebSocket"),
case test_ws_connect_debug(Url, Token) of
{ok, WS} -> {ok, WS};
Other -> error({unexpected, Other})
end.
%% @doc Подписка на канал reports.
-spec test_admin_ws_subscribe_reports(pid()) -> ok.
test_admin_ws_subscribe_reports(WS) ->
ct:pal(" TEST: Admin subscribe to reports"),
ok = test_ws_send(WS, #{action => <<"subscribe">>, channel => <<"reports">>}),
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(WS).
%% @doc Подписка на канал tickets.
-spec test_admin_ws_subscribe_tickets(pid()) -> ok.
test_admin_ws_subscribe_tickets(WS) ->
ct:pal(" TEST: Admin subscribe to tickets"),
ok = test_ws_send(WS, #{action => <<"subscribe">>, channel => <<"tickets">>}),
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(WS).
%% @doc Получение уведомления о создании отчёта.
-spec test_admin_ws_report_notification(pid(), binary(), binary()) -> ok.
test_admin_ws_report_notification(WS, UserToken, EventId) ->
ct:pal(" TEST: Admin receives report notification"),
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
#{target_type => <<"event">>, target_id => EventId, reason => <<"Test">>}),
{ok, #{<<"type">> := <<"report_created">>}} = test_ws_recv(WS, 5000).
%% @doc Ping/Pong.
-spec test_admin_ws_ping(pid()) -> ok.
test_admin_ws_ping(WS) ->
ct:pal(" TEST: Admin Ping/Pong"),
ok = test_ws_send(WS, #{action => <<"ping">>}),
{ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(WS).
%% @doc Отписка от канала reports.
-spec test_admin_ws_unsubscribe(pid()) -> ok.
test_admin_ws_unsubscribe(WS) ->
ct:pal(" TEST: Admin unsubscribe from reports"),
ok = test_ws_send(WS, #{action => <<"unsubscribe">>, channel => <<"reports">>}),
{ok, #{<<"status">> := <<"unsubscribed">>}} = test_ws_recv(WS).
%% @doc Получение метрик узла через /admin/ws/metrics.
-spec test_admin_ws_node_metrics(string(), binary()) -> ok.
test_admin_ws_node_metrics(Url, Token) ->
ct:pal(" TEST: Admin WS receive node metrics"),
{ok, WS} = test_ws_connect_debug(Url, Token),
ok = test_ws_send(WS, #{action => <<"ping">>}),
{ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(WS, 3000),
ct:pal(" Ping/Pong OK waiting for node metric..."),
case test_ws_recv(WS, 15000) of
{ok, #{<<"type">> := <<"node_metric">>, <<"data">> := MetricData}} ->
ct:pal(" OK - Received node metric"),
?assert(is_map(MetricData)),
?assert(maps:is_key(<<"timestamp">>, MetricData)),
?assert(maps:is_key(<<"node">>, MetricData)),
?assert(maps:is_key(<<"memory_total">>, MetricData)),
ct:pal(" Node: ~s, Memory: ~p MB",
[maps:get(<<"node">>, MetricData),
maps:get(<<"memory_total">>, MetricData) div 1048576]);
{error, timeout} ->
ct:pal(" WARNING: No node metric received (timeout) check monitor logs");
Other ->
ct:pal(" ERROR: Unexpected response: ~p", [Other]),
?assert(false, {unexpected_ws_message, Other})
end,
test_ws_close(WS).
%% @doc Отклонение при использовании пользовательского токена для админского сокета.
-spec test_admin_ws_user_token_rejected(string(), binary()) -> ok.
test_admin_ws_user_token_rejected(Url, Token) ->
ct:pal(" TEST: Admin WS with user token rejected"),
?assertMatch({error, {403, _}}, test_ws_connect_debug(Url, Token)).
%% @doc Отклонение при невалидном токене.
-spec test_admin_ws_invalid_token_rejected(string()) -> ok.
test_admin_ws_invalid_token_rejected(Url) ->
ct:pal(" TEST: Admin WS with invalid token rejected"),
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}.
?assertMatch({error, {401, _}}, test_ws_connect_debug(Url, InvalidToken)).
%% ============ WebSocket хелперы с отладкой ============
test_ws_connect_debug(Url, Token) ->