%%%------------------------------------------------------------------- %%% @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, x‑total‑count) %%% - **сортировку** по 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), 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), 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), % Обновляем пользователей, чтобы получить разные роли и статусы % User1: роль bot, статус frozen (для тестов фильтрации) Path1 = <<"/v1/admin/users/", User1Id/binary>>, api_test_runner:admin_put(Path1, Token, #{ <<"role">> => <<"bot">>, <<"status">> => <<"frozen">>, <<"reason">> => <<"Test freeze">> }), % 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_role_bot(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)), ?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"), Path = <<"/v1/admin/users/", UserId/binary>>, Deleted = api_test_runner:admin_delete(Path, Token), ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), 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 Фильтрация по роли bot с отдельным пользователем (гарантирует наличие). -spec test_filter_users_by_role_bot(binary()) -> ok. test_filter_users_by_role_bot(Token) -> ct:pal(" TEST: Filter users by role=bot (dedicated user)"), % Создаём пользователя и сразу назначаем ему роль bot Email = api_test_runner:unique_email(<<"botfilter">>), UserTok = api_test_runner:register_and_login(Email, <<"testpass">>), #{<<"id">> := BotId} = api_test_runner:client_get(<<"/v1/user/me">>, UserTok), Path = <<"/v1/admin/users/", BotId/binary>>, api_test_runner:admin_put(Path, Token, #{<<"role">> => <<"bot">>}), % Теперь фильтр должен вернуть хотя бы этого пользователя Users = api_test_runner:admin_get(<<"/v1/admin/users?role=bot">>, Token), ?assert(is_list(Users)), ?assert(length(Users) >= 1), [?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]).