Files
EventHubBack/test/api/admins/admin_admins_tests.erl
T

315 lines
15 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
%%%-------------------------------------------------------------------
%%% @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]).