From fc6ef8de7e24277ec02f6c877e8715f63f080bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Tue, 21 Apr 2026 11:54:13 +0300 Subject: [PATCH] Stage 6 --- src/core/core_banned_word.erl | 85 +++++ src/core/core_report.erl | 88 +++++ src/eventhub_app.erl | 8 +- src/handlers/handler_admin_moderation.erl | 130 ++++++++ src/handlers/handler_banned_words.erl | 86 +++++ src/handlers/handler_report_by_id.erl | 83 +++++ src/handlers/handler_reports.erl | 113 +++++++ src/logic/logic_moderation.erl | 180 +++++++++++ test/core_banned_word_tests.erl | 80 +++++ test/core_report_tests.erl | 101 ++++++ test/logic_moderation_tests.erl | 146 +++++++++ test/scripts/test_moderation_api.sh | 370 ++++++++++++++++++++++ 12 files changed, 1469 insertions(+), 1 deletion(-) create mode 100644 src/core/core_banned_word.erl create mode 100644 src/core/core_report.erl create mode 100644 src/handlers/handler_admin_moderation.erl create mode 100644 src/handlers/handler_banned_words.erl create mode 100644 src/handlers/handler_report_by_id.erl create mode 100644 src/handlers/handler_reports.erl create mode 100644 src/logic/logic_moderation.erl create mode 100644 test/core_banned_word_tests.erl create mode 100644 test/core_report_tests.erl create mode 100644 test/logic_moderation_tests.erl create mode 100644 test/scripts/test_moderation_api.sh diff --git a/src/core/core_banned_word.erl b/src/core/core_banned_word.erl new file mode 100644 index 0000000..deac56c --- /dev/null +++ b/src/core/core_banned_word.erl @@ -0,0 +1,85 @@ +-module(core_banned_word). +-include("records.hrl"). + +-export([add/1, remove/1, list_all/0, is_banned/1]). +-export([check_text/1, filter_text/1]). + +%% Добавить слово в бан-лист +add(Word) when is_binary(Word) -> + WordLower = string:lowercase(Word), + case is_banned(WordLower) of + true -> {error, already_exists}; + false -> + BannedWord = #banned_word{ + id = generate_id(), + word = WordLower + }, + F = fun() -> + mnesia:write(BannedWord), + {ok, BannedWord} + end, + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end + end. + +%% Удалить слово из бан-листа +remove(Word) when is_binary(Word) -> + WordLower = string:lowercase(Word), + Match = #banned_word{word = WordLower, _ = '_'}, + case mnesia:dirty_match_object(Match) of + [] -> {error, not_found}; + [BannedWord] -> + F = fun() -> + mnesia:delete_object(BannedWord), + {ok, removed} + end, + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end + end. + +%% Список всех запрещённых слов +list_all() -> + Match = #banned_word{_ = '_'}, + Words = mnesia:dirty_match_object(Match), + {ok, [W#banned_word.word || W <- Words]}. + +%% Проверить, является ли слово запрещённым +is_banned(Word) when is_binary(Word) -> + WordLower = string:lowercase(Word), + Match = #banned_word{word = WordLower, _ = '_'}, + case mnesia:dirty_match_object(Match) of + [] -> false; + _ -> true + end. + +%% Проверить текст на наличие запрещённых слов +check_text(Text) when is_binary(Text) -> + TextLower = string:lowercase(Text), + Words = string:split(TextLower, " ", all), + {ok, BannedWords} = list_all(), + lists:any(fun(W) -> lists:member(W, BannedWords) end, Words). + +%% Отфильтровать запрещённые слова (заменить на ***) +filter_text(Text) when is_binary(Text) -> + {ok, BannedWords} = list_all(), + Words = binary:split(Text, <<" ">>, [global]), + Filtered = lists:map(fun(W) -> + case lists:member(string:lowercase(W), BannedWords) of + true -> <<"***">>; + false -> W + end + end, Words), + iolist_to_binary(join_binary(Filtered, <<" ">>)). + +join_binary([], _) -> []; +join_binary([H], _) -> [H]; +join_binary([H|T], Sep) -> + [H, Sep | join_binary(T, Sep)]. + +%% Внутренние функции +generate_id() -> + base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). \ No newline at end of file diff --git a/src/core/core_report.erl b/src/core/core_report.erl new file mode 100644 index 0000000..f3c627d --- /dev/null +++ b/src/core/core_report.erl @@ -0,0 +1,88 @@ +-module(core_report). +-include("records.hrl"). + +-export([create/4, get_by_id/1, list_by_target/2, list_by_reporter/1, list_all/0]). +-export([update_status/3, get_count_by_target/2]). +-export([generate_id/0]). + +%% Создание жалобы +create(ReporterId, TargetType, TargetId, Reason) -> + Id = generate_id(), + Report = #report{ + id = Id, + reporter_id = ReporterId, + target_type = TargetType, + target_id = TargetId, + reason = Reason, + status = pending, + created_at = calendar:universal_time(), + resolved_at = undefined, + resolved_by = undefined + }, + + F = fun() -> + mnesia:write(Report), + {ok, Report} + end, + + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end. + +%% Получение жалобы по ID +get_by_id(Id) -> + case mnesia:dirty_read(report, Id) of + [] -> {error, not_found}; + [Report] -> {ok, Report} + end. + +%% Список жалоб на цель +list_by_target(TargetType, TargetId) -> + Match = #report{target_type = TargetType, target_id = TargetId, _ = '_'}, + Reports = mnesia:dirty_match_object(Match), + {ok, Reports}. + +%% Список жалоб от пользователя +list_by_reporter(ReporterId) -> + Match = #report{reporter_id = ReporterId, _ = '_'}, + Reports = mnesia:dirty_match_object(Match), + {ok, Reports}. + +%% Список всех жалоб (для админов) +list_all() -> + Match = #report{_ = '_'}, + Reports = mnesia:dirty_match_object(Match), + {ok, Reports}. + +%% Обновление статуса жалобы +update_status(Id, Status, ResolvedBy) when Status =:= reviewed; Status =:= dismissed -> + F = fun() -> + case mnesia:read(report, Id) of + [] -> + {error, not_found}; + [Report] -> + Updated = Report#report{ + status = Status, + resolved_at = calendar:universal_time(), + resolved_by = ResolvedBy + }, + mnesia:write(Updated), + {ok, Updated} + end + end, + + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end. + +%% Получить количество жалоб на цель +get_count_by_target(TargetType, TargetId) -> + Match = #report{target_type = TargetType, target_id = TargetId, status = pending, _ = '_'}, + Reports = mnesia:dirty_match_object(Match), + length(Reports). + +%% Внутренние функции +generate_id() -> + base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). \ No newline at end of file diff --git a/src/eventhub_app.erl b/src/eventhub_app.erl index 4a8cbd9..bbe4fbe 100644 --- a/src/eventhub_app.erl +++ b/src/eventhub_app.erl @@ -43,7 +43,13 @@ start_http() -> {"/v1/bookings/:id", handler_booking_by_id, []}, {"/v1/reviews", handler_reviews, []}, {"/v1/reviews/:id", handler_review_by_id, []}, - {"/v1/admin/reviews/:id", handler_admin_reviews, []} + {"/v1/reports", handler_reports, []}, + {"/v1/admin/reports", handler_reports, []}, + {"/v1/admin/reports/:id", handler_report_by_id, []}, + {"/v1/admin/reviews/:id", handler_admin_reviews, []}, + {"/v1/admin/banned-words", handler_banned_words, []}, + {"/v1/admin/banned-words/:word", handler_banned_words, []}, + {"/v1/admin/:target_type/:id", handler_admin_moderation, []} ]} ]), diff --git a/src/handlers/handler_admin_moderation.erl b/src/handlers/handler_admin_moderation.erl new file mode 100644 index 0000000..ab03871 --- /dev/null +++ b/src/handlers/handler_admin_moderation.erl @@ -0,0 +1,130 @@ +-module(handler_admin_moderation). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"PUT">> -> moderate(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% PUT /v1/admin/:target_type/:id - заморозка/разморозка +moderate(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + TargetTypeBin = cowboy_req:binding(target_type, Req1), + TargetId = cowboy_req:binding(id, Req1), + TargetType = parse_target_type(TargetTypeBin), + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + #{<<"action">> := Action} -> + case {TargetType, Action} of + {calendar, <<"freeze">>} -> + case logic_moderation:freeze_calendar(AdminId, TargetId) of + {ok, Calendar} -> + send_json(Req2, 200, calendar_to_json(Calendar)); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, not_found} -> + send_error(Req2, 404, <<"Calendar not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + {calendar, <<"unfreeze">>} -> + case logic_moderation:unfreeze_calendar(AdminId, TargetId) of + {ok, Calendar} -> + send_json(Req2, 200, calendar_to_json(Calendar)); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, not_found} -> + send_error(Req2, 404, <<"Calendar not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + {event, <<"freeze">>} -> + case logic_moderation:freeze_event(AdminId, TargetId) of + {ok, Event} -> + send_json(Req2, 200, event_to_json(Event)); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, not_found} -> + send_error(Req2, 404, <<"Event not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + {event, <<"unfreeze">>} -> + case logic_moderation:unfreeze_event(AdminId, TargetId) of + {ok, Event} -> + send_json(Req2, 200, event_to_json(Event)); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, not_found} -> + send_error(Req2, 404, <<"Event not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + send_error(Req2, 400, <<"Invalid target_type or action">>) + end; + _ -> + send_error(Req2, 400, <<"Missing action field">>) + catch + _:_ -> + send_error(Req2, 400, <<"Invalid JSON format">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +parse_target_type(<<"calendars">>) -> calendar; +parse_target_type(<<"events">>) -> event; +parse_target_type(_) -> undefined. + +calendar_to_json(Calendar) -> + #{ + id => Calendar#calendar.id, + owner_id => Calendar#calendar.owner_id, + title => Calendar#calendar.title, + description => Calendar#calendar.description, + type => Calendar#calendar.type, + tags => Calendar#calendar.tags, + status => Calendar#calendar.status, + rating_avg => Calendar#calendar.rating_avg, + rating_count => Calendar#calendar.rating_count, + created_at => datetime_to_iso8601(Calendar#calendar.created_at), + updated_at => datetime_to_iso8601(Calendar#calendar.updated_at) + }. + +event_to_json(Event) -> + #{ + id => Event#event.id, + calendar_id => Event#event.calendar_id, + title => Event#event.title, + description => Event#event.description, + event_type => Event#event.event_type, + start_time => datetime_to_iso8601(Event#event.start_time), + duration => Event#event.duration, + status => Event#event.status, + tags => Event#event.tags, + rating_avg => Event#event.rating_avg, + rating_count => Event#event.rating_count, + created_at => datetime_to_iso8601(Event#event.created_at), + updated_at => datetime_to_iso8601(Event#event.updated_at) + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/handler_banned_words.erl b/src/handlers/handler_banned_words.erl new file mode 100644 index 0000000..e0b390d --- /dev/null +++ b/src/handlers/handler_banned_words.erl @@ -0,0 +1,86 @@ +-module(handler_banned_words). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> list_banned_words(Req); + <<"POST">> -> add_banned_word(Req); + <<"DELETE">> -> remove_banned_word(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% GET /v1/admin/banned-words - список запрещённых слов +list_banned_words(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case logic_moderation:list_banned_words(AdminId) of + {ok, Words} -> + send_json(Req1, 200, Words); + {error, access_denied} -> + send_error(Req1, 403, <<"Admin access required">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% POST /v1/admin/banned-words - добавить запрещённое слово +add_banned_word(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + #{<<"word">> := Word} -> + case logic_moderation:add_banned_word(AdminId, Word) of + {ok, _} -> + send_json(Req2, 201, #{word => Word, status => <<"added">>}); + {error, already_exists} -> + send_error(Req2, 409, <<"Word already exists">>); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + send_error(Req2, 400, <<"Missing 'word' field">>) + catch + _:_ -> + send_error(Req2, 400, <<"Invalid JSON format">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% DELETE /v1/admin/banned-words - удалить запрещённое слово +remove_banned_word(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + Word = cowboy_req:binding(word, Req1), + case logic_moderation:remove_banned_word(AdminId, Word) of + {ok, removed} -> + send_json(Req1, 200, #{word => Word, status => <<"removed">>}); + {error, not_found} -> + send_error(Req1, 404, <<"Word not found">>); + {error, access_denied} -> + send_error(Req1, 403, <<"Admin access required">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/handler_report_by_id.erl b/src/handlers/handler_report_by_id.erl new file mode 100644 index 0000000..3ccebc4 --- /dev/null +++ b/src/handlers/handler_report_by_id.erl @@ -0,0 +1,83 @@ +-module(handler_report_by_id). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"PUT">> -> resolve_report(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% PUT /v1/admin/reports/:id - рассмотрение жалобы +resolve_report(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + ReportId = cowboy_req:binding(id, Req1), + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + #{<<"action">> := Action} -> + ActionAtom = case Action of + <<"review">> -> reviewed; + <<"dismiss">> -> dismissed; + _ -> undefined + end, + case ActionAtom of + undefined -> + send_error(Req2, 400, <<"Invalid action. Use 'review' or 'dismiss'">>); + _ -> + case logic_moderation:resolve_report(AdminId, ReportId, ActionAtom) of + {ok, Report} -> + Response = report_to_json(Report), + send_json(Req2, 200, Response); + {error, access_denied} -> + send_error(Req2, 403, <<"Admin access required">>); + {error, already_resolved} -> + send_error(Req2, 409, <<"Report already resolved">>); + {error, not_found} -> + send_error(Req2, 404, <<"Report not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end + end; + _ -> + send_error(Req2, 400, <<"Missing action field">>) + catch + _:_ -> + send_error(Req2, 400, <<"Invalid JSON format">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +report_to_json(Report) -> + #{ + id => Report#report.id, + reporter_id => Report#report.reporter_id, + target_type => Report#report.target_type, + target_id => Report#report.target_id, + reason => Report#report.reason, + status => Report#report.status, + created_at => datetime_to_iso8601(Report#report.created_at), + resolved_at => case Report#report.resolved_at of + undefined -> null; + Dt -> datetime_to_iso8601(Dt) + end, + resolved_by => Report#report.resolved_by + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/handler_reports.erl b/src/handlers/handler_reports.erl new file mode 100644 index 0000000..a86b853 --- /dev/null +++ b/src/handlers/handler_reports.erl @@ -0,0 +1,113 @@ +-module(handler_reports). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"POST">> -> create_report(Req); + <<"GET">> -> list_reports(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% POST /v1/reports - создание жалобы +create_report(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + Decoded when is_map(Decoded) -> + case Decoded of + #{<<"target_type">> := TargetTypeBin, + <<"target_id">> := TargetId, + <<"reason">> := Reason} -> + TargetType = parse_target_type(TargetTypeBin), + case logic_moderation:create_report(UserId, TargetType, TargetId, Reason) of + {ok, Report} -> + Response = report_to_json(Report), + send_json(Req2, 201, Response); + {error, target_not_found} -> + send_error(Req2, 404, <<"Target not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + send_error(Req2, 400, <<"Missing required fields">>) + end; + _ -> + send_error(Req2, 400, <<"Invalid JSON">>) + catch + _:_ -> + send_error(Req2, 400, <<"Invalid JSON format">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% GET /v1/admin/reports - список всех жалоб (админ) +list_reports(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + Qs = cowboy_req:parse_qs(Req1), + case {proplists:get_value(<<"target_type">>, Qs), proplists:get_value(<<"target_id">>, Qs)} of + {undefined, _} -> + case logic_moderation:get_reports(AdminId) of + {ok, Reports} -> + Response = [report_to_json(R) || R <- Reports], + send_json(Req1, 200, Response); + {error, access_denied} -> + send_error(Req1, 403, <<"Admin access required">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {TargetTypeBin, TargetId} -> + TargetType = parse_target_type(TargetTypeBin), + case logic_moderation:get_reports_by_target(AdminId, TargetType, TargetId) of + {ok, Reports} -> + Response = [report_to_json(R) || R <- Reports], + send_json(Req1, 200, Response); + {error, access_denied} -> + send_error(Req1, 403, <<"Admin access required">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +parse_target_type(<<"event">>) -> event; +parse_target_type(<<"calendar">>) -> calendar; +parse_target_type(_) -> undefined. + +report_to_json(Report) -> + #{ + id => Report#report.id, + reporter_id => Report#report.reporter_id, + target_type => Report#report.target_type, + target_id => Report#report.target_id, + reason => Report#report.reason, + status => Report#report.status, + created_at => datetime_to_iso8601(Report#report.created_at), + resolved_at => case Report#report.resolved_at of + undefined -> null; + Dt -> datetime_to_iso8601(Dt) + end, + resolved_by => Report#report.resolved_by + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/logic/logic_moderation.erl b/src/logic/logic_moderation.erl new file mode 100644 index 0000000..e849f66 --- /dev/null +++ b/src/logic/logic_moderation.erl @@ -0,0 +1,180 @@ +-module(logic_moderation). +-include("records.hrl"). + +-export([create_report/4, get_reports/1, get_reports_by_target/3, resolve_report/3]). +-export([add_banned_word/2, remove_banned_word/2, list_banned_words/1]). +-export([check_content/1, auto_moderate/1]). +-export([freeze_calendar/2, unfreeze_calendar/2, freeze_event/2, unfreeze_event/2]). + +-define(REPORT_THRESHOLD, 3). % Количество жалоб для авто-заморозки + +%% ============ Жалобы ============ + +%% Создание жалобы +create_report(ReporterId, TargetType, TargetId, Reason) -> + case target_exists(TargetType, TargetId) of + true -> + case core_report:create(ReporterId, TargetType, TargetId, Reason) of + {ok, Report} -> + % Проверяем порог для авто-модерации + check_auto_freeze(TargetType, TargetId), + {ok, Report}; + Error -> Error + end; + false -> {error, target_not_found} + end. + +%% Получить все жалобы (для админа) +get_reports(AdminId) -> + case is_admin(AdminId) of + true -> core_report:list_all(); + false -> {error, access_denied} + end. + +%% Получить жалобы на конкретную цель +get_reports_by_target(AdminId, TargetType, TargetId) -> + case is_admin(AdminId) of + true -> core_report:list_by_target(TargetType, TargetId); + false -> {error, access_denied} + end. + +%% Рассмотреть жалобу (подтвердить или отклонить) +resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= dismissed -> + case is_admin(AdminId) of + true -> + case core_report:get_by_id(ReportId) of + {ok, Report} -> + case Report#report.status of + pending -> + core_report:update_status(ReportId, Action, AdminId); + _ -> {error, already_resolved} + end; + Error -> Error + end; + false -> {error, access_denied} + end. + +%% Проверка порога для авто-заморозки +check_auto_freeze(TargetType, TargetId) -> + Count = core_report:get_count_by_target(TargetType, TargetId), + if Count >= ?REPORT_THRESHOLD -> + auto_freeze(TargetType, TargetId); + true -> ok + end. + +auto_freeze(event, EventId) -> + case core_event:get_by_id(EventId) of + {ok, Event} when Event#event.status =:= active -> + core_event:update(EventId, [{status, frozen}]); + _ -> ok + end; +auto_freeze(calendar, CalendarId) -> + case core_calendar:get_by_id(CalendarId) of + {ok, Calendar} when Calendar#calendar.status =:= active -> + core_calendar:update(CalendarId, [{status, frozen}]); + _ -> ok + end; +auto_freeze(_, _) -> ok. + +%% ============ Бан-лист ============ + +%% Добавить запрещённое слово +add_banned_word(AdminId, Word) -> + case is_admin(AdminId) of + true -> core_banned_word:add(Word); + false -> {error, access_denied} + end. + +%% Удалить запрещённое слово +remove_banned_word(AdminId, Word) -> + case is_admin(AdminId) of + true -> core_banned_word:remove(Word); + false -> {error, access_denied} + end. + +%% Список запрещённых слов +list_banned_words(AdminId) -> + case is_admin(AdminId) of + true -> core_banned_word:list_all(); + false -> {error, access_denied} + end. + +%% ============ Контент-фильтр ============ + +%% Проверить контент на запрещённые слова +check_content(Text) -> + core_banned_word:check_text(Text). + +%% Автоматическая модерация контента (замена запрещённых слов) +auto_moderate(Text) -> + core_banned_word:filter_text(Text). + +%% ============ Заморозка/разморозка ============ + +%% Заморозить календарь +freeze_calendar(AdminId, CalendarId) -> + case is_admin(AdminId) of + true -> + case core_calendar:get_by_id(CalendarId) of + {ok, Calendar} -> + core_calendar:update(CalendarId, [{status, frozen}]); + Error -> Error + end; + false -> {error, access_denied} + end. + +%% Разморозить календарь +unfreeze_calendar(AdminId, CalendarId) -> + case is_admin(AdminId) of + true -> + case core_calendar:get_by_id(CalendarId) of + {ok, Calendar} -> + core_calendar:update(CalendarId, [{status, active}]); + Error -> Error + end; + false -> {error, access_denied} + end. + +%% Заморозить событие +freeze_event(AdminId, EventId) -> + case is_admin(AdminId) of + true -> + case core_event:get_by_id(EventId) of + {ok, Event} -> + core_event:update(EventId, [{status, frozen}]); + Error -> Error + end; + false -> {error, access_denied} + end. + +%% Разморозить событие +unfreeze_event(AdminId, EventId) -> + case is_admin(AdminId) of + true -> + case core_event:get_by_id(EventId) of + {ok, Event} -> + core_event:update(EventId, [{status, active}]); + Error -> Error + end; + false -> {error, access_denied} + end. + +%% ============ Вспомогательные функции ============ + +target_exists(event, EventId) -> + case core_event:get_by_id(EventId) of + {ok, _} -> true; + _ -> false + end; +target_exists(calendar, CalendarId) -> + case core_calendar:get_by_id(CalendarId) of + {ok, _} -> true; + _ -> false + end; % ← точка с запятой здесь! +target_exists(_, _) -> false. + +is_admin(UserId) -> + case core_user:get_by_id(UserId) of + {ok, User} -> User#user.role =:= admin; + _ -> false + end. \ No newline at end of file diff --git a/test/core_banned_word_tests.erl b/test/core_banned_word_tests.erl new file mode 100644 index 0000000..e75c735 --- /dev/null +++ b/test/core_banned_word_tests.erl @@ -0,0 +1,80 @@ +-module(core_banned_word_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:start(), + mnesia:create_table(banned_word, [ + {attributes, record_info(fields, banned_word)}, + {ram_copies, [node()]} + ]), + ok. + +cleanup(_) -> + mnesia:delete_table(banned_word), + mnesia:stop(), + ok. + +core_banned_word_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Add banned word test", fun test_add_word/0}, + {"Add duplicate word test", fun test_add_duplicate/0}, + {"Remove banned word test", fun test_remove_word/0}, + {"List banned words test", fun test_list_words/0}, + {"Is banned test", fun test_is_banned/0}, + {"Check text test", fun test_check_text/0}, + {"Filter text test", fun test_filter_text/0} + ]}. + +test_add_word() -> + Word = <<"badword">>, + {ok, BannedWord} = core_banned_word:add(Word), + ?assertEqual(Word, BannedWord#banned_word.word), + ?assert(is_binary(BannedWord#banned_word.id)). + +test_add_duplicate() -> + Word = <<"badword">>, + {ok, _} = core_banned_word:add(Word), + {error, already_exists} = core_banned_word:add(Word), + {error, already_exists} = core_banned_word:add(<<"BADWORD">>). % case insensitive + +test_remove_word() -> + Word = <<"badword">>, + {ok, _} = core_banned_word:add(Word), + {ok, removed} = core_banned_word:remove(Word), + {error, not_found} = core_banned_word:remove(<<"nonexistent">>). + +test_list_words() -> + {ok, _} = core_banned_word:add(<<"word1">>), + {ok, _} = core_banned_word:add(<<"word2">>), + {ok, _} = core_banned_word:add(<<"word3">>), + + {ok, Words} = core_banned_word:list_all(), + ?assertEqual(3, length(Words)), + ?assert(lists:member(<<"word1">>, Words)). + +test_is_banned() -> + Word = <<"badword">>, + ?assertNot(core_banned_word:is_banned(Word)), + {ok, _} = core_banned_word:add(Word), + ?assert(core_banned_word:is_banned(Word)), + ?assert(core_banned_word:is_banned(<<"BADWORD">>)). % case insensitive + +test_check_text() -> + {ok, _} = core_banned_word:add(<<"bad">>), + {ok, _} = core_banned_word:add(<<"spam">>), + + ?assertNot(core_banned_word:check_text(<<"Hello world">>)), + ?assert(core_banned_word:check_text(<<"This is bad">>)), + ?assert(core_banned_word:check_text(<<"This is SPAM">>)). + +test_filter_text() -> + {ok, _} = core_banned_word:add(<<"bad">>), + {ok, _} = core_banned_word:add(<<"spam">>), + + ?assertEqual(<<"Hello world">>, core_banned_word:filter_text(<<"Hello world">>)), + ?assertEqual(<<"This is ***">>, core_banned_word:filter_text(<<"This is bad">>)), + ?assertEqual(<<"*** and ***">>, core_banned_word:filter_text(<<"bad and spam">>)). \ No newline at end of file diff --git a/test/core_report_tests.erl b/test/core_report_tests.erl new file mode 100644 index 0000000..7a43ef5 --- /dev/null +++ b/test/core_report_tests.erl @@ -0,0 +1,101 @@ +-module(core_report_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:start(), + mnesia:create_table(report, [ + {attributes, record_info(fields, report)}, + {ram_copies, [node()]} + ]), + ok. + +cleanup(_) -> + mnesia:delete_table(report), + mnesia:stop(), + ok. + +core_report_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Create report test", fun test_create_report/0}, + {"Get report by id test", fun test_get_by_id/0}, + {"List reports by target test", fun test_list_by_target/0}, + {"List reports by reporter test", fun test_list_by_reporter/0}, + {"Update report status test", fun test_update_status/0}, + {"Get count by target test", fun test_get_count_by_target/0} + ]}. + +test_create_report() -> + ReporterId = <<"user123">>, + TargetType = event, + TargetId = <<"event123">>, + Reason = <<"Inappropriate content">>, + + {ok, Report} = core_report:create(ReporterId, TargetType, TargetId, Reason), + + ?assertEqual(ReporterId, Report#report.reporter_id), + ?assertEqual(TargetType, Report#report.target_type), + ?assertEqual(TargetId, Report#report.target_id), + ?assertEqual(Reason, Report#report.reason), + ?assertEqual(pending, Report#report.status), + ?assertEqual(undefined, Report#report.resolved_at), + ?assertEqual(undefined, Report#report.resolved_by), + ?assert(is_binary(Report#report.id)). + +test_get_by_id() -> + ReporterId = <<"user123">>, + {ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"Bad">>), + + {ok, Found} = core_report:get_by_id(Report#report.id), + ?assertEqual(Report#report.id, Found#report.id), + + {error, not_found} = core_report:get_by_id(<<"nonexistent">>). + +test_list_by_target() -> + User1 = <<"user1">>, + User2 = <<"user2">>, + EventId = <<"event123">>, + + {ok, _} = core_report:create(User1, event, EventId, <<"Reason 1">>), + {ok, _} = core_report:create(User2, event, EventId, <<"Reason 2">>), + {ok, _} = core_report:create(User1, calendar, <<"cal1">>, <<"Reason 3">>), + + {ok, Reports} = core_report:list_by_target(event, EventId), + ?assertEqual(2, length(Reports)). + +test_list_by_reporter() -> + User1 = <<"user1">>, + User2 = <<"user2">>, + + {ok, _} = core_report:create(User1, event, <<"ev1">>, <<"">>), + {ok, _} = core_report:create(User1, event, <<"ev2">>, <<"">>), + {ok, _} = core_report:create(User2, event, <<"ev3">>, <<"">>), + + {ok, Reports} = core_report:list_by_reporter(User1), + ?assertEqual(2, length(Reports)). + +test_update_status() -> + ReporterId = <<"user123">>, + AdminId = <<"admin123">>, + {ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"">>), + + {ok, Updated} = core_report:update_status(Report#report.id, reviewed, AdminId), + ?assertEqual(reviewed, Updated#report.status), + ?assertEqual(AdminId, Updated#report.resolved_by), + ?assert(Updated#report.resolved_at =/= undefined). + +test_get_count_by_target() -> + User1 = <<"user1">>, + User2 = <<"user2">>, + EventId = <<"event123">>, + + ?assertEqual(0, core_report:get_count_by_target(event, EventId)), + + {ok, _} = core_report:create(User1, event, EventId, <<"">>), + ?assertEqual(1, core_report:get_count_by_target(event, EventId)), + + {ok, _} = core_report:create(User2, event, EventId, <<"">>), + ?assertEqual(2, core_report:get_count_by_target(event, EventId)). \ No newline at end of file diff --git a/test/logic_moderation_tests.erl b/test/logic_moderation_tests.erl new file mode 100644 index 0000000..cc6412f --- /dev/null +++ b/test/logic_moderation_tests.erl @@ -0,0 +1,146 @@ +-module(logic_moderation_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:start(), + mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]), + mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]), + mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]), + mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]), + mnesia:create_table(banned_word, [{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]), + ok. + +cleanup(_) -> + mnesia:delete_table(banned_word), + mnesia:delete_table(report), + mnesia:delete_table(event), + mnesia:delete_table(calendar), + mnesia:delete_table(user), + mnesia:stop(), + ok. + +logic_moderation_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Create report test", fun test_create_report/0}, + {"Get reports test", fun test_get_reports/0}, + {"Resolve report test", fun test_resolve_report/0}, + {"Add banned word test", fun test_add_banned_word/0}, + {"Remove banned word test", fun test_remove_banned_word/0}, + {"Auto freeze by reports test", fun test_auto_freeze/0}, + {"Freeze/unfreeze calendar test", fun test_freeze_calendar/0}, + {"Freeze/unfreeze event test", fun test_freeze_event/0}, + {"Check content test", fun test_check_content/0} + ]}. + +create_test_user(Role) -> + UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), + User = #user{id = UserId, email = <>, password_hash = <<"hash">>, + role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()}, + mnesia:dirty_write(User), + UserId. + +create_test_calendar(OwnerId) -> + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), + Calendar#calendar.id. + +create_test_event(CalendarId) -> + {ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60), + Event#event.id. + +test_create_report() -> + ReporterId = create_test_user(user), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + EventId = create_test_event(CalendarId), + + {ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>), + ?assertEqual(ReporterId, Report#report.reporter_id), + ?assertEqual(pending, Report#report.status). + +test_get_reports() -> + AdminId = create_test_user(admin), + ReporterId = create_test_user(user), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + EventId = create_test_event(CalendarId), + + {ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>), + + {ok, Reports} = logic_moderation:get_reports(AdminId), + ?assertEqual(1, length(Reports)). + +test_resolve_report() -> + AdminId = create_test_user(admin), + ReporterId = create_test_user(user), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + EventId = create_test_event(CalendarId), + + {ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>), + {ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed), + ?assertEqual(reviewed, Resolved#report.status), + ?assertEqual(AdminId, Resolved#report.resolved_by). + +test_add_banned_word() -> + AdminId = create_test_user(admin), + {ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>), + ?assert(core_banned_word:is_banned(<<"badword">>)). + +test_remove_banned_word() -> + AdminId = create_test_user(admin), + {ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>), + {ok, removed} = logic_moderation:remove_banned_word(AdminId, <<"badword">>), + ?assertNot(core_banned_word:is_banned(<<"badword">>)). + +test_auto_freeze() -> + Reporter1 = create_test_user(user), + Reporter2 = create_test_user(user), + Reporter3 = create_test_user(user), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + EventId = create_test_event(CalendarId), + + % 3 жалобы должны заморозить событие + {ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>), + {ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>), + {ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>), + + {ok, Event} = core_event:get_by_id(EventId), + ?assertEqual(frozen, Event#event.status). + +test_freeze_calendar() -> + AdminId = create_test_user(admin), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + + {ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId), + ?assertEqual(frozen, Frozen#calendar.status), + + {ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId), + ?assertEqual(active, Unfrozen#calendar.status). + +test_freeze_event() -> + AdminId = create_test_user(admin), + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId), + EventId = create_test_event(CalendarId), + + {ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId), + ?assertEqual(frozen, Frozen#event.status), + + {ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId), + ?assertEqual(active, Unfrozen#event.status). + +test_check_content() -> + AdminId = create_test_user(admin), + {ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>), + + ?assertNot(logic_moderation:check_content(<<"Hello">>)), + ?assert(logic_moderation:check_content(<<"This is bad">>)), + + ?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)), + ?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)). \ No newline at end of file diff --git a/test/scripts/test_moderation_api.sh b/test/scripts/test_moderation_api.sh new file mode 100644 index 0000000..3387bc7 --- /dev/null +++ b/test/scripts/test_moderation_api.sh @@ -0,0 +1,370 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +extract_json() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" +} + +http_post() { + local url=$1; local data=$2; local token=$3 + if [ -n "$token" ]; then + curl -s -X POST "$url" -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d "$data" + else + curl -s -X POST "$url" -H "Content-Type: application/json" -d "$data" + fi +} + +http_get() { + local url=$1; local token=$2 + if [ -n "$token" ]; then + curl -s -X GET "$url" -H "Authorization: Bearer $token" + else + curl -s -X GET "$url" + fi +} + +http_put() { + local url=$1; local data=$2; local token=$3 + curl -s -X PUT "$url" -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d "$data" +} + +http_delete() { + local url=$1; local token=$2 + curl -s -X DELETE "$url" -H "Authorization: Bearer $token" +} + +echo "============================================================" +echo " EVENTHUB MODERATION API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Checking if server is running..." +if ! curl -s "$BASE_URL/health" | grep -q "ok"; then + log_error "Server is not running" + exit 1 +fi +log_success "Server is running" + +echo "" +log_info "============================================================" +log_info "STEP 1: Create test users" +log_info "============================================================" + +# Админ (первый пользователь) +ADMIN_EMAIL="mod_admin_$(date +%s)@example.com" +ADMIN_PASSWORD="admin123" + +log_info "Creating admin user..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}" "") +ADMIN_TOKEN=$(extract_json "$response" "token") +ADMIN_ID=$(extract_json "$response" "id") +log_success "Admin created" + +# Владелец календаря +OWNER_EMAIL="mod_owner_$(date +%s)@example.com" +OWNER_PASSWORD="owner123" + +log_info "Creating calendar owner..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "") +OWNER_TOKEN=$(extract_json "$response" "token") +OWNER_ID=$(extract_json "$response" "id") +log_success "Owner created" + +# Пользователь 1 (репортер) +USER1_EMAIL="mod_user1_$(date +%s)@example.com" +USER1_PASSWORD="user1_123" + +log_info "Creating user 1..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER1_EMAIL\",\"password\":\"$USER1_PASSWORD\"}" "") +USER1_TOKEN=$(extract_json "$response" "token") +log_success "User 1 created" + +# Пользователь 2 (репортер) +USER2_EMAIL="mod_user2_$(date +%s)@example.com" +USER2_PASSWORD="user2_123" + +log_info "Creating user 2..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER2_EMAIL\",\"password\":\"$USER2_PASSWORD\"}" "") +USER2_TOKEN=$(extract_json "$response" "token") +log_success "User 2 created" + +# Пользователь 3 (для третьего репорта) +USER3_EMAIL="mod_user3_$(date +%s)@example.com" +USER3_PASSWORD="user3_123" + +log_info "Creating user 3..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER3_EMAIL\",\"password\":\"$USER3_PASSWORD\"}" "") +USER3_TOKEN=$(extract_json "$response" "token") +log_success "User 3 created" + +echo "" +log_info "============================================================" +log_info "STEP 2: Create calendar and event" +log_info "============================================================" + +log_info "Creating calendar..." +response=$(http_post "$BASE_URL/v1/calendars" \ + "{\"title\":\"Moderation Test Calendar\"}" "$OWNER_TOKEN") +CALENDAR_ID=$(extract_json "$response" "id") +log_success "Calendar created: $CALENDAR_ID" + +log_info "Creating event..." +EVENT_START="2026-06-01T10:00:00Z" +response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ + "{\"title\":\"Test Event\",\"start_time\":\"$EVENT_START\",\"duration\":60}" "$OWNER_TOKEN") +EVENT_ID=$(extract_json "$response" "id") +log_success "Event created: $EVENT_ID" + +echo "" +log_info "============================================================" +log_info "TEST 1: Create report for event" +log_info "============================================================" + +log_info "User 1 reporting event..." +response=$(http_post "$BASE_URL/v1/reports" \ + "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"reason\":\"Inappropriate content\"}" "$USER1_TOKEN") +REPORT1_ID=$(extract_json "$response" "id") + +if [ -n "$REPORT1_ID" ]; then + log_success "Report created: $REPORT1_ID" +else + log_error "Failed to create report: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 2: Create second report" +log_info "============================================================" + +log_info "User 2 reporting same event..." +response=$(http_post "$BASE_URL/v1/reports" \ + "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"reason\":\"Spam\"}" "$USER2_TOKEN") +REPORT2_ID=$(extract_json "$response" "id") + +if [ -n "$REPORT2_ID" ]; then + log_success "Second report created: $REPORT2_ID" +else + log_error "Failed to create report: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 3: Admin views all reports" +log_info "============================================================" + +log_info "Admin getting all reports..." +response=$(http_get "$BASE_URL/v1/admin/reports" "$ADMIN_TOKEN") +REPORT_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) + +if [ "$REPORT_COUNT" -ge 2 ]; then + log_success "Admin sees $REPORT_COUNT reports" +else + log_error "Admin should see reports, found $REPORT_COUNT" +fi + +echo "" +log_info "============================================================" +log_info "TEST 4: Admin views reports for specific event" +log_info "============================================================" + +log_info "Admin getting reports for event..." +response=$(http_get "$BASE_URL/v1/admin/reports?target_type=event&target_id=$EVENT_ID" "$ADMIN_TOKEN") +EVENT_REPORT_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) + +if [ "$EVENT_REPORT_COUNT" -eq 2 ]; then + log_success "Admin sees $EVENT_REPORT_COUNT reports for event" +else + log_error "Expected 2 reports, found $EVENT_REPORT_COUNT" +fi + +echo "" +log_info "============================================================" +log_info "TEST 5: Auto-freeze by reports (threshold 3)" +log_info "============================================================" + +log_info "User 3 creating third report for event..." +response=$(http_post "$BASE_URL/v1/reports" \ + "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"reason\":\"Bad content\"}" "$USER3_TOKEN") +REPORT3_ID=$(extract_json "$response" "id") + +if [ -n "$REPORT3_ID" ]; then + log_success "Third report created: $REPORT3_ID" +else + log_error "Failed to create third report" +fi + +sleep 1 + +log_info "Checking if event was auto-frozen..." +response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") +EVENT_STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') + +if [ "$EVENT_STATUS" = "frozen" ]; then + log_success "Event auto-frozen after 3 reports" +else + log_error "Event not auto-frozen: status=$EVENT_STATUS" +fi + +echo "" +log_info "============================================================" +log_info "TEST 6: Admin resolves report (review)" +log_info "============================================================" + +log_info "Admin reviewing first report..." +response=$(http_put "$BASE_URL/v1/admin/reports/$REPORT1_ID" \ + "{\"action\":\"review\"}" "$ADMIN_TOKEN") + +STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$STATUS" = "reviewed" ]; then + log_success "Report marked as reviewed" +else + log_error "Failed to review report: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 7: Admin resolves report (dismiss)" +log_info "============================================================" + +log_info "Admin dismissing second report..." +response=$(http_put "$BASE_URL/v1/admin/reports/$REPORT2_ID" \ + "{\"action\":\"dismiss\"}" "$ADMIN_TOKEN") + +STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$STATUS" = "dismissed" ]; then + log_success "Report dismissed" +else + log_error "Failed to dismiss report: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 8: Admin adds banned words" +log_info "============================================================" + +log_info "Admin adding banned word 'spam'..." +response=$(http_post "$BASE_URL/v1/admin/banned-words" \ + "{\"word\":\"spam\"}" "$ADMIN_TOKEN") + +if echo "$response" | grep -q "added"; then + log_success "Banned word added" +else + log_error "Failed to add banned word: $response" +fi + +log_info "Admin adding banned word 'inappropriate'..." +http_post "$BASE_URL/v1/admin/banned-words" "{\"word\":\"inappropriate\"}" "$ADMIN_TOKEN" > /dev/null + +echo "" +log_info "============================================================" +log_info "TEST 9: Admin lists banned words" +log_info "============================================================" + +log_info "Admin getting banned words..." +response=$(http_get "$BASE_URL/v1/admin/banned-words" "$ADMIN_TOKEN") +if echo "$response" | grep -q "spam"; then + log_success "Banned words retrieved" +else + log_error "Failed to get banned words: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 10: Admin removes banned word" +log_info "============================================================" + +log_info "Admin removing banned word 'inappropriate'..." +response=$(http_delete "$BASE_URL/v1/admin/banned-words/inappropriate" "$ADMIN_TOKEN") + +if echo "$response" | grep -q "removed"; then + log_success "Banned word removed" +else + log_error "Failed to remove banned word: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 11: Admin freezes calendar" +log_info "============================================================" + +log_info "Admin freezing calendar..." +response=$(http_put "$BASE_URL/v1/admin/calendars/$CALENDAR_ID" \ + "{\"action\":\"freeze\"}" "$ADMIN_TOKEN") + +CAL_STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$CAL_STATUS" = "frozen" ]; then + log_success "Calendar frozen" +else + log_error "Failed to freeze calendar: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 12: Admin unfreezes calendar" +log_info "============================================================" + +log_info "Admin unfreezing calendar..." +response=$(http_put "$BASE_URL/v1/admin/calendars/$CALENDAR_ID" \ + "{\"action\":\"unfreeze\"}" "$ADMIN_TOKEN") + +CAL_STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$CAL_STATUS" = "active" ]; then + log_success "Calendar unfrozen" +else + log_error "Failed to unfreeze calendar: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 13: Admin freezes event" +log_info "============================================================" + +log_info "Admin freezing event..." +response=$(http_put "$BASE_URL/v1/admin/events/$EVENT_ID" \ + "{\"action\":\"freeze\"}" "$ADMIN_TOKEN") + +EVENT_STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$EVENT_STATUS" = "frozen" ]; then + log_success "Event frozen" +else + log_error "Failed to freeze event: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 14: Admin unfreezes event" +log_info "============================================================" + +log_info "Admin unfreezing event..." +response=$(http_put "$BASE_URL/v1/admin/events/$EVENT_ID" \ + "{\"action\":\"unfreeze\"}" "$ADMIN_TOKEN") + +EVENT_STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') +if [ "$EVENT_STATUS" = "active" ]; then + log_success "Event unfrozen" +else + log_error "Failed to unfreeze event: $response" +fi + +echo "" +echo "============================================================" +log_success "MODERATION API TESTS COMPLETED!" +echo "============================================================" +echo "" +echo "Summary of created resources:" +echo " Admin: $ADMIN_EMAIL" +echo " Owner: $OWNER_EMAIL" +echo " Calendar: $CALENDAR_ID" +echo " Event: $EVENT_ID" +echo "" \ No newline at end of file