296 lines
14 KiB
Erlang
296 lines
14 KiB
Erlang
%%%-------------------------------------------------------------------
|
||
%%% @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)]). |