Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Финал
This commit is contained in:
@@ -2,52 +2,85 @@
|
||||
%%% @doc Тесты административного API для управления тикетами.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/admin/tickets
|
||||
%%% GET /v1/admin/tickets/:id
|
||||
%%% PUT /v1/admin/tickets/:id
|
||||
%%% DELETE /v1/admin/tickets/:id
|
||||
%%% GET /v1/admin/tickets
|
||||
%%% GET /v1/admin/tickets/:id
|
||||
%%% PUT /v1/admin/tickets/:id
|
||||
%%% DELETE /v1/admin/tickets/:id
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение списка тикетов
|
||||
%%% - получение тикета по ID
|
||||
%%% - разрешение (resolve) и закрытие (close) тикета
|
||||
%%% - назначение исполнителя
|
||||
%%% - удаление тикета
|
||||
%%% - фильтрацию по статусу
|
||||
%%% - пагинацию
|
||||
%%% - получение списка тикетов
|
||||
%%% - получение тикета по ID
|
||||
%%% - разрешение (resolve) и закрытие (close) тикета
|
||||
%%% - назначение исполнителя
|
||||
%%% - удаление тикета
|
||||
%%% - фильтрацию по статусу, исполнителю, поиску
|
||||
%%% - комбинации фильтров
|
||||
%%% - пагинацию
|
||||
%%% - сортировку по first_seen, last_seen, status
|
||||
%%% - комбинации фильтрации, сортировки и пагинации
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(admin_tickets_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== Admin Tickets Tests ==="),
|
||||
Token = api_test_runner:get_admin_token(),
|
||||
UserToken = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём два тикета для разных проверок
|
||||
Ticket1 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
|
||||
#{<<"error_message">> => <<"Test bug">>, <<"stacktrace">> => <<"trace">>}),
|
||||
#{<<"id">> := Ticket1Id} = Ticket1,
|
||||
|
||||
Ticket2 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken,
|
||||
#{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}),
|
||||
#{<<"id">> := Ticket2Id} = Ticket2,
|
||||
|
||||
% Получаем ID текущего администратора для теста фильтрации по исполнителю
|
||||
Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token),
|
||||
AdminId = maps:get(<<"id">>, Me),
|
||||
|
||||
% Основные проверки
|
||||
test_list_tickets(Token),
|
||||
test_get_ticket(Token, Ticket1Id),
|
||||
test_resolve_ticket(Token, Ticket1Id),
|
||||
test_close_ticket(Token, Ticket1Id),
|
||||
test_assign_ticket(Token, Ticket1Id),
|
||||
test_filter_tickets(Token),
|
||||
test_assign_ticket(Token, Ticket1Id, AdminId),
|
||||
|
||||
% Фильтрация
|
||||
test_filter_tickets_by_status(Token, Ticket2Id),
|
||||
test_filter_tickets_by_assigned(Token, AdminId),
|
||||
test_filter_tickets_by_search(Token),
|
||||
|
||||
% Комбинации фильтров
|
||||
test_filter_combined_status_assigned(Token, AdminId),
|
||||
test_filter_combined_status_search(Token, Ticket2Id),
|
||||
|
||||
% Сортировка
|
||||
test_sort_by_first_seen_desc(Token),
|
||||
test_sort_by_first_seen_asc(Token),
|
||||
test_sort_by_last_seen_desc(Token),
|
||||
test_sort_by_status_asc(Token),
|
||||
|
||||
% Фильтр + сортировка
|
||||
test_filter_status_sort_first_seen(Token, Ticket2Id),
|
||||
test_filter_assigned_sort_last_seen(Token, AdminId),
|
||||
|
||||
% Пагинация
|
||||
test_ticket_pagination(Token, Ticket2Id),
|
||||
test_pagination_headers(Token),
|
||||
|
||||
% Фильтр + пагинация
|
||||
test_filter_status_with_pagination(Token, Ticket2Id),
|
||||
|
||||
% Фильтр + сортировка + пагинация
|
||||
test_filter_sort_pagination_combined(Token, Ticket2Id),
|
||||
|
||||
% Удаление
|
||||
test_delete_ticket(Token, Ticket1Id),
|
||||
test_delete_ticket(Token, Ticket2Id),
|
||||
|
||||
@@ -59,31 +92,25 @@ test() ->
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов.
|
||||
%% Убеждается, что список не пуст.
|
||||
-spec test_list_tickets(binary()) -> ok.
|
||||
test_list_tickets(Token) ->
|
||||
ct:pal(" TEST: List all tickets"),
|
||||
ct:pal(" TEST: List all tickets"),
|
||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
?assert(length(Tickets) >= 1),
|
||||
ct:pal(" OK: ~p tickets", [length(Tickets)]).
|
||||
ct:pal(" OK: ~p tickets", [length(Tickets)]).
|
||||
|
||||
%% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID.
|
||||
%% Убеждается, что статус нового тикета – open.
|
||||
-spec test_get_ticket(binary(), binary()) -> ok.
|
||||
test_get_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Get ticket by ID"),
|
||||
ct:pal(" TEST: Get ticket by ID"),
|
||||
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
|
||||
Ticket = api_test_runner:admin_get(Path, Token),
|
||||
?assertEqual(TicketId, maps:get(<<"id">>, Ticket)),
|
||||
?assertEqual(<<"open">>, maps:get(<<"status">>, Ticket)),
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc PUT /v1/admin/tickets/:id – разрешение тикета (resolve).
|
||||
%% Ожидается статус resolved после успешного выполнения.
|
||||
-spec test_resolve_ticket(binary(), binary()) -> ok.
|
||||
test_resolve_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Resolve ticket"),
|
||||
ct:pal(" TEST: Resolve ticket"),
|
||||
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
|
||||
Updated = api_test_runner:admin_put(Path, Token, #{
|
||||
<<"status">> => <<"resolved">>,
|
||||
@@ -91,53 +118,156 @@ test_resolve_ticket(Token, TicketId) ->
|
||||
}),
|
||||
?assertEqual(<<"resolved">>, maps:get(<<"status">>, Updated)),
|
||||
?assertEqual(<<"Fixed">>, maps:get(<<"resolution_note">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc PUT /v1/admin/tickets/:id – закрытие тикета (close).
|
||||
-spec test_close_ticket(binary(), binary()) -> ok.
|
||||
test_close_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Close ticket"),
|
||||
ct:pal(" TEST: Close ticket"),
|
||||
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
|
||||
Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"closed">>}),
|
||||
?assertEqual(<<"closed">>, maps:get(<<"status">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc PUT /v1/admin/tickets/:id – назначение исполнителя.
|
||||
-spec test_assign_ticket(binary(), binary()) -> ok.
|
||||
test_assign_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Assign ticket"),
|
||||
Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token),
|
||||
AdminId = maps:get(<<"id">>, Me),
|
||||
test_assign_ticket(Token, TicketId, AdminId) ->
|
||||
ct:pal(" TEST: Assign ticket"),
|
||||
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
|
||||
Updated = api_test_runner:admin_put(Path, Token, #{<<"assigned_to">> => AdminId}),
|
||||
?assertEqual(AdminId, maps:get(<<"assigned_to">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc DELETE /v1/admin/tickets/:id – удаление тикета.
|
||||
-spec test_delete_ticket(binary(), binary()) -> ok.
|
||||
test_delete_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Delete ticket"),
|
||||
ct:pal(" TEST: Delete ticket"),
|
||||
Path = <<"/v1/admin/tickets/", TicketId/binary>>,
|
||||
Deleted = api_test_runner:admin_delete(Path, Token),
|
||||
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)),
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc GET /v1/admin/tickets?status=... – проверяет фильтрацию по статусу open.
|
||||
%% Использует второй тикет, который всё ещё open.
|
||||
-spec test_filter_tickets(binary()) -> ok.
|
||||
test_filter_tickets(Token) ->
|
||||
ct:pal(" TEST: Filter tickets by status=open"),
|
||||
%% ── Фильтрация ──
|
||||
|
||||
%% @doc Фильтрация по статусу open.
|
||||
test_filter_tickets_by_status(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Filter tickets by status=open"),
|
||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?status=open">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
?assert(length(Tickets) >= 1),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
ct:pal(" OK: ~p open tickets", [length(Tickets)]).
|
||||
ct:pal(" OK: ~p open tickets", [length(Tickets)]).
|
||||
|
||||
%% @doc Фильтрация по исполнителю.
|
||||
test_filter_tickets_by_assigned(Token, AdminId) ->
|
||||
ct:pal(" TEST: Filter tickets by assigned_to"),
|
||||
Path = <<"/v1/admin/tickets?assigned_to=", AdminId/binary>>,
|
||||
Tickets = api_test_runner:admin_get(Path, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
|
||||
ct:pal(" OK: ~p tickets assigned to admin", [length(Tickets)]).
|
||||
|
||||
%% @doc Поиск по тексту ошибки.
|
||||
test_filter_tickets_by_search(Token) ->
|
||||
ct:pal(" TEST: Search tickets by q=bug"),
|
||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?q=bug">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
?assert(length(Tickets) >= 1),
|
||||
[?assert(string:str(binary_to_list(maps:get(<<"error_message">>, T)), "bug") > 0) || T <- Tickets],
|
||||
ct:pal(" OK: ~p tickets match search", [length(Tickets)]).
|
||||
|
||||
%% ── Комбинации фильтров ──
|
||||
|
||||
%% @doc Комбинация фильтров: status=open + assigned_to.
|
||||
test_filter_combined_status_assigned(Token, AdminId) ->
|
||||
ct:pal(" TEST: Combined filter (status=open & assigned_to)"),
|
||||
Path = <<"/v1/admin/tickets?status=open&assigned_to=", AdminId/binary>>,
|
||||
Tickets = api_test_runner:admin_get(Path, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
|
||||
ct:pal(" OK: ~p tickets match both criteria", [length(Tickets)]).
|
||||
|
||||
%% @doc Комбинация фильтров: status=open + поиск.
|
||||
test_filter_combined_status_search(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Combined filter (status=open & q=Another)"),
|
||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?status=open&q=Another">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
[?assert(string:str(binary_to_list(maps:get(<<"error_message">>, T)), "Another") > 0) || T <- Tickets],
|
||||
ct:pal(" OK: ~p tickets match both criteria", [length(Tickets)]).
|
||||
|
||||
%% ── Сортировка ──
|
||||
|
||||
%% @doc Сортировка по first_seen по убыванию.
|
||||
test_sort_by_first_seen_desc(Token) ->
|
||||
ct:pal(" TEST: Sort tickets by first_seen desc"),
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?sort=first_seen&order=desc">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
|
||||
Sorted = lists:reverse(lists:sort(Dates)),
|
||||
?assertEqual(Sorted, Dates),
|
||||
ct:pal(" OK: sorted by first_seen desc").
|
||||
|
||||
%% @doc Сортировка по first_seen по возрастанию.
|
||||
test_sort_by_first_seen_asc(Token) ->
|
||||
ct:pal(" TEST: Sort tickets by first_seen asc"),
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?sort=first_seen&order=asc">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
|
||||
?assertEqual(lists:sort(Dates), Dates),
|
||||
ct:pal(" OK: sorted by first_seen asc").
|
||||
|
||||
%% @doc Сортировка по last_seen по убыванию.
|
||||
test_sort_by_last_seen_desc(Token) ->
|
||||
ct:pal(" TEST: Sort tickets by last_seen desc"),
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?sort=last_seen&order=desc">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
Dates = [maps:get(<<"last_seen">>, T) || T <- Tickets],
|
||||
Sorted = lists:reverse(lists:sort(Dates)),
|
||||
?assertEqual(Sorted, Dates),
|
||||
ct:pal(" OK: sorted by last_seen desc").
|
||||
|
||||
%% @doc Сортировка по status по возрастанию.
|
||||
test_sort_by_status_asc(Token) ->
|
||||
ct:pal(" TEST: Sort tickets by status asc"),
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?sort=status&order=asc">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
Statuses = [maps:get(<<"status">>, T) || T <- Tickets],
|
||||
?assertEqual(lists:sort(Statuses), Statuses),
|
||||
ct:pal(" OK: sorted by status asc").
|
||||
|
||||
%% ── Фильтр + сортировка ──
|
||||
|
||||
%% @doc Фильтр status=open + сортировка по first_seen desc.
|
||||
test_filter_status_sort_first_seen(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Filter status=open + sort by first_seen desc"),
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?status=open&sort=first_seen&order=desc">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
Dates = [maps:get(<<"first_seen">>, T) || T <- Tickets],
|
||||
Sorted = lists:reverse(lists:sort(Dates)),
|
||||
?assertEqual(Sorted, Dates),
|
||||
ct:pal(" OK: ~p tickets sorted", [length(Tickets)]).
|
||||
|
||||
%% @doc Фильтр assigned_to + сортировка по last_seen asc.
|
||||
test_filter_assigned_sort_last_seen(Token, AdminId) ->
|
||||
ct:pal(" TEST: Filter assigned_to + sort by last_seen asc"),
|
||||
Path = <<"/v1/admin/tickets?assigned_to=", AdminId/binary, "&sort=last_seen&order=asc">>,
|
||||
{ok, 200, _, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
[?assertEqual(AdminId, maps:get(<<"assigned_to">>, T)) || T <- Tickets],
|
||||
Dates = [maps:get(<<"last_seen">>, T) || T <- Tickets],
|
||||
?assertEqual(lists:sort(Dates), Dates),
|
||||
ct:pal(" OK: ~p tickets sorted", [length(Tickets)]).
|
||||
|
||||
%% ── Пагинация ──
|
||||
|
||||
%% @doc GET /v1/admin/tickets?limit=...&offset=... – проверяет пагинацию.
|
||||
%% Использует второй тикет как гарантированно существующий.
|
||||
-spec test_ticket_pagination(binary(), binary()) -> ok.
|
||||
test_ticket_pagination(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Ticket pagination"),
|
||||
ct:pal(" TEST: Ticket pagination"),
|
||||
Page1 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=0">>, Token),
|
||||
?assert(length(Page1) >= 1),
|
||||
Page2 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=1">>, Token),
|
||||
@@ -149,4 +279,44 @@ test_ticket_pagination(Token, _TicketId) ->
|
||||
?assertNotEqual(Id1, Id2);
|
||||
_ -> ok
|
||||
end,
|
||||
ct:pal(" OK").
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc Проверка заголовков пагинации (x-total-count).
|
||||
test_pagination_headers(Token) ->
|
||||
ct:pal(" TEST: Pagination headers (x-total-count)"),
|
||||
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?limit=2&offset=0">>, Token, <<"">>),
|
||||
Total = list_to_integer(
|
||||
proplists:get_value("x-total-count", Headers, "0")),
|
||||
?assert(Total >= 1),
|
||||
ct:pal(" OK: total count = ~p", [Total]).
|
||||
|
||||
%% ── Фильтр + пагинация ──
|
||||
|
||||
%% @doc Фильтр status=open + пагинация limit=1.
|
||||
test_filter_status_with_pagination(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Filter status=open + pagination (limit=1)"),
|
||||
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?status=open&limit=1&offset=0">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(length(Tickets) =< 1),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
Total = list_to_integer(
|
||||
proplists:get_value("x-total-count", Headers, "0")),
|
||||
?assert(Total >= 1),
|
||||
ct:pal(" OK: total count = ~p", [Total]).
|
||||
|
||||
%% ── Фильтр + сортировка + пагинация ──
|
||||
|
||||
%% @doc Фильтр status=open + сортировка по first_seen desc + пагинация limit=1.
|
||||
test_filter_sort_pagination_combined(Token, _TicketId) ->
|
||||
ct:pal(" TEST: Filter + sort + pagination (status=open, sort=first_seen desc, limit=1)"),
|
||||
{ok, 200, Headers, Body} = api_test_runner:admin_request(get,
|
||||
<<"/v1/admin/tickets?status=open&sort=first_seen&order=desc&limit=1&offset=0">>, Token, <<"">>),
|
||||
Tickets = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(length(Tickets) =< 1),
|
||||
[?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets],
|
||||
Total = list_to_integer(
|
||||
proplists:get_value("x-total-count", Headers, "0")),
|
||||
?assert(Total >= 1),
|
||||
ct:pal(" OK: total count = ~p", [Total]).
|
||||
Reference in New Issue
Block a user