Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Финал
This commit is contained in:
@@ -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]).
|
||||
@@ -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]).
|
||||
@@ -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]).
|
||||
@@ -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]).
|
||||
@@ -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]).
|
||||
@@ -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").
|
||||
@@ -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]).
|
||||
@@ -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]).
|
||||
@@ -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, x‑total‑count)
|
||||
%%% - **сортировку** по 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]).
|
||||
@@ -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]).
|
||||
@@ -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, x‑total‑count)
|
||||
%%% - **сортировку** по email, роли и дате создания
|
||||
%%% - **комбинации сортировки с фильтрацией и пагинацией**
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(admin_users_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== Admin Users Tests ==="),
|
||||
Token = api_test_runner:get_admin_token(),
|
||||
% Создаём несколько тестовых пользователей с разными ролями
|
||||
User1Email = api_test_runner:unique_email(<<"usertest1">>),
|
||||
User1Token = api_test_runner:register_and_login(User1Email, <<"testpass">>),
|
||||
Me1 = api_test_runner:client_get(<<"/v1/user/me">>, User1Token),
|
||||
User1Id = maps:get(<<"id">>, Me1),
|
||||
|
||||
%% Создаём тестового пользователя
|
||||
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]).
|
||||
Reference in New Issue
Block a user