Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Финал

This commit is contained in:
2026-05-26 21:50:12 +03:00
parent ed5b275efb
commit 19b3d0dba0
21 changed files with 1833 additions and 421 deletions
+189 -41
View File
@@ -16,47 +16,75 @@
%%% - удаление (блокировку) администратора
%%% - ошибки 403 для обычного администратора
%%% - ошибки 409 (дубликат email) и 400 (неверная роль) при создании
%%% - **фильтрацию** по role и status
%%% - **комбинации фильтров** (role + status)
%%% - **пагинацию** (limit/offset, x-total-count)
%%% - **сортировку** по email, role, created_at
%%% - **комбинации сортировки с фильтрацией и пагинацией**
%%% @end
%%%-------------------------------------------------------------------
-module(admin_admins_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Admins Tests ==="),
SuperToken = api_test_runner:get_superadmin_token(),
% Создаём нового администратора для проверки CRUD (у него будет свой ID)
AdminEmail = api_test_runner:unique_email(<<"newadmin.admins.tests">>),
AdminPassword = <<"AdminPass123">>,
#{<<"id">> := AdminId} = create_admin(SuperToken, AdminEmail, AdminPassword, <<"admin">>),
% Создаём несколько администраторов для проверки фильтрации
Admin1 = create_admin(SuperToken,
api_test_runner:unique_email(<<"admin1.tests">>),
<<"AdminPass123">>, <<"admin">>),
Admin2 = create_admin(SuperToken,
api_test_runner:unique_email(<<"admin2.tests">>),
<<"AdminPass123">>, <<"moderator">>),
% Третий админ с той же ролью, что и первый, для проверки фильтрации по роли
Admin3 = create_admin(SuperToken,
api_test_runner:unique_email(<<"admin3.tests">>),
<<"AdminPass123">>, <<"admin">>),
% Токен обычного администратора (уже существующего admin@eventhub.local)
AdminToken = api_test_runner:get_admin_token(),
% Тесты с правами суперадмина
% ── Основные CRUD-тесты ──
test_list_admins(SuperToken),
test_get_admin(SuperToken, AdminId),
test_update_admin(SuperToken, AdminId),
test_delete_admin(SuperToken, AdminId),
test_get_admin(SuperToken, maps:get(<<"id">>, Admin1)),
test_update_admin(SuperToken, maps:get(<<"id">>, Admin1)),
test_delete_admin(SuperToken, maps:get(<<"id">>, Admin2)),
% Тесты ограничений для обычного админа (используем готовый токен)
% ── Тесты ограничений для обычного админа ──
test_list_admins_forbidden(AdminToken),
test_create_admin_forbidden(AdminToken),
test_get_admin_forbidden(AdminToken, AdminId),
test_update_admin_forbidden(AdminToken, AdminId),
test_delete_admin_forbidden(AdminToken, AdminId),
test_get_admin_forbidden(AdminToken, maps:get(<<"id">>, Admin3)),
test_update_admin_forbidden(AdminToken, maps:get(<<"id">>, Admin3)),
test_delete_admin_forbidden(AdminToken, maps:get(<<"id">>, Admin3)),
% Тесты валидации при создании
% ── Валидация создания ──
test_create_admin_duplicate_email(SuperToken),
test_create_admin_invalid_role(SuperToken),
% ── Фильтрация ──
test_filter_by_role(SuperToken),
test_filter_by_status(SuperToken),
test_filter_combined(SuperToken),
% ── Пагинация ──
test_pagination_headers(SuperToken),
test_filter_with_pagination(SuperToken),
% ── Сортировка ──
test_sort_by_email_asc(SuperToken),
test_sort_by_role_desc(SuperToken),
test_sort_by_created_at_asc(SuperToken),
% ── Комбинации сортировки с фильтрами и пагинацией ──
test_sort_combined_with_filter(SuperToken),
test_sort_combined_with_filter_and_pagination(SuperToken),
ct:pal("=== All admin admins tests passed ==="),
ok.
@@ -67,88 +95,87 @@ test() ->
%% @doc GET /v1/admin/admins – список администраторов.
-spec test_list_admins(binary()) -> ok.
test_list_admins(Token) ->
ct:pal(" TEST: List all admins"),
ct:pal(" TEST: List all admins"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins">>, Token),
?assert(is_list(Admins)),
?assert(length(Admins) >= 1),
ct:pal(" OK: ~p admins", [length(Admins)]).
ct:pal(" OK: ~p admins", [length(Admins)]).
%% @doc GET /v1/admin/admins/:id получение администратора.
-spec test_get_admin(binary(), binary()) -> ok.
test_get_admin(Token, AdminId) ->
ct:pal(" TEST: Get admin by ID"),
ct:pal(" TEST: Get admin by ID"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Admin = api_test_runner:admin_get(Path, Token),
?assertEqual(AdminId, maps:get(<<"id">>, Admin)),
ct:pal(" OK: ~s", [maps:get(<<"email">>, Admin)]).
ct:pal(" OK: ~s", [maps:get(<<"email">>, Admin)]).
%% @doc PUT /v1/admin/admins/:id обновление администратора.
-spec test_update_admin(binary(), binary()) -> ok.
test_update_admin(Token, AdminId) ->
ct:pal(" TEST: Update admin"),
ct:pal(" TEST: Update admin"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{nickname => <<"UpdatedAdmin">>}),
?assertEqual(<<"UpdatedAdmin">>, maps:get(<<"nickname">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc DELETE /v1/admin/admins/:id удаление (блокировка).
-spec test_delete_admin(binary(), binary()) -> ok.
test_delete_admin(Token, AdminId) ->
ct:pal(" TEST: Delete (block) admin"),
ct:pal(" TEST: Delete (block) admin"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Result = api_test_runner:admin_request(delete, Path, Token),
{ok, 200, _, _} = Result,
ct:pal(" OK: admin blocked (or deleted)"),
ct:pal(" OK: admin blocked (or deleted)"),
% Проверяем, что админ больше не в списке
Admins = api_test_runner:admin_get(<<"/v1/admin/admins">>, Token),
?assertNot(lists:any(fun(A) -> maps:get(<<"id">>, A) =:= AdminId end, Admins)).
%% ── Тесты ограничений ──
-spec test_list_admins_forbidden(binary()) -> ok.
test_list_admins_forbidden(Token) ->
ct:pal(" TEST: List admins as non-superadmin (403)"),
ct:pal(" TEST: List admins as non-superadmin (403)"),
Resp = api_test_runner:admin_request(get, <<"/v1/admin/admins">>, Token),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
ct:pal(" OK: got 403").
-spec test_create_admin_forbidden(binary()) -> ok.
test_create_admin_forbidden(Token) ->
ct:pal(" TEST: Create admin as non-superadmin (403)"),
ct:pal(" TEST: Create admin as non-superadmin (403)"),
Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, Token,
jsx:encode(#{email => <<"x@x.com">>, password => <<"p">>, role => <<"moderator">>})),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
ct:pal(" OK: got 403").
-spec test_get_admin_forbidden(binary(), binary()) -> ok.
test_get_admin_forbidden(Token, AdminId) ->
ct:pal(" TEST: Get admin by ID as non-superadmin (403)"),
ct:pal(" TEST: Get admin by ID as non-superadmin (403)"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Resp = api_test_runner:admin_request(get, Path, Token),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
ct:pal(" OK: got 403").
-spec test_update_admin_forbidden(binary(), binary()) -> ok.
test_update_admin_forbidden(Token, AdminId) ->
ct:pal(" TEST: Update admin as non-superadmin (403)"),
ct:pal(" TEST: Update admin as non-superadmin (403)"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Resp = api_test_runner:admin_request(put, Path, Token, jsx:encode(#{nickname => <<"fail">>})),
Resp = api_test_runner:admin_request(put, Path, Token,
jsx:encode(#{nickname => <<"fail">>})),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
ct:pal(" OK: got 403").
-spec test_delete_admin_forbidden(binary(), binary()) -> ok.
test_delete_admin_forbidden(Token, AdminId) ->
ct:pal(" TEST: Delete admin as non-superadmin (403)"),
ct:pal(" TEST: Delete admin as non-superadmin (403)"),
Path = <<"/v1/admin/admins/", AdminId/binary>>,
Resp = api_test_runner:admin_request(delete, Path, Token),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
ct:pal(" OK: got 403").
%% ── Валидация создания ──
-spec test_create_admin_duplicate_email(binary()) -> ok.
test_create_admin_duplicate_email(SuperToken) ->
ct:pal(" TEST: Create admin with duplicate email (409)"),
ct:pal(" TEST: Create admin with duplicate email (409)"),
Email = api_test_runner:unique_email(<<"dupadmin">>),
% Создаём первого администратора
{ok, 201, _, _} = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken,
@@ -157,15 +184,136 @@ test_create_admin_duplicate_email(SuperToken) ->
Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken,
jsx:encode(#{email => Email, password => <<"Pass1234">>, role => <<"admin">>})),
?assertMatch({ok, 409, _, _}, Resp),
ct:pal(" OK: got 409").
ct:pal(" OK: got 409").
-spec test_create_admin_invalid_role(binary()) -> ok.
test_create_admin_invalid_role(SuperToken) ->
ct:pal(" TEST: Create admin with invalid role (400)"),
ct:pal(" TEST: Create admin with invalid role (400)"),
Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken,
jsx:encode(#{email => <<"badrole@test.local">>, password => <<"Pass1234">>, role => <<"superhero">>})),
?assertMatch({ok, 400, _, _}, Resp),
ct:pal(" OK: got 400").
ct:pal(" OK: got 400").
%% ── Фильтрация и пагинация ──
%% @doc Фильтрация по роли: ?role=admin
-spec test_filter_by_role(binary()) -> ok.
test_filter_by_role(Token) ->
ct:pal(" TEST: Filter admins by role"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?role=admin">>, Token),
?assert(is_list(Admins)),
?assert(length(Admins) >= 1),
[?assertEqual(<<"admin">>, maps:get(<<"role">>, A)) || A <- Admins],
ct:pal(" OK: ~p admins with role=admin", [length(Admins)]).
%% @doc Фильтрация по статусу: ?status=active
-spec test_filter_by_status(binary()) -> ok.
test_filter_by_status(Token) ->
ct:pal(" TEST: Filter admins by status"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?status=active">>, Token),
?assert(is_list(Admins)),
?assert(length(Admins) >= 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, A)) || A <- Admins],
ct:pal(" OK: ~p active admins", [length(Admins)]).
%% @doc Комбинация фильтров: ?role=admin&status=active
-spec test_filter_combined(binary()) -> ok.
test_filter_combined(Token) ->
ct:pal(" TEST: Combined filter (role=admin & status=active)"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?role=admin&status=active">>, Token),
?assert(is_list(Admins)),
[?assertEqual(<<"admin">>, maps:get(<<"role">>, A)) || A <- Admins],
[?assertEqual(<<"active">>, maps:get(<<"status">>, A)) || A <- Admins],
ct:pal(" OK: ~p admins match both criteria", [length(Admins)]).
%% @doc Фильтр с пагинацией: ?role=admin&limit=1&offset=0
-spec test_filter_with_pagination(binary()) -> ok.
test_filter_with_pagination(Token) ->
ct:pal(" TEST: Filter + pagination (role=admin, limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/admins?role=admin&limit=1&offset=0">>, Token),
Admins = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(1, length(Admins)),
?assertEqual(<<"admin">>, maps:get(<<"role">>, hd(Admins))),
% Проверяем, что заголовок x-total-count присутствует и >= 1
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: got 1 admin of ~p total", [Total]).
%% @doc Проверка заголовков пагинации (x-total-count)
-spec test_pagination_headers(binary()) -> ok.
test_pagination_headers(Token) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/admins?limit=2&offset=0">>, Token),
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%% ── Сортировка ──
%% @doc Сортировка по email по возрастанию.
-spec test_sort_by_email_asc(binary()) -> ok.
test_sort_by_email_asc(Token) ->
ct:pal(" TEST: Sort admins by email ascending"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?sort=email&order=asc">>, Token),
Emails = [maps:get(<<"email">>, A) || A <- Admins],
?assertEqual(lists:sort(Emails), Emails),
ct:pal(" OK: sorted by email asc").
%% @doc Сортировка по роли по убыванию.
-spec test_sort_by_role_desc(binary()) -> ok.
test_sort_by_role_desc(Token) ->
ct:pal(" TEST: Sort admins by role descending"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?sort=role&order=desc">>, Token),
Roles = [maps:get(<<"role">>, A) || A <- Admins],
% Проверяем, что список отсортирован по убыванию
?assertEqual(lists:reverse(lists:sort(Roles)), Roles),
ct:pal(" OK: sorted by role desc").
%% @doc Сортировка по дате создания по возрастанию.
-spec test_sort_by_created_at_asc(binary()) -> ok.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort admins by created_at ascending"),
Admins = api_test_runner:admin_get(<<"/v1/admin/admins?sort=created_at&order=asc">>, Token),
Dates = [maps:get(<<"created_at">>, A) || A <- Admins],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
%% @doc Сортировка + фильтр: role=admin, сортировка по email по убыванию.
-spec test_sort_combined_with_filter(binary()) -> ok.
test_sort_combined_with_filter(Token) ->
ct:pal(" TEST: Sort admins (email desc) filtered by role=admin"),
Admins = api_test_runner:admin_get(
<<"/v1/admin/admins?role=admin&sort=email&order=desc">>, Token),
% Убедимся, что все имеют роль admin
[?assertEqual(<<"admin">>, maps:get(<<"role">>, A)) || A <- Admins],
% Проверяем сортировку
Emails = [maps:get(<<"email">>, A) || A <- Admins],
?assertEqual(lists:reverse(lists:sort(Emails)), Emails),
ct:pal(" OK: ~p admins sorted by email desc", [length(Admins)]).
%% @doc Сортировка + фильтр + пагинация: status=active, сортировка по created_at desc, limit=2.
-spec test_sort_combined_with_filter_and_pagination(binary()) -> ok.
test_sort_combined_with_filter_and_pagination(Token) ->
ct:pal(" TEST: Filter + sort + pagination (status=active, sort=created_at desc, limit=2)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/admins?status=active&sort=created_at&order=desc&limit=2&offset=0">>, Token),
Admins = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Admins) =< 2),
% Проверяем, что все активны
[?assertEqual(<<"active">>, maps:get(<<"status">>, A)) || A <- Admins],
% Проверяем сортировку по убыванию дат
Dates = [maps:get(<<"created_at">>, A) || A <- Admins],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
% Проверяем наличие заголовка пагинации
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: got ~p admins (total ~p)", [length(Admins), Total]).
%%%===================================================================
%%% Вспомогательные функции
@@ -174,7 +322,7 @@ test_create_admin_invalid_role(SuperToken) ->
%% @private Создаёт администратора и возвращает его данные.
-spec create_admin(binary(), binary(), binary(), binary()) -> map().
create_admin(Token, Email, Password, Role) ->
ct:pal(" Creating test admin ~s...", [Email]),
ct:pal(" Creating test admin ~s...", [Email]),
{ok, 201, _, Body} = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, Token,
jsx:encode(#{email => Email, password => Password, role => Role})),
jsx:decode(list_to_binary(Body), [return_maps]).