Files
EventHubBack/test/api/admins/admin_stats_tests.erl
T
aleksey 3c6211dd08
CI / test (push) Successful in 9m13s
CI / deploy-ift (push) Successful in 2m4s
CI / e2e-ift (push) Failing after 1m51s
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped
fix: avoid empty node metrics race in CT (collect on start + poll). Refs EventHub/EventHubBack#47
2026-07-20 01:03:53 +03:00

296 lines
14 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 Тесты административного API для получения статистики.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/stats
%%% GET /v1/admin/users/stats
%%% GET /v1/admin/events/stats
%%% GET /v1/admin/reports/stats
%%% GET /v1/admin/reviews/stats
%%% GET /v1/admin/subscriptions/stats
%%% GET /v1/admin/calendars/stats
%%%
%%% Проверяет:
%%% - получение общей статистики для всех ролей администраторов
%%% - для superadmin и admin – наличие ключевых метрик, включая pending_users_total, *_by_day
%%% - для moderator и support ответ непустой
%%% - работу с фильтром по датам (from, to)
%%% - статистику по пользователям (users/stats)
%%% - статистику по событиям (events/stats)
%%% - статистику по жалобам (reports/stats)
%%% - статистику по отзывам (reviews/stats)
%%% - статистику по подпискам (subscriptions/stats)
%%% - статистику по календарям (calendars/stats)
%%% @end
%%%-------------------------------------------------------------------
-module(admin_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Stats Tests ==="),
SuperToken = api_test_runner:get_superadmin_token(),
AdminToken = api_test_runner:get_admin_token(),
ModerToken = api_test_runner:get_moderator_token(),
SupportToken = api_test_runner:get_support_token(),
% Создаём тестовые данные для ненулевой статистики
UserToken = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"StatsCal">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"Stats Event">>,
start_time => api_test_runner:future_date(),
duration => 60
}),
% Бронируем и подтверждаем, чтобы можно было оставить отзыв
#{<<"id">> := BookingId} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, UserToken, #{}),
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, UserToken,
#{action => <<"confirm">>}),
% Оставляем отзыв
api_test_runner:client_post(<<"/v1/reviews">>, UserToken,
#{target_type => <<"event">>, target_id => EventId, rating => 5, comment => <<"Great!">>}),
% Жалоба
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
#{<<"target_type">> => <<"event">>, <<"target_id">> => EventId, <<"reason">> => <<"Test">>}),
% Подписка
SubUserToken = api_test_runner:get_user_token(),
api_test_runner:client_post(<<"/v1/subscription">>, SubUserToken,
#{<<"action">> => <<"start_trial">>}),
% Общая статистика
test_stats_for_role("Superadmin", SuperToken, strict),
test_stats_for_role("Admin", AdminToken, strict),
test_stats_for_role("Moderator", ModerToken, loose),
test_stats_for_role("Support", SupportToken, loose),
test_stats_with_dates(SuperToken),
test_node_metrics_history(SuperToken),
test_avg_ticket_resolution_after_close(SuperToken, UserToken),
test_admin_activity(SuperToken),
% Детальная статистика
test_user_stats(SuperToken),
test_event_stats(SuperToken),
test_report_stats(SuperToken),
test_review_stats(SuperToken),
test_subscription_stats(SuperToken),
test_calendar_stats(SuperToken),
ct:pal("=== All admin stats tests passed ==="),
ok.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
-spec test_stats_for_role(string(), binary(), strict | loose) -> ok.
test_stats_for_role(RoleName, Token, Strictness) ->
ct:pal(" TEST: Get stats for role ~s", [RoleName]),
Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token),
?assert(is_map(Stats)),
case Strictness of
strict ->
% Тотальные значения
?assert(maps:is_key(<<"users_total">>, Stats)),
?assert(maps:is_key(<<"pending_users_total">>, Stats)),
?assert(maps:is_key(<<"events_total">>, Stats)),
?assert(maps:is_key(<<"calendars_total">>, Stats)),
?assert(maps:is_key(<<"reviews_total">>, Stats)),
?assert(maps:is_key(<<"reports_total">>, Stats)),
?assert(maps:is_key(<<"reports_pending">>, Stats)),
?assert(maps:is_key(<<"reports_reviewed">>, Stats)),
?assert(maps:is_key(<<"reports_dismissed">>, Stats)),
?assert(maps:is_key(<<"tickets_total">>, Stats)),
?assert(maps:is_key(<<"tickets_open">>, Stats)),
?assert(maps:is_key(<<"subscriptions_total">>, Stats)),
?assert(maps:is_key(<<"trial_subscriptions">>, Stats)),
% Массивы по дням
?assert(maps:is_key(<<"registrations_by_day">>, Stats)),
?assert(maps:is_key(<<"events_by_day">>, Stats)),
?assert(maps:is_key(<<"reviews_by_day">>, Stats)),
?assert(maps:is_key(<<"calendars_by_day">>, Stats)),
?assert(maps:is_key(<<"reports_by_day">>, Stats)),
?assert(maps:is_key(<<"tickets_by_day">>, Stats)),
?assert(maps:is_key(<<"subscriptions_by_day">>, Stats)),
?assert(maps:is_key(<<"pending_users_by_day">>, Stats)),
?assert(maps:is_key(<<"avg_ticket_resolution_h">>, Stats)),
% Проверка, что они действительно списки
?assert(is_list(maps:get(<<"registrations_by_day">>, Stats))),
?assert(is_list(maps:get(<<"events_by_day">>, Stats))),
?assert(is_list(maps:get(<<"reviews_by_day">>, Stats))),
?assert(is_list(maps:get(<<"calendars_by_day">>, Stats))),
?assert(is_list(maps:get(<<"reports_by_day">>, Stats))),
?assert(is_list(maps:get(<<"tickets_by_day">>, Stats))),
?assert(is_list(maps:get(<<"subscriptions_by_day">>, Stats))),
?assert(is_list(maps:get(<<"pending_users_by_day">>, Stats)));
loose ->
?assert(map_size(Stats) > 0)
end,
ct:pal(" OK: ~p keys", [length(maps:keys(Stats))]).
-spec test_stats_with_dates(binary()) -> ok.
test_stats_with_dates(Token) ->
ct:pal(" TEST: Get stats with date range"),
From = <<"2026-01-01T00:00:00Z">>,
To = <<"2026-12-31T23:59:59Z">>,
Path = <<"/v1/admin/stats?from=", From/binary, "&to=", To/binary>>,
Stats = api_test_runner:admin_get(Path, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"users_total">>, Stats) orelse maps:is_key(<<"users">>, Stats)),
ct:pal(" OK").
%% @doc Тестирует эндпоинт GET /v1/admin/users/stats.
-spec test_user_stats(binary()) -> ok.
test_user_stats(Token) ->
ct:pal(" TEST: User statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/users/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_users">>, Stats)),
?assert(maps:is_key(<<"users_by_role">>, Stats)),
?assert(maps:is_key(<<"users_by_status">>, Stats)),
?assert(maps:is_key(<<"pending_users">>, Stats)),
ct:pal(" OK: total=~p, pending=~p", [maps:get(<<"total_users">>, Stats), maps:get(<<"pending_users">>, Stats)]).
%% @doc Тестирует эндпоинт GET /v1/admin/events/stats.
-spec test_event_stats(binary()) -> ok.
test_event_stats(Token) ->
ct:pal(" TEST: Event statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/events/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_events">>, Stats)),
?assert(maps:is_key(<<"events_by_type">>, Stats)),
?assert(maps:is_key(<<"events_by_status">>, Stats)),
?assert(maps:is_key(<<"top_events_by_rating">>, Stats)),
?assert(is_list(maps:get(<<"top_events_by_rating">>, Stats))),
ct:pal(" OK: total=~p", [maps:get(<<"total_events">>, Stats)]).
%% @doc Тестирует эндпоинт GET /v1/admin/reports/stats.
-spec test_report_stats(binary()) -> ok.
test_report_stats(Token) ->
ct:pal(" TEST: Report statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/reports/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_reports">>, Stats)),
?assert(maps:is_key(<<"reports_by_target_type">>, Stats)),
?assert(maps:is_key(<<"reports_by_status">>, Stats)),
?assert(maps:is_key(<<"top_targets_by_reports">>, Stats)),
?assert(is_list(maps:get(<<"top_targets_by_reports">>, Stats))),
ct:pal(" OK: total=~p", [maps:get(<<"total_reports">>, Stats)]).
%% @doc Тестирует эндпоинт GET /v1/admin/reviews/stats.
-spec test_review_stats(binary()) -> ok.
test_review_stats(Token) ->
ct:pal(" TEST: Review statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/reviews/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_reviews">>, Stats)),
?assert(maps:is_key(<<"reviews_by_target_type">>, Stats)),
?assert(maps:is_key(<<"reviews_by_status">>, Stats)),
?assert(maps:is_key(<<"top_targets_by_reviews">>, Stats)),
?assert(maps:is_key(<<"top_targets_by_positive_reviews">>, Stats)),
?assert(maps:is_key(<<"top_targets_by_negative_reviews">>, Stats)),
ct:pal(" OK: total=~p", [maps:get(<<"total_reviews">>, Stats)]).
%% @doc Тестирует эндпоинт GET /v1/admin/subscriptions/stats.
-spec test_subscription_stats(binary()) -> ok.
test_subscription_stats(Token) ->
ct:pal(" TEST: Subscription statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/subscriptions/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_subscriptions">>, Stats)),
?assert(maps:is_key(<<"subscriptions_by_plan">>, Stats)),
?assert(maps:is_key(<<"subscriptions_by_status">>, Stats)),
?assert(maps:is_key(<<"trial_subscriptions">>, Stats)),
?assert(maps:is_key(<<"ending_paid_subscriptions">>, Stats)),
?assert(is_list(maps:get(<<"ending_paid_subscriptions">>, Stats))),
ct:pal(" OK: total=~p, trial=~p", [maps:get(<<"total_subscriptions">>, Stats), maps:get(<<"trial_subscriptions">>, Stats)]).
%% @doc Тестирует эндпоинт GET /v1/admin/calendars/stats.
-spec test_calendar_stats(binary()) -> ok.
test_calendar_stats(Token) ->
ct:pal(" TEST: Calendar statistics"),
Stats = api_test_runner:admin_get(<<"/v1/admin/calendars/stats">>, Token),
?assert(is_map(Stats)),
?assert(maps:is_key(<<"total_calendars">>, Stats)),
?assert(maps:is_key(<<"calendars_by_type">>, Stats)),
?assert(maps:is_key(<<"calendars_by_status">>, Stats)),
?assert(maps:is_key(<<"top_calendars_by_reviews">>, Stats)),
?assert(maps:is_key(<<"top_calendars_by_positive_reviews">>, Stats)),
?assert(maps:is_key(<<"top_calendars_by_negative_reviews">>, Stats)),
?assert(maps:is_key(<<"top_calendars_by_rating">>, Stats)),
ct:pal(" OK: total=~p", [maps:get(<<"total_calendars">>, Stats)]).
test_node_metrics_history(Token) ->
ct:pal(" TEST: Node metrics history"),
%% node_monitor пишет раз в 5с; в быстром CI stats-кейс может успеть раньше.
case whereis(node_monitor) of
undefined -> ok;
Pid when is_pid(Pid) -> Pid ! collect
end,
Metrics = wait_node_metrics(Token, 20, 500),
?assert(is_list(Metrics)),
?assert(length(Metrics) >= 1),
First = hd(Metrics),
?assert(maps:is_key(<<"timestamp">>, First)),
?assert(maps:is_key(<<"node">>, First)),
?assert(maps:is_key(<<"memory_total">>, First)),
?assert(maps:is_key(<<"memory_available">>, First)),
?assert(maps:is_key(<<"cpu_utilization">>, First)),
ct:pal(" OK: ~p metrics received", [length(Metrics)]).
wait_node_metrics(_Token, 0, _SleepMs) ->
[];
wait_node_metrics(Token, Attempts, SleepMs) ->
Now = calendar:universal_time(),
FromSec = calendar:datetime_to_gregorian_seconds(Now) - 300,
ToSec = calendar:datetime_to_gregorian_seconds(Now) + 300,
FromDT = calendar:gregorian_seconds_to_datetime(FromSec),
ToDT = calendar:gregorian_seconds_to_datetime(ToSec),
From = handler_utils:datetime_to_iso8601(FromDT),
To = handler_utils:datetime_to_iso8601(ToDT),
Path = <<"/v1/admin/nodes/metrics?from=", From/binary, "&to=", To/binary>>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Metrics = jsx:decode(list_to_binary(Body), [return_maps]),
case is_list(Metrics) andalso length(Metrics) >= 1 of
true ->
Metrics;
false ->
timer:sleep(SleepMs),
wait_node_metrics(Token, Attempts - 1, SleepMs)
end.
test_avg_ticket_resolution_after_close(Token, UserToken) ->
ct:pal(" TEST: avg_ticket_resolution_h after closing ticket"),
Ticket = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
#{<<"error_message">> => <<"Resolution metric">>, <<"stacktrace">> => <<"trace">>}),
#{<<"id">> := TicketId} = Ticket,
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
timer:sleep(20000),
Closed = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"closed">>}),
?assertEqual(<<"closed">>, maps:get(<<"status">>, Closed)),
?assertNotEqual(<<"1970-01-01T00:00:00Z">>, maps:get(<<"closed_at">>, Closed)),
Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token),
Avg = maps:get(<<"avg_ticket_resolution_h">>, Stats),
?assert(is_number(Avg)),
?assert(Avg > 0.0),
ct:pal(" OK: avg_ticket_resolution_h=~p", [Avg]).
test_admin_activity(Token) ->
ct:pal(" TEST: admin_activity in dashboard stats"),
Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token),
Activity = maps:get(<<"admin_activity">>, Stats),
?assert(is_list(Activity)),
?assert(length(Activity) >= 1),
First = hd(Activity),
?assert(maps:is_key(<<"admin_id">>, First)),
?assert(maps:is_key(<<"email">>, First)),
?assert(maps:is_key(<<"role">>, First)),
?assert(maps:is_key(<<"actions">>, First)),
?assert(maps:is_key(<<"last_login">>, First)),
?assert(maps:get(<<"actions">>, First) >= 1),
ct:pal(" OK: ~p admin activity rows", [length(Activity)]).