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

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
+286 -42
View File
@@ -1,67 +1,311 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты административного API для управления пользователями.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/users
%%% GET /v1/admin/users/:id
%%% PUT /v1/admin/users/:id
%%% DELETE /v1/admin/users/:id
%%%
%%% Проверяет:
%%% - получение списка пользователей
%%% - получение пользователя по ID
%%% - обновление пользователя (роль, статус)
%%% - мягкое удаление пользователя
%%% - **фильтрацию** по роли, статусу и поиск (q)
%%% - **комбинации фильтров** (role + status)
%%% - **пагинацию** (limit/offset, xtotalcount)
%%% - **сортировку** по email, роли и дате создания
%%% - **комбинации сортировки с фильтрацией и пагинацией**
%%% @end
%%%-------------------------------------------------------------------
-module(admin_users_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Users Tests ==="),
Token = api_test_runner:get_admin_token(),
% Создаём несколько тестовых пользователей с разными ролями
User1Email = api_test_runner:unique_email(<<"usertest1">>),
User1Token = api_test_runner:register_and_login(User1Email, <<"testpass">>),
Me1 = api_test_runner:client_get(<<"/v1/user/me">>, User1Token),
User1Id = maps:get(<<"id">>, Me1),
%% Создаём тестового пользователя
Email = api_test_runner:unique_email(<<"usertest">>),
UserToken = api_test_runner:register_and_login(Email, <<"testpass">>),
Me = api_test_runner:client_get(<<"/v1/user/me">>, UserToken),
UserId = maps:get(<<"id">>, Me),
User2Email = api_test_runner:unique_email(<<"usertest2">>),
User2Token = api_test_runner:register_and_login(User2Email, <<"testpass">>),
Me2 = api_test_runner:client_get(<<"/v1/user/me">>, User2Token),
User2Id = maps:get(<<"id">>, Me2),
test_list_users(Token),
test_get_user(Token, UserId),
test_update_user(Token, UserId),
test_filter_users(Token),
test_delete_user(Token, UserId),
User3Email = api_test_runner:unique_email(<<"usertest3">>),
User3Token = api_test_runner:register_and_login(User3Email, <<"testpass">>),
Me3 = api_test_runner:client_get(<<"/v1/user/me">>, User3Token),
User3Id = maps:get(<<"id">>, Me3),
ct:pal("=== All admin users tests passed ==="),
ok.
test_list_users(Token) ->
ct:pal(" TEST: List all users"),
Users = api_test_runner:admin_get(<<"/v1/admin/users">>, Token),
?assert(is_list(Users)),
?assert(length(Users) >= 1),
ct:pal(" OK: ~p users", [length(Users)]).
test_get_user(Token, UserId) ->
ct:pal(" TEST: Get user by ID"),
Path = <<"/v1/admin/users/", UserId/binary>>,
User = api_test_runner:admin_get(Path, Token),
?assertEqual(UserId, maps:get(<<"id">>, User)),
ct:pal(" OK: ~s", [maps:get(<<"email">>, User)]).
test_update_user(Token, UserId) ->
ct:pal(" TEST: Update user (change role and status)"),
Path = <<"/v1/admin/users/", UserId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{
% Обновляем пользователей, чтобы получить разные роли и статусы
% User1: роль bot, статус frozen (для тестов фильтрации)
Path1 = <<"/v1/admin/users/", User1Id/binary>>,
api_test_runner:admin_put(Path1, Token, #{
<<"role">> => <<"bot">>,
<<"status">> => <<"frozen">>,
<<"reason">> => <<"Test freeze">>
}),
?assertEqual(<<"bot">>, maps:get(<<"role">>, Updated)),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
test_filter_users(Token) ->
ct:pal(" TEST: Filter users by status=frozen"),
Users = api_test_runner:admin_get(<<"/v1/admin/users?status=frozen">>, Token),
% User2: роль user, статус active
Path2 = <<"/v1/admin/users/", User2Id/binary>>,
api_test_runner:admin_put(Path2, Token, #{
<<"role">> => <<"user">>,
<<"status">> => <<"active">>
}),
% User3: оставляем как есть (user / active) для тестов поиска
% ── Основные CRUD-тесты ──
test_list_users(Token),
test_get_user(Token, User1Id),
test_update_user(Token, User1Id),
test_delete_user(Token, User1Id),
% ── Фильтрация ──
test_filter_users_by_role(Token),
test_filter_users_by_status(Token),
test_filter_combined_role_status(Token),
test_filter_users_by_search(Token, User2Email),
% ── Сортировка ──
test_sort_by_email_asc(Token),
test_sort_by_email_desc(Token),
test_sort_by_role_desc(Token),
test_sort_by_created_at_asc(Token),
% ── Комбинации фильтрации и сортировки ──
test_filter_status_sort_email(Token),
test_filter_role_sort_created_at(Token),
% ── Пагинация ──
test_user_pagination(Token),
test_pagination_headers(Token),
% ── Комбинации с пагинацией ──
test_filter_status_with_pagination(Token),
test_filter_sort_pagination_combined(Token),
ct:pal("=== All admin users tests passed ==="),
ok.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
%% @doc GET /v1/admin/users – список пользователей.
-spec test_list_users(binary()) -> ok.
test_list_users(Token) ->
ct:pal(" TEST: List all users"),
Users = api_test_runner:admin_get(<<"/v1/admin/users">>, Token),
?assert(is_list(Users)),
[?assertEqual(<<"frozen">>, maps:get(<<"status">>, U)) || U <- Users],
ct:pal(" OK: ~p frozen users", [length(Users)]).
?assert(length(Users) >= 3),
ct:pal(" OK: ~p users", [length(Users)]).
%% @doc GET /v1/admin/users/:id получение пользователя по ID.
-spec test_get_user(binary(), binary()) -> ok.
test_get_user(Token, UserId) ->
ct:pal(" TEST: Get user by ID"),
Path = <<"/v1/admin/users/", UserId/binary>>,
User = api_test_runner:admin_get(Path, Token),
?assertEqual(UserId, maps:get(<<"id">>, User)),
ct:pal(" OK: ~s", [maps:get(<<"email">>, User)]).
%% @doc PUT /v1/admin/users/:id обновление пользователя.
-spec test_update_user(binary(), binary()) -> ok.
test_update_user(Token, UserId) ->
ct:pal(" TEST: Update user (change nickname)"),
Path = <<"/v1/admin/users/", UserId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{
<<"nickname">> => <<"TestBot">>
}),
?assertEqual(<<"TestBot">>, maps:get(<<"nickname">>, Updated)),
ct:pal(" OK").
%% @doc DELETE /v1/admin/users/:id мягкое удаление.
-spec test_delete_user(binary(), binary()) -> ok.
test_delete_user(Token, UserId) ->
ct:pal(" TEST: Delete (soft-delete) user"),
ct:pal(" TEST: Delete (soft-delete) user"),
Path = <<"/v1/admin/users/", UserId/binary>>,
Deleted = api_test_runner:admin_delete(Path, Token),
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)),
ct:pal(" OK").
ct:pal(" OK").
%% ── Фильтрация ──
%% @doc Фильтрация по роли.
-spec test_filter_users_by_role(binary()) -> ok.
test_filter_users_by_role(Token) ->
ct:pal(" TEST: Filter users by role=bot"),
Users = api_test_runner:admin_get(<<"/v1/admin/users?role=bot">>, Token),
?assert(is_list(Users)),
[?assertEqual(<<"bot">>, maps:get(<<"role">>, U)) || U <- Users],
ct:pal(" OK: ~p bot users", [length(Users)]).
%% @doc Фильтрация по статусу.
-spec test_filter_users_by_status(binary()) -> ok.
test_filter_users_by_status(Token) ->
ct:pal(" TEST: Filter users by status=active"),
Users = api_test_runner:admin_get(<<"/v1/admin/users?status=active">>, Token),
?assert(is_list(Users)),
[?assertEqual(<<"active">>, maps:get(<<"status">>, U)) || U <- Users],
ct:pal(" OK: ~p active users", [length(Users)]).
%% @doc Комбинация фильтров: роль + статус.
-spec test_filter_combined_role_status(binary()) -> ok.
test_filter_combined_role_status(Token) ->
ct:pal(" TEST: Combined filter (role=user & status=active)"),
Users = api_test_runner:admin_get(
<<"/v1/admin/users?role=user&status=active">>, Token),
?assert(is_list(Users)),
[?assertEqual(<<"user">>, maps:get(<<"role">>, U)) || U <- Users],
[?assertEqual(<<"active">>, maps:get(<<"status">>, U)) || U <- Users],
ct:pal(" OK: ~p users match both criteria", [length(Users)]).
%% @doc Поиск по email.
-spec test_filter_users_by_search(binary(), binary()) -> ok.
test_filter_users_by_search(Token, Email) ->
ct:pal(" TEST: Search users by email prefix"),
Prefix = binary:part(Email, 0, 10),
Path = <<"/v1/admin/users?q=", Prefix/binary>>,
Users = api_test_runner:admin_get(Path, Token),
?assert(is_list(Users)),
?assert(length(Users) >= 1),
ct:pal(" OK: ~p users match search", [length(Users)]).
%% ── Сортировка ──
%% @doc Сортировка по email по возрастанию.
-spec test_sort_by_email_asc(binary()) -> ok.
test_sort_by_email_asc(Token) ->
ct:pal(" TEST: Sort users by email asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?sort=email&order=asc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
Emails = [maps:get(<<"email">>, U) || U <- Users],
?assertEqual(lists:sort(Emails), Emails),
ct:pal(" OK: sorted by email asc").
%% @doc Сортировка по email по убыванию.
-spec test_sort_by_email_desc(binary()) -> ok.
test_sort_by_email_desc(Token) ->
ct:pal(" TEST: Sort users by email desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?sort=email&order=desc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
Emails = [maps:get(<<"email">>, U) || U <- Users],
?assertEqual(lists:reverse(lists:sort(Emails)), Emails),
ct:pal(" OK: sorted by email desc").
%% @doc Сортировка по роли по убыванию.
-spec test_sort_by_role_desc(binary()) -> ok.
test_sort_by_role_desc(Token) ->
ct:pal(" TEST: Sort users by role desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?sort=role&order=desc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
Roles = [maps:get(<<"role">>, U) || U <- Users],
?assertEqual(lists:reverse(lists:sort(Roles)), Roles),
ct:pal(" OK: sorted by role desc").
%% @doc Сортировка по created_at по возрастанию.
-spec test_sort_by_created_at_asc(binary()) -> ok.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort users by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?sort=created_at&order=asc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, U) || U <- Users],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
%% ── Комбинации фильтрации и сортировки ──
%% @doc Фильтр status=active + сортировка по email asc.
-spec test_filter_status_sort_email(binary()) -> ok.
test_filter_status_sort_email(Token) ->
ct:pal(" TEST: Filter status=active + sort by email asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?status=active&sort=email&order=asc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, U)) || U <- Users],
Emails = [maps:get(<<"email">>, U) || U <- Users],
?assertEqual(lists:sort(Emails), Emails),
ct:pal(" OK: ~p users sorted", [length(Users)]).
%% @doc Фильтр role=user + сортировка по created_at desc.
-spec test_filter_role_sort_created_at(binary()) -> ok.
test_filter_role_sort_created_at(Token) ->
ct:pal(" TEST: Filter role=user + sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?role=user&sort=created_at&order=desc">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"user">>, maps:get(<<"role">>, U)) || U <- Users],
Dates = [maps:get(<<"created_at">>, U) || U <- Users],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p users sorted", [length(Users)]).
%% ── Пагинация ──
%% @doc GET /v1/admin/users?limit=...&offset=... пагинация.
-spec test_user_pagination(binary()) -> ok.
test_user_pagination(Token) ->
ct:pal(" TEST: User pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/users?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/users?limit=1&offset=1">>, Token),
?assert(length(Page2) >= 1),
Id1 = maps:get(<<"id">>, hd(Page1)),
Id2 = maps:get(<<"id">>, hd(Page2)),
?assertNotEqual(Id1, Id2),
ct:pal(" OK").
%% @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/users?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 Фильтр status=active + пагинация limit=1.
-spec test_filter_status_with_pagination(binary()) -> ok.
test_filter_status_with_pagination(Token) ->
ct:pal(" TEST: Filter status=active + pagination (limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?status=active&limit=1&offset=0">>, Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Users) =< 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, U)) || U <- Users],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%% @doc Фильтр + сортировка + пагинация: status=active + created_at desc + limit=1.
-spec test_filter_sort_pagination_combined(binary()) -> ok.
test_filter_sort_pagination_combined(Token) ->
ct:pal(" TEST: Filter + sort + pagination (status=active, sort=created_at desc, limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/users?status=active&sort=created_at&order=desc&limit=1&offset=0">>,
Token, <<"">>),
Users = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Users) =< 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, U)) || U <- Users],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).