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

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
+189 -41
View File
@@ -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]).
+134 -19
View File
@@ -7,14 +7,20 @@
%%% Проверяет:
%%% - получение списка записей аудита
%%% - фильтрацию по admin_id
%%% - пагинацию
%%% - фильтрацию по action
%%% - комбинацию фильтров (admin_id + action)
%%% - пагинацию (limit/offset, x-total-count)
%%% - сортировку по timestamp, action, admin_id
%%% - комбинации сортировки с фильтрацией и пагинацией
%%% @end
%%%-------------------------------------------------------------------
-module(admin_audit_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Audit Tests ==="),
@@ -23,10 +29,9 @@ test() ->
AdminToken = api_test_runner:get_admin_token(),
SuperToken = api_test_runner:get_superadmin_token(),
% Создаём тестовую запись аудита
% Создаём тестовые записи аудита
Me = api_test_runner:admin_get(<<"/v1/admin/me">>, AdminToken),
AdminId = maps:get(<<"id">>, Me),
Path = <<"/v1/admin/user/", UserId/binary>>,
Body = #{<<"action">> => <<"block">>, <<"reason">> => <<"Test">>},
api_test_runner:admin_put(Path, AdminToken, Body),
@@ -34,30 +39,85 @@ test() ->
Body2 = #{<<"action">> => <<"unblock">>, <<"reason">> => <<"Test">>},
api_test_runner:admin_put(Path, AdminToken, Body2),
% ── Основные тесты ──
test_list_audit(SuperToken),
test_filter_audit(SuperToken, AdminId),
test_audit_pagination(SuperToken),
test_list_admin_forbidden(AdminToken),
% ── Фильтрация ──
test_filter_audit_by_admin_id(SuperToken, AdminId),
test_filter_audit_by_action(SuperToken),
% ── Комбинация фильтров ──
test_filter_audit_combined(SuperToken, AdminId),
% ── Пагинация ──
test_audit_pagination(SuperToken),
test_audit_pagination_headers(SuperToken),
% ── Сортировка ──
test_sort_by_timestamp_desc(SuperToken),
test_sort_by_action_asc(SuperToken),
test_sort_by_admin_id_asc(SuperToken),
% ── Комбинации сортировки с фильтрацией и пагинацией ──
test_sort_combined_with_filter(SuperToken, AdminId),
test_sort_combined_with_filter_and_pagination(SuperToken, AdminId),
ct:pal("=== All admin audit tests passed ==="),
ok.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
%% @doc GET /v1/admin/audit список всех записей аудита.
test_list_audit(Token) ->
ct:pal(" TEST: List all audit records"),
ct:pal(" TEST: List all audit records"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit">>, Token),
?assert(is_list(Records)),
?assert(length(Records) >= 1),
ct:pal(" OK: ~p records", [length(Records)]).
ct:pal(" OK: ~p records", [length(Records)]).
test_filter_audit(Token, AdminId) ->
ct:pal(" TEST: Filter audit by admin_id"),
%% @doc GET /v1/admin/audit запрет для обычного админа.
test_list_admin_forbidden(Token) ->
ct:pal(" TEST: List audit as non-superadmin (403)"),
Resp = api_test_runner:admin_request(get, <<"/v1/admin/audit">>, Token),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
%% ── Фильтрация ──
%% @doc Фильтрация по admin_id.
test_filter_audit_by_admin_id(Token, AdminId) ->
ct:pal(" TEST: Filter audit by admin_id"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit?admin_id=", AdminId/binary>>, Token),
?assert(is_list(Records)),
[?assertEqual(AdminId, maps:get(<<"admin_id">>, R)) || R <- Records],
ct:pal(" OK: ~p records", [length(Records)]).
ct:pal(" OK: ~p records", [length(Records)]).
%% @doc Фильтрация по action.
test_filter_audit_by_action(Token) ->
ct:pal(" TEST: Filter audit by action"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit?action=block_user">>, Token),
?assert(is_list(Records)),
[?assertEqual(<<"block_user">>, maps:get(<<"action">>, R)) || R <- Records],
ct:pal(" OK: ~p records with action=block_user", [length(Records)]).
%% @doc Комбинация фильтров: admin_id + action.
test_filter_audit_combined(Token, AdminId) ->
ct:pal(" TEST: Combined filter (admin_id + action)"),
Records = api_test_runner:admin_get(
<<"/v1/admin/audit?admin_id=", AdminId/binary, "&action=unblock_user">>, Token),
?assert(is_list(Records)),
[?assertEqual(AdminId, maps:get(<<"admin_id">>, R)) || R <- Records],
[?assertEqual(<<"unblock_user">>, maps:get(<<"action">>, R)) || R <- Records],
ct:pal(" OK: ~p records match both criteria", [length(Records)]).
%% ── Пагинация ──
%% @doc Пагинация: limit=1, offset=0.
test_audit_pagination(Token) ->
ct:pal(" TEST: Audit pagination"),
ct:pal(" TEST: Audit pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/audit?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/audit?limit=1&offset=1">>, Token),
@@ -69,11 +129,66 @@ test_audit_pagination(Token) ->
?assertNotEqual(Id1, Id2);
_ -> ok
end,
ct:pal(" OK").
ct:pal(" OK").
-spec test_list_admin_forbidden(binary()) -> ok.
test_list_admin_forbidden(Token) ->
ct:pal(" TEST: List audit as non-superadmin (403)"),
Resp = api_test_runner:admin_request(get, <<"/v1/admin/audit">>, Token),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK: got 403").
%% @doc Проверка заголовков пагинации (x-total-count).
test_audit_pagination_headers(Token) ->
ct:pal(" TEST: Audit pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/audit?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 Сортировка по timestamp по убыванию.
test_sort_by_timestamp_desc(Token) ->
ct:pal(" TEST: Sort audit by timestamp desc"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit?sort=timestamp&order=desc">>, Token),
Timestamps = [maps:get(<<"timestamp">>, R) || R <- Records],
?assertEqual(lists:reverse(lists:sort(Timestamps)), Timestamps),
ct:pal(" OK: sorted by timestamp desc").
%% @doc Сортировка по action по возрастанию.
test_sort_by_action_asc(Token) ->
ct:pal(" TEST: Sort audit by action asc"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit?sort=action&order=asc">>, Token),
Actions = [maps:get(<<"action">>, R) || R <- Records],
?assertEqual(lists:sort(Actions), Actions),
ct:pal(" OK: sorted by action asc").
%% @doc Сортировка по admin_id по возрастанию.
test_sort_by_admin_id_asc(Token) ->
ct:pal(" TEST: Sort audit by admin_id asc"),
Records = api_test_runner:admin_get(<<"/v1/admin/audit?sort=admin_id&order=asc">>, Token),
AdminIds = [maps:get(<<"admin_id">>, R) || R <- Records],
?assertEqual(lists:sort(AdminIds), AdminIds),
ct:pal(" OK: sorted by admin_id asc").
%% ── Комбинации сортировки с фильтрацией и пагинацией ──
%% @doc Сортировка + фильтр: admin_id + action, сортировка по timestamp asc.
test_sort_combined_with_filter(Token, AdminId) ->
ct:pal(" TEST: Sort audit (timestamp asc) filtered by admin_id + action"),
Records = api_test_runner:admin_get(
<<"/v1/admin/audit?admin_id=", AdminId/binary, "&action=unblock_user&sort=timestamp&order=asc">>, Token),
[?assertEqual(AdminId, maps:get(<<"admin_id">>, R)) || R <- Records],
[?assertEqual(<<"unblock_user">>, maps:get(<<"action">>, R)) || R <- Records],
Timestamps = [maps:get(<<"timestamp">>, R) || R <- Records],
?assertEqual(lists:sort(Timestamps), Timestamps),
ct:pal(" OK: ~p records sorted", [length(Records)]).
%% @doc Сортировка + фильтр + пагинация: action=block, сортировка по timestamp desc, limit=1.
test_sort_combined_with_filter_and_pagination(Token, _AdminId) ->
ct:pal(" TEST: Filter + sort + pagination (action=block_user, sort=timestamp desc, limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/audit?action=block_user&sort=timestamp&order=desc&limit=1&offset=0">>, Token),
Records = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Records) =< 1),
[?assertEqual(<<"block_user">>, maps:get(<<"action">>, R)) || R <- Records],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: got ~p records (total ~p)", [length(Records), Total]).
+123 -39
View File
@@ -14,7 +14,10 @@
%%% - редактирование существующего слова
%%% - удаление слова
%%% - массовую загрузку списком
%%% - пагинацию
%%% - **фильтрацию** по слову (q)
%%% - **сортировку** по word, added_at
%%% - **комбинации фильтрации, сортировки и пагинации**
%%% - **пагинацию** (limit/offset, x-total-count)
%%% @end
%%%-------------------------------------------------------------------
-module(admin_banned_words_tests).
@@ -29,23 +32,42 @@ test() ->
ct:pal("=== Admin Banned Words Tests ==="),
Token = api_test_runner:get_admin_token(),
% Добавляем два исходных слова
% Добавляем несколько слов для тестирования
Unique = integer_to_binary(erlang:system_time()),
Word1 = <<"badword1_", Unique/binary>>,
Word2 = <<"badword2_", Unique/binary>>,
Word1 = <<"alpha_", Unique/binary>>,
Word2 = <<"beta_", Unique/binary>>,
Word3 = <<"gamma_", Unique/binary>>,
api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word1}),
api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word2}),
api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word3}),
test_list_words(Token, Word1, Word2),
% ── Основные тесты ──
test_list_words(Token),
test_add_word(Token),
test_update_word(Token, Word1),
test_batch_add_words(Token),
test_words_pagination(Token),
test_delete_word(Token, Word2),
test_update_word_conflict(Token),
test_batch_add_invalid(Token),
test_batch_add_duplicates(Token),
% ── Пагинация ──
test_words_pagination(Token),
test_pagination_headers(Token),
% ── Фильтрация ──
test_filter_by_word(Token, Word3),
% ── Сортировка ──
test_sort_by_word_asc(Token),
test_sort_by_added_at_desc(Token),
% ── Комбинации ──
test_filter_with_sort(Token, Unique),
test_filter_sort_pagination(Token, Unique),
% Удаление beta только после всех тестов фильтрации/сортировки
test_delete_word(Token, Word2),
ct:pal("=== All admin banned words tests passed ==="),
ok.
@@ -54,17 +76,14 @@ test() ->
%%%===================================================================
%% @doc GET /v1/admin/banned-words список всех слов.
-spec test_list_words(binary(), binary(), binary()) -> ok.
test_list_words(Token, Word1, Word2) ->
test_list_words(Token) ->
ct:pal(" TEST: List all banned words"),
Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token),
?assert(is_list(Words)),
?assert(length(Words) >= 2),
?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word1 orelse maps:get(<<"word">>, W) =:= Word2 end, Words)),
?assert(length(Words) >= 3),
ct:pal(" OK: ~p words", [length(Words)]).
%% @doc POST /v1/admin/banned-words добавление нового слова.
-spec test_add_word(binary()) -> ok.
test_add_word(Token) ->
ct:pal(" TEST: Add a new banned word"),
Unique = integer_to_binary(erlang:system_time()),
@@ -76,40 +95,31 @@ test_add_word(Token) ->
ct:pal(" OK").
%% @doc PUT /v1/admin/banned-words/:word редактирование слова.
-spec test_update_word(binary(), binary()) -> ok.
test_update_word(Token, Word) ->
ct:pal(" TEST: Update a banned word"),
NewWord = <<Word/binary, "_updated">>,
Result = api_test_runner:admin_put(<<"/v1/admin/banned-words/", Word/binary>>, Token, #{<<"word">> => NewWord}),
?assertEqual(<<"updated">>, maps:get(<<"status">>, Result)),
% Проверяем, что старое слово отсутствует, а новое присутствует
Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token),
?assertNot(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, Words)),
?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= NewWord end, Words)),
ct:pal(" OK").
%% @doc POST /v1/admin/banned-words/batch массовая загрузка.
-spec test_batch_add_words(binary()) -> ok.
test_batch_add_words(Token) ->
ct:pal(" TEST: Batch add banned words"),
Unique = integer_to_binary(erlang:system_time()),
Words = [
<<"batch1_", Unique/binary>>,
<<"batch2_", Unique/binary>>,
<<"batch3_", Unique/binary>>
],
Words = [<<"batch1_", Unique/binary>>, <<"batch2_", Unique/binary>>, <<"batch3_", Unique/binary>>],
Body = jsx:encode(#{<<"words">> => Words}),
{ok, 200, _, RespBody} = api_test_runner:admin_request(post, <<"/v1/admin/banned-words/batch">>, Token, Body),
Result = jsx:decode(list_to_binary(RespBody), [return_maps]),
?assertEqual(3, maps:get(<<"added">>, Result)),
?assertEqual(0, maps:get(<<"skipped">>, Result)),
% Проверяем наличие всех слов
AllWords = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token),
[?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, AllWords)) || Word <- Words],
ct:pal(" OK").
%% @doc DELETE /v1/admin/banned-words/:word удаление слова.
-spec test_delete_word(binary(), binary()) -> ok.
test_delete_word(Token, Word) ->
ct:pal(" TEST: Delete a banned word"),
Path = <<"/v1/admin/banned-words/", Word/binary>>,
@@ -119,29 +129,17 @@ test_delete_word(Token, Word) ->
?assertNot(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, Words)),
ct:pal(" OK").
%% @doc GET /v1/admin/banned-words?limit=...&offset=... пагинация.
-spec test_words_pagination(binary()) -> ok.
test_words_pagination(Token) ->
ct:pal(" TEST: Banned words pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/banned-words?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/banned-words?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 PUT /v1/admin/banned-words/:word попытка переименовать в уже существующее слово.
%% @doc PUT /v1/admin/banned-words/:word – попытка переименовать в уже существующее слово (409).
test_update_word_conflict(Token) ->
ct:pal(" TEST: Update banned word to existing word"),
ct:pal(" TEST: Update banned word to existing word (conflict)"),
Unique = integer_to_binary(erlang:system_time()),
WordA = <<"conflictA_", Unique/binary>>,
WordB = <<"conflictB_", Unique/binary>>,
api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => WordA}),
api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => WordB}),
Path = <<"/v1/admin/banned-words/", WordA/binary>>,
{ok, 409, _, _} = api_test_runner:admin_request(put, Path, Token, jsx:encode(#{<<"word">> => WordB})),
Resp = api_test_runner:admin_request(put, Path, Token, jsx:encode(#{<<"word">> => WordB})),
?assertMatch({ok, 409, _, _}, Resp),
ct:pal(" OK: got 409 conflict").
%% @doc POST /v1/admin/banned-words/batch с невалидными элементами.
@@ -174,4 +172,90 @@ test_batch_add_duplicates(Token) ->
Result = jsx:decode(list_to_binary(RespBody), [return_maps]),
?assertEqual(2, maps:get(<<"added">>, Result)),
?assertEqual(2, maps:get(<<"skipped">>, Result)),
ct:pal(" OK: duplicates handled").
ct:pal(" OK: duplicates handled").
%% ── Пагинация ──
%% @doc GET /v1/admin/banned-words?limit=...&offset=... пагинация.
test_words_pagination(Token) ->
ct:pal(" TEST: Banned words pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/banned-words?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/banned-words?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).
test_pagination_headers(Token) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/banned-words?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 Фильтрация по слову (q).
test_filter_by_word(Token, Word) ->
ct:pal(" TEST: Filter banned words by word (q)"),
Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?q=", Word/binary>>, Token),
?assert(is_list(Words)),
?assert(length(Words) >= 1),
[?assertEqual(Word, maps:get(<<"word">>, W)) || W <- Words],
ct:pal(" OK: ~p words match", [length(Words)]).
%% ── Сортировка ──
%% @doc Сортировка по word по возрастанию.
test_sort_by_word_asc(Token) ->
ct:pal(" TEST: Sort banned words by word asc"),
Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?sort=word&order=asc">>, Token),
WordList = [maps:get(<<"word">>, W) || W <- Words],
?assertEqual(lists:sort(WordList), WordList),
ct:pal(" OK: sorted by word asc").
%% @doc Сортировка по added_at по убыванию.
test_sort_by_added_at_desc(Token) ->
ct:pal(" TEST: Sort banned words by added_at desc"),
Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?sort=added_at&order=desc">>, Token),
Dates = [maps:get(<<"added_at">>, W) || W <- Words],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by added_at desc").
%% ── Комбинации ──
%% @doc Фильтр + сортировка: q=alpha, сортировка по added_at asc.
test_filter_with_sort(Token, Unique) ->
ct:pal(" TEST: Filter + sort (q=alpha, sort=added_at, order=asc)"),
Q = <<"alpha_", Unique/binary>>,
Words = api_test_runner:admin_get(
<<"/v1/admin/banned-words?q=", Q/binary, "&sort=added_at&order=asc">>, Token),
?assert(length(Words) >= 1),
% Проверяем, что все слова содержат "alpha"
[?assert(binary:match(maps:get(<<"word">>, W), <<"alpha">>) =/= nomatch) || W <- Words],
Dates = [maps:get(<<"added_at">>, W) || W <- Words],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: ~p words", [length(Words)]).
%% @doc Фильтр + сортировка + пагинация: q=beta, sort=word desc, limit=1.
test_filter_sort_pagination(Token, Unique) ->
ct:pal(" TEST: Filter + sort + pagination (q=beta, sort=word desc, limit=1)"),
Q = <<"beta_", Unique/binary>>,
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/banned-words?q=", Q/binary, "&sort=word&order=desc&limit=1&offset=0">>, Token),
Words = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Words) =< 1),
case Words of
[W | _] -> ?assert(binary:match(maps:get(<<"word">>, W), <<"beta">>) =/= nomatch);
_ -> ok
end,
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: got ~p words (total ~p)", [length(Words), Total]).
+110 -21
View File
@@ -5,7 +5,8 @@
%%% GET /v1/admin/calendars/:id
%%% PUT /v1/admin/calendars/:id
%%% DELETE /v1/admin/calendars/:id
%%% Проверяет пагинацию, фильтрацию по статусу/типу, поиск и сортировку.
%%% Проверяет пагинацию, фильтрацию по статусу/типу, поиск и сортировку,
%%% а также комбинации фильтров, сортировки и пагинации.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_calendars_tests).
@@ -36,9 +37,20 @@ test() ->
test_filter_by_status(AdminToken),
test_filter_by_type(AdminToken),
test_search(AdminToken),
%% Комбинации фильтров
test_filter_combined_status_type(AdminToken),
%% Сортировка
test_sort_by_title(AdminToken),
test_sort_by_created_at(AdminToken)
test_sort_by_created_at(AdminToken),
%% Фильтр + сортировка
test_filter_status_with_sort(AdminToken),
test_filter_type_with_sort(AdminToken),
test_filter_search_with_sort(AdminToken),
%% Фильтр + пагинация
test_filter_status_with_pagination(AdminToken),
%% Фильтр + сортировка + пагинация
test_filter_type_with_sort_and_pagination(AdminToken),
test_filter_sort_pagination_combined(AdminToken)
after
cleanup(AdminToken, CalIds)
end,
@@ -52,18 +64,14 @@ test() ->
setup() ->
ct:pal(" Setup: register user and create calendars"),
AdminToken = api_test_runner:get_admin_token(),
Email = api_test_runner:unique_email(<<"caladmin">>),
Password = <<"testpass">>,
UserToken = api_test_runner:register_and_login(Email, Password),
CalId1 = create_calendar(UserToken, <<"Personal Active">>, <<"Desc1">>),
CalId2 = create_calendar(UserToken, <<"Commercial">>, <<"Desc2">>),
CalId3 = create_calendar(UserToken, <<"Frozen">>, <<"Desc3">>),
update_calendar_admin(AdminToken, CalId2, #{<<"type">> => <<"commercial">>}),
update_calendar_admin(AdminToken, CalId3, #{<<"status">> => <<"frozen">>}),
ct:pal(" Created calendars: ~p", [[CalId1, CalId2, CalId3]]),
{AdminToken, UserToken, [CalId1, CalId2, CalId3]}.
@@ -76,10 +84,8 @@ create_calendar(UserToken, Title, Description) ->
-spec update_calendar_admin(binary(), binary(), map()) -> ok.
update_calendar_admin(AdminToken, CalId, Updates) ->
% Получаем текущий календарь
{ok, 200, _, RespBody} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>),
Cal = jsx:decode(list_to_binary(RespBody), [return_maps]),
% Сливаем изменения
UpdatedCal = maps:merge(Cal, Updates),
Body = jsx:encode(UpdatedCal),
{ok, 200, _, _} = api_test_runner:admin_request(put, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, Body),
@@ -110,7 +116,6 @@ test_update_calendar(AdminToken, CalId, UserToken) ->
ct:pal(" TEST: Update calendar (admin)"),
NewTitle = <<"Updated by admin">>,
update_calendar_admin(AdminToken, CalId, #{<<"title">> => NewTitle}),
% Проверяем через пользовательский API
{ok, 200, _, UserResp} = api_test_runner:client_request(get, <<"/v1/calendars/", CalId/binary>>, UserToken, <<"">>),
UserCal = jsx:decode(list_to_binary(UserResp), [return_maps]),
?assertEqual(NewTitle, maps:get(<<"title">>, UserCal)),
@@ -129,7 +134,6 @@ test_update_calendar_invalid_body(AdminToken, CalId) ->
test_delete_calendar(AdminToken, CalId) ->
ct:pal(" TEST: Delete calendar (admin)"),
{ok, 200, _, _} = api_test_runner:admin_request(delete, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>),
% Проверяем, что календарь не удалён физически, а помечен как deleted (soft delete)
{ok, 200, _, RespBody} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>),
Cal = jsx:decode(list_to_binary(RespBody), [return_maps]),
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Cal)),
@@ -148,11 +152,8 @@ test_pagination(AdminToken) ->
{ok, 200, Headers1, Body1} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?limit=2&offset=0">>, AdminToken, <<"">>),
List1 = jsx:decode(list_to_binary(Body1), [return_maps]),
?assertEqual(2, length(List1)),
TotalCount = list_to_integer(
proplists:get_value("x-total-count", Headers1, "0")
),
TotalCount = list_to_integer(proplists:get_value("x-total-count", Headers1, "0")),
?assert(TotalCount >= 3),
{ok, 200, _, Body2} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?limit=2&offset=2">>, AdminToken, <<"">>),
List2 = jsx:decode(list_to_binary(Body2), [return_maps]),
?assert(length(List2) >= 1),
@@ -166,7 +167,6 @@ test_filter_by_status(AdminToken) ->
{ok, 200, _, BodyActive} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?status=active">>, AdminToken, <<"">>),
ActiveList = jsx:decode(list_to_binary(BodyActive), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, C)) || C <- ActiveList],
{ok, 200, _, BodyFrozen} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?status=frozen">>, AdminToken, <<"">>),
FrozenList = jsx:decode(list_to_binary(BodyFrozen), [return_maps]),
[?assertEqual(<<"frozen">>, maps:get(<<"status">>, C)) || C <- FrozenList],
@@ -189,18 +189,30 @@ test_search(AdminToken) ->
HasMatch = lists:any(fun(C) ->
Title = string:lowercase(binary_to_list(maps:get(<<"title">>, C, <<>>))),
Desc = string:lowercase(binary_to_list(maps:get(<<"description">>, C, <<>>))),
string:str(Title, "commercial") > 0 orelse
string:str(Desc, "commercial") > 0
end, Results),
string:str(Title, "commercial") > 0 orelse string:str(Desc, "commercial") > 0
end, Results),
?assert(HasMatch),
ct:pal(" OK: search works").
%%%===================================================================
%%% Комбинации фильтров
%%%===================================================================
test_filter_combined_status_type(AdminToken) ->
ct:pal(" TEST: Combined filter (status=active, type=personal)"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?status=active&type=personal">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, C)) || C <- List],
[?assertEqual(<<"personal">>, maps:get(<<"type">>, C)) || C <- List],
ct:pal(" OK: ~p calendars match both criteria", [length(List)]).
%%%===================================================================
%%% Сортировка
%%%===================================================================
test_sort_by_title(AdminToken) ->
ct:pal(" TEST: Sort by title asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?sort_by=title&sort_order=asc">>, AdminToken, <<"">>),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?sort_by=title&sort_order=asc">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
Titles = [maps:get(<<"title">>, C) || C <- List],
?assertEqual(lists:sort(Titles), Titles),
@@ -208,9 +220,86 @@ test_sort_by_title(AdminToken) ->
test_sort_by_created_at(AdminToken) ->
ct:pal(" TEST: Sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?sort_by=created_at&sort_order=desc">>, AdminToken, <<"">>),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?sort_by=created_at&sort_order=desc">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, C) || C <- List],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by created_at desc").
ct:pal(" OK: sorted by created_at desc").
%%%===================================================================
%%% Фильтр + сортировка
%%%===================================================================
test_filter_status_with_sort(AdminToken) ->
ct:pal(" TEST: Filter status=frozen + sort by title asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?status=frozen&sort_by=title&sort_order=asc">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"frozen">>, maps:get(<<"status">>, C)) || C <- List],
Titles = [maps:get(<<"title">>, C) || C <- List],
?assertEqual(lists:sort(Titles), Titles),
ct:pal(" OK: ~p calendars", [length(List)]).
test_filter_type_with_sort(AdminToken) ->
ct:pal(" TEST: Filter type=commercial + sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?type=commercial&sort_by=created_at&sort_order=desc">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"commercial">>, maps:get(<<"type">>, C)) || C <- List],
Dates = [maps:get(<<"created_at">>, C) || C <- List],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p calendars", [length(List)]).
test_filter_search_with_sort(AdminToken) ->
ct:pal(" TEST: Search q=Personal + sort by title desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?q=Personal&sort_by=title&sort_order=desc">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
[?assert(string:str(string:lowercase(binary_to_list(maps:get(<<"title">>, C))), "personal") > 0
orelse string:str(string:lowercase(binary_to_list(maps:get(<<"description">>, C))), "personal") > 0)
|| C <- List],
Titles = [maps:get(<<"title">>, C) || C <- List],
?assertEqual(lists:reverse(lists:sort(Titles)), Titles),
ct:pal(" OK: ~p calendars", [length(List)]).
%%%===================================================================
%%% Фильтр + пагинация
%%%===================================================================
test_filter_status_with_pagination(AdminToken) ->
ct:pal(" TEST: Filter status=active + pagination (limit=1, offset=0)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?status=active&limit=1&offset=0">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(1, length(List)),
[?assertEqual(<<"active">>, maps:get(<<"status">>, C)) || C <- List],
Total = list_to_integer(proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%%%===================================================================
%%% Фильтр + сортировка + пагинация
%%%===================================================================
test_filter_type_with_sort_and_pagination(AdminToken) ->
ct:pal(" TEST: Filter type=personal + sort by created_at asc + pagination (limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?type=personal&sort_by=created_at&sort_order=asc&limit=1&offset=0">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(1, length(List)),
[?assertEqual(<<"personal">>, maps:get(<<"type">>, C)) || C <- List],
Total = list_to_integer(proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
test_filter_sort_pagination_combined(AdminToken) ->
ct:pal(" TEST: status=active + sort_by=title asc + limit=2"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/calendars?status=active&sort_by=title&sort_order=asc&limit=2&offset=0">>, AdminToken, <<"">>),
List = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, C)) || C <- List],
Titles = [maps:get(<<"title">>, C) || C <- List],
?assertEqual(lists:sort(Titles), Titles),
Total = list_to_integer(proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: ~p calendars (total ~p)", [length(List), Total]).
+120 -30
View File
@@ -1,26 +1,21 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты административного API для управления событиями.
%%% Покрывает эндпоинты:
%%% GET /v1/admin/events
%%% GET /v1/admin/events/:id
%%% PUT /v1/admin/events/:id
%%% DELETE /v1/admin/events/:id
%%% GET /v1/admin/events
%%% GET /v1/admin/events/:id
%%% PUT /v1/admin/events/:id
%%% DELETE /v1/admin/events/:id
%%% Проверяет фильтрацию, сортировку, пагинацию и их комбинации.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_events_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% EUnit test generator
%%%===================================================================
test() ->
ct:pal("=== Admin Events Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
%% Создаём тестовый календарь и два события
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"EventsTestCal">>}),
Event1Id = api_test_runner:create_event(UserToken, CalId, #{
@@ -33,7 +28,6 @@ test() ->
start_time => api_test_runner:future_date(),
duration => 60
}),
%% Выполняем тесты
test_list_events(Token, CalId, Event1Id, Event2Id),
test_get_event(Token, Event1Id),
@@ -42,72 +36,82 @@ test() ->
test_filter_events(Token, CalId),
test_search_events(Token, CalId),
test_event_pagination(Token, CalId),
test_delete_event(Token, Event1Id),
%% Комбинации фильтров
test_filter_combined_status_calendar(Token, CalId),
test_filter_combined_title_status(Token, CalId),
%% Сортировка
test_sort_by_title(Token, CalId),
test_sort_by_start_time(Token, CalId),
%% Фильтр + сортировка
test_filter_status_sort_title(Token, CalId),
test_search_sort_created_at(Token, CalId),
%% Фильтр + сортировка + пагинация
test_filter_sort_pagination_combined(Token, CalId),
%% Проверка заголовка x-total-count
test_pagination_headers(Token, CalId),
test_delete_event(Token, Event1Id),
ct:pal("=== All admin events tests passed ==="),
ok.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
%% @doc GET /v1/admin/events список событий (базовая проверка).
test_list_events(Token, _CalId, _Event1Id, _Event2Id) ->
ct:pal(" TEST: List all events"),
ct:pal(" TEST: List all events"),
Events = api_test_runner:admin_get(<<"/v1/admin/events">>, Token),
?assert(is_list(Events)),
?assert(length(Events) >= 2),
ct:pal(" OK: ~p events found", [length(Events)]).
ct:pal(" OK: ~p events found", [length(Events)]).
%% @doc GET /v1/admin/events/:id получение события по ID.
test_get_event(Token, EventId) ->
ct:pal(" TEST: Get event by ID"),
ct:pal(" TEST: Get event by ID"),
Path = <<"/v1/admin/events/", EventId/binary>>,
Event = api_test_runner:admin_get(Path, Token),
?assertEqual(EventId, maps:get(<<"id">>, Event)),
?assertEqual(<<"active">>, maps:get(<<"status">>, Event)),
ct:pal(" OK: ~s", [maps:get(<<"title">>, Event)]).
ct:pal(" OK: ~s", [maps:get(<<"title">>, Event)]).
%% @doc PUT /v1/admin/events/:id обновление события.
test_update_event(Token, EventId) ->
ct:pal(" TEST: Update event"),
ct:pal(" TEST: Update event"),
Path = <<"/v1/admin/events/", EventId/binary>>,
Updates = #{<<"title">> => <<"Updated by admin">>, <<"description">> => <<"Test update">>},
Updated = api_test_runner:admin_put(Path, Token, Updates),
?assertEqual(<<"Updated by admin">>, maps:get(<<"title">>, Updated)),
?assertEqual(<<"Test update">>, maps:get(<<"description">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc DELETE /v1/admin/events/:id удаление события.
test_delete_event(Token, EventId) ->
ct:pal(" TEST: Delete event"),
ct:pal(" TEST: Delete event"),
Path = <<"/v1/admin/events/", EventId/binary>>,
Deleted = api_test_runner:admin_delete(Path, Token),
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc GET /v1/admin/events?status=active фильтрация по статусу.
test_filter_events(Token, CalId) ->
ct:pal(" TEST: Filter events by status=active"),
ct:pal(" TEST: Filter events by status=active"),
Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&status=active">>,
Events = api_test_runner:admin_get(Path, Token),
?assert(is_list(Events)),
[?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events],
ct:pal(" OK: ~p events", [length(Events)]).
ct:pal(" OK: ~p events", [length(Events)]).
%% @doc GET /v1/admin/events?q=... поиск по подстроке.
test_search_events(Token, CalId) ->
ct:pal(" TEST: Search events by title substring"),
ct:pal(" TEST: Search events by title substring"),
Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&q=Updated">>,
Events = api_test_runner:admin_get(Path, Token),
?assert(is_list(Events)),
?assert(length(Events) >= 1),
[?assertNotEqual(nomatch, binary:match(maps:get(<<"title">>, E), <<"Updated">>)) || E <- Events],
ct:pal(" OK: ~p results", [length(Events)]).
ct:pal(" OK: ~p results", [length(Events)]).
%% @doc GET /v1/admin/events?limit=...&offset=... пагинация.
test_event_pagination(Token, CalId) ->
ct:pal(" TEST: Event pagination"),
ct:pal(" TEST: Event pagination"),
Base = <<"/v1/admin/events?calendar_id=", CalId/binary, "&status=all&sort=title&order=asc">>,
Page1 = api_test_runner:admin_get(<<Base/binary, "&limit=1&offset=0">>, Token),
?assertEqual(1, length(Page1)),
@@ -116,4 +120,90 @@ test_event_pagination(Token, CalId) ->
Id1 = maps:get(<<"id">>, hd(Page1)),
Id2 = maps:get(<<"id">>, hd(Page2)),
?assertNotEqual(Id1, Id2),
ct:pal(" OK").
ct:pal(" OK").
%% ── Комбинации фильтров ──
test_filter_combined_status_calendar(Token, CalId) ->
ct:pal(" TEST: Filter by status=active + calendar_id"),
Path = <<"/v1/admin/events?status=active&calendar_id=", CalId/binary>>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events],
[?assertEqual(CalId, maps:get(<<"calendar_id">>, E)) || E <- Events],
ct:pal(" OK: ~p events", [length(Events)]).
test_filter_combined_title_status(Token, CalId) ->
ct:pal(" TEST: Filter by title=Updated + status=active"),
Path = <<"/v1/admin/events?title=Updated%20by%20admin&status=active&calendar_id=", CalId/binary>>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"Updated by admin">>, maps:get(<<"title">>, E)) || E <- Events],
[?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events],
ct:pal(" OK: ~p events", [length(Events)]).
%% ── Сортировка ──
test_sort_by_title(Token, CalId) ->
ct:pal(" TEST: Sort by title asc"),
Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&sort=title&order=asc">>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
Titles = [maps:get(<<"title">>, E) || E <- Events],
?assertEqual(lists:sort(Titles), Titles),
ct:pal(" OK: sorted by title asc").
test_sort_by_start_time(Token, CalId) ->
ct:pal(" TEST: Sort by start_time desc"),
Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&sort=start_time&order=desc">>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"start_time">>, E) || E <- Events],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by start_time desc").
%% ── Фильтр + сортировка ──
test_filter_status_sort_title(Token, CalId) ->
ct:pal(" TEST: Filter status=active + sort by title desc"),
Path = <<"/v1/admin/events?status=active&calendar_id=", CalId/binary, "&sort=title&order=desc">>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events],
Titles = [maps:get(<<"title">>, E) || E <- Events],
?assertEqual(lists:reverse(lists:sort(Titles)), Titles),
ct:pal(" OK: ~p events", [length(Events)]).
test_search_sort_created_at(Token, CalId) ->
ct:pal(" TEST: Search q=Updated + sort by created_at asc"),
Path = <<"/v1/admin/events?q=Updated&calendar_id=", CalId/binary, "&sort=created_at&order=asc">>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
[?assert(binary:match(maps:get(<<"title">>, E), <<"Updated">>) =/= nomatch) || E <- Events],
Dates = [maps:get(<<"created_at">>, E) || E <- Events],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: ~p events", [length(Events)]).
%% ── Фильтр + сортировка + пагинация ──
test_filter_sort_pagination_combined(Token, CalId) ->
ct:pal(" TEST: status=active + sort=start_time desc + limit=1"),
Path = <<"/v1/admin/events?status=active&calendar_id=", CalId/binary, "&sort=start_time&order=desc&limit=1&offset=0">>,
{ok, 200, Headers, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Events = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Events) =< 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events],
Total = list_to_integer(proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: ~p events (total ~p)", [length(Events), Total]).
%% ── Заголовки пагинации ──
test_pagination_headers(Token, CalId) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&limit=2&offset=0">>,
{ok, 200, Headers, _} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Total = list_to_integer(proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
+48 -34
View File
@@ -2,30 +2,28 @@
%%% @doc Тесты административного API для модерации сущностей.
%%%
%%% Покрывает эндпоинты:
%%% PUT /v1/admin/:target_type/:id
%%% PUT /v1/admin/:target_type/:id
%%%
%%% Проверяет:
%%% - заморозку/разморозку календаря
%%% - заморозку/разморозку события
%%% - скрытие/раскрытие отзыва
%%% - блокировку/разблокировку пользователя
%%% - заморозку/разморозку календаря
%%% - заморозку/разморозку события
%%% - скрытие/раскрытие отзыва
%%% - блокировку/разблокировку пользователя
%%% - наличие записей в аудите после модерации
%%% @end
%%%-------------------------------------------------------------------
-module(admin_moderation_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Moderation Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ModTestCal">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
@@ -33,24 +31,18 @@ test() ->
start_time => api_test_runner:future_date(),
duration => 60
}),
% Создаём пользователя для блокировки
UserEmail = api_test_runner:unique_email(<<"moduser">>),
UserTok = api_test_runner:register_and_login(UserEmail, <<"pass">>),
#{<<"id">> := UserId} = api_test_runner:client_get(<<"/v1/user/me">>, UserTok),
% Создаём отзыв (требуется участие)
#{<<"id">> := BookingId} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, UserTok, #{}),
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, UserToken,
#{action => <<"confirm">>}),
#{<<"id">> := ReviewId} = api_test_runner:client_post(<<"/v1/reviews">>, UserTok, #{
target_type => <<"event">>,
target_id => EventId,
rating => 3,
comment => <<"Moderate me">>
}),
#{<<"id">> := ReviewId} = api_test_runner:client_post(<<"/v1/reviews">>, UserTok,
#{ target_type => <<"event">>, target_id => EventId, rating => 3,
comment => <<"Moderate me">> }),
% Тесты модерации
test_freeze_calendar(Token, CalId),
test_unfreeze_calendar(Token, CalId),
@@ -60,7 +52,8 @@ test() ->
test_unhide_review(Token, ReviewId),
test_block_user(Token, UserId),
test_unblock_user(Token, UserId),
% Проверка аудита после модерации
test_moderate_calendar_audit(Token),
ct:pal("=== All admin moderation tests passed ==="),
ok.
@@ -69,65 +62,86 @@ test() ->
%%%===================================================================
test_freeze_calendar(Token, Id) ->
ct:pal(" TEST: Freeze calendar"),
ct:pal(" TEST: Freeze calendar"),
Path = <<"/v1/admin/calendar/", Id/binary>>,
Body = #{<<"action">> => <<"freeze">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_unfreeze_calendar(Token, Id) ->
ct:pal(" TEST: Unfreeze calendar"),
ct:pal(" TEST: Unfreeze calendar"),
Path = <<"/v1/admin/calendar/", Id/binary>>,
Body = #{<<"action">> => <<"unfreeze">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_freeze_event(Token, Id) ->
ct:pal(" TEST: Freeze event"),
ct:pal(" TEST: Freeze event"),
Path = <<"/v1/admin/event/", Id/binary>>,
Body = #{<<"action">> => <<"freeze">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_unfreeze_event(Token, Id) ->
ct:pal(" TEST: Unfreeze event"),
ct:pal(" TEST: Unfreeze event"),
Path = <<"/v1/admin/event/", Id/binary>>,
Body = #{<<"action">> => <<"unfreeze">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_hide_review(Token, ReviewId) ->
ct:pal(" TEST: Hide review"),
ct:pal(" TEST: Hide review"),
Path = <<"/v1/admin/review/", ReviewId/binary>>,
Body = #{<<"action">> => <<"hide">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"hidden">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_unhide_review(Token, ReviewId) ->
ct:pal(" TEST: Unhide review"),
ct:pal(" TEST: Unhide review"),
Path = <<"/v1/admin/review/", ReviewId/binary>>,
Body = #{<<"action">> => <<"unhide">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"visible">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_block_user(Token, UserId) ->
ct:pal(" TEST: Block user"),
ct:pal(" TEST: Block user"),
Path = <<"/v1/admin/user/", UserId/binary>>,
Body = #{<<"action">> => <<"block">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"blocked">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_unblock_user(Token, UserId) ->
ct:pal(" TEST: Unblock user"),
ct:pal(" TEST: Unblock user"),
Path = <<"/v1/admin/user/", UserId/binary>>,
Body = #{<<"action">> => <<"unblock">>, <<"reason">> => <<"Test">>},
Resp = api_test_runner:admin_put(Path, Token, Body),
?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)),
ct:pal(" OK").
ct:pal(" OK").
test_moderate_calendar_audit(Token) ->
ct:pal(" TEST: Audit record for freeze calendar"),
UserToken2 = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(UserToken2, #{title => <<"AuditCal">>}),
Path = <<"/v1/admin/calendar/", CalId/binary>>,
api_test_runner:admin_put(Path, Token, #{action => <<"freeze">>, reason => <<"Violation">>}),
ct:sleep(200),
SuperToken = api_test_runner:get_superadmin_token(),
AuditRecords = api_test_runner:admin_get(<<"/v1/admin/audit?action=freeze_calendar">>, SuperToken),
?assert(length(AuditRecords) >= 1),
Matching = [R || R <- AuditRecords,
case maps:get(<<"entity_id">>, R) of
#{<<"id">> := Id} -> Id =:= CalId;
_ -> false
end],
?assert(length(Matching) >= 1),
Last = hd(Matching),
?assertEqual(<<"freeze_calendar">>, maps:get(<<"action">>, Last)),
?assertEqual(<<"calendar">>, maps:get(<<"entity_type">>, Last)),
ct:pal(" OK: audit record found").
+139 -34
View File
@@ -2,42 +2,40 @@
%%% @doc Тесты административного API для управления жалобами.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/reports
%%% GET /v1/admin/reports/:id
%%% PUT /v1/admin/reports/:id
%%% GET /v1/admin/reports
%%% GET /v1/admin/reports/:id
%%% PUT /v1/admin/reports/:id
%%%
%%% Проверяет:
%%% - получение списка жалоб
%%% - получение жалобы по ID
%%% - обновление статуса жалобы (review, dismiss)
%%% - фильтрацию по статусу и типу цели
%%% - пагинацию
%%% - получение списка жалоб
%%% - получение жалобы по ID
%%% - обновление статуса жалобы (review, dismiss)
%%% - фильтрацию по статусу и типу цели
%%% - комбинации фильтров (status + target_type)
%%% - сортировку по created_at и status
%%% - комбинации фильтрации с сортировкой
%%% - пагинацию (limit/offset, x-total-count)
%%% - комбинации фильтрации, сортировки и пагинации
%%% @end
%%%-------------------------------------------------------------------
-module(admin_reports_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Reports Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
% Создаём тестовые данные: календарь, событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReportsTestCal">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"Event for reporting">>,
start_time => api_test_runner:future_date(),
duration => 60
}),
% Создаём две жалобы от имени пользователя
#{<<"id">> := Report1Id} = api_test_runner:client_post(<<"/v1/reports">>, UserToken, #{
<<"target_type">> => <<"event">>,
<<"target_id">> => EventId,
@@ -53,8 +51,22 @@ test() ->
test_get_report(Token, Report1Id),
test_review_report(Token, Report1Id),
test_dismiss_report(Token, Report2Id),
test_filter_reports(Token),
test_filter_combined_status_target(Token),
test_sort_by_created_at_desc(Token),
test_sort_by_created_at_asc(Token),
test_sort_by_status_asc(Token),
test_filter_status_sort_created_at(Token),
test_filter_target_sort_status(Token),
test_report_pagination(Token),
test_pagination_headers(Token),
test_filter_status_with_pagination(Token),
test_filter_sort_pagination_combined(Token),
ct:pal("=== All admin reports tests passed ==="),
ok.
@@ -63,63 +75,120 @@ test() ->
%%% Тестовые функции
%%%===================================================================
%% @doc GET /v1/admin/reports – проверяет получение списка жалоб.
-spec test_list_reports(binary(), binary()) -> ok.
test_list_reports(Token, ReportId) ->
ct:pal(" TEST: List all reports"),
ct:pal(" TEST: List all reports"),
Reports = api_test_runner:admin_get(<<"/v1/admin/reports">>, Token),
?assert(is_list(Reports)),
?assert(length(Reports) >= 2),
?assert(lists:any(fun(R) -> maps:get(<<"id">>, R) =:= ReportId end, Reports)),
ct:pal(" OK: ~p reports", [length(Reports)]).
ct:pal(" OK: ~p reports", [length(Reports)]).
%% @doc GET /v1/admin/reports/:id проверяет получение жалобы по ID.
-spec test_get_report(binary(), binary()) -> ok.
test_get_report(Token, ReportId) ->
ct:pal(" TEST: Get report by ID"),
ct:pal(" TEST: Get report by ID"),
Path = <<"/v1/admin/reports/", ReportId/binary>>,
Report = api_test_runner:admin_get(Path, Token),
?assertEqual(ReportId, maps:get(<<"id">>, Report)),
?assertEqual(<<"pending">>, maps:get(<<"status">>, Report)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/reports/:id проверяет изменение статуса на reviewed.
-spec test_review_report(binary(), binary()) -> ok.
test_review_report(Token, ReportId) ->
ct:pal(" TEST: Review report (set status to reviewed)"),
ct:pal(" TEST: Review report (set status to reviewed)"),
Path = <<"/v1/admin/reports/", ReportId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"reviewed">>}),
?assertEqual(<<"reviewed">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/reports/:id проверяет изменение статуса на dismissed.
-spec test_dismiss_report(binary(), binary()) -> ok.
test_dismiss_report(Token, ReportId) ->
ct:pal(" TEST: Dismiss report (set status to dismissed)"),
ct:pal(" TEST: Dismiss report (set status to dismissed)"),
Path = <<"/v1/admin/reports/", ReportId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"dismissed">>}),
?assertEqual(<<"dismissed">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc GET /v1/admin/reports?status=...&target_type=... проверяет фильтрацию.
-spec test_filter_reports(binary()) -> ok.
test_filter_reports(Token) ->
ct:pal(" TEST: Filter reports by status=reviewed"),
ct:pal(" TEST: Filter reports by status=reviewed"),
Reports = api_test_runner:admin_get(<<"/v1/admin/reports?status=reviewed">>, Token),
?assert(is_list(Reports)),
[?assertEqual(<<"reviewed">>, maps:get(<<"status">>, R)) || R <- Reports],
ct:pal(" OK: ~p reviewed reports", [length(Reports)]),
ct:pal(" OK: ~p reviewed reports", [length(Reports)]),
ct:pal(" TEST: Filter reports by target_type=event"),
ct:pal(" TEST: Filter reports by target_type=event"),
Reports2 = api_test_runner:admin_get(<<"/v1/admin/reports?target_type=event">>, Token),
?assert(is_list(Reports2)),
[?assertEqual(<<"event">>, maps:get(<<"target_type">>, R)) || R <- Reports2],
ct:pal(" OK: ~p reports for events", [length(Reports2)]).
ct:pal(" OK: ~p reports for events", [length(Reports2)]).
-spec test_filter_combined_status_target(binary()) -> ok.
test_filter_combined_status_target(Token) ->
ct:pal(" TEST: Combined filter (status=dismissed & target_type=event)"),
Reports = api_test_runner:admin_get(
<<"/v1/admin/reports?status=dismissed&target_type=event">>, Token),
?assert(is_list(Reports)),
[?assertEqual(<<"dismissed">>, maps:get(<<"status">>, R)) || R <- Reports],
[?assertEqual(<<"event">>, maps:get(<<"target_type">>, R)) || R <- Reports],
ct:pal(" OK: ~p reports match both criteria", [length(Reports)]).
-spec test_sort_by_created_at_desc(binary()) -> ok.
test_sort_by_created_at_desc(Token) ->
ct:pal(" TEST: Sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?sort=created_at&order=desc">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reports],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by created_at desc").
-spec test_sort_by_created_at_asc(binary()) -> ok.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?sort=created_at&order=asc">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reports],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
-spec test_sort_by_status_asc(binary()) -> ok.
test_sort_by_status_asc(Token) ->
ct:pal(" TEST: Sort by status asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?sort=status&order=asc">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
Statuses = [maps:get(<<"status">>, R) || R <- Reports],
?assertEqual(lists:sort(Statuses), Statuses),
ct:pal(" OK: sorted by status asc").
-spec test_filter_status_sort_created_at(binary()) -> ok.
test_filter_status_sort_created_at(Token) ->
ct:pal(" TEST: Filter status=reviewed + sort by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?status=reviewed&sort=created_at&order=asc">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"reviewed">>, maps:get(<<"status">>, R)) || R <- Reports],
Dates = [maps:get(<<"created_at">>, R) || R <- Reports],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: ~p reports sorted", [length(Reports)]).
-spec test_filter_target_sort_status(binary()) -> ok.
test_filter_target_sort_status(Token) ->
ct:pal(" TEST: Filter target_type=event + sort by status desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?target_type=event&sort=status&order=desc">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"event">>, maps:get(<<"target_type">>, R)) || R <- Reports],
Statuses = [maps:get(<<"status">>, R) || R <- Reports],
?assertEqual(lists:reverse(lists:sort(Statuses)), Statuses),
ct:pal(" OK: ~p reports sorted", [length(Reports)]).
%% @doc GET /v1/admin/reports?limit=...&offset=... проверяет пагинацию.
-spec test_report_pagination(binary()) -> ok.
test_report_pagination(Token) ->
ct:pal(" TEST: Report pagination"),
ct:pal(" TEST: Report pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/reports?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/reports?limit=1&offset=1">>, Token),
@@ -131,4 +200,40 @@ test_report_pagination(Token) ->
?assertNotEqual(Id1, Id2);
_ -> ok
end,
ct:pal(" OK").
ct:pal(" OK").
-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/reports?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]).
-spec test_filter_status_with_pagination(binary()) -> ok.
test_filter_status_with_pagination(Token) ->
ct:pal(" TEST: Filter status=dismissed + pagination (limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?status=dismissed&limit=1&offset=0">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Reports) =< 1),
[?assertEqual(<<"dismissed">>, maps:get(<<"status">>, R)) || R <- Reports],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
-spec test_filter_sort_pagination_combined(binary()) -> ok.
test_filter_sort_pagination_combined(Token) ->
ct:pal(" TEST: Filter target_type=event + sort by created_at desc + pagination (limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reports?target_type=event&sort=created_at&order=desc&limit=1&offset=0">>, Token, <<"">>),
Reports = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Reports) =< 1),
[?assertEqual(<<"event">>, maps:get(<<"target_type">>, R)) || R <- Reports],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
+156 -38
View File
@@ -1,26 +1,33 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты административного API для управления отзывами.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/reviews
%%% PATCH /v1/admin/reviews
%%% GET /v1/admin/reviews/:id
%%% PUT /v1/admin/reviews/:id
%%% GET /v1/admin/reviews
%%% PATCH /v1/admin/reviews
%%% GET /v1/admin/reviews/:id
%%% PUT /v1/admin/reviews/:id
%%%
%%% Проверяет:
%%% - получение списка отзывов
%%% - фильтрацию по цели, статусу, пользователю
%%% - комбинации фильтров
%%% - сортировку по created_at, rating
%%% - комбинации фильтрации с сортировкой
%%% - пагинацию
%%% - комбинации фильтрации, сортировки и пагинации
%%% @end
%%%-------------------------------------------------------------------
-module(admin_reviews_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% EUnit test generator
%%% Главная тестовая функция
%%%===================================================================
test() ->
ct:pal("=== Admin Reviews Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
%% Создаём тестовые данные: календарь, событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReviewsTestCal">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
@@ -28,46 +35,54 @@ test() ->
start_time => api_test_runner:future_date(),
duration => 60
}),
%% Создаём двух пользователей для отзывов
User1Email = api_test_runner:unique_email(<<"rev1">>),
User1Token = api_test_runner:register_and_login(User1Email, <<"pass1">>),
User2Email = api_test_runner:unique_email(<<"rev2">>),
User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>),
%% Записываем пользователей на событие и подтверждаем бронирования
#{<<"id">> := Booking1Id} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, User1Token, #{}),
#{<<"id">> := Booking2Id} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, User2Token, #{}),
ct:pal(" Confirming bookings as calendar owner..."),
ct:pal(" Confirming bookings as calendar owner..."),
api_test_runner:client_put(<<"/v1/bookings/", Booking1Id/binary>>, UserToken,
#{action => <<"confirm">>}),
api_test_runner:client_put(<<"/v1/bookings/", Booking2Id/binary>>, UserToken,
#{action => <<"confirm">>}),
%% Оставляем отзывы от имени участников
#{<<"id">> := Review1Id} = api_test_runner:client_post(<<"/v1/reviews">>, User1Token, #{
target_type => <<"event">>,
target_id => EventId,
rating => 5,
comment => <<"Great!">>
}),
#{<<"id">> := Review2Id} = api_test_runner:client_post(<<"/v1/reviews">>, User2Token, #{
target_type => <<"event">>,
target_id => EventId,
rating => 4,
comment => <<"Good">>
}),
#{<<"id">> := Review1Id} = api_test_runner:client_post(<<"/v1/reviews">>, User1Token,
#{ target_type => <<"event">>, target_id => EventId, rating => 5,
comment => <<"Great!">> }),
#{<<"id">> := Review2Id} = api_test_runner:client_post(<<"/v1/reviews">>, User2Token,
#{ target_type => <<"event">>, target_id => EventId, rating => 4,
comment => <<"Good">> }),
%% Получаем ID первого пользователя для теста фильтрации по user_id
#{<<"id">> := User1Id} = api_test_runner:client_get(<<"/v1/user/me">>, User1Token),
%% Запускаем тесты
test_list_reviews(Token),
test_filter_reviews(Token, EventId),
test_filter_by_user(Token, User1Id),
% Сначала меняем статусы, чтобы потом проверить фильтрацию по статусам
test_bulk_update_status(Token, Review1Id, Review2Id),
test_filter_by_status(Token),
test_filter_combined_status_target(Token, EventId),
test_get_review(Token, Review1Id),
test_update_review(Token, Review1Id),
test_sort_by_created_at_desc(Token),
test_sort_by_created_at_asc(Token),
test_sort_by_rating_asc(Token),
test_filter_status_sort_created_at(Token),
test_filter_sort_pagination_combined(Token, EventId),
test_review_pagination(Token),
test_pagination_headers(Token),
ct:pal("=== All admin reviews tests passed ==="),
ok.
@@ -78,54 +93,147 @@ test() ->
%% @doc GET /v1/admin/reviews список отзывов.
test_list_reviews(Token) ->
ct:pal(" TEST: List all reviews"),
ct:pal(" TEST: List all reviews"),
Reviews = api_test_runner:admin_get(<<"/v1/admin/reviews">>, Token),
?assert(is_list(Reviews)),
?assert(length(Reviews) >= 2),
ct:pal(" OK: ~p reviews", [length(Reviews)]).
ct:pal(" OK: ~p reviews", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?target_type=event&target_id=... фильтрация по цели.
test_filter_reviews(Token, EventId) ->
ct:pal(" TEST: Filter reviews by target"),
ct:pal(" TEST: Filter reviews by target"),
Path = <<"/v1/admin/reviews?target_type=event&target_id=", EventId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
?assert(length(Reviews) >= 2),
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews for event", [length(Reviews)]).
ct:pal(" OK: ~p reviews for event", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?user_id=... фильтрация по пользователю.
test_filter_by_user(Token, UserId) ->
ct:pal(" TEST: Filter reviews by user_id"),
Path = <<"/v1/admin/reviews?user_id=", UserId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
[?assertEqual(UserId, maps:get(<<"user_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews by user", [length(Reviews)]).
%% @doc PATCH /v1/admin/reviews – массовое обновление статусов.
test_bulk_update_status(Token, Review1Id, Review2Id) ->
ct:pal(" TEST: Bulk update review statuses"),
ct:pal(" TEST: Bulk update review statuses"),
Body = [
#{<<"id">> => Review1Id, <<"status">> => <<"visible">>},
#{<<"id">> => Review2Id, <<"status">> => <<"hidden">>}
],
#{<<"updated_count">> := Count} = api_test_runner:admin_patch(<<"/v1/admin/reviews">>, Token, Body),
#{<<"updated_count">> := Count} = api_test_runner:admin_patch(
<<"/v1/admin/reviews">>, Token, Body),
?assertEqual(2, Count),
ct:pal(" OK: updated ~p reviews", [Count]).
ct:pal(" OK: updated ~p reviews", [Count]).
%% @doc GET /v1/admin/reviews?status=visible фильтрация по статусу.
test_filter_by_status(Token) ->
ct:pal(" TEST: Filter reviews by status=visible"),
Reviews = api_test_runner:admin_get(<<"/v1/admin/reviews?status=visible">>, Token),
?assert(is_list(Reviews)),
[?assertEqual(<<"visible">>, maps:get(<<"status">>, R)) || R <- Reviews],
ct:pal(" OK: ~p visible reviews", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?status=hidden&target_type=event комбинация фильтров.
test_filter_combined_status_target(Token, EventId) ->
ct:pal(" TEST: Combined filter (status=hidden & target_type=event)"),
Path = <<"/v1/admin/reviews?status=hidden&target_type=event&target_id=", EventId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
[?assertEqual(<<"hidden">>, maps:get(<<"status">>, R)) || R <- Reviews],
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews match both criteria", [length(Reviews)]).
%% @doc GET /v1/admin/reviews/:id получение отзыва по ID.
test_get_review(Token, ReviewId) ->
ct:pal(" TEST: Get review by ID"),
ct:pal(" TEST: Get review by ID"),
Path = <<"/v1/admin/reviews/", ReviewId/binary>>,
Review = api_test_runner:admin_get(Path, Token),
?assertEqual(ReviewId, maps:get(<<"id">>, Review)),
?assertEqual(<<"visible">>, maps:get(<<"status">>, Review)),
ct:pal(" OK: ~s", [maps:get(<<"comment">>, Review)]).
ct:pal(" OK: ~s", [maps:get(<<"comment">>, Review)]).
%% @doc PUT /v1/admin/reviews/:id обновление отзыва.
test_update_review(Token, ReviewId) ->
ct:pal(" TEST: Update review"),
ct:pal(" TEST: Update review"),
Path = <<"/v1/admin/reviews/", ReviewId/binary>>,
Updates = #{<<"comment">> => <<"Updated by admin">>},
Updated = api_test_runner:admin_put(Path, Token, Updates),
?assertEqual(<<"Updated by admin">>, maps:get(<<"comment">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% ── Сортировка ──
%% @doc Сортировка по created_at по убыванию.
test_sort_by_created_at_desc(Token) ->
ct:pal(" TEST: Sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=created_at&order=desc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by created_at desc").
%% @doc Сортировка по created_at по возрастанию.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=created_at&order=asc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
%% @doc Сортировка по rating по возрастанию.
test_sort_by_rating_asc(Token) ->
ct:pal(" TEST: Sort by rating asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=rating&order=asc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Ratings = [maps:get(<<"rating">>, R) || R <- Reviews],
?assertEqual(lists:sort(Ratings), Ratings),
ct:pal(" OK: sorted by rating asc").
%% ── Фильтр + сортировка ──
%% @doc Фильтр status=visible + сортировка по created_at desc.
test_filter_status_sort_created_at(Token) ->
ct:pal(" TEST: Filter status=visible + sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?status=visible&sort=created_at&order=desc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"visible">>, maps:get(<<"status">>, R)) || R <- Reviews],
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p reviews sorted", [length(Reviews)]).
%% ── Фильтр + сортировка + пагинация ──
%% @doc Фильтр target_type=event + сортировка по rating desc + пагинация limit=1.
test_filter_sort_pagination_combined(Token, EventId) ->
ct:pal(" TEST: Filter + sort + pagination (target_type=event, sort=rating desc, limit=1)"),
Path = <<"/v1/admin/reviews?target_type=event&target_id=", EventId/binary,
"&sort=rating&order=desc&limit=1&offset=0">>,
{ok, 200, Headers, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Reviews) =< 1),
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%% ── Пагинация ──
%% @doc GET /v1/admin/reviews?limit=...&offset=... пагинация.
test_review_pagination(Token) ->
ct:pal(" TEST: Review pagination"),
ct:pal(" TEST: Review pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=0">>, Token),
?assertEqual(1, length(Page1)),
Page2 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=1">>, Token),
@@ -133,4 +241,14 @@ test_review_pagination(Token) ->
Id1 = maps:get(<<"id">>, hd(Page1)),
Id2 = maps:get(<<"id">>, hd(Page2)),
?assertNotEqual(Id1, Id2),
ct:pal(" OK").
ct:pal(" OK").
%% @doc Проверка заголовков пагинации (x-total-count).
test_pagination_headers(Token) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?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]).
+188 -29
View File
@@ -2,29 +2,30 @@
%%% @doc Тесты административного API для управления подписками.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/subscriptions
%%% GET /v1/admin/subscriptions/:id
%%% PUT /v1/admin/subscriptions/:id
%%% DELETE /v1/admin/subscriptions/:id
%%% GET /v1/admin/subscriptions
%%% GET /v1/admin/subscriptions/:id
%%% PUT /v1/admin/subscriptions/:id
%%% DELETE /v1/admin/subscriptions/:id
%%%
%%% Проверяет:
%%% - получение списка подписок
%%% - получение подписки по ID
%%% - обновление подписки (изменение плана, статуса)
%%% - удаление подписки
%%% - фильтрацию по статусу и плану
%%% - пагинацию
%%% - получение списка подписок
%%% - получение подписки по ID
%%% - обновление подписки (изменение плана, статуса)
%%% - удаление подписки
%%% - **фильтрацию** по статусу и плану
%%% - **комбинации фильтров** (status + plan)
%%% - **пагинацию** (limit/offset, xtotalcount)
%%% - **сортировку** по created_at и expires_at
%%% - **комбинации сортировки с фильтрацией и пагинацией**
%%% @end
%%%-------------------------------------------------------------------
-module(admin_subscriptions_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Subscriptions Tests ==="),
@@ -34,20 +35,43 @@ test() ->
% Создаём две подписки для разных проверок
#{<<"id">> := Sub1Id} = api_test_runner:client_post(<<"/v1/subscription">>, UserToken,
#{<<"action">> => <<"start_trial">>}),
% Для второй подписки создаём нового пользователя
User2Email = api_test_runner:unique_email(<<"sub2">>),
User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>),
#{<<"id">> := Sub2Id} = api_test_runner:client_post(<<"/v1/subscription">>, User2Token,
#{<<"action">> => <<"start_trial">>}),
% Теперь две подписки в системе
% Основные проверки
test_list_subscriptions(Token, Sub1Id),
test_get_subscription(Token, Sub1Id),
test_update_subscription(Token, Sub1Id),
% После обновления Sub1Id имеет план biannual и статус active
% Фильтрация
test_filter_subscriptions(Token),
% Комбинации фильтров
test_filter_combined_status_plan(Token),
test_filter_combined_plan_status(Token),
% Сортировка
test_sort_by_created_at_desc(Token),
test_sort_by_created_at_asc(Token),
test_sort_by_expires_at_asc(Token),
% Фильтр + сортировка
test_filter_status_sort_created_at(Token),
test_filter_plan_sort_expires_at(Token),
% Пагинация
test_subscription_pagination(Token),
test_pagination_headers(Token),
% Фильтр + пагинация
test_filter_status_with_pagination(Token),
% Фильтр + сортировка + пагинация
test_filter_sort_pagination_combined(Token),
% Удаляем вторую подписку, чтобы проверить delete
test_delete_subscription(Token, Sub2Id),
@@ -61,27 +85,27 @@ test() ->
%% @doc GET /v1/admin/subscriptions список подписок.
-spec test_list_subscriptions(binary(), binary()) -> ok.
test_list_subscriptions(Token, SubId) ->
ct:pal(" TEST: List all subscriptions"),
ct:pal(" TEST: List all subscriptions"),
Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions">>, Token),
?assert(is_list(Subs)),
?assert(length(Subs) >= 2),
?assert(lists:any(fun(S) -> maps:get(<<"id">>, S) =:= SubId end, Subs)),
ct:pal(" OK: ~p subscriptions", [length(Subs)]).
ct:pal(" OK: ~p subscriptions", [length(Subs)]).
%% @doc GET /v1/admin/subscriptions/:id получение подписки по ID.
-spec test_get_subscription(binary(), binary()) -> ok.
test_get_subscription(Token, SubId) ->
ct:pal(" TEST: Get subscription by ID"),
ct:pal(" TEST: Get subscription by ID"),
Path = <<"/v1/admin/subscriptions/", SubId/binary>>,
Sub = api_test_runner:admin_get(Path, Token),
?assertEqual(SubId, maps:get(<<"id">>, Sub)),
?assert(maps:is_key(<<"plan">>, Sub)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/subscriptions/:id обновление подписки (план + статус).
-spec test_update_subscription(binary(), binary()) -> ok.
test_update_subscription(Token, SubId) ->
ct:pal(" TEST: Update subscription (change plan)"),
ct:pal(" TEST: Update subscription (change plan)"),
Path = <<"/v1/admin/subscriptions/", SubId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{
<<"plan">> => <<"biannual">>,
@@ -89,38 +113,129 @@ test_update_subscription(Token, SubId) ->
}),
?assertEqual(<<"biannual">>, maps:get(<<"plan">>, Updated)),
?assertEqual(<<"active">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc DELETE /v1/admin/subscriptions/:id удаление подписки.
-spec test_delete_subscription(binary(), binary()) -> ok.
test_delete_subscription(Token, SubId) ->
ct:pal(" TEST: Delete subscription"),
ct:pal(" TEST: Delete subscription"),
Path = <<"/v1/admin/subscriptions/", SubId/binary>>,
Deleted = api_test_runner:admin_delete(Path, Token),
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)),
ct:pal(" OK").
ct:pal(" OK").
%% ── Фильтрация ──
%% @doc GET /v1/admin/subscriptions?status=...&plan=... фильтрация.
-spec test_filter_subscriptions(binary()) -> ok.
test_filter_subscriptions(Token) ->
ct:pal(" TEST: Filter subscriptions by status=active"),
ct:pal(" TEST: Filter subscriptions by status=active"),
Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions?status=active">>, Token),
?assert(is_list(Subs)),
?assert(length(Subs) >= 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
ct:pal(" OK: ~p active subscriptions", [length(Subs)]),
ct:pal(" OK: ~p active subscriptions", [length(Subs)]),
ct:pal(" TEST: Filter subscriptions by plan=biannual"),
ct:pal(" TEST: Filter subscriptions by plan=biannual"),
Subs2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?plan=biannual">>, Token),
?assert(is_list(Subs2)),
?assert(length(Subs2) >= 1),
[?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs2],
ct:pal(" OK: ~p biannual subscriptions", [length(Subs2)]).
ct:pal(" OK: ~p biannual subscriptions", [length(Subs2)]).
%% ── Комбинации фильтров ──
%% @doc Комбинация фильтров: status=active&plan=biannual
-spec test_filter_combined_status_plan(binary()) -> ok.
test_filter_combined_status_plan(Token) ->
ct:pal(" TEST: Combined filter (status=active & plan=biannual)"),
Subs = api_test_runner:admin_get(
<<"/v1/admin/subscriptions?status=active&plan=biannual">>, Token),
?assert(is_list(Subs)),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
[?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs],
ct:pal(" OK: ~p subscriptions match both criteria", [length(Subs)]).
%% @doc Комбинация фильтров: plan=biannual&status=active (обратный порядок)
-spec test_filter_combined_plan_status(binary()) -> ok.
test_filter_combined_plan_status(Token) ->
ct:pal(" TEST: Combined filter (plan=biannual & status=active)"),
Subs = api_test_runner:admin_get(
<<"/v1/admin/subscriptions?plan=biannual&status=active">>, Token),
?assert(is_list(Subs)),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
[?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs],
ct:pal(" OK: ~p subscriptions match both criteria", [length(Subs)]).
%% ── Сортировка ──
%% @doc Сортировка по created_at по убыванию.
-spec test_sort_by_created_at_desc(binary()) -> ok.
test_sort_by_created_at_desc(Token) ->
ct:pal(" TEST: Sort subscriptions by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/subscriptions?sort=created_at&order=desc">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, S) || S <- Subs],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by created_at desc").
%% @doc Сортировка по created_at по возрастанию.
-spec test_sort_by_created_at_asc(binary()) -> ok.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort subscriptions by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/subscriptions?sort=created_at&order=asc">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, S) || S <- Subs],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
%% @doc Сортировка по expires_at по возрастанию.
-spec test_sort_by_expires_at_asc(binary()) -> ok.
test_sort_by_expires_at_asc(Token) ->
ct:pal(" TEST: Sort subscriptions by expires_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/subscriptions?sort=expires_at&order=asc">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"expires_at">>, S) || S <- Subs],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by expires_at asc").
%% ── Фильтр + сортировка ──
%% @doc Фильтр status=active + сортировка по created_at desc.
-spec test_filter_status_sort_created_at(binary()) -> ok.
test_filter_status_sort_created_at(Token) ->
ct:pal(" TEST: Filter status=active + sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/subscriptions?status=active&sort=created_at&order=desc">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
Dates = [maps:get(<<"created_at">>, S) || S <- Subs],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p subscriptions sorted", [length(Subs)]).
%% @doc Фильтр plan=biannual + сортировка по expires_at asc.
-spec test_filter_plan_sort_expires_at(binary()) -> ok.
test_filter_plan_sort_expires_at(Token) ->
ct:pal(" TEST: Filter plan=biannual + sort by expires_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/subscriptions?plan=biannual&sort=expires_at&order=asc">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs],
Dates = [maps:get(<<"expires_at">>, S) || S <- Subs],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: ~p subscriptions sorted", [length(Subs)]).
%% ── Пагинация ──
%% @doc GET /v1/admin/subscriptions?limit=...&offset=... пагинация.
-spec test_subscription_pagination(binary()) -> ok.
test_subscription_pagination(Token) ->
ct:pal(" TEST: Subscription pagination"),
ct:pal(" TEST: Subscription pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=1">>, Token),
@@ -128,4 +243,48 @@ test_subscription_pagination(Token) ->
Id1 = maps:get(<<"id">>, hd(Page1)),
Id2 = maps:get(<<"id">>, hd(Page2)),
?assertNotEqual(Id1, Id2),
ct:pal(" OK").
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/subscriptions?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/subscriptions?status=active&limit=1&offset=0">>, Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Subs) =< 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
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/subscriptions?status=active&sort=created_at&order=desc&limit=1&offset=0">>,
Token, <<"">>),
Subs = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Subs) =< 1),
[?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
+221 -51
View File
@@ -2,52 +2,85 @@
%%% @doc Тесты административного API для управления тикетами.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/tickets
%%% GET /v1/admin/tickets/:id
%%% PUT /v1/admin/tickets/:id
%%% DELETE /v1/admin/tickets/:id
%%% GET /v1/admin/tickets
%%% GET /v1/admin/tickets/:id
%%% PUT /v1/admin/tickets/:id
%%% DELETE /v1/admin/tickets/:id
%%%
%%% Проверяет:
%%% - получение списка тикетов
%%% - получение тикета по ID
%%% - разрешение (resolve) и закрытие (close) тикета
%%% - назначение исполнителя
%%% - удаление тикета
%%% - фильтрацию по статусу
%%% - пагинацию
%%% - получение списка тикетов
%%% - получение тикета по ID
%%% - разрешение (resolve) и закрытие (close) тикета
%%% - назначение исполнителя
%%% - удаление тикета
%%% - фильтрацию по статусу, исполнителю, поиску
%%% - комбинации фильтров
%%% - пагинацию
%%% - сортировку по first_seen, last_seen, status
%%% - комбинации фильтрации, сортировки и пагинации
%%% @end
%%%-------------------------------------------------------------------
-module(admin_tickets_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
-spec test() -> ok.
test() ->
ct:pal("=== Admin Tickets Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
% Создаём два тикета для разных проверок
Ticket1 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
#{<<"error_message">> => <<"Test bug">>, <<"stacktrace">> => <<"trace">>}),
#{<<"id">> := Ticket1Id} = Ticket1,
Ticket2 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
#{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}),
#{<<"id">> := Ticket2Id} = Ticket2,
% Получаем ID текущего администратора для теста фильтрации по исполнителю
Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token),
AdminId = maps:get(<<"id">>, Me),
% Основные проверки
test_list_tickets(Token),
test_get_ticket(Token, Ticket1Id),
test_resolve_ticket(Token, Ticket1Id),
test_close_ticket(Token, Ticket1Id),
test_assign_ticket(Token, Ticket1Id),
test_filter_tickets(Token),
test_assign_ticket(Token, Ticket1Id, AdminId),
% Фильтрация
test_filter_tickets_by_status(Token, Ticket2Id),
test_filter_tickets_by_assigned(Token, AdminId),
test_filter_tickets_by_search(Token),
% Комбинации фильтров
test_filter_combined_status_assigned(Token, AdminId),
test_filter_combined_status_search(Token, Ticket2Id),
% Сортировка
test_sort_by_first_seen_desc(Token),
test_sort_by_first_seen_asc(Token),
test_sort_by_last_seen_desc(Token),
test_sort_by_status_asc(Token),
% Фильтр + сортировка
test_filter_status_sort_first_seen(Token, Ticket2Id),
test_filter_assigned_sort_last_seen(Token, AdminId),
% Пагинация
test_ticket_pagination(Token, Ticket2Id),
test_pagination_headers(Token),
% Фильтр + пагинация
test_filter_status_with_pagination(Token, Ticket2Id),
% Фильтр + сортировка + пагинация
test_filter_sort_pagination_combined(Token, Ticket2Id),
% Удаление
test_delete_ticket(Token, Ticket1Id),
test_delete_ticket(Token, Ticket2Id),
@@ -59,31 +92,25 @@ test() ->
%%%===================================================================
%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов.
%% Убеждается, что список не пуст.
-spec test_list_tickets(binary()) -> ok.
test_list_tickets(Token) ->
ct:pal(" TEST: List all tickets"),
ct:pal(" TEST: List all tickets"),
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token),
?assert(is_list(Tickets)),
?assert(length(Tickets) >= 1),
ct:pal(" OK: ~p tickets", [length(Tickets)]).
ct:pal(" OK: ~p tickets", [length(Tickets)]).
%% @doc GET /v1/admin/tickets/:id проверяет получение тикета по ID.
%% Убеждается, что статус нового тикета – open.
-spec test_get_ticket(binary(), binary()) -> ok.
test_get_ticket(Token, TicketId) ->
ct:pal(" TEST: Get ticket by ID"),
ct:pal(" TEST: Get ticket by ID"),
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
Ticket = api_test_runner:admin_get(Path, Token),
?assertEqual(TicketId, maps:get(<<"id">>, Ticket)),
?assertEqual(<<"open">>, maps:get(<<"status">>, Ticket)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/tickets/:id разрешение тикета (resolve).
%% Ожидается статус resolved после успешного выполнения.
-spec test_resolve_ticket(binary(), binary()) -> ok.
test_resolve_ticket(Token, TicketId) ->
ct:pal(" TEST: Resolve ticket"),
ct:pal(" TEST: Resolve ticket"),
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{
<<"status">> => <<"resolved">>,
@@ -91,53 +118,156 @@ test_resolve_ticket(Token, TicketId) ->
}),
?assertEqual(<<"resolved">>, maps:get(<<"status">>, Updated)),
?assertEqual(<<"Fixed">>, maps:get(<<"resolution_note">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/tickets/:id закрытие тикета (close).
-spec test_close_ticket(binary(), binary()) -> ok.
test_close_ticket(Token, TicketId) ->
ct:pal(" TEST: Close ticket"),
ct:pal(" TEST: Close ticket"),
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"closed">>}),
?assertEqual(<<"closed">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc PUT /v1/admin/tickets/:id назначение исполнителя.
-spec test_assign_ticket(binary(), binary()) -> ok.
test_assign_ticket(Token, TicketId) ->
ct:pal(" TEST: Assign ticket"),
Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token),
AdminId = maps:get(<<"id">>, Me),
test_assign_ticket(Token, TicketId, AdminId) ->
ct:pal(" TEST: Assign ticket"),
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{<<"assigned_to">> => AdminId}),
?assertEqual(AdminId, maps:get(<<"assigned_to">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc DELETE /v1/admin/tickets/:id удаление тикета.
-spec test_delete_ticket(binary(), binary()) -> ok.
test_delete_ticket(Token, TicketId) ->
ct:pal(" TEST: Delete ticket"),
ct:pal(" TEST: Delete ticket"),
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
Deleted = api_test_runner:admin_delete(Path, Token),
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)),
ct:pal(" OK").
ct:pal(" OK").
%% @doc GET /v1/admin/tickets?status=... проверяет фильтрацию по статусу open.
%% Использует второй тикет, который всё ещё open.
-spec test_filter_tickets(binary()) -> ok.
test_filter_tickets(Token) ->
ct:pal(" TEST: Filter tickets by status=open"),
%% ── Фильтрация ──
%% @doc Фильтрация по статусу open.
test_filter_tickets_by_status(Token, _TicketId) ->
ct:pal(" TEST: Filter tickets by status=open"),
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?status=open">>, Token),
?assert(is_list(Tickets)),
?assert(length(Tickets) >= 1),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
ct:pal(" OK: ~p open tickets", [length(Tickets)]).
ct:pal(" OK: ~p open tickets", [length(Tickets)]).
%% @doc Фильтрация по исполнителю.
test_filter_tickets_by_assigned(Token, AdminId) ->
ct:pal(" TEST: Filter tickets by assigned_to"),
Path = <<"/v1/admin/tickets?assigned_to=", AdminId/binary>>,
Tickets = api_test_runner:admin_get(Path, Token),
?assert(is_list(Tickets)),
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
ct:pal(" OK: ~p tickets assigned to admin", [length(Tickets)]).
%% @doc Поиск по тексту ошибки.
test_filter_tickets_by_search(Token) ->
ct:pal(" TEST: Search tickets by q=bug"),
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?q=bug">>, Token),
?assert(is_list(Tickets)),
?assert(length(Tickets) >= 1),
[?assert(string:str(binary_to_list(maps:get(<<"error_message">>, T)), "bug") > 0) || T <- Tickets],
ct:pal(" OK: ~p tickets match search", [length(Tickets)]).
%% ── Комбинации фильтров ──
%% @doc Комбинация фильтров: status=open + assigned_to.
test_filter_combined_status_assigned(Token, AdminId) ->
ct:pal(" TEST: Combined filter (status=open & assigned_to)"),
Path = <<"/v1/admin/tickets?status=open&assigned_to=", AdminId/binary>>,
Tickets = api_test_runner:admin_get(Path, Token),
?assert(is_list(Tickets)),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
ct:pal(" OK: ~p tickets match both criteria", [length(Tickets)]).
%% @doc Комбинация фильтров: status=open + поиск.
test_filter_combined_status_search(Token, _TicketId) ->
ct:pal(" TEST: Combined filter (status=open & q=Another)"),
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?status=open&q=Another">>, Token),
?assert(is_list(Tickets)),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
[?assert(string:str(binary_to_list(maps:get(<<"error_message">>, T)), "Another") > 0) || T <- Tickets],
ct:pal(" OK: ~p tickets match both criteria", [length(Tickets)]).
%% ── Сортировка ──
%% @doc Сортировка по first_seen по убыванию.
test_sort_by_first_seen_desc(Token) ->
ct:pal(" TEST: Sort tickets by first_seen desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?sort=first_seen&order=desc">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by first_seen desc").
%% @doc Сортировка по first_seen по возрастанию.
test_sort_by_first_seen_asc(Token) ->
ct:pal(" TEST: Sort tickets by first_seen asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?sort=first_seen&order=asc">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by first_seen asc").
%% @doc Сортировка по last_seen по убыванию.
test_sort_by_last_seen_desc(Token) ->
ct:pal(" TEST: Sort tickets by last_seen desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?sort=last_seen&order=desc">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"last_seen">>, T) || T <- Tickets],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by last_seen desc").
%% @doc Сортировка по status по возрастанию.
test_sort_by_status_asc(Token) ->
ct:pal(" TEST: Sort tickets by status asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?sort=status&order=asc">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
Statuses = [maps:get(<<"status">>, T) || T <- Tickets],
?assertEqual(lists:sort(Statuses), Statuses),
ct:pal(" OK: sorted by status asc").
%% ── Фильтр + сортировка ──
%% @doc Фильтр status=open + сортировка по first_seen desc.
test_filter_status_sort_first_seen(Token, _TicketId) ->
ct:pal(" TEST: Filter status=open + sort by first_seen desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?status=open&sort=first_seen&order=desc">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p tickets sorted", [length(Tickets)]).
%% @doc Фильтр assigned_to + сортировка по last_seen asc.
test_filter_assigned_sort_last_seen(Token, AdminId) ->
ct:pal(" TEST: Filter assigned_to + sort by last_seen asc"),
Path = <<"/v1/admin/tickets?assigned_to=", AdminId/binary, "&sort=last_seen&order=asc">>,
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
Dates = [maps:get(<<"last_seen">>, T) || T <- Tickets],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: ~p tickets sorted", [length(Tickets)]).
%% ── Пагинация ──
%% @doc GET /v1/admin/tickets?limit=...&offset=... проверяет пагинацию.
%% Использует второй тикет как гарантированно существующий.
-spec test_ticket_pagination(binary(), binary()) -> ok.
test_ticket_pagination(Token, _TicketId) ->
ct:pal(" TEST: Ticket pagination"),
ct:pal(" TEST: Ticket pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=0">>, Token),
?assert(length(Page1) >= 1),
Page2 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=1">>, Token),
@@ -149,4 +279,44 @@ test_ticket_pagination(Token, _TicketId) ->
?assertNotEqual(Id1, Id2);
_ -> ok
end,
ct:pal(" OK").
ct:pal(" OK").
%% @doc Проверка заголовков пагинации (x-total-count).
test_pagination_headers(Token) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?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=open + пагинация limit=1.
test_filter_status_with_pagination(Token, _TicketId) ->
ct:pal(" TEST: Filter status=open + pagination (limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?status=open&limit=1&offset=0">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Tickets) =< 1),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%% ── Фильтр + сортировка + пагинация ──
%% @doc Фильтр status=open + сортировка по first_seen desc + пагинация limit=1.
test_filter_sort_pagination_combined(Token, _TicketId) ->
ct:pal(" TEST: Filter + sort + pagination (status=open, sort=first_seen desc, limit=1)"),
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/tickets?status=open&sort=first_seen&order=desc&limit=1&offset=0">>, Token, <<"">>),
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Tickets) =< 1),
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
+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]).