%%%------------------------------------------------------------------- %%% @doc Тесты административного API для аудита. %%% %%% Покрывает эндпоинты: %%% GET /v1/admin/audit %%% %%% Проверяет: %%% - получение списка записей аудита %%% - фильтрацию по 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 ==="), UserToken = api_test_runner:get_user_token(), #{<<"id">> := UserId} = api_test_runner:client_get(<<"/v1/user/me">>, UserToken), 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), ct:sleep(200), Body2 = #{<<"action">> => <<"unblock">>, <<"reason">> => <<"Test">>}, api_test_runner:admin_put(Path, AdminToken, Body2), % ── Основные тесты ── test_list_audit(SuperToken), test_list_admin_forbidden(AdminToken), test_login_audit_ip(SuperToken), % ── Фильтрация ── 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"), 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)]). %% @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 Логин с X-Forwarded-For записывает IP клиента в аудит. test_login_audit_ip(Token) -> ct:pal(" TEST: Login audit stores X-Forwarded-For IP"), Email = api_test_runner:admin_super_email(), Password = api_test_runner:admin_super_password(), Body = jsx:encode(#{<<"email">> => Email, <<"password">> => Password}), {ok, 200, _, _} = api_test_runner:admin_request( post, <<"/v1/admin/login">>, <<>>, Body, [{"X-Forwarded-For", "203.0.113.99"}] ), Records = api_test_runner:admin_get(<<"/v1/admin/audit?action=login">>, Token), Forwarded = [R || R <- Records, maps:get(<<"ip">>, R) =:= <<"203.0.113.99">>], ?assert(length(Forwarded) >= 1), Login = hd(Forwarded), ?assertEqual(<<"203.0.113.99">>, maps:get(<<"ip">>, Login)), ct:pal(" OK: ip=~s", [maps:get(<<"ip">>, Login)]). %% ── Фильтрация ── %% @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)]). %% @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"), 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), ?assert(length(Page2) >= 0), case {Page1, Page2} of {[First|_], [Second|_]} -> Id1 = maps:get(<<"id">>, First), Id2 = maps:get(<<"id">>, Second), ?assertNotEqual(Id1, Id2); _ -> ok end, ct:pal(" OK"). %% @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]).