-module(api_admin_tests). -export([test/0]). %% Учётные данные по умолчанию -define(FALLBACK_ADMIN_SUPER_EMAIL, <<"superadmin@eventhub.local">>). -define(FALLBACK_ADMIN_SUPER_PASSWORD, <<"123456">>). -define(FALLBACK_ADMIN_MODER_EMAIL, <<"moderator@eventhub.local">>). -define(FALLBACK_ADMIN_MODER_PASSWORD, <<"123456">>). -define(FALLBACK_ADMIN_SUPPORT_EMAIL, <<"support@eventhub.local">>). -define(FALLBACK_ADMIN_SUPPORT_PASSWORD,<<"123456">>). test() -> ct:pal("Testing admin panel API...~n"), AdminURL = api_test_runner:get_admin_url(), UserURL = api_test_runner:get_base_url(), % Получаем токен суперадмина AdminToken = api_test_runner:get_admin_token(), %% TEST 1: Admin healthcheck (public) ct:pal(" TEST 1: Admin healthcheck... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/health", []}, [], []), ct:pal("OK~n"), %% TEST 2: Admin login (дополнительная проверка) ct:pal(" TEST 2: Admin login (attempt)... "), LoginBody = jsx:encode(#{ <<"email">> => ?FALLBACK_ADMIN_SUPER_EMAIL, <<"password">> => ?FALLBACK_ADMIN_SUPER_PASSWORD }), case httpc:request(post, {AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []) of {ok, {{_, 200, _}, _, _}} -> ct:pal("OK (logged in)~n"); _ -> ct:pal("SKIPPED (credentials not found, using runner token)~n") end, %% TEST 3: Admin stats (superadmin) ct:pal(" TEST 3: Admin stats (superadmin)... "), {ok, {{_, 200, _}, _, StatsResp1}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), Stats1 = jsx:decode(list_to_binary(StatsResp1), [return_maps]), ct:pal(" OK (keys: ~p)~n", [maps:keys(Stats1)]), %% TEST 4: List users ct:pal(" TEST 4: List users... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/users", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 5: Get user by ID ct:pal(" TEST 5: Get user by ID... "), UserId = api_test_runner:get_user_id(), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/users/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 6: List reports ct:pal(" TEST 6: List reports... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% ── TEST 7: Full moderation flow (create event, report, resolve) ── ct:pal(" TEST 7: Moderation flow... "), UserToken = api_test_runner:get_user_token(), CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ModerationTest">>}), EventId = api_test_runner:create_event(UserToken, CalId, #{ title => <<"Event to report">>, start_time => api_SUITE:future_date(), duration => 60 }), % Подаём жалобу на это событие CreateBody = jsx:encode(#{ <<"target_type">> => <<"event">>, <<"target_id">> => EventId, <<"reason">> => <<"Inappropriate content">> }), {ok, {{_, 201, _}, _, CreateResp}} = httpc:request(post, {UserURL ++ "/v1/reports", [{"Authorization", "Bearer " ++ binary_to_list(UserToken)}], "application/json", CreateBody}, [], []), #{<<"id">> := ReportId} = jsx:decode(list_to_binary(CreateResp), [return_maps]), % Администратор изменяет статус жалобы EditBody = jsx:encode(#{ <<"status">> => <<"reviewed">>, <<"reason">> => <<"Issue resolved">> }), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {AdminURL ++ "/v1/admin/reports/" ++ binary_to_list(ReportId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", EditBody}, [], []), ct:pal("OK~n"), %% TEST 8: List banned words ct:pal(" TEST 8: List banned words... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 9: Add banned word ct:pal(" TEST 9: Add banned word... "), BannedWordBody = jsx:encode(#{<<"word">> => <<"test_banned_word">>}), {ok, {{_, 201, _}, _, _}} = httpc:request(post, {AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", BannedWordBody}, [], []), ct:pal("OK~n"), %% TEST 10: Delete banned word ct:pal(" TEST 10: Delete banned word... "), {ok, {{_, 200, _}, _, _}} = httpc:request(delete, {AdminURL ++ "/v1/admin/banned-words/test_banned_word", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 11: List tickets ct:pal(" TEST 11: List tickets... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 12: Create ticket ct:pal(" TEST 12: Create ticket... "), TicketBody = jsx:encode(#{ <<"error_message">> => <<"Test error">>, <<"stacktrace">> => <<"trace">> }), {ok, {{_, 201, _}, _, TicketResp}} = httpc:request(post, {AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", TicketBody}, [], []), #{<<"id">> := TicketId} = jsx:decode(list_to_binary(TicketResp), [return_maps]), ct:pal("OK~n"), %% TEST 13: Get ticket by ID ct:pal(" TEST 13: Get ticket by ID... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 14: Update ticket ct:pal(" TEST 14: Update ticket... "), UpdateTicketBody = jsx:encode(#{<<"status">> => <<"closed">>}), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateTicketBody}, [], []), ct:pal("OK~n"), %% TEST 15: Delete ticket ct:pal(" TEST 15: Delete ticket... "), {ok, {{_, 200, _}, _, _}} = httpc:request(delete, {AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 16: Ticket stats ct:pal(" TEST 16: Ticket stats... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 17: List subscriptions ct:pal(" TEST 17: List subscriptions... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 18: Create subscription ct:pal(" TEST 18: Create subscription... "), SubBody = jsx:encode(#{ <<"user_id">> => UserId, <<"plan">> => <<"monthly">> }), {ok, {{_, 201, _}, _, SubResp}} = httpc:request(post, {AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", SubBody}, [], []), #{<<"id">> := SubId} = jsx:decode(list_to_binary(SubResp), [return_maps]), ct:pal("OK~n"), %% TEST 19: Get subscription by ID ct:pal(" TEST 19: Get subscription by ID... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 20: Update subscription ct:pal(" TEST 20: Update subscription... "), UpdateSubBody = jsx:encode(#{<<"status">> => <<"cancelled">>}), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateSubBody}, [], []), ct:pal("OK~n"), %% TEST 21: Delete subscription ct:pal(" TEST 21: Delete subscription... "), {ok, {{_, 200, _}, _, _}} = httpc:request(delete, {AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ct:pal("OK~n"), %% TEST 22: Moderation - block user ct:pal(" TEST 22: Moderation - block user... "), ModBody = jsx:encode(#{ <<"action">> => <<"block">>, <<"reason">> => <<"test">> }), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", ModBody}, [], []), ct:pal("OK~n"), %% TEST 23: Moderation - unblock user ct:pal(" TEST 23: Moderation - unblock user... "), UnblockBody = jsx:encode(#{ <<"action">> => <<"unblock">>, <<"reason">> => <<"restore">> }), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UnblockBody}, [], []), ct:pal("OK~n"), %% ======================================================== %% Admin Reviews list tests %% ======================================================== %% Подготовка тестовых данных для отзывов ct:pal(" Preparing test data for reviews... "), UserToken = api_test_runner:get_user_token(), % Создаем календарь и событие (отдельные переменные, чтобы не перекрыть TEST 7) RevCalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReviewsTest">>}), RevEventId = api_test_runner:create_event(UserToken, RevCalId, #{ title => <<"Event for review testing">>, start_time => api_SUITE:future_date(), duration => 60 }), ct:pal("OK (calendar: ~s, event: ~s)~n", [RevCalId, RevEventId]), ParticipantEmail = api_test_runner:unique_email(<<"rev_1">>), ParticipantEmail2 = api_test_runner:unique_email(<<"rev_2">>), ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"part123">>), ParticipantToken2 = api_test_runner:register_and_login(ParticipantEmail2, <<"part123">>), % Создаём и подтверждаем бронирование BookingId = api_test_runner:extract_json( api_test_runner:http_post("/v1/events/" ++ binary_to_list(RevEventId) ++ "/bookings", #{}, ParticipantToken), <<"id">>), api_test_runner:http_put("/v1/bookings/" ++ binary_to_list(BookingId), #{action => <<"confirm">>}, UserToken), Booking2Id = api_test_runner:extract_json( api_test_runner:http_post("/v1/events/" ++ binary_to_list(RevEventId) ++ "/bookings", #{}, ParticipantToken2), <<"id">>), api_test_runner:http_put("/v1/bookings/" ++ binary_to_list(Booking2Id), #{action => <<"confirm">>}, UserToken), ReviewId = api_test_runner:extract_json( api_test_runner:http_post("/v1/reviews", #{target_type => <<"event">>, target_id => RevEventId, rating => 5, comment => <<"Great!">>}, ParticipantToken), <<"id">>), ct:pal(" Review2Id: ~p~n", [ReviewId]), Review2Id = api_test_runner:extract_json( api_test_runner:http_post("/v1/reviews", #{target_type => <<"event">>, target_id => RevEventId, rating => 5, comment => <<"Great!">>}, ParticipantToken2), <<"id">>), ct:pal(" Review2Id: ~p~n", [Review2Id]), %% TEST 24: List all reviews (GET /v1/admin/reviews) ct:pal(" TEST 24: List all reviews... "), {ok, {{_, 200, _}, _, ListReviewsResp}} = httpc:request(get, {AdminURL ++ "/v1/admin/reviews", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), ReviewsList = jsx:decode(list_to_binary(ListReviewsResp), [return_maps]), true = is_list(ReviewsList), ct:pal(" OK (count: ~p)~n", [length(ReviewsList)]), %% TEST 25: List reviews with filters (GET /v1/admin/reviews?target_type=event&target_id=...) ct:pal(" TEST 25: List reviews with filters... "), FilterURL = AdminURL ++ "/v1/admin/reviews?target_type=event&target_id=" ++ binary_to_list(RevEventId), {ok, {{_, 200, _}, _, FilteredResp}} = httpc:request(get, {FilterURL, [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), FilteredList = jsx:decode(list_to_binary(FilteredResp), [return_maps]), ct:pal(" OK (filtered count: ~p)~n", [length(FilteredList)]), %% TEST 26: Bulk update review statuses (PATCH /v1/admin/reviews) ct:pal(" TEST 26: Bulk update review statuses... "), case ReviewsList of [FirstReview, SecondReview | _] -> FirstId = maps:get(<<"id">>, FirstReview), SecondId = maps:get(<<"id">>, SecondReview), PatchBody = jsx:encode([ #{<<"id">> => FirstId, <<"status">> => <<"visible">>}, #{<<"id">> => SecondId, <<"status">> => <<"hidden">>} ]), ct:pal(" OK (PatchBody: ~p)~n", [PatchBody]), {ok, {{_, 200, _}, _, PatchResp}} = httpc:request(patch, {AdminURL ++ "/v1/admin/reviews", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", PatchBody}, [], []), #{<<"updated_count">> := UpdatedCount} = jsx:decode(list_to_binary(PatchResp), [return_maps]), true = (UpdatedCount =:= 2), ct:pal(" OK (updated: ~p)~n", [UpdatedCount]); _ -> ct:pal("SKIPPED (not enough reviews for bulk update)~n") end, %% TEST 27: Method not allowed (POST /v1/admin/reviews → 405) ct:pal(" TEST 27: POST method not allowed... "), {ok, {{_, 405, _}, _, _}} = httpc:request(post, {AdminURL ++ "/v1/admin/reviews", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", <<"{}">>}, [], []), ct:pal("OK~n"), ct:pal("~n✅ Admin API tests passed!~n"), {?MODULE, ok}.