%%%------------------------------------------------------------------- %%% @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)]).