%%%------------------------------------------------------------------- %%% @doc Тесты административного API для управления тикетами. %%% %%% Покрывает эндпоинты: %%% GET /v1/admin/tickets %%% GET /v1/admin/tickets/:id %%% PUT /v1/admin/tickets/:id %%% DELETE /v1/admin/tickets/:id %%% %%% Проверяет: %%% - получение списка тикетов %%% - получение тикета по 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, 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), ct:pal("=== All admin tickets tests passed ==="), ok. %%%=================================================================== %%% Тестовые функции %%%=================================================================== %% @doc GET /v1/admin/tickets – проверяет получение списка тикетов. test_list_tickets(Token) -> 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)]). %% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID. test_get_ticket(Token, TicketId) -> 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"). %% @doc PUT /v1/admin/tickets/:id – разрешение тикета (resolve). test_resolve_ticket(Token, TicketId) -> ct:pal(" TEST: Resolve ticket"), Path = <<"/v1/admin/tickets/", TicketId/binary>>, Updated = api_test_runner:admin_put(Path, Token, #{ <<"status">> => <<"resolved">>, <<"resolution_note">> => <<"Fixed">> }), ?assertEqual(<<"resolved">>, maps:get(<<"status">>, Updated)), ?assertEqual(<<"Fixed">>, maps:get(<<"resolution_note">>, Updated)), ct:pal(" OK"). %% @doc PUT /v1/admin/tickets/:id – закрытие тикета (close). test_close_ticket(Token, TicketId) -> 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"). %% @doc PUT /v1/admin/tickets/:id – назначение исполнителя. 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"). %% @doc DELETE /v1/admin/tickets/:id – удаление тикета. test_delete_ticket(Token, TicketId) -> 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"). %% ── Фильтрация ── %% @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)]). %% @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=... – проверяет пагинацию. test_ticket_pagination(Token, _TicketId) -> 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), ?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_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]).