%%%------------------------------------------------------------------- %%% @doc Тесты административного API для управления администраторами. %%% %%% Покрывает эндпоинты: %%% GET /v1/admin/admins %%% POST /v1/admin/admins %%% GET /v1/admin/admins/:id %%% PUT /v1/admin/admins/:id %%% DELETE /v1/admin/admins/:id %%% %%% Проверяет: %%% - получение списка администраторов (любой администратор) %%% - создание нового администратора (только суперадмин) %%% - получение администратора по ID (любой администратор) %%% - обновление администратора (только суперадмин) %%% - удаление (блокировку) администратора (только суперадмин) %%% - ошибки 403 для обычного администратора (POST/PUT/DELETE) %%% - ошибки 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(), % Создаём несколько администраторов для проверки фильтрации 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, maps:get(<<"id">>, Admin1)), test_update_admin(SuperToken, maps:get(<<"id">>, Admin1)), test_delete_admin(SuperToken, maps:get(<<"id">>, Admin2)), % ── GET-запросы от обычного администратора (должны проходить) ── test_list_admins(AdminToken), test_get_admin(AdminToken, maps:get(<<"id">>, Admin3)), % ── POST/PUT/DELETE от обычного админа (по-прежнему 403) ── test_create_admin_forbidden(AdminToken), 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. %%%=================================================================== %%% Тестовые функции %%%=================================================================== %% @doc GET /v1/admin/admins – список администраторов. -spec test_list_admins(binary()) -> ok. test_list_admins(Token) -> 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)]). %% @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"), 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)]). %% @doc PUT /v1/admin/admins/:id – обновление администратора. -spec test_update_admin(binary(), binary()) -> ok. test_update_admin(Token, AdminId) -> 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"). %% @doc DELETE /v1/admin/admins/:id – удаление (блокировка). -spec test_delete_admin(binary(), binary()) -> ok. test_delete_admin(Token, AdminId) -> 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)"), % Проверяем, что админ больше не в списке Admins = api_test_runner:admin_get(<<"/v1/admin/admins">>, Token), ?assertNot(lists:any(fun(A) -> maps:get(<<"id">>, A) =:= AdminId end, Admins)). %% ── Тесты ограничений ── -spec test_create_admin_forbidden(binary()) -> ok. test_create_admin_forbidden(Token) -> 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"). -spec test_update_admin_forbidden(binary(), binary()) -> ok. test_update_admin_forbidden(Token, AdminId) -> 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">>})), ?assertMatch({ok, 403, _, _}, Resp), 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)"), Path = <<"/v1/admin/admins/", AdminId/binary>>, Resp = api_test_runner:admin_request(delete, Path, Token), ?assertMatch({ok, 403, _, _}, Resp), 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)"), Email = api_test_runner:unique_email(<<"dupadmin">>), % Создаём первого администратора {ok, 201, _, _} = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken, jsx:encode(#{email => Email, password => <<"Pass1234">>, role => <<"admin">>})), % Пытаемся создать второго с тем же email 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"). -spec test_create_admin_invalid_role(binary()) -> ok. test_create_admin_invalid_role(SuperToken) -> 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"). %% ── Фильтрация и пагинация ── %% @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]). %%%=================================================================== %%% Вспомогательные функции %%%=================================================================== %% @private Создаёт администратора и возвращает его данные. -spec create_admin(binary(), binary(), binary(), binary()) -> map(). create_admin(Token, Email, Password, Role) -> 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]).