Files
EventHubBack/test/api/admins/admin_tickets_tests.erl
T

324 lines
14 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
%%%-------------------------------------------------------------------
%%% @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">> => api_test_runner:unique_ticket_message(<<"Test bug">>),
<<"stacktrace">> => <<"trace">>}),
#{<<"id">> := Ticket1Id} = Ticket1,
Ticket2 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
#{<<"error_message">> => api_test_runner:unique_ticket_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]).