Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Финал
This commit is contained in:
@@ -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]).
|
||||
Reference in New Issue
Block a user