Добавить автомодерацию: политики, порог жалоб, settings/hits API и тесты.
CI / test (push) Failing after 37m9s
CI / deploy-ift (push) Has been skipped
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

Refs EventHub/EventHubBack#37
Refs EventHub/EventHubBack#38
Refs EventHub/EventHubBack#39
Refs EventHub/EventHubBack#40
This commit is contained in:
2026-07-17 18:09:31 +03:00
parent 4fa802702f
commit e9d1a13900
27 changed files with 1779 additions and 85 deletions
+140
View File
@@ -0,0 +1,140 @@
%%%-------------------------------------------------------------------
%%% @doc API-тесты автомодерации (admin settings + hits queue).
%%%-------------------------------------------------------------------
-module(admin_automod_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
-spec test() -> ok.
test() ->
ct:pal("=== Admin Automod Tests ==="),
Admin = api_test_runner:get_admin_token(),
Moder = api_test_runner:get_moderator_token(),
test_get_settings(Admin),
test_put_settings(Admin),
test_put_settings_forbidden_moderator(Moder),
Unique = integer_to_binary(erlang:system_time()),
BadWord = <<"am_spam_", Unique/binary>>,
test_seed_banned_word(Admin, BadWord),
test_flag_creates_hit(Admin, BadWord),
test_list_hits_filter(Admin),
test_approve_restores(Admin, Moder, BadWord),
test_reject_keeps_frozen(Admin, BadWord),
test_restore_defaults(Admin),
ct:pal("=== All admin automod tests passed ==="),
ok.
test_get_settings(Token) ->
ct:pal(" TEST: GET automod settings"),
S = api_test_runner:admin_get(<<"/v1/admin/automod/settings">>, Token),
?assert(maps:is_key(<<"keyword_action">>, S)),
?assert(maps:is_key(<<"match_mode">>, S)),
?assert(maps:is_key(<<"report_threshold">>, S)),
ct:pal(" OK").
test_put_settings(Token) ->
ct:pal(" TEST: PUT automod settings"),
S = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Token, #{
<<"keyword_action">> => <<"flag">>,
<<"match_mode">> => <<"word_boundary">>,
<<"report_threshold">> => 3
}),
?assertEqual(<<"flag">>, maps:get(<<"keyword_action">>, S)),
?assertEqual(3, maps:get(<<"report_threshold">>, S)),
ct:pal(" OK").
test_put_settings_forbidden_moderator(Token) ->
ct:pal(" TEST: moderator cannot PUT settings"),
{ok, Status, _, _} = api_test_runner:admin_request(put,
<<"/v1/admin/automod/settings">>, Token,
jsx:encode(#{<<"report_threshold">> => 9})),
?assertEqual(403, Status),
ct:pal(" OK").
test_seed_banned_word(Token, Word) ->
ct:pal(" TEST: seed banned word ~s", [Word]),
_ = api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word}),
ct:pal(" OK").
test_flag_creates_hit(AdminToken, BadWord) ->
ct:pal(" TEST: flag policy creates hit via user content"),
api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, AdminToken, #{
<<"keyword_action">> => <<"flag">>,
<<"match_mode">> => <<"word_boundary">>,
<<"report_threshold">> => 3
}),
User = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(User, #{title => <<"AutomodCal">>}),
Title = <<"Party with ", BadWord/binary>>,
{ok, 201, _, Body} = api_test_runner:client_request(post,
<<"/v1/calendars/", CalId/binary, "/events">>, User,
jsx:encode(#{
title => Title,
start_time => api_test_runner:future_date_iso8601(),
duration => 60
})),
#{<<"id">> := EventId, <<"status">> := Status} =
jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(<<"frozen">>, Status),
Hits = api_test_runner:admin_get(<<"/v1/admin/automod/hits?status=open">>, AdminToken),
?assert(lists:any(fun(H) ->
maps:get(<<"entity_id">>, H) =:= EventId andalso
maps:get(<<"trigger">>, H) =:= <<"keyword">>
end, Hits)),
put({automod_event, BadWord}, EventId),
ct:pal(" OK: event ~s frozen + hit", [EventId]).
test_list_hits_filter(Token) ->
ct:pal(" TEST: list hits filter by trigger"),
Hits = api_test_runner:admin_get(<<"/v1/admin/automod/hits?trigger=keyword">>, Token),
?assert(is_list(Hits)),
?assert(lists:all(fun(H) -> maps:get(<<"trigger">>, H) =:= <<"keyword">> end, Hits)),
ct:pal(" OK: ~p", [length(Hits)]).
test_approve_restores(AdminToken, ModerToken, BadWord) ->
ct:pal(" TEST: approve hit restores entity"),
EventId = get({automod_event, BadWord}),
Hits = api_test_runner:admin_get(<<"/v1/admin/automod/hits?status=open">>, AdminToken),
Hit = hd([H || H <- Hits, maps:get(<<"entity_id">>, H) =:= EventId]),
HitId = maps:get(<<"id">>, Hit),
Resolved = api_test_runner:admin_put(
<<"/v1/admin/automod/hits/", HitId/binary>>, ModerToken,
#{<<"status">> => <<"approved">>}),
?assertEqual(<<"approved">>, maps:get(<<"status">>, Resolved)),
%% admin event fetch
Ev = api_test_runner:admin_get(<<"/v1/admin/events/", EventId/binary>>, AdminToken),
?assertEqual(<<"active">>, maps:get(<<"status">>, Ev)),
ct:pal(" OK").
test_reject_keeps_frozen(AdminToken, BadWord) ->
ct:pal(" TEST: reject hit keeps freeze"),
User = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(User, #{title => <<"AutomodCal2">>}),
Title = <<"Again ", BadWord/binary>>,
{ok, 201, _, Body} = api_test_runner:client_request(post,
<<"/v1/calendars/", CalId/binary, "/events">>, User,
jsx:encode(#{
title => Title,
start_time => api_test_runner:future_date_iso8601(),
duration => 45
})),
#{<<"id">> := EventId} = jsx:decode(list_to_binary(Body), [return_maps]),
Hits = api_test_runner:admin_get(<<"/v1/admin/automod/hits?status=open">>, AdminToken),
Hit = hd([H || H <- Hits, maps:get(<<"entity_id">>, H) =:= EventId]),
HitId = maps:get(<<"id">>, Hit),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/hits/", HitId/binary>>, AdminToken,
#{<<"status">> => <<"rejected">>}),
Ev = api_test_runner:admin_get(<<"/v1/admin/events/", EventId/binary>>, AdminToken),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Ev)),
ct:pal(" OK").
test_restore_defaults(Token) ->
ct:pal(" TEST: restore default settings"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Token, #{
<<"keyword_action">> => <<"flag">>,
<<"match_mode">> => <<"word_boundary">>,
<<"report_threshold">> => 3
}),
ct:pal(" OK").
+152
View File
@@ -0,0 +1,152 @@
%%%-------------------------------------------------------------------
%%% @doc User-facing automod API: content reject + report threshold + review reports.
%%%-------------------------------------------------------------------
-module(user_automod_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
-spec test() -> ok.
test() ->
ct:pal("=== User Automod Tests ==="),
Admin = api_test_runner:get_admin_token(),
Unique = integer_to_binary(erlang:system_time()),
BadWord = <<"uam_bad_", Unique/binary>>,
_ = api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Admin, #{<<"word">> => BadWord}),
test_reject_event_create(Admin, BadWord),
test_reject_calendar_create(Admin, BadWord),
test_report_threshold_freezes_event(Admin),
test_report_on_review(Admin),
test_dismissed_reports_do_not_freeze(Admin),
%% restore defaults
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"keyword_action">> => <<"flag">>,
<<"match_mode">> => <<"word_boundary">>,
<<"report_threshold">> => 3
}),
ct:pal("=== All user automod tests passed ==="),
ok.
test_reject_event_create(Admin, BadWord) ->
ct:pal(" TEST: reject policy returns 400 on event create"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"keyword_action">> => <<"reject">>
}),
User = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(User, #{title => <<"CleanCal">>}),
{ok, Status, _, Body} = api_test_runner:client_request(post,
<<"/v1/calendars/", CalId/binary, "/events">>, User,
jsx:encode(#{
title => <<"Has ", BadWord/binary>>,
start_time => api_test_runner:future_date_iso8601(),
duration => 30
})),
?assertEqual(400, Status),
?assert(binary:match(list_to_binary(Body), <<"banned">>) =/= nomatch),
ct:pal(" OK").
test_reject_calendar_create(Admin, BadWord) ->
ct:pal(" TEST: reject policy returns 400 on calendar create"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"keyword_action">> => <<"reject">>
}),
User = api_test_runner:get_user_token(),
{ok, Status, _, _} = api_test_runner:client_request(post, <<"/v1/calendars">>, User,
jsx:encode(#{title => <<"Cal ", BadWord/binary>>, description => <<"x">>})),
?assertEqual(400, Status),
ct:pal(" OK").
test_report_threshold_freezes_event(Admin) ->
ct:pal(" TEST: 3 pending reports freeze event"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"keyword_action">> => <<"flag">>,
<<"report_threshold">> => 3
}),
Owner = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(Owner, #{title => <<"RepThrCal">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, Owner,
#{title => <<"Clean event">>,
start_time => api_test_runner:future_date_iso8601(),
duration => 60}),
lists:foreach(fun(_) ->
Tok = api_test_runner:get_user_token(),
{ok, 201, _, _} = api_test_runner:client_request(post, <<"/v1/reports">>, Tok,
jsx:encode(#{
target_type => <<"event">>,
target_id => EventId,
reason => <<"spam">>
}))
end, [1, 2, 3]),
Ev = api_test_runner:admin_get(<<"/v1/admin/events/", EventId/binary>>, Admin),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Ev)),
Hits = api_test_runner:admin_get(<<"/v1/admin/automod/hits?trigger=report_threshold">>, Admin),
?assert(lists:any(fun(H) -> maps:get(<<"entity_id">>, H) =:= EventId end, Hits)),
ct:pal(" OK").
test_report_on_review(Admin) ->
ct:pal(" TEST: report on review + threshold hides"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"report_threshold">> => 3
}),
Owner = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(Owner, #{title => <<"RevRepCal">>}),
Reviewer = api_test_runner:get_user_token(),
{ok, 201, _, RevBody} = api_test_runner:client_request(post, <<"/v1/reviews">>, Reviewer,
jsx:encode(#{
target_type => <<"calendar">>,
target_id => CalId,
rating => 2,
comment => <<"meh">>
})),
#{<<"id">> := ReviewId} = jsx:decode(list_to_binary(RevBody), [return_maps]),
lists:foreach(fun(_) ->
Tok = api_test_runner:get_user_token(),
{ok, 201, _, _} = api_test_runner:client_request(post, <<"/v1/reports">>, Tok,
jsx:encode(#{
target_type => <<"review">>,
target_id => ReviewId,
reason => <<"abuse">>
}))
end, [1, 2, 3]),
Rev = api_test_runner:admin_get(<<"/v1/admin/reviews/", ReviewId/binary>>, Admin),
?assertEqual(<<"hidden">>, maps:get(<<"status">>, Rev)),
ct:pal(" OK").
test_dismissed_reports_do_not_freeze(Admin) ->
ct:pal(" TEST: dismissed reports do not freeze"),
_ = api_test_runner:admin_put(<<"/v1/admin/automod/settings">>, Admin, #{
<<"report_threshold">> => 3
}),
Owner = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(Owner, #{title => <<"DismissCal">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, Owner,
#{title => <<"Still clean">>,
start_time => api_test_runner:future_date_iso8601(),
duration => 60}),
ReportIds = lists:map(fun(_) ->
Tok = api_test_runner:get_user_token(),
{ok, 201, _, B} = api_test_runner:client_request(post, <<"/v1/reports">>, Tok,
jsx:encode(#{
target_type => <<"event">>,
target_id => EventId,
reason => <<"noise">>
})),
maps:get(<<"id">>, jsx:decode(list_to_binary(B), [return_maps]))
end, [1, 2]),
lists:foreach(fun(Rid) ->
_ = api_test_runner:admin_put(<<"/v1/admin/reports/", Rid/binary>>, Admin,
#{<<"status">> => <<"dismissed">>})
end, ReportIds),
Tok3 = api_test_runner:get_user_token(),
{ok, 201, _, _} = api_test_runner:client_request(post, <<"/v1/reports">>, Tok3,
jsx:encode(#{
target_type => <<"event">>,
target_id => EventId,
reason => <<"one pending">>
})),
Ev = api_test_runner:admin_get(<<"/v1/admin/events/", EventId/binary>>, Admin),
?assertEqual(<<"active">>, maps:get(<<"status">>, Ev)),
ct:pal(" OK").