From e3a08cfa04a3b912029de8897f4b7db5d782aac6 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 17:03:09 +0300 Subject: [PATCH] Stage 9 --- src/admin_app.erl | 49 +++++ src/config/sys.config | 2 +- src/eventhub_app.erl | 29 ++- src/handlers/admin_handler_health.erl | 7 + src/handlers/admin_handler_stats.erl | 78 +++++++ src/handlers/admin_handler_user_by_id.erl | 127 +++++++++++ src/handlers/admin_handler_users.erl | 57 +++++ test/admin_handler_stats_tests.erl | 107 +++++++++ test/admin_handler_user_by_id_tests.erl | 28 +++ test/admin_handler_users_tests.erl | 45 ++++ test/scripts/test_admin_api.sh | 256 ++++++++++++++++++++++ 11 files changed, 783 insertions(+), 2 deletions(-) create mode 100644 src/admin_app.erl create mode 100644 src/handlers/admin_handler_health.erl create mode 100644 src/handlers/admin_handler_stats.erl create mode 100644 src/handlers/admin_handler_user_by_id.erl create mode 100644 src/handlers/admin_handler_users.erl create mode 100644 test/admin_handler_stats_tests.erl create mode 100644 test/admin_handler_user_by_id_tests.erl create mode 100644 test/admin_handler_users_tests.erl create mode 100644 test/scripts/test_admin_api.sh diff --git a/src/admin_app.erl b/src/admin_app.erl new file mode 100644 index 0000000..f3c27e7 --- /dev/null +++ b/src/admin_app.erl @@ -0,0 +1,49 @@ +-module(admin_app). +-behaviour(application). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + application:ensure_all_started(cowboy), + start_admin_http(), + {ok, self()}. + +stop(_State) -> + ok. + +start_admin_http() -> + Port = application:get_env(eventhub, admin_http_port, 8445), + + Dispatch = cowboy_router:compile([ + {'_', [ + {"/admin/health", admin_handler_health, []}, + {"/admin/stats", admin_handler_stats, []}, + {"/admin/users", admin_handler_users, []}, + {"/admin/users/:id", admin_handler_user_by_id, []}, + {"/admin/calendars", admin_handler_calendars, []}, + {"/admin/calendars/:id", admin_handler_calendar_by_id, []}, + {"/admin/events", admin_handler_events, []}, + {"/admin/events/:id", admin_handler_event_by_id, []}, + {"/admin/reports", handler_reports, []}, + {"/admin/reports/:id", handler_report_by_id, []}, + {"/admin/tickets", handler_tickets, []}, + {"/admin/tickets/:id", handler_ticket_by_id, []}, + {"/admin/tickets/stats", handler_ticket_stats, []}, + {"/admin/subscriptions", handler_admin_subscriptions, []}, + {"/admin/banned-words", handler_banned_words, []} + ]} + ]), + + Middlewares = [ + cowboy_router, + cowboy_handler + ], + + Env = #{dispatch => Dispatch}, + + cowboy:start_clear(admin_http, [{port, Port}], #{ + env => Env, + middlewares => Middlewares + }), + + io:format("Admin HTTP server started on port ~p~n", [Port]). \ No newline at end of file diff --git a/src/config/sys.config b/src/config/sys.config index 072024a..7d3270f 100644 --- a/src/config/sys.config +++ b/src/config/sys.config @@ -4,7 +4,7 @@ {ws_port, 8081}, {admin_http_port, 8445}, {admin_ws_port, 8446}, - {jwt_secret, <<"change_me_in_production">>}, + {jwt_secret, <<"my-super-secret-key-for-jwt-32-bytes!">>}, {argon2_params, #{t_cost => 2, m_cost => 19, parallelism => 1}} ]}, {mnesia, [ diff --git a/src/eventhub_app.erl b/src/eventhub_app.erl index 6c54b0c..a01b9d1 100644 --- a/src/eventhub_app.erl +++ b/src/eventhub_app.erl @@ -12,6 +12,7 @@ start(_StartType, _StartArgs) -> ok = infra_mnesia:init_tables(), ok = infra_mnesia:wait_for_tables(), start_http(), + start_admin_http(), {ok, Pid}; Error -> Error @@ -76,4 +77,30 @@ start_http() -> middlewares => Middlewares }), - io:format("HTTP server started on port ~p~n", [Port]). \ No newline at end of file + io:format("HTTP server started on port ~p~n", [Port]). + +start_admin_http() -> + Port = application:get_env(eventhub, admin_http_port, 8445), + + Dispatch = cowboy_router:compile([ + {'_', [ + {"/admin/health", admin_handler_health, []}, + {"/admin/stats", admin_handler_stats, []}, + {"/admin/users", admin_handler_users, []}, + {"/admin/users/:id", admin_handler_user_by_id, []} + ]} + ]), + + Middlewares = [ + cowboy_router, + cowboy_handler + ], + + Env = #{dispatch => Dispatch}, + + cowboy:start_clear(admin_http, [{port, Port}], #{ + env => Env, + middlewares => Middlewares + }), + + io:format("Admin HTTP server started on port ~p~n", [Port]). \ No newline at end of file diff --git a/src/handlers/admin_handler_health.erl b/src/handlers/admin_handler_health.erl new file mode 100644 index 0000000..250c81f --- /dev/null +++ b/src/handlers/admin_handler_health.erl @@ -0,0 +1,7 @@ +-module(admin_handler_health). + +-export([init/2]). + +init(Req, _Opts) -> + Body = jsx:encode(#{status => <<"ok">>, service => <<"admin">>}), + cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/admin_handler_stats.erl b/src/handlers/admin_handler_stats.erl new file mode 100644 index 0000000..586c368 --- /dev/null +++ b/src/handlers/admin_handler_stats.erl @@ -0,0 +1,78 @@ +-module(admin_handler_stats). +-include("records.hrl"). + +-export([init/2]). +-export([count_users/0, count_calendars/0, count_events/0, count_bookings/0, + count_reviews/0, count_reports/0, count_tickets/0, count_subscriptions/0]). +-export([is_admin/1]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> get_stats(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +get_stats(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case is_admin(AdminId) of + true -> + Stats = #{ + users => count_users(), + calendars => count_calendars(), + events => count_events(), + bookings => count_bookings(), + reviews => count_reviews(), + reports => count_reports(), + tickets => count_tickets(), + subscriptions => count_subscriptions() + }, + send_json(Req1, 200, Stats); + false -> + send_error(Req1, 403, <<"Admin access required">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +is_admin(UserId) -> + case core_user:get_by_id(UserId) of + {ok, User} -> User#user.role =:= admin; + _ -> false + end. + +count_users() -> + length(mnesia:dirty_match_object(#user{_ = '_'})). + +count_calendars() -> + length(mnesia:dirty_match_object(#calendar{_ = '_'})). + +count_events() -> + length(mnesia:dirty_match_object(#event{is_instance = false, _ = '_'})). + +count_bookings() -> + length(mnesia:dirty_match_object(#booking{_ = '_'})). + +count_reviews() -> + length(mnesia:dirty_match_object(#review{_ = '_'})). + +count_reports() -> + length(mnesia:dirty_match_object(#report{_ = '_'})). + +count_tickets() -> + length(mnesia:dirty_match_object(#ticket{_ = '_'})). + +count_subscriptions() -> + length(mnesia:dirty_match_object(#subscription{_ = '_'})). + +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/admin_handler_user_by_id.erl b/src/handlers/admin_handler_user_by_id.erl new file mode 100644 index 0000000..3209872 --- /dev/null +++ b/src/handlers/admin_handler_user_by_id.erl @@ -0,0 +1,127 @@ +-module(admin_handler_user_by_id). +-include("records.hrl"). + +-export([init/2]). +-export([user_to_json/1, convert_updates/1]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> get_user(Req); + <<"PUT">> -> update_user(Req); + <<"DELETE">> -> delete_user(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +get_user(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case is_admin(AdminId) of + true -> + UserId = cowboy_req:binding(id, Req1), + case core_user:get_by_id(UserId) of + {ok, User} -> + case User#user.status of + deleted -> send_error(Req1, 404, <<"User not found">>); + _ -> send_json(Req1, 200, user_to_json(User)) + end; + {error, not_found} -> + send_error(Req1, 404, <<"User not found">>) + end; + false -> + send_error(Req1, 403, <<"Admin access required">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +update_user(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case is_admin(AdminId) of + true -> + UserId = cowboy_req:binding(id, Req1), + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + Decoded when is_map(Decoded) -> + Updates = maps:to_list(Decoded), + % Преобразуем бинарные значения в атомы где нужно + ConvertedUpdates = convert_updates(Updates), + case core_user:update(UserId, ConvertedUpdates) of + {ok, User} -> + send_json(Req2, 200, user_to_json(User)); + {error, not_found} -> + send_error(Req2, 404, <<"User not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + send_error(Req2, 400, <<"Invalid JSON">>) + catch + _:_ -> send_error(Req2, 400, <<"Invalid JSON format">>) + end; + false -> + send_error(Req1, 403, <<"Admin access required">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +convert_updates(Updates) -> + lists:map(fun + ({<<"status">>, <<"active">>}) -> {status, active}; + ({<<"status">>, <<"frozen">>}) -> {status, frozen}; + ({<<"status">>, <<"deleted">>}) -> {status, deleted}; + ({<<"role">>, <<"user">>}) -> {role, user}; + ({<<"role">>, <<"admin">>}) -> {role, admin}; + (Other) -> Other + end, Updates). + +delete_user(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case is_admin(AdminId) of + true -> + UserId = cowboy_req:binding(id, Req1), + case core_user:delete(UserId) of + {ok, _} -> + send_json(Req1, 200, #{status => <<"deleted">>}); + {error, not_found} -> + send_error(Req1, 404, <<"User not found">>) + end; + false -> + send_error(Req1, 403, <<"Admin access required">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +is_admin(UserId) -> + case core_user:get_by_id(UserId) of + {ok, User} -> User#user.role =:= admin; + _ -> false + end. + +user_to_json(User) -> + #{ + id => User#user.id, + email => User#user.email, + role => User#user.role, + status => User#user.status, + created_at => datetime_to_iso8601(User#user.created_at), + updated_at => datetime_to_iso8601(User#user.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/admin_handler_users.erl b/src/handlers/admin_handler_users.erl new file mode 100644 index 0000000..12bed58 --- /dev/null +++ b/src/handlers/admin_handler_users.erl @@ -0,0 +1,57 @@ +-module(admin_handler_users). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> list_users(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +list_users(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case is_admin(AdminId) of + true -> + Users = mnesia:dirty_match_object(#user{_ = '_'}), + ActiveUsers = [U || U <- Users, U#user.status =/= deleted], + Response = [user_to_json(U) || U <- ActiveUsers], + send_json(Req1, 200, Response); + false -> + send_error(Req1, 403, <<"Admin access required">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +is_admin(UserId) -> + case core_user:get_by_id(UserId) of + {ok, User} -> User#user.role =:= admin; + _ -> false + end. + +user_to_json(User) -> + #{ + id => User#user.id, + email => User#user.email, + role => User#user.role, + status => User#user.status, + created_at => datetime_to_iso8601(User#user.created_at), + updated_at => datetime_to_iso8601(User#user.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/test/admin_handler_stats_tests.erl b/test/admin_handler_stats_tests.erl new file mode 100644 index 0000000..adc208f --- /dev/null +++ b/test/admin_handler_stats_tests.erl @@ -0,0 +1,107 @@ +-module(admin_handler_stats_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(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]), + mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]), + mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]), + mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]), + mnesia:create_table(subscription, [{attributes, record_info(fields, subscription)}, {ram_copies, [node()]}]), + ok. + +cleanup(_) -> + mnesia:delete_table(subscription), + mnesia:delete_table(ticket), + mnesia:delete_table(report), + mnesia:delete_table(review), + mnesia:delete_table(booking), + mnesia:delete_table(event), + mnesia:delete_table(calendar), + mnesia:delete_table(user), + mnesia:stop(), + ok. + +admin_stats_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Count users", fun test_count_users/0}, + {"Count calendars", fun test_count_calendars/0}, + {"Count events", fun test_count_events/0}, + {"Count bookings", fun test_count_bookings/0}, + {"Count reviews", fun test_count_reviews/0}, + {"Count reports", fun test_count_reports/0}, + {"Count tickets", fun test_count_tickets/0}, + {"Count subscriptions", fun test_count_subscriptions/0} + ]}. + +create_test_user() -> + UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), + User = #user{id = UserId, email = <>, password_hash = <<"hash">>, + role = user, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()}, + mnesia:dirty_write(User), + UserId. + +test_count_users() -> + ?assertEqual(0, admin_handler_stats:count_users()), + create_test_user(), + create_test_user(), + ?assertEqual(2, admin_handler_stats:count_users()). + +test_count_calendars() -> + ?assertEqual(0, admin_handler_stats:count_calendars()), + UserId = create_test_user(), + core_calendar:create(UserId, <<"Cal1">>, <<"">>, manual), + core_calendar:create(UserId, <<"Cal2">>, <<"">>, auto), + ?assertEqual(2, admin_handler_stats:count_calendars()). + +test_count_events() -> + ?assertEqual(0, admin_handler_stats:count_events()), + UserId = create_test_user(), + {ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual), + core_event:create(Cal#calendar.id, <<"Ev1">>, {{2026,6,1},{10,0,0}}, 60), + core_event:create(Cal#calendar.id, <<"Ev2">>, {{2026,6,2},{10,0,0}}, 60), + ?assertEqual(2, admin_handler_stats:count_events()). + +test_count_bookings() -> + ?assertEqual(0, admin_handler_stats:count_bookings()), + UserId = create_test_user(), + ParticipantId = create_test_user(), + {ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual), + {ok, Ev} = core_event:create(Cal#calendar.id, <<"Ev">>, {{2026,6,1},{10,0,0}}, 60), + core_booking:create(Ev#event.id, ParticipantId), + core_booking:create(Ev#event.id, ParticipantId), + ?assertEqual(2, admin_handler_stats:count_bookings()). + +test_count_reviews() -> + ?assertEqual(0, admin_handler_stats:count_reviews()), + UserId = create_test_user(), + core_review:create(UserId, calendar, <<"cal1">>, 5, <<"Great">>), + core_review:create(UserId, event, <<"ev1">>, 4, <<"Good">>), + ?assertEqual(2, admin_handler_stats:count_reviews()). + +test_count_reports() -> + ?assertEqual(0, admin_handler_stats:count_reports()), + UserId = create_test_user(), + core_report:create(UserId, event, <<"ev1">>, <<"Bad">>), + core_report:create(UserId, calendar, <<"cal1">>, <<"Spam">>), + ?assertEqual(2, admin_handler_stats:count_reports()). + +test_count_tickets() -> + ?assertEqual(0, admin_handler_stats:count_tickets()), + core_ticket:create_or_update(<<"Error1">>, <<"">>, #{}), + core_ticket:create_or_update(<<"Error2">>, <<"">>, #{}), + ?assertEqual(2, admin_handler_stats:count_tickets()). + +test_count_subscriptions() -> + ?assertEqual(0, admin_handler_stats:count_subscriptions()), + UserId = create_test_user(), + core_subscription:create(UserId, trial, false), + core_subscription:create(UserId, monthly, true), + ?assertEqual(2, admin_handler_stats:count_subscriptions()). \ No newline at end of file diff --git a/test/admin_handler_user_by_id_tests.erl b/test/admin_handler_user_by_id_tests.erl new file mode 100644 index 0000000..d20c8e4 --- /dev/null +++ b/test/admin_handler_user_by_id_tests.erl @@ -0,0 +1,28 @@ +-module(admin_handler_user_by_id_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()]}]), + ok. + +cleanup(_) -> + mnesia:delete_table(user), + mnesia:stop(), + ok. + +admin_user_by_id_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Convert updates test", fun test_convert_updates/0} + ]}. + +test_convert_updates() -> + Updates = [{<<"status">>, <<"frozen">>}, {<<"role">>, <<"admin">>}, {<<"email">>, <<"test@test.com">>}], + Converted = admin_handler_user_by_id:convert_updates(Updates), + ?assertEqual({status, frozen}, lists:keyfind(status, 1, Converted)), + ?assertEqual({role, admin}, lists:keyfind(role, 1, Converted)), + ?assertEqual({<<"email">>, <<"test@test.com">>}, lists:keyfind(<<"email">>, 1, Converted)). \ No newline at end of file diff --git a/test/admin_handler_users_tests.erl b/test/admin_handler_users_tests.erl new file mode 100644 index 0000000..3d32a26 --- /dev/null +++ b/test/admin_handler_users_tests.erl @@ -0,0 +1,45 @@ +-module(admin_handler_users_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()]}]), + ok. + +cleanup(_) -> + mnesia:delete_table(user), + mnesia:stop(), + ok. + +admin_users_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"User to JSON conversion", fun test_user_to_json/0}, + {"Is admin check", fun test_is_admin/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. + +test_user_to_json() -> + UserId = create_test_user(user), + {ok, User} = core_user:get_by_id(UserId), + Json = admin_handler_user_by_id:user_to_json(User), + ?assert(is_map(Json)), + ?assertEqual(UserId, maps:get(id, Json)), + ?assertEqual(user, maps:get(role, Json)), + ?assertEqual(active, maps:get(status, Json)). + +test_is_admin() -> + AdminId = create_test_user(admin), + UserId = create_test_user(user), + ?assert(admin_handler_stats:is_admin(AdminId)), + ?assertNot(admin_handler_stats:is_admin(UserId)), + ?assertNot(admin_handler_stats:is_admin(<<"nonexistent">>)). \ No newline at end of file diff --git a/test/scripts/test_admin_api.sh b/test/scripts/test_admin_api.sh new file mode 100644 index 0000000..d296261 --- /dev/null +++ b/test/scripts/test_admin_api.sh @@ -0,0 +1,256 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" +ADMIN_URL="http://localhost:8445" + +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/\"$//" +} + +extract_json_number() { + echo "$1" | grep -o "\"$2\":[0-9]*" | head -1 | sed "s/\"$2\"://" +} + +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 ADMIN API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Checking if servers are running..." +if ! curl -s "$BASE_URL/health" | grep -q "ok"; then + log_error "Main server is not running on port 8080" + exit 1 +fi +log_success "Main server is running" + +if ! curl -s "$ADMIN_URL/admin/health" | grep -q "ok"; then + log_error "Admin server is not running on port 8445" + exit 1 +fi +log_success "Admin server is running" + +echo "" +log_info "============================================================" +log_info "STEP 1: Create test users" +log_info "============================================================" + +# Админ (первый пользователь) +ADMIN_EMAIL="admin_test_$(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: $ADMIN_ID" + +# Обычный пользователь +USER_EMAIL="user_test_$(date +%s)@example.com" +USER_PASSWORD="user123" + +log_info "Creating regular user..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER_EMAIL\",\"password\":\"$USER_PASSWORD\"}" "") +USER_TOKEN=$(extract_json "$response" "token") +USER_ID=$(extract_json "$response" "id") +log_success "User created: $USER_ID" + +echo "" +log_info "============================================================" +log_info "TEST 1: Admin healthcheck" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/health" "") +if echo "$response" | grep -q "admin"; then + log_success "Admin healthcheck passed: $response" +else + log_error "Admin healthcheck failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 2: Admin stats (requires auth)" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/stats" "$ADMIN_TOKEN") +if echo "$response" | grep -q "users"; then + log_success "Admin stats retrieved" + USERS=$(extract_json_number "$response" "users") + log_info "Users: $USERS" +else + log_error "Admin stats failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 3: Admin stats without token (should fail)" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/stats" "") +if echo "$response" | grep -q "Missing"; then + log_success "Unauthorized access correctly rejected" +else + log_error "Should reject unauthorized: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 4: Admin stats with user token (should fail)" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/stats" "$USER_TOKEN") +if echo "$response" | grep -q "Admin access required"; then + log_success "User token correctly rejected" +else + log_error "Should reject user token: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 5: List all users (admin)" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/users" "$ADMIN_TOKEN") +USER_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) + +if [ "$USER_COUNT" -ge 2 ]; then + log_success "Admin sees $USER_COUNT users" +else + log_error "Admin should see at least 2 users: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 6: Get specific user (admin)" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/users/$USER_ID" "$ADMIN_TOKEN") +if echo "$response" | grep -q "$USER_ID"; then + log_success "Admin can view user $USER_ID" +else + log_error "Admin cannot view user: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 7: Update user (admin)" +log_info "============================================================" + +response=$(http_put "$ADMIN_URL/admin/users/$USER_ID" "{\"status\":\"frozen\"}" "$ADMIN_TOKEN") +if echo "$response" | grep -q "frozen"; then + log_success "User status updated to frozen" +else + log_error "Failed to update user: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 8: Verify user status changed" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/users/$USER_ID" "$ADMIN_TOKEN") +if echo "$response" | grep -q "frozen"; then + log_success "User status confirmed as frozen" +else + log_error "User status not updated: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 9: Restore user status" +log_info "============================================================" + +response=$(http_put "$ADMIN_URL/admin/users/$USER_ID" "{\"status\":\"active\"}" "$ADMIN_TOKEN") +if echo "$response" | grep -q "active"; then + log_success "User status restored to active" +else + log_error "Failed to restore user: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 10: User cannot access admin endpoints" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/users" "$USER_TOKEN") +if echo "$response" | grep -q "Admin access required"; then + log_success "User correctly denied access to admin users list" +else + log_error "User should be denied: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 11: Delete user (admin)" +log_info "============================================================" + +# Создаём пользователя для удаления +DELETE_EMAIL="delete_me_$(date +%s)@example.com" +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$DELETE_EMAIL\",\"password\":\"pass123\"}" "") +DELETE_ID=$(extract_json "$response" "id") +log_info "Created user to delete: $DELETE_ID" + +response=$(http_delete "$ADMIN_URL/admin/users/$DELETE_ID" "$ADMIN_TOKEN") +if echo "$response" | grep -q "deleted"; then + log_success "User deleted successfully" +else + log_error "Failed to delete user: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 12: Verify user deleted" +log_info "============================================================" + +response=$(http_get "$ADMIN_URL/admin/users/$DELETE_ID" "$ADMIN_TOKEN") +if echo "$response" | grep -q "not found"; then + log_success "Deleted user not found" +else + log_error "Deleted user still accessible: $response" +fi + +echo "" +echo "============================================================" +log_success "ADMIN API TESTS COMPLETED!" +echo "============================================================" +echo "" +echo "Summary:" +echo " Admin: $ADMIN_EMAIL" +echo " User: $USER_EMAIL" +echo "" \ No newline at end of file