diff --git a/src/core/core_booking.erl b/src/core/core_booking.erl index 00fd3bf..c7f03ec 100644 --- a/src/core/core_booking.erl +++ b/src/core/core_booking.erl @@ -4,6 +4,7 @@ -export([create/2, get_by_id/1, get_by_event_and_user/2, list_by_event/1, list_by_user/1]). -export([update_status/2, delete/1]). -export([generate_id/0]). +-export([count_bookings/0]). %% Создание бронирования create(EventId, UserId) -> @@ -97,6 +98,8 @@ delete(Id) -> {aborted, Reason} -> {error, Reason} end. +count_bookings() -> mnesia:table_info(booking, size). + %% Внутренние функции 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_calendar.erl b/src/core/core_calendar.erl index 3bf938b..89c7d2b 100644 --- a/src/core/core_calendar.erl +++ b/src/core/core_calendar.erl @@ -3,6 +3,7 @@ -export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]). -export([generate_id/0]). +-export([count_calendars/0]). %% Создание календаря create(OwnerId, Title, Description, Confirmation) -> @@ -95,6 +96,8 @@ update(Id, Updates) -> delete(Id) -> update(Id, [{status, deleted}]). +count_calendars() -> mnesia:table_info(calendar, size). + %% Внутренние функции generate_id() -> base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). diff --git a/src/core/core_event.erl b/src/core/core_event.erl index 2483702..cd1d515 100644 --- a/src/core/core_event.erl +++ b/src/core/core_event.erl @@ -4,6 +4,7 @@ -export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1, update/2, delete/1, materialize_occurrence/3]). -export([generate_id/0]). +-export([count_events/0]). %% Создание одиночного события create(CalendarId, Title, StartTime, Duration) -> @@ -167,6 +168,9 @@ update(Id, Updates) -> delete(Id) -> update(Id, [{status, deleted}]). +count_events() -> + mnesia:table_info(event, size). + %% Внутренние функции generate_id() -> base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). diff --git a/src/core/core_report.erl b/src/core/core_report.erl index f3c627d..3e3c2a4 100644 --- a/src/core/core_report.erl +++ b/src/core/core_report.erl @@ -4,6 +4,7 @@ -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]). +-export([count_reports_by_status/1, count_reports_by_admin/2]). %% Создание жалобы create(ReporterId, TargetType, TargetId, Reason) -> @@ -83,6 +84,14 @@ get_count_by_target(TargetType, TargetId) -> Reports = mnesia:dirty_match_object(Match), length(Reports). +count_reports_by_status(Status) -> + Match = #report{status = Status, _ = '_'}, + length(mnesia:dirty_match_object(Match)). + +count_reports_by_admin(AdminId, Status) -> + Match = #report{resolved_by = AdminId, status = Status, _ = '_'}, + length(mnesia:dirty_match_object(Match)). + %% Внутренние функции 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_review.erl b/src/core/core_review.erl index 34aab5c..411a75f 100644 --- a/src/core/core_review.erl +++ b/src/core/core_review.erl @@ -5,6 +5,7 @@ update/2, delete/1, hide/1, unhide/1]). -export([get_average_rating/2, has_user_reviewed/3]). -export([generate_id/0]). +-export([count_reviews/0]). %% Создание отзыва create(UserId, TargetType, TargetId, Rating, Comment) -> @@ -113,6 +114,8 @@ has_user_reviewed(UserId, TargetType, TargetId) -> _ -> true end. +count_reviews() -> mnesia:table_info(review, size). + %% Внутренние функции generate_id() -> base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). diff --git a/src/core/core_subscription.erl b/src/core/core_subscription.erl index 162e297..0bef79f 100644 --- a/src/core/core_subscription.erl +++ b/src/core/core_subscription.erl @@ -10,6 +10,7 @@ update_subscription/2, delete_subscription/1 ]). +-export([count_subscription/0]). -define(TRIAL_DAYS, 30). @@ -214,4 +215,6 @@ apply_updates(Sub, Updates) -> Acc#subscription{expires_at = Value, updated_at = calendar:universal_time()}; _ -> Acc end - end, Sub, maps:to_list(Updates)). \ No newline at end of file + end, Sub, maps:to_list(Updates)). + +count_subscription() -> mnesia:table_info(subscription, size). \ No newline at end of file diff --git a/src/core/core_ticket.erl b/src/core/core_ticket.erl index 4f68d0a..a52303a 100644 --- a/src/core/core_ticket.erl +++ b/src/core/core_ticket.erl @@ -7,6 +7,7 @@ stats/0, create_ticket/1, list_by_user/1]). +-export([count_tickets_by_status/1, count_tickets_by_admin/2]). list_all() -> mnesia:dirty_match_object(#ticket{_ = '_'}). @@ -83,4 +84,12 @@ apply_updates(Ticket, Updates) -> end, Ticket, maps:to_list(Updates)). count_by_status(Status, Tickets) -> - length([T || T <- Tickets, T#ticket.status =:= Status]). \ No newline at end of file + length([T || T <- Tickets, T#ticket.status =:= Status]). + +count_tickets_by_status(Status) -> + Match = #ticket{status = Status, _ = '_'}, + length(mnesia:dirty_match_object(Match)). + +count_tickets_by_admin(AdminId, Status) -> + Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'}, + length(mnesia:dirty_match_object(Match)). \ No newline at end of file diff --git a/src/core/core_user.erl b/src/core/core_user.erl index 331ce41..67da224 100644 --- a/src/core/core_user.erl +++ b/src/core/core_user.erl @@ -6,6 +6,7 @@ -export([generate_id/0]). -export([list_users/0]). -export([block/1, unblock/1]). +-export([count_users/0]). %% Создание пользователя create(Email, Password) -> @@ -122,6 +123,9 @@ unblock(Id) -> Error -> Error end. +count_users() -> + mnesia:table_info(user, size). + %% Внутренние функции generate_id() -> base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). diff --git a/src/handlers/admin/admin_handler_stats.erl b/src/handlers/admin/admin_handler_stats.erl index 4220e80..d67cf0b 100644 --- a/src/handlers/admin/admin_handler_stats.erl +++ b/src/handlers/admin/admin_handler_stats.erl @@ -13,16 +13,8 @@ get_stats(Req) -> {ok, AdminId, Req1} -> case admin_utils: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() - }, + {ok, Admin} = core_admin:get_by_id(AdminId), + Stats = logic_stats:get_stats(Admin#admin.role, AdminId), send_json(Req1, 200, Stats); false -> send_error(Req1, 403, <<"Admin access required">>) @@ -31,15 +23,6 @@ get_stats(Req) -> send_error(Req1, Code, Message) 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), diff --git a/src/handlers/admin/admin_handler_users.erl b/src/handlers/admin/admin_handler_users.erl index 1d1f04e..554888f 100644 --- a/src/handlers/admin/admin_handler_users.erl +++ b/src/handlers/admin/admin_handler_users.erl @@ -13,8 +13,8 @@ list_users(Req) -> {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of true -> - Users = core_user:list_users(), - send_json(Req1, 200, [user_to_json(U) || U <- Users]); + {ok, Users} = core_user:list_users(), + send_json(Req1, 200, [user_to_map(U) || U <- Users]); false -> send_error(Req1, 403, <<"Admin access required">>) end; @@ -22,14 +22,23 @@ list_users(Req) -> send_error(Req1, Code, Message) end. -user_to_json(U) -> +user_to_map(User) when is_map(User) -> #{ - id => U#user.id, - email => U#user.email, - role => U#user.role, - status => U#user.status, - created_at => datetime_to_iso8601(U#user.created_at), - updated_at => datetime_to_iso8601(U#user.updated_at) + id => maps:get(id, User), + email => maps:get(email, User), + role => maps:get(role, User, <<"user">>), + status => maps:get(status, User, <<"active">>), + created_at => datetime_to_iso8601(maps:get(created_at, User)), + updated_at => datetime_to_iso8601(maps:get(updated_at, User)) + }; +user_to_map(User) -> + #{ + id => User#user.id, + email => User#user.email, + role => atom_to_binary(User#user.role, utf8), + status => atom_to_binary(User#user.status, utf8), + created_at => datetime_to_iso8601(User#user.created_at), + updated_at => datetime_to_iso8601(User#user.updated_at) }. datetime_to_iso8601({{Y,M,D},{H,Min,S}}) -> diff --git a/src/handlers/admin/admin_ws_handler.erl b/src/handlers/admin/admin_ws_handler.erl index 6150d01..9b36d81 100644 --- a/src/handlers/admin/admin_ws_handler.erl +++ b/src/handlers/admin/admin_ws_handler.erl @@ -22,7 +22,7 @@ init(Req, _Opts) -> case logic_auth:verify_jwt(Token) of {ok, UserId, Role} -> io:format("[ADMIN_WS] UserId: ~s, Role: ~s~n", [UserId, Role]), - case admin_utils:is_admin(Role) of + case lists:member(Role, [<<"admin">>, <<"superadmin">>, <<"moderator">>, <<"support">>]) of true -> io:format("[ADMIN_WS] Admin access granted~n"), {cowboy_websocket, Req, #state{admin_id = UserId}}; diff --git a/src/infra/eventhub_auth.erl b/src/infra/eventhub_auth.erl index c06fef2..a175dbd 100644 --- a/src/infra/eventhub_auth.erl +++ b/src/infra/eventhub_auth.erl @@ -133,7 +133,7 @@ authenticate_admin_request(_Req, Email, Password) -> case logic_auth:authenticate_admin(Email, Password) of {ok, AdminMap} -> Role = maps:get(role, AdminMap, <<"admin">>), - case admin_utils:is_admin(Role) of + case is_admin_role(Role) of true -> AdminId = maps:get(id, AdminMap), Token = generate_admin_token(AdminId, Role), @@ -143,6 +143,9 @@ authenticate_admin_request(_Req, Email, Password) -> Error -> Error end. +is_admin_role(Role) -> + lists:member(Role, [<<"admin">>, <<"superadmin">>, <<"moderator">>, <<"support">>]). + %% ========== REFRESH TOKEN ========== -spec generate_refresh_token(UserId :: binary()) -> {binary(), calendar:datetime()}. diff --git a/src/logic/logic_auth.erl b/src/logic/logic_auth.erl index c148a38..22956e6 100644 --- a/src/logic/logic_auth.erl +++ b/src/logic/logic_auth.erl @@ -17,7 +17,15 @@ generate_jwt(UserId, Role) -> eventhub_auth:generate_user_token(UserId, Role). verify_jwt(Token) -> - eventhub_auth:verify_user_token(Token). + case eventhub_auth:verify_user_token(Token) of + {ok, UserId, Role} -> {ok, UserId, Role}; + {error, _} -> + % Если не подошёл пользовательский, пробуем админский + case eventhub_auth:verify_admin_token(Token) of + {ok, AdminId, Role} -> {ok, AdminId, Role}; + {error, Reason} -> {error, Reason} + end + end. generate_refresh_token(UserId) -> eventhub_auth:generate_refresh_token(UserId). diff --git a/src/logic/logic_stats.erl b/src/logic/logic_stats.erl new file mode 100644 index 0000000..c56e39d --- /dev/null +++ b/src/logic/logic_stats.erl @@ -0,0 +1,29 @@ +-module(logic_stats). +-export([get_stats/2]). + +-include("records.hrl"). + +-spec get_stats(Role :: atom(), AdminId :: binary()) -> map(). +get_stats(superadmin, _AdminId) -> + #{ + users => core_user:count_users(), + calendars => core_calendar:count_calendars(), + events => core_event:count_events(), + bookings => core_booking:count_bookings(), + reviews => core_review:count_reviews(), + reports_total => core_report:count_reports_by_status(pending), + tickets_open => core_ticket:count_tickets_by_status(open), + subscriptions => core_subscription:count_subscription() + }; +get_stats(moderator, AdminId) -> + #{ + reports_reviewed => core_report:count_reports_by_admin(AdminId, reviewed), + events_moderated => 0 % пока заглушка, можно добавить позже + }; +get_stats(support, AdminId) -> + #{ + tickets_assigned => core_ticket:count_tickets_by_admin(AdminId, open), + reports_pending => core_report:count_reports_by_status(pending) + }; +get_stats(_, _) -> + #{}. \ No newline at end of file diff --git a/test/api/api_admin_tests.erl b/test/api/api_admin_tests.erl index 9e5d442..4bac0f5 100644 --- a/test/api/api_admin_tests.erl +++ b/test/api/api_admin_tests.erl @@ -15,7 +15,7 @@ test() -> %% TEST 2: Admin login (дополнительная проверка) io:format(" TEST 2: Admin login (attempt)... "), - LoginBody = jsx:encode(#{<<"email">> => <<"global_admin@test.com">>, <<"password">> => <<"admin123">>}), + LoginBody = jsx:encode(#{<<"email">> => <<"admin@eventhub.local">>, <<"password">> => <<"123456">>}), case httpc:request(post, {AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []) of {ok, {{_, 200, _}, _, _}} -> io:format("OK (logged in)~n"); @@ -23,11 +23,19 @@ test() -> io:format("SKIPPED (credentials not found, using runner token)~n") end, - %% TEST 3: Admin stats - io:format(" TEST 3: Admin stats... "), - {ok, {{_, 200, _}, _, _}} = httpc:request(get, - {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format("OK~n"), + %% TEST 3: Admin stats (superadmin) + io:format(" TEST 3: Admin stats (superadmin)... "), + % Логинимся под суперадмином (данные из api_test_runner) + LoginBody = jsx:encode(#{<<"email">> => <<"admin@eventhub.local">>, <<"password">> => <<"123456">>}), + {ok, {{_, 200, _}, _, LoginResp}} = httpc:request(post, + {AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []), + #{<<"token">> := SuperToken} = jsx:decode(list_to_binary(LoginResp), [return_maps]), + + % Запрашиваем статистику + {ok, {{_, 200, _}, _, StatsResp}} = httpc:request(get, + {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperToken)}]}, [], []), + Stats = jsx:decode(list_to_binary(StatsResp), [return_maps]), + io:format(" OK (keys: ~p)~n", [maps:keys(Stats)]), %% TEST 4: List users io:format(" TEST 4: List users... "), diff --git a/test/api/api_test_runner.erl b/test/api/api_test_runner.erl index 077f1ee..7e8ec8a 100644 --- a/test/api/api_test_runner.erl +++ b/test/api/api_test_runner.erl @@ -1,4 +1,7 @@ -module(api_test_runner). + +-include("records.hrl"). + -export([run_all/0, run/1]). -export([http_post/2, http_post/3, http_get/1, http_get/2, http_put/3, http_delete/2]). -export([extract_json/2, extract_json/3, assert_status/2]). @@ -10,8 +13,8 @@ -define(ADMIN_URL, "http://localhost:8445"). %% ============ Глобальные переменные для тестов ============ --define(ADMIN_EMAIL, <<"global_admin@test.com">>). --define(ADMIN_PASSWORD, <<"admin123">>). +-define(ADMIN_EMAIL, <<"admin@eventhub.local">>). +-define(ADMIN_PASSWORD, <<"123456">>). -define(USER_EMAIL, <<"global_user@test.com">>). -define(USER_PASSWORD, <<"user123">>). @@ -21,27 +24,29 @@ init_global_users() -> undefined -> io:format("~n=== Initializing global test users ===~n"), - % Создаём или логиним админа - AdminToken = register_and_login(?ADMIN_EMAIL, ?ADMIN_PASSWORD), - {ok, {{_, 200, _}, _, MeResp}} = http_get("/v1/user/me", AdminToken), - #{<<"id">> := AdminId, <<"role">> := Role} = jsx:decode(list_to_binary(MeResp), [return_maps]), - - io:format("Admin ID: ~s, Current role: ~s~n", [AdminId, Role]), - - % Проверяем, что админ действительно админ - case Role of - <<"admin">> -> - io:format("✓ Admin already has admin role~n"), + % ---------- АДМИНИСТРАТОР ---------- + % Проверяем, существует ли админ в таблице admin + case core_admin:get_by_email(?ADMIN_EMAIL) of + {ok, Admin} -> + io:format("Admin already exists: ~s~n", [Admin#admin.id]), ok; - _ -> - io:format("⚠ Admin role is '~s', attempting to promote...~n", [Role]), - promote_to_admin(AdminToken, AdminId) + {error, not_found} -> + % Создаём суперадмина напрямую + {ok, Admin} = core_admin:create(?ADMIN_EMAIL, ?ADMIN_PASSWORD, superadmin), + io:format("Admin created: ~s~n", [Admin#admin.id]) end, + % Логинимся через админский API + LoginBody = jsx:encode(#{<<"email">> => ?ADMIN_EMAIL, <<"password">> => ?ADMIN_PASSWORD}), + {ok, {{_, 200, _}, _, LoginResp}} = httpc:request(post, + {?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []), + #{<<"token">> := AdminToken, <<"user">> := #{<<"id">> := AdminId}} = + jsx:decode(list_to_binary(LoginResp), [return_maps]), + put(admin_token, AdminToken), put(admin_id, AdminId), - % Создаём или логиним обычного пользователя + % ---------- ПОЛЬЗОВАТЕЛЬ ---------- UserToken = register_and_login(?USER_EMAIL, ?USER_PASSWORD), {ok, {{_, 200, _}, _, UserMeResp}} = http_get("/v1/user/me", UserToken), #{<<"id">> := UserId} = jsx:decode(list_to_binary(UserMeResp), [return_maps]), @@ -49,7 +54,7 @@ init_global_users() -> put(user_token, UserToken), put(user_id, UserId), - io:format("User ID: ~s~n", [UserId]), + io:format("Admin ID: ~s, User ID: ~s~n", [AdminId, UserId]), io:format("=== Global users initialized ===~n~n"), ok; _ -> @@ -57,32 +62,6 @@ init_global_users() -> ok end. -%% Попытка повысить роль через разные методы -promote_to_admin(AdminToken, AdminId) -> - io:format("Attempting to promote user ~s to admin...~n", [AdminId]), - - % Метод 1: Прямое обновление через core_user (если доступно) - try - {ok, _User} = core_user:get_by_id(AdminId), - core_user:update(AdminId, [{role, admin}]), - io:format("✓ Promoted via core_user~n") - catch - _:_ -> - io:format(" Method 1 (core_user) failed~n") - end, - - % Проверяем, сработало ли - {ok, {{_, 200, _}, _, CheckResp}} = http_get("/v1/user/me", AdminToken), - #{<<"role">> := NewRole} = jsx:decode(list_to_binary(CheckResp), [return_maps]), - - case NewRole of - <<"admin">> -> - io:format("✓ User is now admin~n"); - _ -> - io:format("⚠ WARNING: User still has role '~s'~n", [NewRole]), - io:format(" Some admin tests may fail~n") - end. - get_admin_token() -> init_global_users(), get(admin_token). diff --git a/test/api/api_tickets_tests.erl b/test/api/api_tickets_tests.erl index d09849b..fdfed4d 100644 --- a/test/api/api_tickets_tests.erl +++ b/test/api/api_tickets_tests.erl @@ -2,6 +2,7 @@ -export([test/0]). -define(ADMIN_BASE_URL, "http://localhost:8445"). +-define(BASE_URL, "http://localhost:8080"). test() -> io:format("Testing tickets API...~n"), @@ -9,70 +10,62 @@ test() -> AdminToken = api_test_runner:get_admin_token(), %% TEST 1: Create ticket (user) - io:format(" TEST 1: Create ticket...~n"), - io:format(" POST /v1/tickets~n"), + io:format(" TEST 1: Create ticket... "), TicketId = api_test_runner:extract_json( api_test_runner:http_post("/v1/tickets", #{error_message => <<"Bug">>, stacktrace => <<"Something broke">>}, Token), <<"id">>), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 2: Get my tickets (user) - io:format(" TEST 2: Get my tickets...~n"), - io:format(" GET /v1/tickets~n"), + io:format(" TEST 2: Get my tickets... "), {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/tickets", Token), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 3: Get single ticket (user) - io:format(" TEST 3: Get single ticket...~n"), - io:format(" GET /v1/tickets/~s~n", [TicketId]), + io:format(" TEST 3: Get single ticket... "), {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get( "/v1/tickets/" ++ binary_to_list(TicketId), Token), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 4: Admin lists all tickets - io:format(" TEST 4: Admin lists all tickets...~n"), - io:format(" GET ~s/v1/admin/tickets~n", [?ADMIN_BASE_URL]), + io:format(" TEST 4: Admin lists all tickets... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {?ADMIN_BASE_URL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 5: Admin updates ticket status - io:format(" TEST 5: Admin updates ticket status...~n"), - io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]), + io:format(" TEST 5: Admin updates ticket status... "), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", jsx:encode(#{status => <<"in_progress">>})}, [], []), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 6: Admin assigns ticket - io:format(" TEST 6: Admin assigns ticket...~n"), - io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]), + io:format(" TEST 6: Admin assigns ticket... "), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", jsx:encode(#{assigned_to => AdminToken})}, [], []), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 7: Admin views ticket stats - io:format(" TEST 7: Admin views ticket stats...~n"), - io:format(" GET ~s/v1/admin/tickets/stats~n", [?ADMIN_BASE_URL]), + io:format(" TEST 7: Admin views ticket stats... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {?ADMIN_BASE_URL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format(" OK~n"), + io:format("OK~n"), %% TEST 8: Admin deletes ticket - io:format(" TEST 8: Admin deletes ticket...~n"), - io:format(" DELETE ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]), + io:format(" TEST 8: Admin deletes ticket... "), {ok, {{_, 200, _}, _, _}} = httpc:request(delete, {?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format(" OK~n"), + io:format("OK~n"), io:format("~n✅ Tickets API tests passed!~n"), {?MODULE, ok}. \ No newline at end of file diff --git a/test/unit/admin_handler_stats_tests.erl b/test/unit/admin_handler_stats_tests.erl index 8972d7b..b0220b4 100644 --- a/test/unit/admin_handler_stats_tests.erl +++ b/test/unit/admin_handler_stats_tests.erl @@ -2,88 +2,81 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). --define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>). --define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>). - setup() -> ok = meck:new(cowboy_req, [non_strict]), ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(mnesia, [non_strict]), - ok = meck:expect(mnesia, dirty_match_object, fun(_) -> [] end), - application:set_env(eventhub, jwt_secret, ?JWT_SECRET), - application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET), - {ok, _} = application:ensure_all_started(jose), + ok = meck:new(admin_utils, [non_strict]), + ok = meck:new(core_admin, [non_strict]), + ok = meck:new(logic_stats, [non_strict]), + ok = meck:expect(cowboy_req, reply, + fun(Code, _, _, _) -> put(test_reply, Code) end), ok. cleanup(_) -> - application:unset_env(eventhub, jwt_secret), - application:unset_env(eventhub, admin_jwt_secret), - application:stop(jose), - meck:unload(mnesia), - meck:unload(core_user), + meck:unload(logic_stats), + meck:unload(core_admin), + meck:unload(admin_utils), meck:unload(handler_auth), meck:unload(cowboy_req). admin_stats_test_() -> {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/stats with admin role returns 200 and dashboard data", - fun test_stats_admin/0}, - {"GET /admin/stats with non-admin role returns 403", - fun test_stats_forbidden/0}, - {"POST /admin/stats returns 405", - fun test_stats_wrong_method/0}, - {"Count functions return 0 with empty DB", - fun test_count_functions/0} + {"GET /admin/stats as superadmin returns 200 with system metrics", fun test_superadmin/0}, + {"GET /admin/stats as moderator returns 200 with own metrics", fun test_moderator/0}, + {"GET /admin/stats as support returns 200 with assigned tickets", fun test_support/0}, + {"GET /admin/stats with non‑admin token returns 403", fun test_forbidden/0}, + {"POST /admin/stats returns 405", fun test_wrong_method/0} ]}. -%% ── Успешный GET с ролью админа ──────────────────────────── -test_stats_admin() -> +%% --- Суперадмин --- +test_superadmin() -> ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - % Администратор с ролью superadmin - AdminUser = #user{id = <<"adm1">>, role = superadmin, _ = '_'}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), + ok = meck:expect(core_admin, get_by_id, + fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end), + ok = meck:expect(logic_stats, get_stats, + fun(superadmin, _) -> #{users => 10, events => 25} end), {ok, _, _} = admin_handler_stats:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - Stats = jsx:decode(RespBody, [return_maps]), - ?assert(is_map_key(<<"users">>, Stats)), - ?assert(is_map_key(<<"events">>, Stats)). + ?assertEqual(200, erase(test_reply)). -%% ── Обычный пользователь получает 403 ───────────────────── -test_stats_forbidden() -> +%% --- Модератор --- +test_moderator() -> + ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), + ok = meck:expect(handler_auth, authenticate, + fun(Req) -> {ok, <<"mod1">>, Req} end), + ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), + ok = meck:expect(core_admin, get_by_id, + fun(<<"mod1">>) -> {ok, #admin{id = <<"mod1">>, role = moderator}} end), + ok = meck:expect(logic_stats, get_stats, + fun(moderator, _) -> #{reports_reviewed => 5} end), + {ok, _, _} = admin_handler_stats:init(req, []), + ?assertEqual(200, erase(test_reply)). + +%% --- Поддержка --- +test_support() -> + ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), + ok = meck:expect(handler_auth, authenticate, + fun(Req) -> {ok, <<"sup1">>, Req} end), + ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), + ok = meck:expect(core_admin, get_by_id, + fun(<<"sup1">>) -> {ok, #admin{id = <<"sup1">>, role = support}} end), + ok = meck:expect(logic_stats, get_stats, + fun(support, _) -> #{tickets_assigned => 3} end), + {ok, _, _} = admin_handler_stats:init(req, []), + ?assertEqual(200, erase(test_reply)). + +%% --- Не админ --- +test_forbidden() -> ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), {ok, _, _} = admin_handler_stats:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(403, Status), - ?assertEqual(#{<<"error">> => <<"Admin access required">>}, jsx:decode(RespBody, [return_maps])). + ?assertEqual(403, erase(test_reply)). -%% ── Неверный метод ────────────────────────────────────── -test_stats_wrong_method() -> +%% --- Неверный метод --- +test_wrong_method() -> ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), {ok, _, _} = admin_handler_stats:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - ?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])). - -%% ── Функции подсчёта (мок mnesia) ────────────────────── -test_count_functions() -> - ?assertEqual(0, admin_handler_stats:count_users()), - ?assertEqual(0, admin_handler_stats:count_events()). \ No newline at end of file + ?assertEqual(405, erase(test_reply)). \ No newline at end of file