diff --git a/src/handlers/admin/admin_handler_admins.erl b/src/handlers/admin/admin_handler_admins.erl index 30bd0f5..e473057 100644 --- a/src/handlers/admin/admin_handler_admins.erl +++ b/src/handlers/admin/admin_handler_admins.erl @@ -130,6 +130,7 @@ create_admin(Req) -> _ -> case logic_admin:create_admin(Email, Password, Role) of {ok, Admin} -> + % Аудит создания администратора admin_utils:log_admin_action(SuperAdminId, <<"create_admin">>, <<"admin">>, Email, Req2), handler_utils:send_json(Req2, 201, handler_utils:admin_to_json(Admin)); {error, email_exists} -> @@ -158,7 +159,14 @@ create_admin(Req) -> parse_admin_filters(Req) -> Qs = cowboy_req:parse_qs(Req), #{ - role => proplists:get_value(<<"role">>, Qs), - status => proplists:get_value(<<"status">>, Qs), + role => to_atom(proplists:get_value(<<"role">>, Qs)), + status => to_atom(proplists:get_value(<<"status">>, Qs)), q => proplists:get_value(<<"q">>, Qs) - }. \ No newline at end of file + }. + +%% @private Безопасное преобразование бинарного значения в атом. +to_atom(undefined) -> undefined; +to_atom(Bin) when is_binary(Bin) -> + try binary_to_existing_atom(Bin, utf8) + catch error:badarg -> Bin + end. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_audit.erl b/src/handlers/admin/admin_handler_audit.erl index d1f65f1..e8976ea 100644 --- a/src/handlers/admin/admin_handler_audit.erl +++ b/src/handlers/admin/admin_handler_audit.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @doc Административный обработчик журнала аудита. -%%% GET – список записей аудита с пагинацией и фильтрацией. +%%% GET – список записей аудита с пагинацией, фильтрацией и сортировкой. %%% Доступно только суперадмину. %%% @end %%%------------------------------------------------------------------- @@ -33,6 +33,8 @@ trails() -> #{name => <<"action">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by action">>}, #{name => <<"date_from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start timestamp (ISO8601)">>}, #{name => <<"date_to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End timestamp (ISO8601)">>}, + #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"timestamp">>, <<"action">>, <<"admin_id">>, <<"entity_type">>]}, description => <<"Sort field">>}, + #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], @@ -77,16 +79,20 @@ audit_schema() -> -spec list_audit(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. list_audit(Req) -> case handler_utils:is_superadmin(Req) of - {ok, SuperAdminId, Req1} -> + {ok, _SuperAdminId, Req1} -> Filters = parse_audit_filters(Req1), - Pagination = handler_utils:parse_pagination_params(Req1), + Pagination0 = handler_utils:parse_pagination_params(Req1), + Pagination = Pagination0#{ + sort => maps:get(sort, Pagination0, <<"timestamp">>), + order => maps:get(order, Pagination0, <<"desc">>) + }, AllRecords = core_admin_audit:list(Filters), - Total = length(AllRecords), - Page = lists:sublist(AllRecords, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), + Sorted = sort_audit_records(AllRecords, Pagination), + Total = length(Sorted), + Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), Json = [handler_utils:audit_to_json(R) || R <- Page], ExtraHeaders = handler_utils:pagination_headers(Pagination, Total), - % Аудит доступа к журналу - admin_utils:log_admin_action(SuperAdminId, <<"view_audit">>, <<"audit">>, <<"list">>, Req1), + % Аудит чтения списка аудита НЕ пишем, чтобы не замусоривать журнал handler_utils:send_json(Req1, 200, Json, ExtraHeaders); {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) @@ -105,4 +111,22 @@ parse_audit_filters(Req) -> {action, proplists:get_value(<<"action">>, Qs)}, {date_from, handler_utils:parse_datetime_qs(proplists:get_value(<<"date_from">>, Qs))}, {date_to, handler_utils:parse_datetime_qs(proplists:get_value(<<"date_to">>, Qs))} - ]. \ No newline at end of file + ]. + +%% @private Сортирует записи аудита по указанному полю. +sort_audit_records(Records, #{sort := Sort, order := Order}) -> + Field = binary_to_existing_atom(Sort, utf8), + lists:sort(fun(A, B) -> + VA = audit_field(A, Field), + VB = audit_field(B, Field), + case Order of + <<"desc">> -> VA > VB; + _ -> VA < VB + end + end, Records). + +audit_field(#admin_audit{timestamp = V}, timestamp) -> V; +audit_field(#admin_audit{action = V}, action) -> V; +audit_field(#admin_audit{admin_id = V}, admin_id) -> V; +audit_field(#admin_audit{entity_type = V}, entity_type) -> V; +audit_field(_, _) -> undefined. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_banned_words.erl b/src/handlers/admin/admin_handler_banned_words.erl index 90665d8..d294698 100644 --- a/src/handlers/admin/admin_handler_banned_words.erl +++ b/src/handlers/admin/admin_handler_banned_words.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @doc Административный обработчик бан-слов. -%%% GET – список всех слов с пагинацией. +%%% GET – список всех слов с пагинацией, фильтрацией и сортировкой. %%% POST – добавить новое слово. %%% PUT – редактировать существующее слово. %%% DELETE – удалить слово по :word. @@ -37,6 +37,9 @@ trails() -> description => <<"List all banned words (admin)">>, tags => [<<"Banned Words">>], parameters => [ + #{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search by word">>}, + #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"word">>, <<"added_at">>]}, description => <<"Sort field">>}, + #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], @@ -141,11 +144,27 @@ banned_word_schema() -> list_words(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> + Qs = cowboy_req:parse_qs(Req1), + Q = proplists:get_value(<<"q">>, Qs), + SortBy = proplists:get_value(<<"sort">>, Qs, <<"word">>), + SortOrder = proplists:get_value(<<"order">>, Qs, <<"asc">>), Pagination = handler_utils:parse_pagination_params(Req1), AllWords = core_banned_words:list_banned_words(), - BannedWords = lists:sort(AllWords), - Total = length(BannedWords), - Page = lists:sublist(BannedWords, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), + % Фильтрация + Filtered = case Q of + undefined -> AllWords; + _ -> [W || W <- AllWords, string:str(string:lowercase(binary_to_list(W#banned_word.word)), string:lowercase(binary_to_list(Q))) > 0] + end, + % Сортировка + Sorted = case {SortBy, SortOrder} of + {<<"word">>, <<"asc">>} -> lists:keysort(#banned_word.word, Filtered); + {<<"word">>, <<"desc">>} -> lists:reverse(lists:keysort(#banned_word.word, Filtered)); + {<<"added_at">>, <<"asc">>} -> lists:keysort(#banned_word.added_at, Filtered); + {<<"added_at">>, <<"desc">>} -> lists:reverse(lists:keysort(#banned_word.added_at, Filtered)); + _ -> Filtered + end, + Total = length(Sorted), + Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), Json = [word_to_map(W) || W <- Page], ExtraHeaders = handler_utils:pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); diff --git a/src/handlers/admin/admin_handler_reports.erl b/src/handlers/admin/admin_handler_reports.erl index 6534f1d..225ce06 100644 --- a/src/handlers/admin/admin_handler_reports.erl +++ b/src/handlers/admin/admin_handler_reports.erl @@ -32,6 +32,12 @@ trails() -> description => <<"Filter by target type">>}, #{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search in reason">>}, + #{name => <<"sort">>, in => <<"query">>, + schema => #{type => string, enum => [<<"created_at">>, <<"status">>]}, + description => <<"Sort field">>}, + #{name => <<"order">>, in => <<"query">>, + schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, + description => <<"Sort direction">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, @@ -69,7 +75,11 @@ list_reports(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> Filters = parse_report_filters(Req1), - Pagination = handler_utils:parse_pagination_params(Req1), + Pagination0 = handler_utils:parse_pagination_params(Req1), + Pagination = Pagination0#{ + sort => maps:get(sort, Pagination0, <<"created_at">>), + order => maps:get(order, Pagination0, <<"desc">>) + }, case logic_report:list_reports(AdminId) of {ok, AllReports} -> Filtered = apply_report_filters(AllReports, Filters), @@ -89,9 +99,19 @@ list_reports(Req) -> parse_report_filters(Req) -> Qs = cowboy_req:parse_qs(Req), - #{ status => proplists:get_value(<<"status">>, Qs), - target_type => proplists:get_value(<<"target_type">>, Qs), - q => proplists:get_value(<<"q">>, Qs) }. + StatusBin = proplists:get_value(<<"status">>, Qs), + TargetBin = proplists:get_value(<<"target_type">>, Qs), + #{ + status => convert_to_atom(StatusBin), + target_type => convert_to_atom(TargetBin), + q => proplists:get_value(<<"q">>, Qs) + }. + +convert_to_atom(undefined) -> undefined; +convert_to_atom(Bin) when is_binary(Bin) -> + try binary_to_existing_atom(Bin, utf8) + catch error:badarg -> Bin + end. apply_report_filters(Reports, Filters) -> Status = maps:get(status, Filters, undefined), diff --git a/src/handlers/admin/admin_handler_reviews.erl b/src/handlers/admin/admin_handler_reviews.erl index ed875db..0884deb 100644 --- a/src/handlers/admin/admin_handler_reviews.erl +++ b/src/handlers/admin/admin_handler_reviews.erl @@ -31,10 +31,10 @@ trails() -> #{name => <<"target_id">>, in => <<"query">>, schema => #{type => string}, description => <<"ID of target">>}, #{name => <<"user_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by user">>}, #{name => <<"status">>, in => <<"query">>, schema => #{type => string}, description => <<"visible, hidden, deleted, or all">>}, - #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, - #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>}, #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"created_at">>, <<"updated_at">>, <<"rating">>]}, description => <<"Sort field">>}, - #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>} + #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}, + #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, + #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], responses => #{ 200 => #{ description => <<"Array of reviews">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => review_schema() }}} } @@ -83,16 +83,11 @@ list_reviews(Req) -> target_type => normalize_target_type(proplists:get_value(<<"target_type">>, Qs)), target_id => proplists:get_value(<<"target_id">>, Qs), user_id => proplists:get_value(<<"user_id">>, Qs), - status => proplists:get_value(<<"status">>, Qs) + status => normalize_status(proplists:get_value(<<"status">>, Qs)) }, Filters = maps:filter(fun(_, V) -> V =/= undefined end, Filters0), Pagination = handler_utils:parse_pagination_params(Req1), - % Убедимся, что в Pagination есть ключи sort и order со значениями по умолчанию - PaginationWithSort = Pagination#{ - sort => maps:get(sort, Pagination, <<"created_at">>), - order => maps:get(order, Pagination, <<"desc">>) - }, - {ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, PaginationWithSort), + {ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, Pagination), Json = [handler_utils:review_to_json(R) || R <- Reviews], ExtraHeaders = handler_utils:pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); @@ -105,6 +100,12 @@ normalize_target_type(<<"event">>) -> event; normalize_target_type(<<"calendar">>) -> calendar; normalize_target_type(Other) -> Other. +%% @private Преобразует бинарный status в атом. +normalize_status(<<"visible">>) -> visible; +normalize_status(<<"hidden">>) -> hidden; +normalize_status(<<"deleted">>) -> deleted; +normalize_status(Other) -> Other. + bulk_update_reviews(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> diff --git a/src/handlers/admin/admin_handler_tickets.erl b/src/handlers/admin/admin_handler_tickets.erl index f0347f5..810eb1f 100644 --- a/src/handlers/admin/admin_handler_tickets.erl +++ b/src/handlers/admin/admin_handler_tickets.erl @@ -24,6 +24,8 @@ trails() -> #{name => <<"status">>, in => <<"query">>, schema => #{type => string, enum => [<<"open">>, <<"in_progress">>, <<"resolved">>, <<"closed">>]}, description => <<"Filter by status">>}, #{name => <<"assigned_to">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by assigned admin ID">>}, #{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search in error_message">>}, + #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"first_seen">>, <<"last_seen">>, <<"status">>]}, description => <<"Sort field">>}, + #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], @@ -51,8 +53,7 @@ trails() -> ]. ticket_schema() -> - #{ - type => object, + #{ type => object, properties => #{ id => #{type => string}, reporter_id => #{type => string}, @@ -70,8 +71,7 @@ ticket_schema() -> }. ticket_update_schema() -> - #{ - type => object, + #{ type => object, properties => #{ status => #{type => string, enum => [<<"open">>, <<"in_progress">>, <<"resolved">>, <<"closed">>]}, assigned_to => #{type => string}, @@ -85,7 +85,11 @@ list_tickets(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> Filters = parse_ticket_filters(Req1), - Pagination = handler_utils:parse_pagination_params(Req1), + Pagination0 = handler_utils:parse_pagination_params(Req1), + Pagination = Pagination0#{ + sort => maps:get(sort, Pagination0, <<"first_seen">>), + order => maps:get(order, Pagination0, <<"desc">>) + }, TicketsResult = case maps:get(status, Filters, undefined) of undefined -> logic_ticket:list_tickets(AdminId); StatusBin -> diff --git a/src/handlers/handler_utils.erl b/src/handlers/handler_utils.erl index 5a5fbdf..e98a26e 100644 --- a/src/handlers/handler_utils.erl +++ b/src/handlers/handler_utils.erl @@ -123,14 +123,11 @@ parse_int_qs(Bin, Default) -> -spec pagination_headers(map(), non_neg_integer()) -> map(). pagination_headers(#{limit := Limit, offset := Offset}, Total) -> - NextOffset = Offset + Limit, - HasMore = NextOffset < Total, + RangeEnd = min(Offset + Limit - 1, Total - 1), #{ + <<"content-range">> => iolist_to_binary(io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])), <<"x-total-count">> => integer_to_binary(Total), - <<"x-next-offset">> => case HasMore of - true -> integer_to_binary(NextOffset); - false -> <<"">> - end + <<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">> }. %% @doc Преобразует бинарный ISO8601 параметр в datetime(). diff --git a/src/logic/logic_review.erl b/src/logic/logic_review.erl index 3669d6c..bfda17d 100644 --- a/src/logic/logic_review.erl +++ b/src/logic/logic_review.erl @@ -208,6 +208,11 @@ list_admin_reviews(Filters) -> %% Вспомогательная функция: фильтрация списка по proplist apply_filters(Reviews, []) -> Reviews; +apply_filters(Reviews, [{status, Status} | Rest]) -> + apply_filters( + [R || R <- Reviews, R#review.status =:= Status], + Rest + ); apply_filters(Reviews, [{target_type, Type} | Rest]) -> apply_filters( [R || R <- Reviews, R#review.target_type =:= Type], @@ -224,7 +229,7 @@ apply_filters(Reviews, [{user_id, UserId} | Rest]) -> Rest ); apply_filters(Reviews, [_ | Rest]) -> - apply_filters(Reviews, Rest). % Игнорируем неизвестные фильтры + apply_filters(Reviews, Rest). %%%------------------------------------------------------------------- %%% @doc Массово обновить статусы отзывов. diff --git a/test/api/admins/admin_admins_tests.erl b/test/api/admins/admin_admins_tests.erl index 48426f7..734a29a 100644 --- a/test/api/admins/admin_admins_tests.erl +++ b/test/api/admins/admin_admins_tests.erl @@ -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]). \ No newline at end of file diff --git a/test/api/admins/admin_audit_tests.erl b/test/api/admins/admin_audit_tests.erl index 09f483a..7b1484f 100644 --- a/test/api/admins/admin_audit_tests.erl +++ b/test/api/admins/admin_audit_tests.erl @@ -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"). \ No newline at end of file +%% @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]). \ No newline at end of file diff --git a/test/api/admins/admin_banned_words_tests.erl b/test/api/admins/admin_banned_words_tests.erl index 6cfe6be..e8c3802 100644 --- a/test/api/admins/admin_banned_words_tests.erl +++ b/test/api/admins/admin_banned_words_tests.erl @@ -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 = <>, 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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_calendars_tests.erl b/test/api/admins/admin_calendars_tests.erl index 4d8f19c..c3c42e8 100644 --- a/test/api/admins/admin_calendars_tests.erl +++ b/test/api/admins/admin_calendars_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_events_tests.erl b/test/api/admins/admin_events_tests.erl index 2634598..47cea96 100644 --- a/test/api/admins/admin_events_tests.erl +++ b/test/api/admins/admin_events_tests.erl @@ -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(<>, 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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_moderation_tests.erl b/test/api/admins/admin_moderation_tests.erl index f02d217..e5d17d4 100644 --- a/test/api/admins/admin_moderation_tests.erl +++ b/test/api/admins/admin_moderation_tests.erl @@ -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"). \ No newline at end of file + 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"). \ No newline at end of file diff --git a/test/api/admins/admin_reports_tests.erl b/test/api/admins/admin_reports_tests.erl index 202066b..f8ed1d2 100644 --- a/test/api/admins/admin_reports_tests.erl +++ b/test/api/admins/admin_reports_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_reviews_tests.erl b/test/api/admins/admin_reviews_tests.erl index 53f345d..5b49c11 100644 --- a/test/api/admins/admin_reviews_tests.erl +++ b/test/api/admins/admin_reviews_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_subscriptions_tests.erl b/test/api/admins/admin_subscriptions_tests.erl index 3228122..44ac4bf 100644 --- a/test/api/admins/admin_subscriptions_tests.erl +++ b/test/api/admins/admin_subscriptions_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_tickets_tests.erl b/test/api/admins/admin_tickets_tests.erl index 47df0ba..670a3c5 100644 --- a/test/api/admins/admin_tickets_tests.erl +++ b/test/api/admins/admin_tickets_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api/admins/admin_users_tests.erl b/test/api/admins/admin_users_tests.erl index 7faeea1..5d8e1d0 100644 --- a/test/api/admins/admin_users_tests.erl +++ b/test/api/admins/admin_users_tests.erl @@ -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"). \ No newline at end of file + 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]). \ No newline at end of file diff --git a/test/api_admins_SUITE.erl b/test/api_admins_SUITE.erl index 15bec28..ebf79f5 100644 --- a/test/api_admins_SUITE.erl +++ b/test/api_admins_SUITE.erl @@ -64,7 +64,6 @@ init_per_suite(Config) -> [{started_by_us, false} | Config]; false -> ct:pal("Local mode: starting application..."), -%% ok = application:load(eventhub), {ok, _} = application:ensure_all_started(eventhub), timer:sleep(1000), [{started_by_us, true} | Config] diff --git a/test/api_users_SUITE.erl b/test/api_users_SUITE.erl index 7540e35..7e9f6be 100644 --- a/test/api_users_SUITE.erl +++ b/test/api_users_SUITE.erl @@ -69,7 +69,6 @@ init_per_suite(Config) -> [{started_by_us, false} | Config]; false -> ct:pal("Local mode: starting application..."), -%% ok = application:load(eventhub), {ok, _} = application:ensure_all_started(eventhub), timer:sleep(1000), [{started_by_us, true} | Config]