Добавить авто-регистрацию тикетов (hash/dedupe/500/WS). Refs EventHub/EventHubBack#35
CI / test (push) Successful in 21m45s
CI / deploy-ift (push) Successful in 6m8s
CI / e2e-ift (push) Failing after 6m13s
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

This commit is contained in:
2026-07-17 00:44:09 +03:00
parent 0a52712258
commit a696d25486
12 changed files with 590 additions and 59 deletions
@@ -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.
+20 -1
View File
@@ -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">>),
+4 -1
View File
@@ -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()).