Добавить авто-регистрацию тикетов (hash/dedupe/500/WS). Refs EventHub/EventHubBack#35
This commit is contained in:
@@ -36,6 +36,8 @@ test() ->
|
||||
#{error_message => <<"Something broke">>, stacktrace => <<"line 42">>}),
|
||||
|
||||
test_create_ticket(Token),
|
||||
test_create_ticket_dedupe(Token),
|
||||
test_create_ticket_manual(Token),
|
||||
test_create_ticket_missing_fields(Token),
|
||||
test_create_ticket_unauthorized(),
|
||||
test_list_tickets(Token, TicketId),
|
||||
@@ -47,7 +49,6 @@ test() ->
|
||||
|
||||
ct:pal("=== All user tickets tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
@@ -57,13 +58,57 @@ test() ->
|
||||
test_create_ticket(Token) ->
|
||||
ct:pal(" TEST: Create a ticket"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token,
|
||||
jsx:encode(#{error_message => <<"Test bug">>, stacktrace => <<"trace">>})),
|
||||
jsx:encode(#{
|
||||
error_message => <<"Test bug">>,
|
||||
stacktrace => <<"trace">>,
|
||||
source => <<"frontend">>,
|
||||
context => #{route => <<"/test">>, build => <<"dev">>}
|
||||
})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := Id, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
Decoded = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
#{<<"id">> := Id, <<"status">> := Status, <<"source">> := Source, <<"error_hash">> := Hash} = Decoded,
|
||||
?assert(is_binary(Id)),
|
||||
?assertEqual(<<"open">>, Status),
|
||||
?assertEqual(<<"frontend">>, Source),
|
||||
?assert(is_binary(Hash)),
|
||||
?assert(byte_size(Hash) > 0),
|
||||
ct:pal(" OK: ticket ~s created", [Id]).
|
||||
|
||||
%% @doc Повторный POST с тем же сообщением увеличивает count.
|
||||
-spec test_create_ticket_dedupe(binary()) -> ok.
|
||||
test_create_ticket_dedupe(Token) ->
|
||||
ct:pal(" TEST: Dedupe ticket by hash"),
|
||||
Payload = jsx:encode(#{
|
||||
error_message => <<"Dedupe me">>,
|
||||
stacktrace => <<"same stack">>,
|
||||
source => <<"frontend">>
|
||||
}),
|
||||
{ok, 201, _, Body1} = api_test_runner:client_request(post, <<"/v1/tickets">>, Token, Payload),
|
||||
#{<<"id">> := Id1, <<"count">> := Count1} = jsx:decode(list_to_binary(Body1), [return_maps]),
|
||||
{ok, 201, _, Body2} = api_test_runner:client_request(post, <<"/v1/tickets">>, Token, Payload),
|
||||
#{<<"id">> := Id2, <<"count">> := Count2} = jsx:decode(list_to_binary(Body2), [return_maps]),
|
||||
?assertEqual(Id1, Id2),
|
||||
?assertEqual(Count1 + 1, Count2),
|
||||
ct:pal(" OK: count bumped to ~p", [Count2]).
|
||||
|
||||
%% @doc Ручной клиентский репорт: source=manual без stacktrace.
|
||||
-spec test_create_ticket_manual(binary()) -> ok.
|
||||
test_create_ticket_manual(Token) ->
|
||||
ct:pal(" TEST: Create manual ticket"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token,
|
||||
jsx:encode(#{
|
||||
error_message => <<"Button does nothing">>,
|
||||
source => <<"manual">>,
|
||||
context => #{steps => <<"1. Open calendar\n2. Click share">>}
|
||||
})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"source">> := Source, <<"error_hash">> := Hash} =
|
||||
jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"manual">>, Source),
|
||||
?assert(is_binary(Hash)),
|
||||
?assert(byte_size(Hash) > 0),
|
||||
ct:pal(" OK: manual ticket created").
|
||||
|
||||
%% @doc Отсутствие обязательного поля error_message: 400 Bad Request.
|
||||
-spec test_create_ticket_missing_fields(binary()) -> ok.
|
||||
test_create_ticket_missing_fields(Token) ->
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
-module(handler_ticket_auto_report_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Фикстуры
|
||||
%% ----------------------------------------------------------------
|
||||
setup() ->
|
||||
catch mnesia:stop(),
|
||||
case mnesia:start() of
|
||||
{atomic, ok} -> ok;
|
||||
ok -> ok
|
||||
end,
|
||||
{atomic, ok} = mnesia:create_table(ticket, [
|
||||
{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
|
||||
%% rate-limit ETS может остаться между тестами
|
||||
catch ets:delete(eventhub_ticket_rate),
|
||||
ok.
|
||||
|
||||
cleanup(_) ->
|
||||
catch mnesia:delete_table(ticket),
|
||||
catch ets:delete(eventhub_ticket_rate),
|
||||
mnesia:stop().
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Тесты: авто-регистрация backend-ошибок через handler_utils
|
||||
%% ----------------------------------------------------------------
|
||||
handler_ticket_auto_report_test_() ->
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"maybe_report creates backend ticket async", fun test_maybe_report_creates/0},
|
||||
{"maybe_report skips /tickets routes", fun test_maybe_report_skips_tickets/0},
|
||||
{"duplicate maybe_report bumps count", fun test_maybe_report_dedupe/0}
|
||||
]}.
|
||||
|
||||
test_maybe_report_creates() ->
|
||||
Before = length(core_ticket:list_all()),
|
||||
ok = handler_utils:maybe_report_internal_error(
|
||||
#{<<"route">> => <<"/v1/calendars">>, <<"method">> => <<"GET">>},
|
||||
<<"simulated_500">>,
|
||||
#{}
|
||||
),
|
||||
Ticket = wait_for_ticket(<<"simulated_500">>, 20),
|
||||
?assertEqual(Before + 1, length(core_ticket:list_all())),
|
||||
?assertEqual(<<"backend">>, Ticket#ticket.source),
|
||||
?assert(is_binary(Ticket#ticket.error_hash)),
|
||||
?assert(byte_size(Ticket#ticket.error_hash) > 0).
|
||||
|
||||
test_maybe_report_skips_tickets() ->
|
||||
Before = length(core_ticket:list_all()),
|
||||
ok = handler_utils:maybe_report_internal_error(
|
||||
#{<<"route">> => <<"/v1/tickets">>, <<"method">> => <<"POST">>},
|
||||
<<"ticket_path_should_not_report">>,
|
||||
#{}
|
||||
),
|
||||
timer:sleep(200),
|
||||
?assertEqual(Before, length(core_ticket:list_all())).
|
||||
|
||||
test_maybe_report_dedupe() ->
|
||||
Msg = <<"same_backend_error">>,
|
||||
ok = handler_utils:maybe_report_internal_error(
|
||||
#{<<"route">> => <<"/v1/events">>, <<"method">> => <<"POST">>}, Msg, #{}),
|
||||
T1 = wait_for_ticket(Msg, 20),
|
||||
ok = handler_utils:maybe_report_internal_error(
|
||||
#{<<"route">> => <<"/v1/events">>, <<"method">> => <<"POST">>}, Msg, #{}),
|
||||
T2 = wait_for_ticket_count(Msg, 2, 20),
|
||||
?assertEqual(T1#ticket.id, T2#ticket.id),
|
||||
?assertEqual(2, T2#ticket.count).
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Helpers
|
||||
%% ----------------------------------------------------------------
|
||||
wait_for_ticket(Msg, 0) ->
|
||||
error({timeout_waiting_ticket, Msg});
|
||||
wait_for_ticket(Msg, Retries) ->
|
||||
case [T || T <- core_ticket:list_all(), T#ticket.error_message =:= Msg] of
|
||||
[T | _] -> T;
|
||||
[] ->
|
||||
timer:sleep(50),
|
||||
wait_for_ticket(Msg, Retries - 1)
|
||||
end.
|
||||
|
||||
wait_for_ticket_count(Msg, MinCount, 0) ->
|
||||
error({timeout_waiting_count, Msg, MinCount});
|
||||
wait_for_ticket_count(Msg, MinCount, Retries) ->
|
||||
case [T || T <- core_ticket:list_all(), T#ticket.error_message =:= Msg] of
|
||||
[T | _] when T#ticket.count >= MinCount -> T;
|
||||
_ ->
|
||||
timer:sleep(50),
|
||||
wait_for_ticket_count(Msg, MinCount, Retries - 1)
|
||||
end.
|
||||
@@ -42,6 +42,8 @@ logic_ticket_test_() ->
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"Report error creates ticket", fun test_report_error/0},
|
||||
{"Report duplicate error increments count", fun test_report_duplicate/0},
|
||||
{"Report after close creates new ticket", fun test_report_after_close/0},
|
||||
{"Frontend source set on ticket", fun test_report_frontend_source/0},
|
||||
{"List tickets as admin", fun test_list_tickets/0},
|
||||
{"List tickets as non-admin returns error", fun test_list_tickets_forbidden/0},
|
||||
{"Update status as admin", fun test_update_status/0},
|
||||
@@ -55,12 +57,18 @@ logic_ticket_test_() ->
|
||||
report(ErrorMsg) ->
|
||||
logic_ticket:report_error(ErrorMsg, <<"stack">>, #{<<"reporter_id">> => <<"user1">>}).
|
||||
|
||||
report(Source, ErrorMsg) ->
|
||||
logic_ticket:report_error(Source, ErrorMsg, <<"stack">>, #{<<"reporter_id">> => <<"user1">>}).
|
||||
|
||||
%% --- Тесты ---
|
||||
|
||||
test_report_error() ->
|
||||
{ok, Ticket} = report(<<"Error1">>),
|
||||
?assertEqual(<<"Error1">>, Ticket#ticket.error_message),
|
||||
?assertEqual(1, Ticket#ticket.count).
|
||||
?assertEqual(1, Ticket#ticket.count),
|
||||
?assertEqual(<<"backend">>, Ticket#ticket.source),
|
||||
?assert(is_binary(Ticket#ticket.error_hash)),
|
||||
?assert(byte_size(Ticket#ticket.error_hash) > 0).
|
||||
|
||||
test_report_duplicate() ->
|
||||
{ok, T1} = report(<<"Dup">>),
|
||||
@@ -70,6 +78,17 @@ test_report_duplicate() ->
|
||||
% Проверяем, что это тот же тикет, а не новый
|
||||
?assertEqual(T1#ticket.id, T2#ticket.id).
|
||||
|
||||
test_report_after_close() ->
|
||||
{ok, T1} = report(<<"ClosedThenAgain">>),
|
||||
{ok, _} = logic_ticket:close_ticket(<<"admin1">>, T1#ticket.id),
|
||||
{ok, T2} = report(<<"ClosedThenAgain">>),
|
||||
?assertEqual(1, T2#ticket.count),
|
||||
?assertNotEqual(T1#ticket.id, T2#ticket.id).
|
||||
|
||||
test_report_frontend_source() ->
|
||||
{ok, Ticket} = report(frontend, <<"FE crash">>),
|
||||
?assertEqual(<<"frontend">>, Ticket#ticket.source).
|
||||
|
||||
test_list_tickets() ->
|
||||
{ok, _} = report(<<"E1">>),
|
||||
Tickets = logic_ticket:list_tickets(<<"admin1">>),
|
||||
|
||||
@@ -38,7 +38,8 @@ test_ensure_applied() ->
|
||||
?assertEqual([], maps:get(pending, Status)),
|
||||
Applied = maps:get(applied, Status),
|
||||
?assert(lists:member("20260501120000_base_schema", Applied)),
|
||||
?assert(lists:member("20260504150000_test_migration", Applied)).
|
||||
?assert(lists:member("20260504150000_test_migration", Applied)),
|
||||
?assert(lists:member("20260716230000_ticket_source_and_hash_index", Applied)).
|
||||
|
||||
test_ensure_applied_idempotent() ->
|
||||
?assertEqual(ok, migration_engine:ensure_applied()),
|
||||
@@ -58,6 +59,8 @@ test_join_wait() ->
|
||||
version = "20260501120000_base_schema", applied_at = Now}),
|
||||
mnesia:dirty_write(#schema_migration{
|
||||
version = "20260504150000_test_migration", applied_at = Now}),
|
||||
mnesia:dirty_write(#schema_migration{
|
||||
version = "20260716230000_ticket_source_and_hash_index", applied_at = Now}),
|
||||
mnesia:dirty_delete({schema_migration, ?LOCK_VERSION})
|
||||
end),
|
||||
?assertEqual(ok, migration_engine:ensure_applied()).
|
||||
|
||||
Reference in New Issue
Block a user