From 40806df62a1608bc42982a148d62508752e0e383 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: Wed, 13 May 2026 23:02:59 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2.=20=D0=A7=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=8C=203=20https://git.sabilin.com/EventHub/EventHubBac?= =?UTF-8?q?k/issues/21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- Makefile | 22 +- docker/traefik/dynamic_conf.yml | 2 +- rebar.config | 2 +- src/core/core_admin.erl | 12 +- src/core/core_user.erl | 10 +- src/eventhub_app.erl | 14 +- src/handlers/admin/admin_handler_admins.erl | 134 +- .../admin/admin_handler_admins_by_id.erl | 206 ++ src/handlers/admin/admin_handler_audit.erl | 95 +- src/handlers/admin/admin_handler_health.erl | 2 +- src/handlers/admin/admin_handler_me.erl | 47 +- src/handlers/admin/admin_handler_reviews.erl | 24 +- .../admin/admin_handler_reviews_by_id.erl | 20 +- .../admin/admin_handler_subscriptions.erl | 18 +- .../admin/admin_handler_user_by_id.erl | 69 +- src/handlers/handler_calendar_by_id.erl | 84 +- src/handlers/handler_health.erl | 2 +- src/handlers/handler_utils.erl | 17 +- src/handlers/swagger_docs_handler.erl | 28 +- src/infra/admin_utils.erl | 8 +- src/logic/logic_admin.erl | 23 +- src/logic/logic_review.erl | 2 +- src/logic/logic_search.erl | 28 +- src/logic/logic_ticket.erl | 2 +- src/logic/logic_user.erl | 14 +- src/swagger/admin-swagger.json | 1897 +++++++++-------- src/swagger/client-swagger.json | 1204 +++++------ src/swagger/trails.erl | 6 +- test/api/admins/admin_admins_tests.erl | 180 ++ test/api/admins/admin_audit_tests.erl | 79 + test/api/admins/admin_banned_words_tests.erl | 94 + test/api/admins/admin_events_tests.erl | 119 ++ test/api/admins/admin_me_tests.erl | 36 + test/api/admins/admin_moderation_tests.erl | 133 ++ test/api/admins/admin_reports_tests.erl | 134 ++ test/api/admins/admin_reviews_tests.erl | 136 ++ test/api/admins/admin_stats_tests.erl | 77 + test/api/admins/admin_subscriptions_tests.erl | 131 ++ test/api/admins/admin_tickets_tests.erl | 153 ++ test/api/admins/admin_users_tests.erl | 67 + test/api/admins/admin_websocket_tests.erl | 258 +++ test/api/api_admin_tests.erl | 510 ----- test/api/api_auth_tests.erl | 64 - test/api/api_booking_tests.erl | 83 - test/api/api_calendar_tests.erl | 61 - test/api/api_event_tests.erl | 70 - test/api/api_moderation_tests.erl | 72 - test/api/api_reviews_tests.erl | 58 - test/api/api_search_tests.erl | 54 - test/api/api_subscription_tests.erl | 36 - test/api/api_test_runner.erl | 513 ++--- test/api/api_tickets_tests.erl | 72 - test/api/users/user_bookings_tests.erl | 115 + test/api/users/user_calendar_by_id_tests.erl | 87 + test/api/users/user_calendar_view_tests.erl | 61 + test/api/users/user_calendars_tests.erl | 51 + test/api/users/user_event_by_id_tests.erl | 114 + test/api/users/user_events_tests.erl | 80 + test/api/users/user_login_tests.erl | 90 + test/api/users/user_me_tests.erl | 55 + test/api/users/user_my_bookings_tests.erl | 69 + test/api/users/user_my_reviews_tests.erl | 75 + .../users/user_occurrence_cancel_tests.erl | 99 + test/api/users/user_refresh_tests.erl | 88 + test/api/users/user_register_tests.erl | 76 + test/api/users/user_reports_tests.erl | 101 + test/api/users/user_review_by_id_tests.erl | 126 ++ test/api/users/user_reviews_tests.erl | 114 + test/api/users/user_search_tests.erl | 126 ++ test/api/users/user_subscription_tests.erl | 78 + test/api/users/user_tickets_tests.erl | 137 ++ .../user_websocket_tests.erl} | 106 +- test/api_SUITE.erl | 130 -- test/api_admins_SUITE.erl | 143 ++ test/api_users_SUITE.erl | 171 ++ test/scripts/run_tests.sh | 50 - test/scripts/start_server_bg.sh | 39 - test/scripts/test_admin_api.sh | 256 --- test/scripts/test_all.sh | 298 --- test/scripts/test_auth_api.sh | 217 -- test/scripts/test_booking_api.sh | 265 --- test/scripts/test_calendar_api.sh | 217 -- test/scripts/test_event_api.sh | 212 -- test/scripts/test_moderation_api.sh | 370 ---- test/scripts/test_reviews_api.sh | 454 ---- test/scripts/test_runner.sh | 228 -- test/scripts/test_search_api.sh | 393 ---- test/scripts/test_subscription_api.sh | 217 -- test/scripts/test_tickets_api.sh | 282 --- test/scripts/test_websocket_api.sh | 313 --- 91 files changed, 6138 insertions(+), 7150 deletions(-) create mode 100644 src/handlers/admin/admin_handler_admins_by_id.erl create mode 100644 test/api/admins/admin_admins_tests.erl create mode 100644 test/api/admins/admin_audit_tests.erl create mode 100644 test/api/admins/admin_banned_words_tests.erl create mode 100644 test/api/admins/admin_events_tests.erl create mode 100644 test/api/admins/admin_me_tests.erl create mode 100644 test/api/admins/admin_moderation_tests.erl create mode 100644 test/api/admins/admin_reports_tests.erl create mode 100644 test/api/admins/admin_reviews_tests.erl create mode 100644 test/api/admins/admin_stats_tests.erl create mode 100644 test/api/admins/admin_subscriptions_tests.erl create mode 100644 test/api/admins/admin_tickets_tests.erl create mode 100644 test/api/admins/admin_users_tests.erl create mode 100644 test/api/admins/admin_websocket_tests.erl delete mode 100644 test/api/api_admin_tests.erl delete mode 100644 test/api/api_auth_tests.erl delete mode 100644 test/api/api_booking_tests.erl delete mode 100644 test/api/api_calendar_tests.erl delete mode 100644 test/api/api_event_tests.erl delete mode 100644 test/api/api_moderation_tests.erl delete mode 100644 test/api/api_reviews_tests.erl delete mode 100644 test/api/api_search_tests.erl delete mode 100644 test/api/api_subscription_tests.erl delete mode 100644 test/api/api_tickets_tests.erl create mode 100644 test/api/users/user_bookings_tests.erl create mode 100644 test/api/users/user_calendar_by_id_tests.erl create mode 100644 test/api/users/user_calendar_view_tests.erl create mode 100644 test/api/users/user_calendars_tests.erl create mode 100644 test/api/users/user_event_by_id_tests.erl create mode 100644 test/api/users/user_events_tests.erl create mode 100644 test/api/users/user_login_tests.erl create mode 100644 test/api/users/user_me_tests.erl create mode 100644 test/api/users/user_my_bookings_tests.erl create mode 100644 test/api/users/user_my_reviews_tests.erl create mode 100644 test/api/users/user_occurrence_cancel_tests.erl create mode 100644 test/api/users/user_refresh_tests.erl create mode 100644 test/api/users/user_register_tests.erl create mode 100644 test/api/users/user_reports_tests.erl create mode 100644 test/api/users/user_review_by_id_tests.erl create mode 100644 test/api/users/user_reviews_tests.erl create mode 100644 test/api/users/user_search_tests.erl create mode 100644 test/api/users/user_subscription_tests.erl create mode 100644 test/api/users/user_tickets_tests.erl rename test/api/{api_websocket_tests.erl => users/user_websocket_tests.erl} (75%) delete mode 100644 test/api_SUITE.erl create mode 100644 test/api_admins_SUITE.erl create mode 100644 test/api_users_SUITE.erl delete mode 100644 test/scripts/run_tests.sh delete mode 100644 test/scripts/start_server_bg.sh delete mode 100644 test/scripts/test_admin_api.sh delete mode 100644 test/scripts/test_all.sh delete mode 100644 test/scripts/test_auth_api.sh delete mode 100644 test/scripts/test_booking_api.sh delete mode 100644 test/scripts/test_calendar_api.sh delete mode 100644 test/scripts/test_event_api.sh delete mode 100644 test/scripts/test_moderation_api.sh delete mode 100644 test/scripts/test_reviews_api.sh delete mode 100644 test/scripts/test_runner.sh delete mode 100644 test/scripts/test_search_api.sh delete mode 100644 test/scripts/test_subscription_api.sh delete mode 100644 test/scripts/test_tickets_api.sh delete mode 100644 test/scripts/test_websocket_api.sh diff --git a/.gitignore b/.gitignore index b8b2ca7..e60fc5d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ rebar3.crashdump /rebar.lock /build/ docker/.env -/Mnesia.*/ \ No newline at end of file +/Mnesia.*/ +/doc/ diff --git a/Makefile b/Makefile index a058242..c41829f 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ clean: ## Очистить проект @echo "Очистка проекта..." @$(REBAR3) clean #@rm -rf _build build/ct_run.* deps logs *.log - @rm -rf build/ct_run.* deps logs *.log + @rm -rf _build logs/ct_run.* deps doc *.log @echo "✓ Очистка завершена" deps: ## Установить зависимости @@ -98,25 +98,15 @@ eunit-verbose: ## Запустить EUnit тесты с подробным вы @echo "Запуск EUnit тестов (verbose)..." @$(REBAR3) eunit --sname $(SNAME)_test --verbose -test-api: test-ct - -test-ct: ## Запустить Common Test для API +test-api: ## Запустить Common Test для API @echo "Cleaning old data..." - @rm -rf Mnesia.* @rm -rf logs/test/ct/ct_run.* - @$(REBAR3) ct --sname $(SNAME)_api_test + @$(REBAR3) ct --sname $(SNAME)_api_test #-v -test-ct-verbose: ## Запустить Common Test с подробным выводом - @ct_run -suite test/api_SUITE \ - -pa _build/default/lib/*/ebin \ - -pa test/api \ - -logdir build \ - -verbosity 50 +test-api-remote: + @CT_MODE=remote API_HOST=http://localhost:8080 WS_HOST=ws://localhost:8081 ADMIN_API_HOST=http://localhost:8445 ADMIN_WS_HOST=ws://localhost:8446 rebar3 ct -test-remote: - @CT_MODE=remote API_HOST=http://localhost:8080 ADMIN_API_HOST=http://localhost:8445 rebar3 ct - -test-remote-cluster: +test-api-remote-cluster: @rm -rf logs/test/ct/ct_run.* @CT_MODE=remote \ API_HOST=https://api.eventhub.local/api \ diff --git a/docker/traefik/dynamic_conf.yml b/docker/traefik/dynamic_conf.yml index c053798..e649074 100644 --- a/docker/traefik/dynamic_conf.yml +++ b/docker/traefik/dynamic_conf.yml @@ -154,7 +154,7 @@ http: servers: - url: "http://eventhub:8445" healthCheck: - path: "/v1/admin/health" + path: "/admin/health" interval: "10s" timeout: "3s" admin-api-fallback: diff --git a/rebar.config b/rebar.config index 53634de..9d919ec 100644 --- a/rebar.config +++ b/rebar.config @@ -34,7 +34,7 @@ ]} ]}, {test, [ - {erl_opts, [debug_info, {i, "include"}, {d, 'TEST'}]}, + {erl_opts, [debug_info, nowarn_export_all, {i, "include"}, {d, 'TEST'}]}, {src_dirs, ["src", "test/unit"]}, {deps, [ {meck, "0.9.2"} diff --git a/src/core/core_admin.erl b/src/core/core_admin.erl index 3c2256a..8fc0c6e 100644 --- a/src/core/core_admin.erl +++ b/src/core/core_admin.erl @@ -2,7 +2,7 @@ -include("records.hrl"). -export([create/3, get_by_email/1, get_by_id/1, list_all/0, update_role/2, block/1, unblock/1, update_last_login/1]). --export([update/2]). +-export([update/2, delete/1]). create(Email, Password, Role) -> case get_by_email(Email) of @@ -90,6 +90,16 @@ update_status(Id, Status) -> Error -> Error end. +%% Физическое удаление администратора из базы +-spec delete(binary()) -> {ok, deleted} | {error, not_found}. +delete(AdminId) -> + case get_by_id(AdminId) of + {ok, _Admin} -> + mnesia:dirty_delete(admin, AdminId), + {ok, deleted}; + Error -> Error + end. + %%%=================================================================== %%% ВНУТРЕННИЕ ФУНКЦИИ %%%=================================================================== diff --git a/src/core/core_user.erl b/src/core/core_user.erl index 5e2b3ea..1391d66 100644 --- a/src/core/core_user.erl +++ b/src/core/core_user.erl @@ -177,8 +177,16 @@ apply_updates(User, Updates) -> set_field(email, Value, U) -> U#user{email = Value}; set_field(password_hash, Value, U) -> U#user{password_hash = Value}; -set_field(role, Value, U) when Value =:= user; Value =:= admin -> U#user{role = Value}; +set_field(role, Value, U) when Value =:= user; Value =:= admin; Value =:= bot -> U#user{role = Value}; set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted -> U#user{status = Value}; +set_field(reason, Value, U) -> U#user{reason = Value}; +set_field(nickname, Value, U) -> U#user{nickname = Value}; +set_field(avatar_url, Value, U) -> U#user{avatar_url = Value}; +set_field(timezone, Value, U) -> U#user{timezone = Value}; +set_field(language, Value, U) -> U#user{language = Value}; +set_field(social_links, Value, U) -> U#user{social_links = Value}; +set_field(phone, Value, U) -> U#user{phone = Value}; +set_field(preferences, Value, U) -> U#user{preferences = Value}; set_field(_, _, U) -> U. %% ------------------------------------------------------------------ diff --git a/src/eventhub_app.erl b/src/eventhub_app.erl index 220b782..be45051 100644 --- a/src/eventhub_app.erl +++ b/src/eventhub_app.erl @@ -35,9 +35,9 @@ start(_StartType, _StartArgs) -> end, case Nodes of [] -> - io:format("Cluster: no nodes found or first node~n"); + io:format("~nCluster: no nodes found or first node~n"); _ -> - io:format("Cluster: discovered nodes ~p, joining cluster~n", [Nodes]), + io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]), application:set_env(eventhub, extra_db_nodes, Nodes) end, ok = infra_mnesia:init_tables(), @@ -102,7 +102,7 @@ start_admin_http() -> Dispatch = cowboy_router:compile([ {'_', [ % ================== БАЗОВЫЕ ================== - {"/v1/admin/health", admin_handler_health, []}, + {"/admin/health", admin_handler_health, []}, {"/v1/admin/stats", admin_handler_stats, []}, {"/v1/admin/login", admin_handler_login, []}, % ================== ПОЛЬЗОВАТЕЛИ ================== @@ -127,13 +127,13 @@ start_admin_http() -> % ================== ПОДПИСКИ ================== {"/v1/admin/subscriptions", admin_handler_subscriptions, []}, {"/v1/admin/subscriptions/:id", admin_handler_subscriptions_by_id, []}, - % ================== МОДЕРАЦИЯ (общий маршрут) ================== - {"/v1/admin/:target_type/:id", admin_handler_moderation, []}, % ================== Управление ролями (только для superadmin) ================== {"/v1/admin/me", admin_handler_me, []}, {"/v1/admin/admins", admin_handler_admins, []}, - {"/v1/admin/admins/:id", admin_handler_admins, []}, - {"/v1/admin/audit", admin_handler_audit, []} + {"/v1/admin/admins/:id", admin_handler_admins_by_id, []}, + {"/v1/admin/audit", admin_handler_audit, []}, + % ================== МОДЕРАЦИЯ (общий маршрут) ================== + {"/v1/admin/:target_type/:id", admin_handler_moderation, []} ]} ]), diff --git a/src/handlers/admin/admin_handler_admins.erl b/src/handlers/admin/admin_handler_admins.erl index 6df4940..cda695d 100644 --- a/src/handlers/admin/admin_handler_admins.erl +++ b/src/handlers/admin/admin_handler_admins.erl @@ -1,3 +1,9 @@ +%%%------------------------------------------------------------------- +%%% @doc Административный обработчик списка администраторов. +%%% GET – список всех администраторов (только для superadmin). +%%% POST – создать нового администратора (только для superadmin). +%%% @end +%%%------------------------------------------------------------------- -module(admin_handler_admins). -behaviour(cowboy_handler). @@ -6,37 +12,66 @@ -include("records.hrl"). +%%% cowboy_handler callback +-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of - <<"GET">> -> list_admins(Req); - _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) + <<"GET">> -> list_admins(Req); + <<"POST">> -> create_admin(Req); + _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. +%%% Swagger metadata +-spec trails() -> [map()]. trails() -> - [ - #{ - path => <<"/v1/admin/admins">>, - method => <<"GET">>, - description => <<"List all admins (superadmin only)">>, - tags => [<<"Admins">>], - parameters => [ - #{name => <<"role">>, in => <<"query">>, schema => #{type => string}}, - #{name => <<"status">>, in => <<"query">>, schema => #{type => string}}, - #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}}, - #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}} - ], - responses => #{ - 200 => #{ - description => <<"Array of admins">>, - content => #{<<"application/json">> => #{schema => #{ - type => array, - items => admin_schema() - }}} - } + ListGet = #{ + path => <<"/v1/admin/admins">>, + method => <<"GET">>, + description => <<"List all admins (superadmin only)">>, + tags => [<<"Admins">>], + parameters => [ + #{name => <<"role">>, in => <<"query">>, schema => #{type => string}}, + #{name => <<"status">>, in => <<"query">>, schema => #{type => string}}, + #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}}, + #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}} + ], + responses => #{ + 200 => #{ + description => <<"Array of admins">>, + content => #{<<"application/json">> => #{schema => #{ + type => array, + items => admin_schema() + }}} } } - ]. + }, + PostCreate = #{ + path => <<"/v1/admin/admins">>, + method => <<"POST">>, + description => <<"Create a new admin (superadmin only)">>, + tags => [<<"Admins">>], + requestBody => #{ + required => true, + content => #{<<"application/json">> => #{schema => #{ + type => object, + required => [<<"email">>, <<"password">>, <<"role">>], + properties => #{ + email => #{type => string, format => <<"email">>}, + password => #{type => string}, + role => #{type => string, enum => [<<"superadmin">>, <<"admin">>, <<"moderator">>, <<"support">>]} + } + }}} + }, + responses => #{ + 201 => #{description => <<"Admin created">>}, + 400 => #{description => <<"Invalid fields">>}, + 403 => #{description => <<"Only superadmin can create admins">>}, + 409 => #{description => <<"Email already exists">>} + } + }, + [ListGet, PostCreate]. +-spec admin_schema() -> map(). admin_schema() -> #{ type => object, @@ -57,8 +92,12 @@ admin_schema() -> } }. +%%% Internal functions + +%% @doc GET /v1/admin/admins – список администраторов. +-spec list_admins(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. list_admins(Req) -> - case handler_utils:auth_admin(Req) of + case handler_utils:is_superadmin(Req) of {ok, _AdminId, Req1} -> Filters = parse_admin_filters(Req1), Pagination = handler_utils:parse_pagination_params(Req1), @@ -70,6 +109,41 @@ list_admins(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +%% @doc POST /v1/admin/admins – создание администратора. +-spec create_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. +create_admin(Req) -> + case handler_utils:is_superadmin(Req) of + {ok, _AdminId, Req1} -> + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + #{<<"email">> := Email, <<"password">> := Password, <<"role">> := RoleBin} -> + Role = try binary_to_existing_atom(RoleBin, utf8) + catch error:badarg -> undefined + end, + case Role of + undefined -> + handler_utils:send_error(Req2, 400, <<"Invalid role">>); + _ -> + case logic_admin:create_admin(Email, Password, Role) of + {ok, Admin} -> + handler_utils:send_json(Req2, 201, admin_to_json(Admin)); + {error, email_exists} -> + handler_utils:send_error(Req2, 409, <<"Email already exists">>); + {error, invalid_role} -> + handler_utils:send_error(Req2, 400, <<"Invalid role">>); + {error, _} -> + handler_utils:send_error(Req2, 500, <<"Internal server error">>) + end + end; + _ -> handler_utils:send_error(Req2, 400, <<"Missing fields">>) + catch + _:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON">>) + end; + {error, Code, Msg, Req1} -> + handler_utils:send_error(Req1, Code, Msg) + end. + +-spec parse_admin_filters(cowboy_req:req()) -> map(). parse_admin_filters(Req) -> Qs = cowboy_req:parse_qs(Req), #{ @@ -77,23 +151,25 @@ parse_admin_filters(Req) -> status => proplists:get_value(<<"status">>, Qs) }. +-spec admin_to_json(#admin{}) -> map(). admin_to_json(Admin) -> #{ id => Admin#admin.id, email => Admin#admin.email, - role => Admin#admin.role, - status => Admin#admin.status, + role => atom_to_binary(Admin#admin.role, utf8), + status => atom_to_binary(Admin#admin.status, utf8), nickname => Admin#admin.nickname, avatar_url => Admin#admin.avatar_url, timezone => Admin#admin.timezone, language => Admin#admin.language, phone => Admin#admin.phone, preferences => Admin#admin.preferences, - last_login => handler_utils:parse_datetime(Admin#admin.last_login), % требует доработки – лучше общую функцию - created_at => handler_utils:parse_datetime(Admin#admin.created_at), - updated_at => handler_utils:parse_datetime(Admin#admin.updated_at) + last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login), + created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at), + updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at) }. +-spec pagination_headers(map(), non_neg_integer()) -> map(). pagination_headers(#{limit := Limit, offset := Offset}, Total) -> RangeEnd = min(Offset + Limit - 1, Total - 1), #{ diff --git a/src/handlers/admin/admin_handler_admins_by_id.erl b/src/handlers/admin/admin_handler_admins_by_id.erl new file mode 100644 index 0000000..2463383 --- /dev/null +++ b/src/handlers/admin/admin_handler_admins_by_id.erl @@ -0,0 +1,206 @@ +%%%------------------------------------------------------------------- +%%% @doc Административный обработчик конкретного администратора. +%%% GET /v1/admin/admins/:id – получить администратора +%%% PUT /v1/admin/admins/:id – обновить администратора +%%% DELETE /v1/admin/admins/:id – удалить администратора +%%% +%%% Все операции доступны только суперадмину. +%%% @end +%%%------------------------------------------------------------------- +-module(admin_handler_admins_by_id). +-behaviour(cowboy_handler). + +-export([init/2]). +-export([trails/0]). + +-include("records.hrl"). + +%%% cowboy_handler callback +-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. +init(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> get_admin(Req); + <<"PUT">> -> update_admin(Req); + <<"DELETE">> -> delete_admin(Req); + _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) + end. + +%%% Swagger metadata +-spec trails() -> [map()]. +trails() -> + IdParam = [#{name => <<"id">>, in => <<"path">>, required => true, schema => #{type => string}}], + [ + #{ % GET by id + path => <<"/v1/admin/admins/:id">>, + method => <<"GET">>, + description => <<"Get admin by ID (superadmin only)">>, + tags => [<<"Admins">>], + parameters => IdParam, + responses => #{ + 200 => #{ + description => <<"Admin details">>, + content => #{<<"application/json">> => #{schema => admin_schema()}} + }, + 404 => #{description => <<"Admin not found">>} + } + }, + #{ % PUT update + path => <<"/v1/admin/admins/:id">>, + method => <<"PUT">>, + description => <<"Update admin (superadmin only)">>, + tags => [<<"Admins">>], + parameters => IdParam, + requestBody => #{ + required => true, + content => #{<<"application/json">> => #{schema => admin_update_schema()}} + }, + responses => #{ + 200 => #{description => <<"Admin updated">>}, + 404 => #{description => <<"Admin not found">>} + } + }, + #{ % DELETE + path => <<"/v1/admin/admins/:id">>, + method => <<"DELETE">>, + description => <<"Delete admin (superadmin only)">>, + tags => [<<"Admins">>], + parameters => IdParam, + responses => #{ + 200 => #{description => <<"Admin deleted">>}, + 404 => #{description => <<"Admin not found">>} + } + } + ]. + +-spec admin_schema() -> map(). +admin_schema() -> + #{ + type => object, + properties => #{ + id => #{type => string}, + email => #{type => string}, + role => #{type => string, enum => [<<"superadmin">>, <<"admin">>, <<"moderator">>, <<"support">>]}, + status => #{type => string, enum => [<<"active">>, <<"blocked">>]}, + nickname => #{type => string, nullable => true}, + avatar_url => #{type => string, nullable => true}, + timezone => #{type => string, nullable => true}, + language => #{type => string, nullable => true}, + phone => #{type => string, nullable => true}, + preferences => #{type => object, nullable => true}, + last_login => #{type => string, format => <<"date-time">>}, + created_at => #{type => string, format => <<"date-time">>}, + updated_at => #{type => string, format => <<"date-time">>} + } + }. + +-spec admin_update_schema() -> map(). +admin_update_schema() -> + #{ + type => object, + properties => #{ + nickname => #{type => string}, + avatar_url => #{type => string}, + timezone => #{type => string}, + language => #{type => string}, + phone => #{type => string}, + preferences => #{type => object} + } + }. + +%%% Internal functions + +%% @doc GET /v1/admin/admins/:id – получение администратора. +-spec get_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. +get_admin(Req) -> + case handler_utils:is_superadmin(Req) of + {ok, _AdminId, Req1} -> + Id = cowboy_req:binding(id, Req1), + case logic_admin:get_admin(Id) of + {ok, Admin} -> + handler_utils:send_json(Req1, 200, admin_to_json(Admin)); + {error, not_found} -> + handler_utils:send_error(Req1, 404, <<"Admin not found">>); + {error, _} -> + handler_utils:send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Msg, Req1} -> + handler_utils:send_error(Req1, Code, Msg) + end. + +%% @doc PUT /v1/admin/admins/:id – обновление администратора. +-spec update_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. +update_admin(Req) -> + case handler_utils:is_superadmin(Req) of + {ok, _AdminId, Req1} -> + Id = cowboy_req:binding(id, Req1), + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + UpdatesMap when is_map(UpdatesMap) -> + Updates0 = maps:to_list(UpdatesMap), + Updates = convert_admin_fields(Updates0), + case logic_admin:update_admin(Id, Updates) of + {ok, Admin} -> + handler_utils:send_json(Req2, 200, admin_to_json(Admin)); + {error, not_found} -> + handler_utils:send_error(Req2, 404, <<"Admin not found">>); + {error, _} -> + handler_utils:send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + handler_utils:send_error(Req2, 400, <<"Invalid JSON">>) + catch + _:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON format">>) + end; + {error, Code, Msg, Req1} -> + handler_utils:send_error(Req1, Code, Msg) + end. + +%% @doc DELETE /v1/admin/admins/:id – физическое удаление администратора. +-spec delete_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. +delete_admin(Req) -> + case handler_utils:is_superadmin(Req) of + {ok, _AdminId, Req1} -> + Id = cowboy_req:binding(id, Req1), + case logic_admin:delete_admin(Id) of + {ok, _} -> + handler_utils:send_json(Req1, 200, #{status => <<"deleted">>}); + {error, not_found} -> + handler_utils:send_error(Req1, 404, <<"Admin not found">>); + {error, _} -> + handler_utils:send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Msg, Req1} -> + handler_utils:send_error(Req1, Code, Msg) + end. + +%% @private Преобразует бинарные ключи в атомы для обновлений администратора. +-spec convert_admin_fields([{binary(), term()}]) -> [{atom(), term()}]. +convert_admin_fields(Updates) -> + lists:map(fun + ({<<"nickname">>, V}) -> {nickname, V}; + ({<<"avatar_url">>, V}) -> {avatar_url, V}; + ({<<"timezone">>, V}) -> {timezone, V}; + ({<<"language">>, V}) -> {language, V}; + ({<<"phone">>, V}) -> {phone, V}; + ({<<"preferences">>, V}) -> {preferences, V}; + (Other) -> Other + end, Updates). + +%% @private Преобразует запись администратора в JSON-совместимую карту. +-spec admin_to_json(#admin{}) -> map(). +admin_to_json(Admin) -> + #{ + id => Admin#admin.id, + email => Admin#admin.email, + role => atom_to_binary(Admin#admin.role, utf8), + status => atom_to_binary(Admin#admin.status, utf8), + nickname => Admin#admin.nickname, + avatar_url => Admin#admin.avatar_url, + timezone => Admin#admin.timezone, + language => Admin#admin.language, + phone => Admin#admin.phone, + preferences => Admin#admin.preferences, + last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login), + created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at), + updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at) + }. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_audit.erl b/src/handlers/admin/admin_handler_audit.erl index 09a2f57..f696dcb 100644 --- a/src/handlers/admin/admin_handler_audit.erl +++ b/src/handlers/admin/admin_handler_audit.erl @@ -1,6 +1,7 @@ %%%------------------------------------------------------------------- %%% @doc Административный обработчик журнала аудита. %%% GET – список записей аудита с пагинацией и фильтрацией. +%%% Доступно только суперадмину. %%% @end %%%------------------------------------------------------------------- -module(admin_handler_audit). @@ -26,16 +27,15 @@ trails() -> #{ path => <<"/v1/admin/audit">>, method => <<"GET">>, - description => <<"List audit records (admin)">>, + description => <<"List audit records (superadmin only)">>, tags => [<<"Audit">>], parameters => [ - #{name => <<"admin_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by admin ID">>}, - #{name => <<"action">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by action">>}, - #{name => <<"entity_type">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by entity type">>}, - #{name => <<"from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start timestamp (ISO8601)">>}, - #{name => <<"to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End timestamp (ISO8601)">>}, - #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, - #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} + #{name => <<"admin_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by admin ID">>}, + #{name => <<"action">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by action">>}, + #{name => <<"date_from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start timestamp (ISO8601)">>}, + #{name => <<"date_to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End timestamp (ISO8601)">>}, + #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, + #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], responses => #{ 200 => #{ @@ -71,16 +71,13 @@ audit_schema() -> %% @doc Получить список записей аудита с пагинацией и фильтрацией. -spec list_audit(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. list_audit(Req) -> - case handler_utils:auth_admin(Req) of + case handler_utils:is_superadmin(Req) of {ok, _AdminId, Req1} -> Filters = parse_audit_filters(Req1), Pagination = handler_utils:parse_pagination_params(Req1), - %% Предполагается, что core_admin_audit (или аналогичный) предоставляет list_all/0 - {ok, AllRecords} = core_admin_audit:list(), - Filtered = apply_filters(AllRecords, Filters), - Sorted = sort_audit(Filtered, Pagination), - Total = length(Sorted), - Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), + AllRecords = core_admin_audit:list(Filters), + Total = length(AllRecords), + Page = lists:sublist(AllRecords, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), Json = [audit_to_json(R) || R <- Page], ExtraHeaders = pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); @@ -88,65 +85,24 @@ list_audit(Req) -> handler_utils:send_error(Req1, Code, Msg) end. -%% @private Извлечь фильтры из query string. --spec parse_audit_filters(cowboy_req:req()) -> map(). +-spec parse_audit_filters(cowboy_req:req()) -> proplists:proplist(). parse_audit_filters(Req) -> Qs = cowboy_req:parse_qs(Req), - #{ - admin_id => proplists:get_value(<<"admin_id">>, Qs), - action => proplists:get_value(<<"action">>, Qs), - entity_type => proplists:get_value(<<"entity_type">>, Qs), - from => handler_utils:parse_datetime_qs(proplists:get_value(<<"from">>, Qs)), - to => handler_utils:parse_datetime_qs(proplists:get_value(<<"to">>, Qs)) - }. + [ + {admin_id, proplists:get_value(<<"admin_id">>, Qs)}, + {action, proplists:get_value(<<"action">>, Qs)}, + {date_from, parse_datetime_qs(proplists:get_value(<<"date_from">>, Qs))}, + {date_to, parse_datetime_qs(proplists:get_value(<<"date_to">>, Qs))} + ]. -%% @private Применить фильтры к списку записей аудита. --spec apply_filters([#admin_audit{}], map()) -> [#admin_audit{}]. -apply_filters(Records, Filters) -> - AdminId = maps:get(admin_id, Filters, undefined), - Action = maps:get(action, Filters, undefined), - EntityType = maps:get(entity_type, Filters, undefined), - From = maps:get(from, Filters, undefined), - To = maps:get(to, Filters, undefined), - R1 = case AdminId of - undefined -> Records; - _ -> [R || R <- Records, R#admin_audit.admin_id =:= AdminId] - end, - R2 = case Action of - undefined -> R1; - _ -> [R || R <- R1, R#admin_audit.action =:= Action] - end, - R3 = case EntityType of - undefined -> R2; - _ -> [R || R <- R2, R#admin_audit.entity_type =:= EntityType] - end, - R4 = case From of - undefined -> R3; - _ -> [R || R <- R3, R#admin_audit.timestamp >= From] - end, - case To of - undefined -> R4; - _ -> [R || R <- R4, R#admin_audit.timestamp =< To] +-spec parse_datetime_qs(binary() | undefined) -> calendar:datetime() | undefined. +parse_datetime_qs(undefined) -> undefined; +parse_datetime_qs(Bin) -> + case handler_utils:parse_datetime(Bin) of + {ok, Dt} -> Dt; + _ -> undefined end. -%% @private Отсортировать записи аудита. --spec sort_audit([#admin_audit{}], map()) -> [#admin_audit{}]. -sort_audit(Records, #{sort := Sort, order := Order}) -> - Field = binary_to_existing_atom(Sort, utf8), - lists:sort( - fun(A, B) -> - ValA = audit_field(A, Field), - ValB = audit_field(B, Field), - if Order == <<"asc">> -> ValA =< ValB; - true -> ValA >= ValB - end - end, Records). - -audit_field(#admin_audit{timestamp = V}, timestamp) -> V; -audit_field(#admin_audit{action = V}, action) -> V; -audit_field(_, _) -> undefined. - -%% @private Преобразовать запись аудита в JSON-карту. -spec audit_to_json(#admin_audit{}) -> map(). audit_to_json(A) -> #{ @@ -162,7 +118,6 @@ audit_to_json(A) -> reason => A#admin_audit.reason }. -%% @private Сформировать заголовки пагинации. -spec pagination_headers(map(), non_neg_integer()) -> map(). pagination_headers(#{limit := Limit, offset := Offset}, Total) -> RangeEnd = min(Offset + Limit - 1, Total - 1), diff --git a/src/handlers/admin/admin_handler_health.erl b/src/handlers/admin/admin_handler_health.erl index a56e84b..9722999 100644 --- a/src/handlers/admin/admin_handler_health.erl +++ b/src/handlers/admin/admin_handler_health.erl @@ -17,7 +17,7 @@ init(Req, _State) -> trails() -> [ #{ - path => <<"/v1/admin/health">>, + path => <<"/admin/health">>, method => <<"GET">>, description => <<"Admin API health check">>, tags => [<<"Health">>], diff --git a/src/handlers/admin/admin_handler_me.erl b/src/handlers/admin/admin_handler_me.erl index 326fe37..83d145a 100644 --- a/src/handlers/admin/admin_handler_me.erl +++ b/src/handlers/admin/admin_handler_me.erl @@ -1,3 +1,9 @@ +%%%------------------------------------------------------------------- +%%% @doc Административный обработчик профиля текущего администратора. +%%% GET – получить свой профиль. +%%% PUT – обновить свой профиль. +%%% @end +%%%------------------------------------------------------------------- -module(admin_handler_me). -behaviour(cowboy_handler). @@ -6,6 +12,8 @@ -include("records.hrl"). +%%% cowboy_handler callback +-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_me(Req); @@ -13,6 +21,8 @@ init(Req, _Opts) -> _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. +%%% Swagger metadata +-spec trails() -> [map()]. trails() -> [ #{ % GET @@ -42,6 +52,7 @@ trails() -> } ]. +-spec admin_schema() -> map(). admin_schema() -> #{ type => object, @@ -62,6 +73,7 @@ admin_schema() -> } }. +-spec admin_update_schema() -> map(). admin_update_schema() -> #{ type => object, @@ -75,6 +87,9 @@ admin_update_schema() -> } }. +%%% Internal functions + +-spec get_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_me(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> @@ -90,13 +105,15 @@ get_me(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +-spec update_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. update_me(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of UpdatesMap when is_map(UpdatesMap) -> - Updates = maps:to_list(UpdatesMap), + Updates0 = maps:to_list(UpdatesMap), + Updates = convert_admin_fields(Updates0), case logic_admin:update_admin(AdminId, Updates) of {ok, Admin} -> handler_utils:send_json(Req2, 200, admin_to_json(Admin)); @@ -114,6 +131,21 @@ update_me(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +%% @private Преобразует бинарные ключи в атомы для обновлений администратора. +-spec convert_admin_fields([{binary(), term()}]) -> [{atom(), term()}]. +convert_admin_fields(Updates) -> + lists:map(fun + ({<<"nickname">>, V}) -> {nickname, V}; + ({<<"avatar_url">>, V}) -> {avatar_url, V}; + ({<<"timezone">>, V}) -> {timezone, V}; + ({<<"language">>, V}) -> {language, V}; + ({<<"phone">>, V}) -> {phone, V}; + ({<<"preferences">>, V}) -> {preferences, V}; + (Other) -> Other + end, Updates). + +%% @private Формирует JSON-представление администратора. +-spec admin_to_json(#admin{}) -> map(). admin_to_json(Admin) -> #{ id => Admin#admin.id, @@ -126,12 +158,7 @@ admin_to_json(Admin) -> language => Admin#admin.language, phone => Admin#admin.phone, preferences => Admin#admin.preferences, - last_login => datetime_to_iso8601(Admin#admin.last_login), - created_at => datetime_to_iso8601(Admin#admin.created_at), - updated_at => datetime_to_iso8601(Admin#admin.updated_at) - }. - -datetime_to_iso8601(undefined) -> undefined; -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])). \ No newline at end of file + last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login), + created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at), + updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at) + }. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_reviews.erl b/src/handlers/admin/admin_handler_reviews.erl index f9799f6..c6f6c44 100644 --- a/src/handlers/admin/admin_handler_reviews.erl +++ b/src/handlers/admin/admin_handler_reviews.erl @@ -88,7 +88,15 @@ review_schema() -> list_reviews(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> - Filters = parse_review_filters(Req1), + Qs = cowboy_req:parse_qs(Req1), + Filters0 = #{ + target_type => normalize_target_type(proplists:get_value(<<"target_type">>, Qs)), + target_id => proplists:get_value(<<"target_id">>, Qs), + user_id => proplists:get_value(<<"user_id">>, Qs), + status => proplists:get_value(<<"status">>, Qs) + }, + % Убираем ключи со значением undefined + Filters = maps:filter(fun(_, V) -> V =/= undefined end, Filters0), Pagination = handler_utils:parse_pagination_params(Req1), {ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, Pagination), Json = [handler_utils:review_to_json(R) || R <- Reviews], @@ -98,6 +106,11 @@ list_reviews(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +%% @private Преобразует бинарный target_type в атом. +normalize_target_type(<<"event">>) -> event; +normalize_target_type(<<"calendar">>) -> calendar; +normalize_target_type(Other) -> Other. + bulk_update_reviews(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -118,15 +131,6 @@ bulk_update_reviews(Req) -> handler_utils:send_error(Req1, Code, Msg) end. -parse_review_filters(Req) -> - Qs = cowboy_req:parse_qs(Req), - #{ - target_type => proplists:get_value(<<"target_type">>, Qs), - target_id => proplists:get_value(<<"target_id">>, Qs), - user_id => proplists:get_value(<<"user_id">>, Qs), - status => proplists:get_value(<<"status">>, Qs) - }. - pagination_headers(#{limit := Limit, offset := Offset}, Total) -> RangeEnd = min(Offset + Limit - 1, Total - 1), #{ diff --git a/src/handlers/admin/admin_handler_reviews_by_id.erl b/src/handlers/admin/admin_handler_reviews_by_id.erl index 725cccf..b75ecab 100644 --- a/src/handlers/admin/admin_handler_reviews_by_id.erl +++ b/src/handlers/admin/admin_handler_reviews_by_id.erl @@ -109,7 +109,8 @@ update_review(Req) -> try jsx:decode(Body, [return_maps]) of UpdatesMap when is_map(UpdatesMap) -> Updates = maps:to_list(UpdatesMap), - case logic_review:update_review_admin(ReviewId, Updates) of + Converted = convert_review_fields(Updates), + case logic_review:update_review_admin(ReviewId, Converted) of {ok, Review} -> handler_utils:send_json(Req2, 200, handler_utils:review_to_json(Review)); {error, not_found} -> @@ -124,4 +125,19 @@ update_review(Req) -> end; {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) - end. \ No newline at end of file + end. + +%% @private Преобразует бинарные ключи и значения в атомы, где необходимо. +convert_review_fields(Updates) -> + lists:map(fun convert_field/1, Updates). + +convert_field({<<"status">>, Val}) -> + try binary_to_existing_atom(Val, utf8) of + Atom -> {status, Atom} + catch + error:badarg -> {status, Val} + end; +convert_field({<<"reason">>, Val}) -> {reason, Val}; +convert_field({<<"comment">>, Val}) -> {comment, Val}; +convert_field({<<"rating">>, Val}) -> {rating, Val}; +convert_field(Other) -> Other. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_subscriptions.erl b/src/handlers/admin/admin_handler_subscriptions.erl index f5dc544..7278ded 100644 --- a/src/handlers/admin/admin_handler_subscriptions.erl +++ b/src/handlers/admin/admin_handler_subscriptions.erl @@ -87,15 +87,21 @@ parse_subscription_filters(Req) -> }. apply_filters(Subs, Filters) -> - Plan = maps:get(plan, Filters, undefined), - Status = maps:get(status, Filters, undefined), - F1 = case Plan of + PlanBin = maps:get(plan, Filters, undefined), + StatusBin = maps:get(status, Filters, undefined), + F1 = case PlanBin of undefined -> Subs; - _ -> [S || S <- Subs, S#subscription.plan =:= Plan] + _ -> + Plan = try binary_to_existing_atom(PlanBin, utf8) + catch error:badarg -> PlanBin end, + [S || S <- Subs, S#subscription.plan =:= Plan] end, - case Status of + case StatusBin of undefined -> F1; - _ -> [S || S <- F1, S#subscription.status =:= Status] + _ -> + Status = try binary_to_existing_atom(StatusBin, utf8) + catch error:badarg -> StatusBin end, + [S || S <- F1, S#subscription.status =:= Status] end. sort_subscriptions(Subs, #{sort := Sort, order := Order}) -> diff --git a/src/handlers/admin/admin_handler_user_by_id.erl b/src/handlers/admin/admin_handler_user_by_id.erl index 3379f8d..375bd9f 100644 --- a/src/handlers/admin/admin_handler_user_by_id.erl +++ b/src/handlers/admin/admin_handler_user_by_id.erl @@ -1,3 +1,10 @@ +%%%------------------------------------------------------------------- +%%% @doc Административный обработчик конкретного пользователя. +%%% GET – получить пользователя по ID. +%%% PUT – обновить пользователя (роль, статус, причина). +%%% DELETE – мягко удалить пользователя. +%%% @end +%%%------------------------------------------------------------------- -module(admin_handler_user_by_id). -behaviour(cowboy_handler). @@ -6,6 +13,8 @@ -include("records.hrl"). +%%% cowboy_handler callback +-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_user(Req); @@ -14,8 +23,16 @@ init(Req, _Opts) -> _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. +%%% Swagger metadata +-spec trails() -> [map()]. trails() -> - BaseParams = [#{name => <<"id">>, in => <<"path">>, required => true, schema => #{type => string}}], + BaseParams = [#{ + name => <<"id">>, + in => <<"path">>, + description => <<"User ID">>, + required => true, + schema => #{type => string} + }], [ #{ % GET path => <<"/v1/admin/users/:id">>, @@ -56,20 +73,21 @@ trails() -> } ]. +-spec user_schema() -> map(). user_schema() -> #{ type => object, properties => #{ id => #{type => string}, email => #{type => string}, - role => #{type => string}, - status => #{type => string}, + role => #{type => string, enum => [<<"user">>, <<"bot">>]}, + status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]}, reason => #{type => string, nullable => true}, nickname => #{type => string, nullable => true}, avatar_url => #{type => string, nullable => true}, timezone => #{type => string, nullable => true}, language => #{type => string, nullable => true}, - social_links => #{type => array, items => #{type => string}}, + social_links => #{type => array, items => #{type => string}, nullable => true}, phone => #{type => string, nullable => true}, preferences => #{type => object, nullable => true}, last_login => #{type => string, format => <<"date-time">>}, @@ -78,21 +96,20 @@ user_schema() -> } }. +-spec user_update_schema() -> map(). user_update_schema() -> #{ type => object, properties => #{ - role => #{type => string, enum => [<<"user">>, <<"bot">>]}, - status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]}, - reason => #{type => string}, - nickname => #{type => string}, - timezone => #{type => string}, - language => #{type => string}, - phone => #{type => string}, - preferences => #{type => object} + role => #{type => string, enum => [<<"user">>, <<"bot">>]}, + status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]}, + reason => #{type => string} } }. +%%% Internal functions + +-spec get_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_user(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -109,6 +126,7 @@ get_user(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +-spec update_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. update_user(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -117,7 +135,8 @@ update_user(Req) -> try jsx:decode(Body, [return_maps]) of UpdatesMap when is_map(UpdatesMap) -> Updates = maps:to_list(UpdatesMap), - case logic_user:update_user_admin(UserId, Updates) of + Converted = convert_user_fields(Updates), + case logic_user:update_user_admin(UserId, Converted) of {ok, User} -> handler_utils:send_json(Req2, 200, handler_utils:user_to_json(User)); {error, not_found} -> @@ -134,6 +153,7 @@ update_user(Req) -> handler_utils:send_error(Req1, Code, Msg) end. +-spec delete_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. delete_user(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -148,4 +168,25 @@ delete_user(Req) -> end; {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) - end. \ No newline at end of file + end. + +%% @private Преобразует бинарные ключи и значения в атомы, где необходимо. +-spec convert_user_fields([{binary(), term()}]) -> [{atom(), term()}]. +convert_user_fields(Updates) -> + lists:map(fun convert_field/1, Updates). + +-spec convert_field({binary(), term()}) -> {atom(), term()}. +convert_field({<<"role">>, <<"user">>}) -> {role, user}; +convert_field({<<"role">>, <<"bot">>}) -> {role, bot}; +convert_field({<<"status">>, <<"active">>}) -> {status, active}; +convert_field({<<"status">>, <<"frozen">>}) -> {status, frozen}; +convert_field({<<"status">>, <<"deleted">>}) -> {status, deleted}; +convert_field({<<"reason">>, Val}) -> {reason, Val}; +convert_field({<<"nickname">>, Val}) -> {nickname, Val}; +convert_field({<<"avatar_url">>, Val}) -> {avatar_url, Val}; +convert_field({<<"timezone">>, Val}) -> {timezone, Val}; +convert_field({<<"language">>, Val}) -> {language, Val}; +convert_field({<<"social_links">>, Val}) -> {social_links, Val}; +convert_field({<<"phone">>, Val}) -> {phone, Val}; +convert_field({<<"preferences">>, Val}) -> {preferences, Val}; +convert_field(Other) -> Other. \ No newline at end of file diff --git a/src/handlers/handler_calendar_by_id.erl b/src/handlers/handler_calendar_by_id.erl index 5ec024a..009623c 100644 --- a/src/handlers/handler_calendar_by_id.erl +++ b/src/handlers/handler_calendar_by_id.erl @@ -1,6 +1,5 @@ %%%------------------------------------------------------------------- %%% @doc Обработчик конкретного календаря (клиентский API). -%%% %%% GET – получить информацию о календаре. %%% PUT – обновить календарь (владельцем). %%% DELETE – удалить календарь (владельцем). @@ -16,11 +15,7 @@ %%% cowboy_handler callback -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. -init(Req, Opts) -> - handle(Req, Opts). - --spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. -handle(Req, _Opts) -> +init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_calendar(Req); <<"PUT">> -> update_calendar(Req); @@ -31,15 +26,13 @@ handle(Req, _Opts) -> %%% Swagger metadata -spec trails() -> [map()]. trails() -> - BaseParams = [ - #{ - name => <<"id">>, - in => <<"path">>, - description => <<"Calendar ID">>, - required => true, - schema => #{type => string} - } - ], + BaseParams = [#{ + name => <<"id">>, + in => <<"path">>, + description => <<"Calendar ID">>, + required => true, + schema => #{type => string} + }], [ #{ % GET path => <<"/v1/calendars/:id">>, @@ -51,9 +44,7 @@ trails() -> 200 => #{ description => <<"Calendar details">>, content => #{<<"application/json">> => #{schema => calendar_schema()}} - }, - 403 => #{description => <<"Access denied">>}, - 404 => #{description => <<"Calendar not found">>} + } } }, #{ % PUT @@ -67,10 +58,7 @@ trails() -> content => #{<<"application/json">> => #{schema => calendar_update_schema()}} }, responses => #{ - 200 => #{description => <<"Calendar updated">>}, - 400 => #{description => <<"Invalid request">>}, - 403 => #{description => <<"Access denied">>}, - 404 => #{description => <<"Calendar not found">>} + 200 => #{description => <<"Calendar updated">>} } }, #{ % DELETE @@ -80,9 +68,7 @@ trails() -> tags => [<<"Calendars">>], parameters => BaseParams, responses => #{ - 200 => #{description => <<"Calendar deleted">>}, - 403 => #{description => <<"Access denied">>}, - 404 => #{description => <<"Calendar not found">>} + 200 => #{description => <<"Calendar deleted">>} } } ]. @@ -90,24 +76,18 @@ trails() -> -spec calendar_schema() -> map(). calendar_schema() -> #{ - type => object, + type => object, properties => #{ id => #{type => string}, owner_id => #{type => string}, title => #{type => string}, description => #{type => string}, - short_name => #{type => string, nullable => true}, - category => #{type => string, nullable => true}, - color => #{type => string, nullable => true}, - image_url => #{type => string, nullable => true}, - settings => #{type => object, nullable => true}, - tags => #{type => array, items => #{type => string}}, type => #{type => string, enum => [<<"personal">>, <<"commercial">>]}, - confirmation => #{type => string, description => <<"auto, manual, or {timeout, N}">>}, + confirmation => #{type => string, enum => [<<"auto">>, <<"manual">>]}, + tags => #{type => array, items => #{type => string}}, rating_avg => #{type => number, format => float}, rating_count => #{type => integer}, - status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]}, - reason => #{type => string, nullable => true}, + status => #{type => string}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } @@ -116,22 +96,18 @@ calendar_schema() -> -spec calendar_update_schema() -> map(). calendar_update_schema() -> #{ - type => object, + type => object, properties => #{ title => #{type => string}, description => #{type => string}, - type => #{type => string, enum => [<<"personal">>, <<"commercial">>]}, - confirmation => #{type => string, description => <<"auto, manual, or {timeout, N}">>}, + type => #{type => string}, + confirmation => #{type => string}, tags => #{type => array, items => #{type => string}} } }. -%%%=================================================================== -%%% HTTP-методы -%%%=================================================================== +%%% Internal functions -%% @doc GET /v1/calendars/:id — получение календаря. --spec get_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_calendar(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> @@ -150,8 +126,6 @@ get_calendar(Req) -> handler_utils:send_error(Req1, Code, Message) end. -%% @doc PUT /v1/calendars/:id — обновление календаря. --spec update_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. update_calendar(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> @@ -159,7 +133,8 @@ update_calendar(Req) -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of UpdatesMap when is_map(UpdatesMap) -> - Updates = maps:to_list(UpdatesMap), + Updates0 = maps:to_list(UpdatesMap), + Updates = convert_calendar_fields(Updates0), case logic_calendar:update_calendar(UserId, CalendarId, Updates) of {ok, Calendar} -> handler_utils:send_json(Req2, 200, handler_utils:calendar_to_json(Calendar)); @@ -179,8 +154,6 @@ update_calendar(Req) -> handler_utils:send_error(Req1, Code, Message) end. -%% @doc DELETE /v1/calendars/:id — удаление календаря. --spec delete_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. delete_calendar(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> @@ -197,4 +170,17 @@ delete_calendar(Req) -> end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) - end. \ No newline at end of file + end. + +%% @private Преобразует бинарные ключи и значения в атомы для обновлений календаря. +-spec convert_calendar_fields([{binary(), term()}]) -> [{atom(), term()}]. +convert_calendar_fields(Updates) -> + lists:map(fun convert_field/1, Updates). + +-spec convert_field({binary(), term()}) -> {atom(), term()}. +convert_field({<<"title">>, Val}) -> {title, Val}; +convert_field({<<"description">>, Val}) -> {description, Val}; +convert_field({<<"type">>, Val}) -> {type, Val}; +convert_field({<<"confirmation">>, Val}) -> {confirmation, Val}; +convert_field({<<"tags">>, Val}) -> {tags, Val}; +convert_field(Other) -> Other. \ No newline at end of file diff --git a/src/handlers/handler_health.erl b/src/handlers/handler_health.erl index 26608cc..f19c273 100644 --- a/src/handlers/handler_health.erl +++ b/src/handlers/handler_health.erl @@ -19,7 +19,7 @@ init(Req, Opts) -> trails() -> [ #{ - path => <<"/v1/health">>, + path => <<"/health">>, method => <<"GET">>, description => <<"API health check">>, tags => [<<"Health">>], diff --git a/src/handlers/handler_utils.erl b/src/handlers/handler_utils.erl index ee3b478..5d3efc1 100644 --- a/src/handlers/handler_utils.erl +++ b/src/handlers/handler_utils.erl @@ -25,8 +25,8 @@ ticket_to_json/1, calendar_to_json/1, subscription_to_json/1, - trails_for_crud/4 -]). + trails_for_crud/4, + is_superadmin/1]). -include("records.hrl"). @@ -48,6 +48,19 @@ auth_admin(Req) -> {error, Code, Msg, Req1} end. +%% @doc Проверяет, что запрос выполняет суперадмин. +-spec is_superadmin(cowboy_req:req()) -> + {ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}. +is_superadmin(Req) -> + case handler_utils:auth_admin(Req) of + {ok, AdminId, Req1} -> + case admin_utils:is_superadmin(AdminId) of + true -> {ok, AdminId, Req1}; + false -> {error, 403, <<"Only superadmin allowed">>, Req1} + end; + Error -> Error + end. + %% @doc Проверяет, что запрос содержит валидный токен пользователя. -spec auth_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}. diff --git a/src/handlers/swagger_docs_handler.erl b/src/handlers/swagger_docs_handler.erl index 0e52499..24cf3cf 100644 --- a/src/handlers/swagger_docs_handler.erl +++ b/src/handlers/swagger_docs_handler.erl @@ -7,8 +7,8 @@ %%% GET / – индексная страница с выбором API %%% GET /admin/ – Swagger UI для административного API %%% GET /admin/swagger.json – OpenAPI-спецификация (admin) -%%% GET /client/ – Swagger UI для клиентского API -%%% GET /client/swagger.json – OpenAPI-спецификация (client) +%%% GET /user/ – Swagger UI для клиентского API +%%% GET /user/swagger.json – OpenAPI-спецификация (user) %%% @end %%%------------------------------------------------------------------- -module(swagger_docs_handler). @@ -35,12 +35,12 @@ handle(<<"/admin/">>, Req) -> serve_ui(admin, Req); handle(<<"/admin/swagger.json">>, Req) -> serve_json(admin, Req); -handle(<<"/client">>, Req) -> - redirect_to_slash(<<"/client/">>, Req); -handle(<<"/client/">>, Req) -> - serve_ui(client, Req); -handle(<<"/client/swagger.json">>, Req) -> - serve_json(client, Req); +handle(<<"/user">>, Req) -> + redirect_to_slash(<<"/user/">>, Req); +handle(<<"/user/">>, Req) -> + serve_ui(user, Req); +handle(<<"/user/swagger.json">>, Req) -> + serve_json(user, Req); handle(_, Req) -> cowboy_req:reply(404, #{}, <<"Not Found">>, Req), {ok, [], []}. @@ -58,7 +58,7 @@ serve_index(Req) ->

EventHub API Documentation

">>, @@ -69,11 +69,11 @@ serve_index(Req) -> %% Swagger UI %%-------------------------------------------------------------------- --spec serve_ui(admin | client, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}. +-spec serve_ui(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}. serve_ui(Api, Req) -> {Title, SpecUrl} = case Api of admin -> {<<"EventHub Admin API">>, <<"/admin/swagger.json">>}; - client -> {<<"EventHub Client API">>, <<"/client/swagger.json">>} + user -> {<<"EventHub User API">>, <<"/user/swagger.json">>} end, Html = iolist_to_binary([ "", Title, @@ -91,18 +91,18 @@ serve_ui(Api, Req) -> %% OpenAPI JSON %%-------------------------------------------------------------------- --spec serve_json(admin | client, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}. +-spec serve_json(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}. serve_json(Api, Req) -> Trails = case Api of admin -> trails:admin(); - client -> trails:client() + user -> trails:user() end, OpenApi = #{ openapi => <<"3.0.3">>, info => #{ title => case Api of admin -> <<"EventHub Admin API">>; - client -> <<"EventHub Client API">> + user -> <<"EventHub User API">> end, version => <<"1.0.0">> }, diff --git a/src/infra/admin_utils.erl b/src/infra/admin_utils.erl index 033ecb2..2ee51f3 100644 --- a/src/infra/admin_utils.erl +++ b/src/infra/admin_utils.erl @@ -1,7 +1,7 @@ -module(admin_utils). -include("records.hrl"). --export([is_admin/1, check_role/2, get_permissions/1]). +-export([is_admin/1, check_role/2, get_permissions/1, is_superadmin/1]). -export([client_ip/1]). is_admin(UserId) -> @@ -10,6 +10,12 @@ is_admin(UserId) -> _ -> false end. +is_superadmin(AdminId) -> + case core_admin:get_by_id(AdminId) of + {ok, #admin{role = superadmin}} -> true; + _ -> false + end. + %% Проверка конкретной роли (или одной из списка ролей) -spec check_role(UserId :: binary(), RequiredRole :: atom() | [atom()]) -> boolean(). check_role(UserId, RequiredRoles) when is_list(RequiredRoles) -> diff --git a/src/logic/logic_admin.erl b/src/logic/logic_admin.erl index 137aa05..459858d 100644 --- a/src/logic/logic_admin.erl +++ b/src/logic/logic_admin.erl @@ -1,9 +1,23 @@ -module(logic_admin). --export([list_admins/2, get_admin/1, update_admin/2]). +-export([list_admins/2, get_admin/1, update_admin/2, delete_admin/1, create_admin/3]). -include("records.hrl"). +%% @doc Создаёт нового администратора (доступно только суперадмину). +%% Роль может быть superadmin, admin, moderator или support. +-spec create_admin(binary(), binary(), atom()) -> + {ok, #admin{}} | {error, email_exists | invalid_role}. +create_admin(Email, Password, Role) when Role =:= superadmin; Role =:= admin; Role =:= moderator; Role =:= support -> + case core_admin:get_by_email(Email) of + {ok, _} -> {error, email_exists}; + {error, not_found} -> + {ok, PasswordHash} = logic_auth:hash_password(Password), + core_admin:create(Email, PasswordHash, Role) + end; +create_admin(_, _, _) -> + {error, invalid_role}. + %%%------------------------------------------------------------------- %%% Административный список администраторов с пагинацией %%%------------------------------------------------------------------- @@ -39,6 +53,13 @@ update_admin(AdminId, Updates) -> Error end. +%%%------------------------------------------------------------------- +%%% Удалить администратора (физическое удаление из базы) +%%%------------------------------------------------------------------- +-spec delete_admin(binary()) -> {ok, deleted} | {error, not_found}. +delete_admin(AdminId) -> + core_admin:delete(AdminId). + %%%=================================================================== %%% Внутренние функции %%%=================================================================== diff --git a/src/logic/logic_review.erl b/src/logic/logic_review.erl index 4d59f8c..3669d6c 100644 --- a/src/logic/logic_review.erl +++ b/src/logic/logic_review.erl @@ -268,7 +268,7 @@ target_exists(_, _) -> false. update_target_rating(event, EventId) -> {Avg, Count} = core_review:get_average_rating(event, EventId), - io:format("Updating event ~p rating: avg=~p, count=~p~n", [EventId, Avg, Count]), +%% io:format("Updating event ~p rating: avg=~p, count=~p~n", [EventId, Avg, Count]), core_event:update(EventId, [{rating_avg, Avg}, {rating_count, Count}]); update_target_rating(calendar, CalendarId) -> {Avg, Count} = core_review:get_average_rating(calendar, CalendarId), diff --git a/src/logic/logic_search.erl b/src/logic/logic_search.erl index d2097ec..5513575 100644 --- a/src/logic/logic_search.erl +++ b/src/logic/logic_search.erl @@ -13,10 +13,20 @@ search(Type, Query, UserId, Params) -> Offset = maps:get(offset, Params, 0), case Type of - <<"event">> -> search_events(Query, UserId, Params, Limit, Offset); - <<"calendar">> -> search_calendars(Query, UserId, Params, Limit, Offset); - _ -> search_all(Query, UserId, Params, Limit, Offset) - end. +<<"event">> -> +{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset), +{ok, Total, #{<<"events">> => Events}}; +<<"calendar">> -> +{ok, Total, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset), +{ok, Total, #{<<"calendars">> => Calendars}}; +_ -> +{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset), +{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset), +{ok, EventsTotal + CalendarsTotal, #{ +<<"events">> => Events, +<<"calendars">> => Calendars +}} +end. %% ============ Поиск событий ============ search_events(Query, UserId, Params, Limit, Offset) -> @@ -37,16 +47,6 @@ search_calendars(Query, UserId, Params, Limit, Offset) -> {ok, length(Filtered), format_calendars(Paginated)}. -%% ============ Поиск всего ============ -search_all(Query, UserId, Params, Limit, Offset) -> - {ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset), - {ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset), - - {ok, EventsTotal + CalendarsTotal, #{ - events => Events, - calendars => Calendars - }}. - %% ============ Получение данных ============ get_all_events() -> Match = #event{status = active, is_instance = false, _ = '_'}, diff --git a/src/logic/logic_ticket.erl b/src/logic/logic_ticket.erl index 4ab4413..a94be85 100644 --- a/src/logic/logic_ticket.erl +++ b/src/logic/logic_ticket.erl @@ -93,7 +93,7 @@ resolve_ticket(AdminId, TicketId, ResolutionNote) -> case admin_utils:is_admin(AdminId) of true -> core_ticket:update_ticket(TicketId, #{ - <<"status">> => <<"closed">>, + <<"status">> => <<"resolved">>, <<"resolution_note">> => ResolutionNote }); false -> {error, access_denied} diff --git a/src/logic/logic_user.erl b/src/logic/logic_user.erl index 0a8884b..d6cc203 100644 --- a/src/logic/logic_user.erl +++ b/src/logic/logic_user.erl @@ -45,9 +45,8 @@ update_user_admin(UserId, Updates) -> -spec delete_user_admin(binary()) -> {ok, #user{}} | {error, not_found}. delete_user_admin(UserId) -> case core_user:get_by_id(UserId) of - {ok, User} -> - UpdatedUser = User#user{status = deleted}, - core_user:update(UserId, UpdatedUser); + {ok, _User} -> + core_user:delete(UserId); Error -> Error end. @@ -58,15 +57,18 @@ delete_user_admin(UserId) -> apply_filters(Users, Filters) -> Role = maps:get(role, Filters, undefined), - Status = maps:get(status, Filters, undefined), + StatusBin = maps:get(status, Filters, undefined), Q = maps:get(q, Filters, undefined), F1 = case Role of undefined -> Users; _ -> [U || U <- Users, U#user.role =:= Role] end, - F2 = case Status of + F2 = case StatusBin of undefined -> F1; - _ -> [U || U <- F1, U#user.status =:= Status] + _ -> + StatusAtom = try binary_to_existing_atom(StatusBin, utf8) + catch error:badarg -> StatusBin end, + [U || U <- F1, U#user.status =:= StatusAtom] end, case Q of undefined -> F2; diff --git a/src/swagger/admin-swagger.json b/src/swagger/admin-swagger.json index b428277..746c263 100644 --- a/src/swagger/admin-swagger.json +++ b/src/swagger/admin-swagger.json @@ -4,9 +4,64 @@ "title": "EventHub Admin API" }, "paths": { + "/admin/health": { + "get": { + "description": "Admin API health check", + "tags": [ + "Health" + ], + "responses": { + "200": { + "description": "API is healthy", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/v1/admin/admins": { "get": { "description": "List all admins (superadmin only)", + "parameters": [ + { + "in": "query", + "name": "role", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "schema": { + "type": "integer" + } + } + ], "tags": [ "Admins" ], @@ -39,14 +94,18 @@ "support" ] }, - "email": { + "created_at": { "type": "string", - "format": "email" + "format": "date-time" }, "nickname": { "type": "string", "nullable": true }, + "email": { + "type": "string", + "format": "email" + }, "avatar_url": { "type": "string", "nullable": true @@ -71,10 +130,6 @@ "type": "string", "format": "date-time" }, - "created_at": { - "type": "string", - "format": "date-time" - }, "updated_at": { "type": "string", "format": "date-time" @@ -85,25 +140,104 @@ } } } + } + }, + "post": { + "description": "Create a new admin (superadmin only)", + "tags": [ + "Admins" + ], + "responses": { + "201": { + "description": "Admin created" + }, + "400": { + "description": "Invalid fields" + }, + "403": { + "description": "Only superadmin can create admins" + }, + "409": { + "description": "Email already exists" + } }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "password", + "role" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "superadmin", + "admin", + "moderator", + "support" + ] + }, + "password": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + } + } + } + } + } + } + }, + "/v1/admin/audit": { + "get": { + "description": "List audit records (superadmin only)", "parameters": [ { "in": "query", - "name": "role", + "name": "admin_id", + "description": "Filter by admin ID", "schema": { "type": "string" } }, { "in": "query", - "name": "status", + "name": "action", + "description": "Filter by action", "schema": { "type": "string" } }, + { + "in": "query", + "name": "date_from", + "description": "Start timestamp (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "date_to", + "description": "End timestamp (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + } + }, { "in": "query", "name": "limit", + "description": "Page size", "schema": { "type": "integer" } @@ -111,16 +245,12 @@ { "in": "query", "name": "offset", + "description": "Offset", "schema": { "type": "integer" } } - ] - } - }, - "/v1/admin/audit": { - "get": { - "description": "List audit records (admin)", + ], "tags": [ "Audit" ], @@ -155,16 +285,16 @@ "type": "string", "format": "email" }, - "admin_id": { + "entity_id": { "type": "string" }, "action": { "type": "string" }, - "entity_type": { + "admin_id": { "type": "string" }, - "entity_id": { + "entity_type": { "type": "string" } } @@ -173,50 +303,13 @@ } } } - }, + } + } + }, + "/v1/admin/banned-words": { + "get": { + "description": "List all banned words (admin)", "parameters": [ - { - "in": "query", - "name": "admin_id", - "description": "Filter by admin ID", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "action", - "description": "Filter by action", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "entity_type", - "description": "Filter by entity type", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "from", - "description": "Start timestamp (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "in": "query", - "name": "to", - "description": "End timestamp (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - } - }, { "in": "query", "name": "limit", @@ -233,12 +326,7 @@ "type": "integer" } } - ] - } - }, - "/v1/admin/banned-words": { - "get": { - "description": "List all banned words (admin)", + ], "tags": [ "Banned Words" ], @@ -273,25 +361,7 @@ } } } - }, - "parameters": [ - { - "in": "query", - "name": "limit", - "description": "Page size", - "schema": { - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "description": "Offset", - "schema": { - "type": "integer" - } - } - ] + } }, "post": { "description": "Add a new banned word", @@ -307,39 +377,28 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "word" + ], "properties": { "word": { "type": "string" } - }, - "required": [ - "word" - ] + } } } - }, - "required": true + } } } }, "/v1/admin/banned-words/:word": { "delete": { "description": "Remove a banned word", - "tags": [ - "Banned Words" - ], - "responses": { - "200": { - "description": "Word removed" - }, - "404": { - "description": "Word not found" - } - }, "parameters": [ { "in": "path", @@ -350,26 +409,23 @@ }, "required": true } - ] + ], + "tags": [ + "Banned Words" + ], + "responses": { + "200": { + "description": "Word removed" + }, + "404": { + "description": "Word not found" + } + } } }, "/v1/admin/calendar/:id": { "put": { "description": "Moderate calendar - unfreeze", - "tags": [ - "Moderation" - ], - "responses": { - "200": { - "description": "Moderation applied successfully" - }, - "400": { - "description": "Bad request" - }, - "404": { - "description": "Entity not found" - } - }, "parameters": [ { "in": "path", @@ -396,11 +452,29 @@ "required": true } ], + "tags": [ + "Moderation" + ], + "responses": { + "200": { + "description": "Moderation applied successfully" + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Entity not found" + } + }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { "reason": { "type": "string" @@ -412,34 +486,16 @@ "unfreeze" ] } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, "/v1/admin/event/:id": { "put": { "description": "Moderate event - unfreeze", - "tags": [ - "Moderation" - ], - "responses": { - "200": { - "description": "Moderation applied successfully" - }, - "400": { - "description": "Bad request" - }, - "404": { - "description": "Entity not found" - } - }, "parameters": [ { "in": "path", @@ -466,11 +522,29 @@ "required": true } ], + "tags": [ + "Moderation" + ], + "responses": { + "200": { + "description": "Moderation applied successfully" + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Entity not found" + } + }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { "reason": { "type": "string" @@ -482,143 +556,16 @@ "unfreeze" ] } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, "/v1/admin/events": { "get": { "description": "Search and list events (admin)", - "tags": [ - "Events" - ], - "responses": { - "200": { - "description": "Array of events with Content-Range header", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "reason": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "enum": [ - "active", - "cancelled", - "completed" - ] - }, - "description": { - "type": "string" - }, - "title": { - "type": "string" - }, - "location": { - "type": "object", - "nullable": true - }, - "duration": { - "type": "integer" - }, - "calendar_id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "rating_avg": { - "type": "number", - "format": "float" - }, - "rating_count": { - "type": "integer" - }, - "event_type": { - "type": "string", - "enum": [ - "single", - "recurring" - ] - }, - "start_time": { - "type": "string", - "format": "date-time" - }, - "master_id": { - "type": "string", - "nullable": true - }, - "is_instance": { - "type": "boolean" - }, - "specialist_id": { - "type": "string", - "nullable": true - }, - "capacity": { - "type": "integer", - "nullable": true - }, - "online_link": { - "type": "string", - "nullable": true - }, - "attachments": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "edit_history": { - "type": "array", - "items": { - "type": "object" - }, - "nullable": true - }, - "recurrence": { - "type": "object", - "nullable": true - } - } - } - } - } - } - }, - "405": { - "description": "Method not allowed" - } - }, "parameters": [ { "in": "query", @@ -714,20 +661,135 @@ }, "required": false } - ] - } - }, - "/v1/admin/events/:id": { - "delete": { - "description": "Soft-delete event (admin)", + ], "tags": [ "Events" ], "responses": { "200": { - "description": "Event status set to deleted" + "description": "Array of events with Content-Range header", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "active", + "cancelled", + "completed" + ] + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "location": { + "type": "object", + "nullable": true + }, + "duration": { + "type": "integer" + }, + "start_time": { + "type": "string", + "format": "date-time" + }, + "calendar_id": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "event_type": { + "type": "string", + "enum": [ + "single", + "recurring" + ] + }, + "master_id": { + "type": "string", + "nullable": true + }, + "specialist_id": { + "type": "string", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "is_instance": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "capacity": { + "type": "integer", + "nullable": true + }, + "online_link": { + "type": "string", + "nullable": true + }, + "rating_avg": { + "type": "number", + "format": "float" + }, + "rating_count": { + "type": "integer" + }, + "attachments": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "edit_history": { + "type": "array", + "items": { + "type": "object" + }, + "nullable": true + }, + "recurrence": { + "type": "object", + "nullable": true + } + } + } + } + } + } + }, + "405": { + "description": "Method not allowed" } - }, + } + } + }, + "/v1/admin/events/:id": { + "delete": { + "description": "Soft-delete event (admin)", "parameters": [ { "in": "path", @@ -738,10 +800,29 @@ }, "required": true } - ] + ], + "tags": [ + "Events" + ], + "responses": { + "200": { + "description": "Event status set to deleted" + } + } }, "get": { "description": "Get event by ID (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Event ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Events" ], @@ -781,6 +862,10 @@ "duration": { "type": "integer" }, + "start_time": { + "type": "string", + "format": "date-time" + }, "calendar_id": { "type": "string" }, @@ -788,23 +873,6 @@ "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "rating_avg": { - "type": "number", - "format": "float" - }, - "rating_count": { - "type": "integer" - }, "event_type": { "type": "string", "enum": [ @@ -812,21 +880,27 @@ "recurring" ] }, - "start_time": { - "type": "string", - "format": "date-time" - }, "master_id": { "type": "string", "nullable": true }, - "is_instance": { - "type": "boolean" - }, "specialist_id": { "type": "string", "nullable": true }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "is_instance": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "capacity": { "type": "integer", "nullable": true @@ -835,6 +909,13 @@ "type": "string", "nullable": true }, + "rating_avg": { + "type": "number", + "format": "float" + }, + "rating_count": { + "type": "integer" + }, "attachments": { "type": "array", "items": { @@ -858,7 +939,10 @@ } } } - }, + } + }, + "put": { + "description": "Update event (admin)", "parameters": [ { "in": "path", @@ -869,10 +953,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update event (admin)", + ], "tags": [ "Events" ], @@ -881,18 +962,8 @@ "description": "Updated event" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Event ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -931,12 +1002,6 @@ "duration": { "type": "integer" }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, "start_time": { "type": "string", "format": "date-time" @@ -944,6 +1009,12 @@ "specialist_id": { "type": "string" }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "capacity": { "type": "integer" }, @@ -953,32 +1024,6 @@ } } } - }, - "required": true - } - } - }, - "/v1/admin/health": { - "get": { - "description": "Admin API health check", - "tags": [ - "Health" - ], - "responses": { - "200": { - "description": "API is healthy", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string" - } - } - } - } - } } } } @@ -1001,10 +1046,15 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "email", + "password" + ], "properties": { "password": { "type": "string", @@ -1014,15 +1064,10 @@ "type": "string", "format": "email" } - }, - "required": [ - "email", - "password" - ] + } } } - }, - "required": true + } } } }, @@ -1049,13 +1094,17 @@ "role": { "type": "string" }, - "email": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time" }, "nickname": { "type": "string", "nullable": true }, + "email": { + "type": "string" + }, "avatar_url": { "type": "string", "nullable": true @@ -1080,10 +1129,6 @@ "type": "string", "format": "date-time" }, - "created_at": { - "type": "string", - "format": "date-time" - }, "updated_at": { "type": "string", "format": "date-time" @@ -1106,6 +1151,7 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1132,75 +1178,13 @@ } } } - }, - "required": true + } } } }, "/v1/admin/reports": { "get": { "description": "List all reports (admin)", - "tags": [ - "Reports" - ], - "responses": { - "200": { - "description": "Array of reports", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "pending", - "reviewed", - "dismissed" - ] - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "target_type": { - "type": "string", - "enum": [ - "calendar", - "event", - "review" - ] - }, - "target_id": { - "type": "string" - }, - "reporter_id": { - "type": "string" - }, - "resolved_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "resolved_by": { - "type": "string", - "nullable": true - } - } - } - } - } - } - } - }, "parameters": [ { "in": "query", @@ -1252,12 +1236,84 @@ "type": "integer" } } - ] + ], + "tags": [ + "Reports" + ], + "responses": { + "200": { + "description": "Array of reports", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "reviewed", + "dismissed" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "reporter_id": { + "type": "string" + }, + "target_type": { + "type": "string", + "enum": [ + "calendar", + "event", + "review" + ] + }, + "target_id": { + "type": "string" + }, + "resolved_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resolved_by": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } } }, "/v1/admin/reports/:id": { "get": { "description": "Get report by ID (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Report ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Reports" ], @@ -1287,6 +1343,9 @@ "type": "string", "format": "date-time" }, + "reporter_id": { + "type": "string" + }, "target_type": { "type": "string", "enum": [ @@ -1298,9 +1357,6 @@ "target_id": { "type": "string" }, - "reporter_id": { - "type": "string" - }, "resolved_at": { "type": "string", "format": "date-time", @@ -1318,7 +1374,10 @@ "404": { "description": "Report not found" } - }, + } + }, + "put": { + "description": "Update report status (admin)", "parameters": [ { "in": "path", @@ -1329,10 +1388,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update report status (admin)", + ], "tags": [ "Reports" ], @@ -1344,18 +1400,8 @@ "description": "Report not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Report ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1371,28 +1417,13 @@ } } } - }, - "required": true + } } } }, "/v1/admin/review/:id": { "put": { "description": "Moderate review - unhide", - "tags": [ - "Moderation" - ], - "responses": { - "200": { - "description": "Moderation applied successfully" - }, - "400": { - "description": "Bad request" - }, - "404": { - "description": "Entity not found" - } - }, "parameters": [ { "in": "path", @@ -1419,11 +1450,29 @@ "required": true } ], + "tags": [ + "Moderation" + ], + "responses": { + "200": { + "description": "Moderation applied successfully" + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Entity not found" + } + }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { "reason": { "type": "string" @@ -1435,90 +1484,16 @@ "unhide" ] } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, "/v1/admin/reviews": { "get": { "description": "List all reviews (admin)", - "tags": [ - "Reviews" - ], - "responses": { - "200": { - "description": "Array of reviews", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "reason": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "enum": [ - "visible", - "hidden", - "deleted" - ] - }, - "comment": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "target_type": { - "type": "string", - "enum": [ - "calendar", - "event" - ] - }, - "target_id": { - "type": "string" - }, - "rating": { - "maximum": 5, - "type": "integer", - "minimum": 1 - }, - "likes": { - "type": "integer" - }, - "dislikes": { - "type": "integer" - } - } - } - } - } - } - } - }, "parameters": [ { "in": "query", @@ -1568,7 +1543,77 @@ "type": "integer" } } - ] + ], + "tags": [ + "Reviews" + ], + "responses": { + "200": { + "description": "Array of reviews", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "visible", + "hidden", + "deleted" + ] + }, + "comment": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "target_type": { + "type": "string", + "enum": [ + "calendar", + "event" + ] + }, + "target_id": { + "type": "string" + }, + "rating": { + "maximum": 5, + "type": "integer", + "minimum": 1 + }, + "likes": { + "type": "integer" + }, + "dislikes": { + "type": "integer" + } + } + } + } + } + } + } + } }, "patch": { "description": "Bulk update review statuses", @@ -1581,6 +1626,7 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1603,14 +1649,24 @@ } } } - }, - "required": true + } } } }, "/v1/admin/reviews/:id": { "get": { "description": "Get review by ID (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Review ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Reviews" ], @@ -1640,13 +1696,13 @@ "comment": { "type": "string" }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, + "user_id": { + "type": "string" + }, "updated_at": { "type": "string", "format": "date-time" @@ -1677,7 +1733,10 @@ } } } - }, + } + }, + "put": { + "description": "Update review (admin)", "parameters": [ { "in": "path", @@ -1688,10 +1747,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update review (admin)", + ], "tags": [ "Reviews" ], @@ -1700,18 +1756,8 @@ "description": "Updated review" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Review ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1739,14 +1785,33 @@ } } } - }, - "required": true + } } } }, "/v1/admin/stats": { "get": { "description": "Get admin dashboard statistics", + "parameters": [ + { + "in": "query", + "name": "from", + "description": "Start date (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "to", + "description": "End date (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], "tags": [ "Statistics" ], @@ -1791,94 +1856,12 @@ "403": { "description": "Admin access required" } - }, - "parameters": [ - { - "in": "query", - "name": "from", - "description": "Start date (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "in": "query", - "name": "to", - "description": "End date (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - } - } - ] + } } }, "/v1/admin/subscriptions": { "get": { "description": "List all subscriptions (admin)", - "tags": [ - "Subscriptions" - ], - "responses": { - "200": { - "description": "Array of subscriptions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "expired", - "cancelled" - ] - }, - "started_at": { - "type": "string", - "format": "date-time" - }, - "user_id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "expires_at": { - "type": "string", - "format": "date-time" - }, - "plan": { - "type": "string", - "enum": [ - "monthly", - "quarterly", - "biannual", - "annual" - ] - }, - "trial_used": { - "type": "boolean" - } - } - } - } - } - } - } - }, "parameters": [ { "in": "query", @@ -1923,23 +1906,74 @@ "type": "integer" } } - ] - } - }, - "/v1/admin/subscriptions/:id": { - "delete": { - "description": "Delete subscription (admin)", + ], "tags": [ "Subscriptions" ], "responses": { "200": { - "description": "Subscription deleted" - }, - "404": { - "description": "Subscription not found" + "description": "Array of subscriptions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "expired", + "cancelled" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "plan": { + "type": "string", + "enum": [ + "monthly", + "quarterly", + "biannual", + "annual" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "trial_used": { + "type": "boolean" + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } } - }, + } + } + }, + "/v1/admin/subscriptions/:id": { + "delete": { + "description": "Delete subscription (admin)", "parameters": [ { "in": "path", @@ -1950,10 +1984,32 @@ }, "required": true } - ] + ], + "tags": [ + "Subscriptions" + ], + "responses": { + "200": { + "description": "Subscription deleted" + }, + "404": { + "description": "Subscription not found" + } + } }, "get": { "description": "Get subscription by ID (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Subscription ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Subscriptions" ], @@ -1980,21 +2036,6 @@ "type": "string", "format": "date-time" }, - "user_id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "expires_at": { - "type": "string", - "format": "date-time" - }, "plan": { "type": "string", "enum": [ @@ -2004,8 +2045,23 @@ "annual" ] }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "trial_used": { "type": "boolean" + }, + "expires_at": { + "type": "string", + "format": "date-time" } } } @@ -2015,7 +2071,10 @@ "404": { "description": "Subscription not found" } - }, + } + }, + "put": { + "description": "Update subscription (admin)", "parameters": [ { "in": "path", @@ -2026,10 +2085,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update subscription (admin)", + ], "tags": [ "Subscriptions" ], @@ -2041,18 +2097,8 @@ "description": "Subscription not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Subscription ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -2066,11 +2112,6 @@ "cancelled" ] }, - "expires_at": { - "type": "string", - "format": "date-time", - "description": "New expiration date" - }, "plan": { "type": "string", "enum": [ @@ -2082,18 +2123,70 @@ }, "trial_used": { "type": "boolean" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "New expiration date" } } } } - }, - "required": true + } } } }, "/v1/admin/tickets": { "get": { "description": "List all tickets (admin)", + "parameters": [ + { + "in": "query", + "name": "status", + "description": "Filter by status", + "schema": { + "type": "string", + "enum": [ + "open", + "in_progress", + "resolved", + "closed" + ] + } + }, + { + "in": "query", + "name": "assigned_to", + "description": "Filter by assigned admin ID", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "description": "Search in error message", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "description": "Page size", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "description": "Offset", + "schema": { + "type": "integer" + } + } + ], "tags": [ "Tickets" ], @@ -2159,71 +2252,12 @@ } } } - }, - "parameters": [ - { - "in": "query", - "name": "status", - "description": "Filter by status", - "schema": { - "type": "string", - "enum": [ - "open", - "in_progress", - "resolved", - "closed" - ] - } - }, - { - "in": "query", - "name": "assigned_to", - "description": "Filter by assigned admin ID", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "q", - "description": "Search in error message", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "limit", - "description": "Page size", - "schema": { - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "description": "Offset", - "schema": { - "type": "integer" - } - } - ] + } } }, "/v1/admin/tickets/:id": { "delete": { "description": "Delete ticket (admin)", - "tags": [ - "Tickets" - ], - "responses": { - "200": { - "description": "Ticket deleted" - }, - "404": { - "description": "Ticket not found" - } - }, "parameters": [ { "in": "path", @@ -2234,10 +2268,32 @@ }, "required": true } - ] + ], + "tags": [ + "Tickets" + ], + "responses": { + "200": { + "description": "Ticket deleted" + }, + "404": { + "description": "Ticket not found" + } + } }, "get": { "description": "Get ticket by ID (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Ticket ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Tickets" ], @@ -2303,7 +2359,10 @@ "404": { "description": "Ticket not found" } - }, + } + }, + "put": { + "description": "Update ticket (admin)", "parameters": [ { "in": "path", @@ -2314,10 +2373,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update ticket (admin)", + ], "tags": [ "Tickets" ], @@ -2329,18 +2385,8 @@ "description": "Ticket not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Ticket ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -2364,8 +2410,7 @@ } } } - }, - "required": true + } } } }, @@ -2417,20 +2462,6 @@ "/v1/admin/user/:id": { "put": { "description": "Moderate user - unblock", - "tags": [ - "Moderation" - ], - "responses": { - "200": { - "description": "Moderation applied successfully" - }, - "400": { - "description": "Bad request" - }, - "404": { - "description": "Entity not found" - } - }, "parameters": [ { "in": "path", @@ -2457,11 +2488,29 @@ "required": true } ], + "tags": [ + "Moderation" + ], + "responses": { + "200": { + "description": "Moderation applied successfully" + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Entity not found" + } + }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { "reason": { "type": "string" @@ -2473,109 +2522,16 @@ "unblock" ] } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, "/v1/admin/users": { "get": { "description": "List all users (admin)", - "tags": [ - "Users" - ], - "responses": { - "200": { - "description": "Array of users", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "reason": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "enum": [ - "active", - "frozen", - "deleted" - ] - }, - "role": { - "type": "string", - "enum": [ - "user", - "bot" - ] - }, - "email": { - "type": "string", - "format": "email" - }, - "nickname": { - "type": "string", - "nullable": true - }, - "avatar_url": { - "type": "string", - "nullable": true - }, - "timezone": { - "type": "string", - "nullable": true - }, - "language": { - "type": "string", - "nullable": true - }, - "social_links": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "preferences": { - "type": "object", - "nullable": true - }, - "last_login": { - "type": "string", - "format": "date-time" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - } - } - } - } - } - }, "parameters": [ { "in": "query", @@ -2622,12 +2578,112 @@ "type": "integer" } } - ] + ], + "tags": [ + "Users" + ], + "responses": { + "200": { + "description": "Array of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "active", + "frozen", + "deleted" + ] + }, + "role": { + "type": "string", + "enum": [ + "user", + "bot" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "nickname": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email" + }, + "avatar_url": { + "type": "string", + "nullable": true + }, + "timezone": { + "type": "string", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "social_links": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "preferences": { + "type": "object", + "nullable": true + }, + "last_login": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + } } }, "/v1/admin/users/:id": { "delete": { "description": "Soft-delete user (admin)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "User ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Users" ], @@ -2635,20 +2691,21 @@ "200": { "description": "User status set to deleted" } - }, + } + }, + "get": { + "description": "Get user by ID (admin)", "parameters": [ { "in": "path", "name": "id", + "description": "User ID", "schema": { "type": "string" }, "required": true } - ] - }, - "get": { - "description": "Get user by ID (admin)", + ], "tags": [ "Users" ], @@ -2668,18 +2725,31 @@ "nullable": true }, "status": { - "type": "string" + "type": "string", + "enum": [ + "active", + "frozen", + "deleted" + ] }, "role": { - "type": "string" + "type": "string", + "enum": [ + "user", + "bot" + ] }, - "email": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time" }, "nickname": { "type": "string", "nullable": true }, + "email": { + "type": "string" + }, "avatar_url": { "type": "string", "nullable": true @@ -2696,7 +2766,8 @@ "type": "array", "items": { "type": "string" - } + }, + "nullable": true }, "phone": { "type": "string", @@ -2710,10 +2781,6 @@ "type": "string", "format": "date-time" }, - "created_at": { - "type": "string", - "format": "date-time" - }, "updated_at": { "type": "string", "format": "date-time" @@ -2723,20 +2790,21 @@ } } } - }, + } + }, + "put": { + "description": "Update user (admin)", "parameters": [ { "in": "path", "name": "id", + "description": "User ID", "schema": { "type": "string" }, "required": true } - ] - }, - "put": { - "description": "Update user (admin)", + ], "tags": [ "Users" ], @@ -2745,17 +2813,8 @@ "description": "Updated user" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -2778,27 +2837,11 @@ "user", "bot" ] - }, - "nickname": { - "type": "string" - }, - "timezone": { - "type": "string" - }, - "language": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "preferences": { - "type": "object" } } } } - }, - "required": true + } } } } diff --git a/src/swagger/client-swagger.json b/src/swagger/client-swagger.json index 992199a..bd249f9 100644 --- a/src/swagger/client-swagger.json +++ b/src/swagger/client-swagger.json @@ -4,20 +4,34 @@ "title": "EventHub Client API" }, "paths": { - "/v1/bookings/:id": { - "delete": { - "description": "Cancel booking (participant)", + "/health": { + "get": { + "description": "API health check", "tags": [ - "Bookings" + "Health" ], "responses": { "200": { - "description": "Booking cancelled" - }, - "404": { - "description": "Booking not found" + "description": "API is healthy", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + } } - }, + } + } + }, + "/v1/bookings/:id": { + "delete": { + "description": "Cancel booking (participant)", "parameters": [ { "in": "path", @@ -28,10 +42,32 @@ }, "required": true } - ] + ], + "tags": [ + "Bookings" + ], + "responses": { + "200": { + "description": "Booking cancelled" + }, + "404": { + "description": "Booking not found" + } + } }, "get": { "description": "Get booking by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Booking ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Bookings" ], @@ -54,20 +90,20 @@ "cancelled" ] }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "event_id": { "type": "string" }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "notes": { "type": "string", "nullable": true @@ -88,7 +124,10 @@ "404": { "description": "Booking not found" } - }, + } + }, + "put": { + "description": "Confirm or decline a booking (owner)", "parameters": [ { "in": "path", @@ -99,10 +138,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Confirm or decline a booking (owner)", + ], "tags": [ "Bookings" ], @@ -117,22 +153,15 @@ "description": "Booking not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Booking ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { "action": { "type": "string", @@ -141,14 +170,10 @@ "decline" ] } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, @@ -204,10 +229,6 @@ "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "owner_id": { "type": "string" }, @@ -215,6 +236,23 @@ "type": "string", "nullable": true }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "rating_avg": { + "type": "number", + "format": "float" + }, + "rating_count": { + "type": "integer" + }, "color": { "type": "string", "nullable": true @@ -227,22 +265,9 @@ "type": "object", "nullable": true }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, "confirmation": { "type": "string", "description": "auto, manual, or {timeout, N}" - }, - "rating_avg": { - "type": "number", - "format": "float" - }, - "rating_count": { - "type": "integer" } } } @@ -272,10 +297,14 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "title" + ], "properties": { "type": { "type": "string", @@ -300,20 +329,47 @@ "type": "string", "description": "auto, manual, or {timeout, N}" } - }, - "required": [ - "title" - ] + } } } - }, - "required": true + } } } }, "/v1/calendars/:calendar_id/events": { "get": { "description": "List events of a calendar with optional date range", + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "description": "Calendar ID", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "from", + "description": "Start datetime (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": false + }, + { + "in": "query", + "name": "to", + "description": "End datetime (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": false + } + ], "tags": [ "Events" ], @@ -355,6 +411,10 @@ "duration": { "type": "integer" }, + "start_time": { + "type": "string", + "format": "date-time" + }, "calendar_id": { "type": "string" }, @@ -362,23 +422,6 @@ "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "rating_avg": { - "type": "number", - "format": "float" - }, - "rating_count": { - "type": "integer" - }, "event_type": { "type": "string", "enum": [ @@ -386,21 +429,27 @@ "recurring" ] }, - "start_time": { - "type": "string", - "format": "date-time" - }, "master_id": { "type": "string", "nullable": true }, - "is_instance": { - "type": "boolean" - }, "specialist_id": { "type": "string", "nullable": true }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "is_instance": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "capacity": { "type": "integer", "nullable": true @@ -409,6 +458,13 @@ "type": "string", "nullable": true }, + "rating_avg": { + "type": "number", + "format": "float" + }, + "rating_count": { + "type": "integer" + }, "attachments": { "type": "array", "items": { @@ -439,7 +495,10 @@ "404": { "description": "Calendar not found" } - }, + } + }, + "post": { + "description": "Create a new event (single or recurring)", "parameters": [ { "in": "path", @@ -449,31 +508,8 @@ "type": "string" }, "required": true - }, - { - "in": "query", - "name": "from", - "description": "Start datetime (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - }, - "required": false - }, - { - "in": "query", - "name": "to", - "description": "End datetime (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - }, - "required": false } - ] - }, - "post": { - "description": "Create a new event (single or recurring)", + ], "tags": [ "Events" ], @@ -491,22 +527,17 @@ "description": "Calendar not found" } }, - "parameters": [ - { - "in": "path", - "name": "calendar_id", - "description": "Calendar ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "title", + "start_time", + "duration" + ], "properties": { "description": { "type": "string" @@ -534,16 +565,16 @@ "type": "integer", "description": "Duration in minutes" }, + "start_time": { + "type": "string", + "format": "date-time" + }, "tags": { "type": "array", "items": { "type": "string" } }, - "start_time": { - "type": "string", - "format": "date-time" - }, "capacity": { "type": "integer" }, @@ -554,22 +585,37 @@ "type": "object", "description": "Recurrence rule (RFC 5545)" } - }, - "required": [ - "title", - "start_time", - "duration" - ] + } } } - }, - "required": true + } } } }, "/v1/calendars/:calendar_id/view": { "get": { "description": "Get calendar HTML view for a specific month", + "parameters": [ + { + "in": "path", + "name": "calendar_id", + "description": "Calendar ID", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "month", + "description": "Month in YYYY-MM format", + "schema": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}$" + }, + "required": true + } + ], "tags": [ "Calendars" ], @@ -593,47 +639,12 @@ "403": { "description": "Access denied" } - }, - "parameters": [ - { - "in": "path", - "name": "calendar_id", - "description": "Calendar ID", - "schema": { - "type": "string" - }, - "required": true - }, - { - "in": "query", - "name": "month", - "description": "Month in YYYY-MM format", - "schema": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}$" - }, - "required": true - } - ] + } } }, "/v1/calendars/:id": { "delete": { "description": "Delete calendar", - "tags": [ - "Calendars" - ], - "responses": { - "200": { - "description": "Calendar deleted" - }, - "403": { - "description": "Access denied" - }, - "404": { - "description": "Calendar not found" - } - }, "parameters": [ { "in": "path", @@ -644,10 +655,29 @@ }, "required": true } - ] + ], + "tags": [ + "Calendars" + ], + "responses": { + "200": { + "description": "Calendar deleted" + } + } }, "get": { "description": "Get calendar by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Calendar ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Calendars" ], @@ -662,17 +692,8 @@ "id": { "type": "string" }, - "reason": { - "type": "string", - "nullable": true - }, "status": { - "type": "string", - "enum": [ - "active", - "frozen", - "deleted" - ] + "type": "string" }, "type": { "type": "string", @@ -687,36 +708,16 @@ "title": { "type": "string" }, - "category": { - "type": "string", - "nullable": true - }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "owner_id": { "type": "string" }, - "short_name": { + "updated_at": { "type": "string", - "nullable": true - }, - "color": { - "type": "string", - "nullable": true - }, - "image_url": { - "type": "string", - "nullable": true - }, - "settings": { - "type": "object", - "nullable": true + "format": "date-time" }, "tags": { "type": "array", @@ -724,29 +725,29 @@ "type": "string" } }, - "confirmation": { - "type": "string", - "description": "auto, manual, or {timeout, N}" - }, "rating_avg": { "type": "number", "format": "float" }, "rating_count": { "type": "integer" + }, + "confirmation": { + "type": "string", + "enum": [ + "auto", + "manual" + ] } } } } } - }, - "403": { - "description": "Access denied" - }, - "404": { - "description": "Calendar not found" } - }, + } + }, + "put": { + "description": "Update calendar", "parameters": [ { "in": "path", @@ -757,50 +758,24 @@ }, "required": true } - ] - }, - "put": { - "description": "Update calendar", + ], "tags": [ "Calendars" ], "responses": { "200": { "description": "Calendar updated" - }, - "400": { - "description": "Invalid request" - }, - "403": { - "description": "Access denied" - }, - "404": { - "description": "Calendar not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Calendar ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "type": { - "type": "string", - "enum": [ - "personal", - "commercial" - ] + "type": "string" }, "description": { "type": "string" @@ -815,20 +790,29 @@ } }, "confirmation": { - "type": "string", - "description": "auto, manual, or {timeout, N}" + "type": "string" } } } } - }, - "required": true + } } } }, "/v1/events/:id": { "delete": { "description": "Delete event", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Event ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Events" ], @@ -842,7 +826,10 @@ "404": { "description": "Event not found" } - }, + } + }, + "get": { + "description": "Get event by ID", "parameters": [ { "in": "path", @@ -853,10 +840,7 @@ }, "required": true } - ] - }, - "get": { - "description": "Get event by ID", + ], "tags": [ "Events" ], @@ -896,6 +880,10 @@ "duration": { "type": "integer" }, + "start_time": { + "type": "string", + "format": "date-time" + }, "calendar_id": { "type": "string" }, @@ -903,23 +891,6 @@ "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "rating_avg": { - "type": "number", - "format": "float" - }, - "rating_count": { - "type": "integer" - }, "event_type": { "type": "string", "enum": [ @@ -927,21 +898,27 @@ "recurring" ] }, - "start_time": { - "type": "string", - "format": "date-time" - }, "master_id": { "type": "string", "nullable": true }, - "is_instance": { - "type": "boolean" - }, "specialist_id": { "type": "string", "nullable": true }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "is_instance": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "capacity": { "type": "integer", "nullable": true @@ -950,6 +927,13 @@ "type": "string", "nullable": true }, + "rating_avg": { + "type": "number", + "format": "float" + }, + "rating_count": { + "type": "integer" + }, "attachments": { "type": "array", "items": { @@ -979,7 +963,10 @@ "404": { "description": "Event not found" } - }, + } + }, + "put": { + "description": "Update event", "parameters": [ { "in": "path", @@ -990,10 +977,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update event", + ], "tags": [ "Events" ], @@ -1011,18 +995,8 @@ "description": "Event not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Event ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1061,12 +1035,6 @@ "duration": { "type": "integer" }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, "start_time": { "type": "string", "format": "date-time" @@ -1074,6 +1042,12 @@ "specialist_id": { "type": "string" }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "capacity": { "type": "integer" }, @@ -1083,14 +1057,24 @@ } } } - }, - "required": true + } } } }, "/v1/events/:id/bookings": { "get": { "description": "List bookings for an event (owner only)", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Event ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Bookings" ], @@ -1115,20 +1099,20 @@ "cancelled" ] }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "event_id": { "type": "string" }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "notes": { "type": "string", "nullable": true @@ -1153,7 +1137,10 @@ "404": { "description": "Event not found" } - }, + } + }, + "post": { + "description": "Create a booking for an event", "parameters": [ { "in": "path", @@ -1164,10 +1151,7 @@ }, "required": true } - ] - }, - "post": { - "description": "Create a booking for an event", + ], "tags": [ "Bookings" ], @@ -1190,20 +1174,20 @@ "cancelled" ] }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "event_id": { "type": "string" }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "notes": { "type": "string", "nullable": true @@ -1233,7 +1217,12 @@ "409": { "description": "Already booked" } - }, + } + } + }, + "/v1/events/:id/occurrences": { + "get": { + "description": "Get event occurrences in a date range", "parameters": [ { "in": "path", @@ -1243,13 +1232,28 @@ "type": "string" }, "required": true + }, + { + "in": "query", + "name": "from", + "description": "Start datetime (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "in": "query", + "name": "to", + "description": "End datetime (ISO8601)", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true } - ] - } - }, - "/v1/events/:id/occurrences": { - "get": { - "description": "Get event occurrences in a date range", + ], "tags": [ "Events" ], @@ -1306,60 +1310,12 @@ "404": { "description": "Event not found" } - }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Event ID", - "schema": { - "type": "string" - }, - "required": true - }, - { - "in": "query", - "name": "from", - "description": "Start datetime (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - }, - "required": true - }, - { - "in": "query", - "name": "to", - "description": "End datetime (ISO8601)", - "schema": { - "type": "string", - "format": "date-time" - }, - "required": true - } - ] + } } }, "/v1/events/:id/occurrences/:start_time": { "delete": { "description": "Cancel a specific occurrence", - "tags": [ - "Events" - ], - "responses": { - "200": { - "description": "Occurrence cancelled" - }, - "400": { - "description": "Missing or invalid parameters" - }, - "403": { - "description": "Access denied" - }, - "404": { - "description": "Event not found" - } - }, "parameters": [ { "in": "path", @@ -1380,30 +1336,22 @@ }, "required": true } - ] - } - }, - "/v1/health": { - "get": { - "description": "API health check", + ], "tags": [ - "Health" + "Events" ], "responses": { "200": { - "description": "API is healthy", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string" - } - } - } - } - } + "description": "Occurrence cancelled" + }, + "400": { + "description": "Missing or invalid parameters" + }, + "403": { + "description": "Access denied" + }, + "404": { + "description": "Event not found" } } } @@ -1429,10 +1377,15 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "email", + "password" + ], "properties": { "password": { "type": "string", @@ -1442,15 +1395,10 @@ "type": "string", "format": "email" } - }, - "required": [ - "email", - "password" - ] + } } } - }, - "required": true + } } } }, @@ -1472,22 +1420,22 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "refresh_token" + ], "properties": { "refresh_token": { "type": "string" } - }, - "required": [ - "refresh_token" - ] + } } } - }, - "required": true + } } } }, @@ -1509,10 +1457,15 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "email", + "password" + ], "properties": { "password": { "type": "string", @@ -1522,21 +1475,34 @@ "type": "string", "format": "email" } - }, - "required": [ - "email", - "password" - ] + } } } - }, - "required": true + } } } }, "/v1/reports": { "get": { "description": "List reports (admin/moderator only)", + "parameters": [ + { + "in": "query", + "name": "target_type", + "description": "Filter by target type", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "target_id", + "description": "Filter by target ID", + "schema": { + "type": "string" + } + } + ], "tags": [ "Reports" ], @@ -1568,6 +1534,9 @@ "type": "string", "format": "date-time" }, + "reporter_id": { + "type": "string" + }, "target_type": { "type": "string", "enum": [ @@ -1579,9 +1548,6 @@ "target_id": { "type": "string" }, - "reporter_id": { - "type": "string" - }, "resolved_at": { "type": "string", "format": "date-time", @@ -1600,25 +1566,7 @@ "403": { "description": "Access denied (admin/mod only)" } - }, - "parameters": [ - { - "in": "query", - "name": "target_type", - "description": "Filter by target type", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "target_id", - "description": "Filter by target ID", - "schema": { - "type": "string" - } - } - ] + } }, "post": { "description": "Create a new report (complaint)", @@ -1637,10 +1585,16 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "target_type", + "target_id", + "reason" + ], "properties": { "reason": { "type": "string" @@ -1655,22 +1609,40 @@ "target_id": { "type": "string" } - }, - "required": [ - "target_type", - "target_id", - "reason" - ] + } } } - }, - "required": true + } } } }, "/v1/reviews": { "get": { "description": "List reviews for a target", + "parameters": [ + { + "in": "query", + "name": "target_type", + "description": "calendar or event", + "schema": { + "type": "string", + "enum": [ + "calendar", + "event" + ] + }, + "required": true + }, + { + "in": "query", + "name": "target_id", + "description": "ID of the target", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Reviews" ], @@ -1702,13 +1674,13 @@ "comment": { "type": "string" }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, + "user_id": { + "type": "string" + }, "updated_at": { "type": "string", "format": "date-time" @@ -1743,31 +1715,7 @@ "400": { "description": "Missing target_type or target_id" } - }, - "parameters": [ - { - "in": "query", - "name": "target_type", - "description": "calendar or event", - "schema": { - "type": "string", - "enum": [ - "calendar", - "event" - ] - }, - "required": true - }, - { - "in": "query", - "name": "target_id", - "description": "ID of the target", - "schema": { - "type": "string" - }, - "required": true - } - ] + } }, "post": { "description": "Create a new review", @@ -1792,10 +1740,17 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "target_type", + "target_id", + "rating", + "comment" + ], "properties": { "comment": { "type": "string" @@ -1815,23 +1770,27 @@ "type": "integer", "minimum": 1 } - }, - "required": [ - "target_type", - "target_id", - "rating", - "comment" - ] + } } } - }, - "required": true + } } } }, "/v1/reviews/:id": { "delete": { "description": "Delete review", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Review ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Reviews" ], @@ -1845,7 +1804,10 @@ "404": { "description": "Review not found" } - }, + } + }, + "get": { + "description": "Get review by ID", "parameters": [ { "in": "path", @@ -1856,10 +1818,7 @@ }, "required": true } - ] - }, - "get": { - "description": "Get review by ID", + ], "tags": [ "Reviews" ], @@ -1889,13 +1848,13 @@ "comment": { "type": "string" }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, + "user_id": { + "type": "string" + }, "updated_at": { "type": "string", "format": "date-time" @@ -1932,7 +1891,10 @@ "404": { "description": "Review not found" } - }, + } + }, + "put": { + "description": "Update review", "parameters": [ { "in": "path", @@ -1943,10 +1905,7 @@ }, "required": true } - ] - }, - "put": { - "description": "Update review", + ], "tags": [ "Reviews" ], @@ -1964,18 +1923,8 @@ "description": "Review not found" } }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Review ID", - "schema": { - "type": "string" - }, - "required": true - } - ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { @@ -1992,52 +1941,13 @@ } } } - }, - "required": true + } } } }, "/v1/search": { "get": { "description": "Search calendars and events", - "tags": [ - "Search" - ], - "responses": { - "200": { - "description": "Search results with pagination", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "limit": { - "type": "integer" - }, - "results": { - "type": "array", - "items": { - "type": "object" - } - } - } - } - } - } - }, - "400": { - "description": "Invalid parameters" - }, - "500": { - "description": "Search failed" - } - }, "parameters": [ { "in": "query", @@ -2152,7 +2062,45 @@ "format": "date-time" } } - ] + ], + "tags": [ + "Search" + ], + "responses": { + "200": { + "description": "Search results with pagination", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid parameters" + }, + "500": { + "description": "Search failed" + } + } } }, "/v1/subscription": { @@ -2184,21 +2132,6 @@ "type": "string", "format": "date-time" }, - "user_id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "expires_at": { - "type": "string", - "format": "date-time" - }, "plan": { "type": "string", "enum": [ @@ -2208,8 +2141,23 @@ "annual" ] }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "trial_used": { "type": "boolean" + }, + "expires_at": { + "type": "string", + "format": "date-time" } } } @@ -2241,18 +2189,15 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { - "action": { - "type": "string", - "enum": [ - "start_trial", - "activate" - ] - }, "plan": { "type": "string", "enum": [ @@ -2262,24 +2207,45 @@ "annual" ] }, + "action": { + "type": "string", + "enum": [ + "start_trial", + "activate" + ] + }, "payment_info": { "type": "object", "description": "Payment information" } - }, - "required": [ - "action" - ] + } } } - }, - "required": true + } } } }, "/v1/tickets": { "get": { "description": "List tickets (admin sees all, user sees own)", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Page size", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "description": "Offset", + "schema": { + "type": "integer" + } + } + ], "tags": [ "Tickets" ], @@ -2348,25 +2314,7 @@ "401": { "description": "Unauthorized" } - }, - "parameters": [ - { - "in": "query", - "name": "limit", - "description": "Page size", - "schema": { - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "description": "Offset", - "schema": { - "type": "integer" - } - } - ] + } }, "post": { "description": "Create a new ticket (bug report)", @@ -2385,10 +2333,14 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "error_message" + ], "properties": { "context": { "type": "string" @@ -2399,20 +2351,27 @@ "error_message": { "type": "string" } - }, - "required": [ - "error_message" - ] + } } } - }, - "required": true + } } } }, "/v1/tickets/:id": { "get": { "description": "Get a user's own ticket by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Ticket ID", + "schema": { + "type": "string" + }, + "required": true + } + ], "tags": [ "Tickets" ], @@ -2481,18 +2440,7 @@ "404": { "description": "Ticket not found" } - }, - "parameters": [ - { - "in": "path", - "name": "id", - "description": "Ticket ID", - "schema": { - "type": "string" - }, - "required": true - } - ] + } } }, "/v1/user/bookings": { @@ -2522,20 +2470,20 @@ "cancelled" ] }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, - "updated_at": { - "type": "string", - "format": "date-time" - }, "event_id": { "type": "string" }, + "user_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, "notes": { "type": "string", "nullable": true @@ -2596,14 +2544,18 @@ "bot" ] }, - "email": { + "created_at": { "type": "string", - "format": "email" + "format": "date-time" }, "nickname": { "type": "string", "nullable": true }, + "email": { + "type": "string", + "format": "email" + }, "avatar_url": { "type": "string", "nullable": true @@ -2635,10 +2587,6 @@ "type": "string", "format": "date-time" }, - "created_at": { - "type": "string", - "format": "date-time" - }, "updated_at": { "type": "string", "format": "date-time" @@ -2691,13 +2639,13 @@ "comment": { "type": "string" }, - "user_id": { - "type": "string" - }, "created_at": { "type": "string", "format": "date-time" }, + "user_id": { + "type": "string" + }, "updated_at": { "type": "string", "format": "date-time" diff --git a/src/swagger/trails.erl b/src/swagger/trails.erl index be1e9e8..0a46897 100644 --- a/src/swagger/trails.erl +++ b/src/swagger/trails.erl @@ -1,5 +1,5 @@ -module(trails). --export([admin/0, client/0, all/0]). +-export([admin/0, user/0, all/0]). admin() -> Modules = [ @@ -37,7 +37,7 @@ admin() -> ], lists:flatmap(fun trails_from_module/1, Modules). -client() -> +user() -> Modules = [ handler_health, handler_register, @@ -65,7 +65,7 @@ client() -> lists:flatmap(fun trails_from_module/1, Modules). all() -> - admin() ++ client(). + admin() ++ user(). trails_from_module(Module) -> try Module:trails() of diff --git a/test/api/admins/admin_admins_tests.erl b/test/api/admins/admin_admins_tests.erl new file mode 100644 index 0000000..48426f7 --- /dev/null +++ b/test/api/admins/admin_admins_tests.erl @@ -0,0 +1,180 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления администраторами. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/admins +%%% POST /v1/admin/admins +%%% GET /v1/admin/admins/:id +%%% PUT /v1/admin/admins/:id +%%% DELETE /v1/admin/admins/:id +%%% +%%% Проверяет: +%%% - получение списка администраторов (только суперадмин) +%%% - создание нового администратора +%%% - получение администратора по ID +%%% - обновление администратора +%%% - удаление (блокировку) администратора +%%% - ошибки 403 для обычного администратора +%%% - ошибки 409 (дубликат email) и 400 (неверная роль) при создании +%%% @end +%%%------------------------------------------------------------------- +-module(admin_admins_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Admins Tests ==="), + SuperToken = api_test_runner:get_superadmin_token(), + + % Создаём нового администратора для проверки CRUD (у него будет свой ID) + AdminEmail = api_test_runner:unique_email(<<"newadmin.admins.tests">>), + AdminPassword = <<"AdminPass123">>, + #{<<"id">> := AdminId} = create_admin(SuperToken, AdminEmail, AdminPassword, <<"admin">>), + + % Токен обычного администратора (уже существующего admin@eventhub.local) + AdminToken = api_test_runner:get_admin_token(), + + % Тесты с правами суперадмина + test_list_admins(SuperToken), + test_get_admin(SuperToken, AdminId), + test_update_admin(SuperToken, AdminId), + test_delete_admin(SuperToken, AdminId), + + % Тесты ограничений для обычного админа (используем готовый токен) + test_list_admins_forbidden(AdminToken), + test_create_admin_forbidden(AdminToken), + test_get_admin_forbidden(AdminToken, AdminId), + test_update_admin_forbidden(AdminToken, AdminId), + test_delete_admin_forbidden(AdminToken, AdminId), + + % Тесты валидации при создании + test_create_admin_duplicate_email(SuperToken), + test_create_admin_invalid_role(SuperToken), + + ct:pal("=== All admin admins tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/admins – список администраторов. +-spec test_list_admins(binary()) -> ok. +test_list_admins(Token) -> + ct:pal(" TEST: List all admins"), + Admins = api_test_runner:admin_get(<<"/v1/admin/admins">>, Token), + ?assert(is_list(Admins)), + ?assert(length(Admins) >= 1), + ct:pal(" OK: ~p admins", [length(Admins)]). + +%% @doc GET /v1/admin/admins/:id – получение администратора. +-spec test_get_admin(binary(), binary()) -> ok. +test_get_admin(Token, AdminId) -> + ct:pal(" TEST: Get admin by ID"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Admin = api_test_runner:admin_get(Path, Token), + ?assertEqual(AdminId, maps:get(<<"id">>, Admin)), + ct:pal(" OK: ~s", [maps:get(<<"email">>, Admin)]). + +%% @doc PUT /v1/admin/admins/:id – обновление администратора. +-spec test_update_admin(binary(), binary()) -> ok. +test_update_admin(Token, AdminId) -> + ct:pal(" TEST: Update admin"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{nickname => <<"UpdatedAdmin">>}), + ?assertEqual(<<"UpdatedAdmin">>, maps:get(<<"nickname">>, Updated)), + ct:pal(" OK"). + +%% @doc DELETE /v1/admin/admins/:id – удаление (блокировка). +-spec test_delete_admin(binary(), binary()) -> ok. +test_delete_admin(Token, AdminId) -> + ct:pal(" TEST: Delete (block) admin"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Result = api_test_runner:admin_request(delete, Path, Token), + {ok, 200, _, _} = Result, + ct:pal(" OK: admin blocked (or deleted)"), + % Проверяем, что админ больше не в списке + Admins = api_test_runner:admin_get(<<"/v1/admin/admins">>, Token), + ?assertNot(lists:any(fun(A) -> maps:get(<<"id">>, A) =:= AdminId end, Admins)). + +%% ── Тесты ограничений ── + +-spec test_list_admins_forbidden(binary()) -> ok. +test_list_admins_forbidden(Token) -> + ct:pal(" TEST: List admins as non-superadmin (403)"), + Resp = api_test_runner:admin_request(get, <<"/v1/admin/admins">>, Token), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +-spec test_create_admin_forbidden(binary()) -> ok. +test_create_admin_forbidden(Token) -> + ct:pal(" TEST: Create admin as non-superadmin (403)"), + Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, Token, + jsx:encode(#{email => <<"x@x.com">>, password => <<"p">>, role => <<"moderator">>})), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +-spec test_get_admin_forbidden(binary(), binary()) -> ok. +test_get_admin_forbidden(Token, AdminId) -> + ct:pal(" TEST: Get admin by ID as non-superadmin (403)"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Resp = api_test_runner:admin_request(get, Path, Token), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +-spec test_update_admin_forbidden(binary(), binary()) -> ok. +test_update_admin_forbidden(Token, AdminId) -> + ct:pal(" TEST: Update admin as non-superadmin (403)"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Resp = api_test_runner:admin_request(put, Path, Token, jsx:encode(#{nickname => <<"fail">>})), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +-spec test_delete_admin_forbidden(binary(), binary()) -> ok. +test_delete_admin_forbidden(Token, AdminId) -> + ct:pal(" TEST: Delete admin as non-superadmin (403)"), + Path = <<"/v1/admin/admins/", AdminId/binary>>, + Resp = api_test_runner:admin_request(delete, Path, Token), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +%% ── Валидация создания ── + +-spec test_create_admin_duplicate_email(binary()) -> ok. +test_create_admin_duplicate_email(SuperToken) -> + ct:pal(" TEST: Create admin with duplicate email (409)"), + Email = api_test_runner:unique_email(<<"dupadmin">>), + % Создаём первого администратора + {ok, 201, _, _} = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken, + jsx:encode(#{email => Email, password => <<"Pass1234">>, role => <<"admin">>})), + % Пытаемся создать второго с тем же email + Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken, + jsx:encode(#{email => Email, password => <<"Pass1234">>, role => <<"admin">>})), + ?assertMatch({ok, 409, _, _}, Resp), + ct:pal(" OK: got 409"). + +-spec test_create_admin_invalid_role(binary()) -> ok. +test_create_admin_invalid_role(SuperToken) -> + ct:pal(" TEST: Create admin with invalid role (400)"), + Resp = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, SuperToken, + jsx:encode(#{email => <<"badrole@test.local">>, password => <<"Pass1234">>, role => <<"superhero">>})), + ?assertMatch({ok, 400, _, _}, Resp), + ct:pal(" OK: got 400"). + +%%%=================================================================== +%%% Вспомогательные функции +%%%=================================================================== + +%% @private Создаёт администратора и возвращает его данные. +-spec create_admin(binary(), binary(), binary(), binary()) -> map(). +create_admin(Token, Email, Password, Role) -> + ct:pal(" Creating test admin ~s...", [Email]), + {ok, 201, _, Body} = api_test_runner:admin_request(post, <<"/v1/admin/admins">>, Token, + jsx:encode(#{email => Email, password => Password, role => Role})), + jsx:decode(list_to_binary(Body), [return_maps]). \ No newline at end of file diff --git a/test/api/admins/admin_audit_tests.erl b/test/api/admins/admin_audit_tests.erl new file mode 100644 index 0000000..09f483a --- /dev/null +++ b/test/api/admins/admin_audit_tests.erl @@ -0,0 +1,79 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для аудита. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/audit +%%% +%%% Проверяет: +%%% - получение списка записей аудита +%%% - фильтрацию по admin_id +%%% - пагинацию +%%% @end +%%%------------------------------------------------------------------- +-module(admin_audit_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Audit Tests ==="), + UserToken = api_test_runner:get_user_token(), + #{<<"id">> := UserId} = api_test_runner:client_get(<<"/v1/user/me">>, UserToken), + AdminToken = api_test_runner:get_admin_token(), + SuperToken = api_test_runner:get_superadmin_token(), + + % Создаём тестовую запись аудита + Me = api_test_runner:admin_get(<<"/v1/admin/me">>, AdminToken), + AdminId = maps:get(<<"id">>, Me), + + Path = <<"/v1/admin/user/", UserId/binary>>, + Body = #{<<"action">> => <<"block">>, <<"reason">> => <<"Test">>}, + api_test_runner:admin_put(Path, AdminToken, Body), + ct:sleep(200), + Body2 = #{<<"action">> => <<"unblock">>, <<"reason">> => <<"Test">>}, + api_test_runner:admin_put(Path, AdminToken, Body2), + + test_list_audit(SuperToken), + test_filter_audit(SuperToken, AdminId), + test_audit_pagination(SuperToken), + test_list_admin_forbidden(AdminToken), + + ct:pal("=== All admin audit tests passed ==="), + ok. + +test_list_audit(Token) -> + ct:pal(" TEST: List all audit records"), + Records = api_test_runner:admin_get(<<"/v1/admin/audit">>, Token), + ?assert(is_list(Records)), + ?assert(length(Records) >= 1), + ct:pal(" OK: ~p records", [length(Records)]). + +test_filter_audit(Token, AdminId) -> + ct:pal(" TEST: Filter audit by admin_id"), + Records = api_test_runner:admin_get(<<"/v1/admin/audit?admin_id=", AdminId/binary>>, Token), + ?assert(is_list(Records)), + [?assertEqual(AdminId, maps:get(<<"admin_id">>, R)) || R <- Records], + ct:pal(" OK: ~p records", [length(Records)]). + +test_audit_pagination(Token) -> + ct:pal(" TEST: Audit pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/audit?limit=1&offset=0">>, Token), + ?assert(length(Page1) >= 1), + Page2 = api_test_runner:admin_get(<<"/v1/admin/audit?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 0), + case {Page1, Page2} of + {[First|_], [Second|_]} -> + Id1 = maps:get(<<"id">>, First), + Id2 = maps:get(<<"id">>, Second), + ?assertNotEqual(Id1, Id2); + _ -> ok + end, + ct:pal(" OK"). + +-spec test_list_admin_forbidden(binary()) -> ok. +test_list_admin_forbidden(Token) -> + ct:pal(" TEST: List audit as non-superadmin (403)"), + Resp = api_test_runner:admin_request(get, <<"/v1/admin/audit">>, Token), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). \ No newline at end of file diff --git a/test/api/admins/admin_banned_words_tests.erl b/test/api/admins/admin_banned_words_tests.erl new file mode 100644 index 0000000..a6049b6 --- /dev/null +++ b/test/api/admins/admin_banned_words_tests.erl @@ -0,0 +1,94 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления бан-словами. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/banned-words +%%% POST /v1/admin/banned-words +%%% DELETE /v1/admin/banned-words/:word +%%% +%%% Проверяет: +%%% - получение списка слов +%%% - добавление нового слова +%%% - удаление слова +%%% - пагинацию +%%% @end +%%%------------------------------------------------------------------- +-module(admin_banned_words_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Banned Words Tests ==="), + Token = api_test_runner:get_admin_token(), + + % Добавляем два исходных слова + Unique = integer_to_binary(erlang:system_time()), + Word1 = <<"badword1_", Unique/binary>>, + Word2 = <<"badword2_", Unique/binary>>, + api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word1}), + api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word2}), + + test_list_words(Token, Word1, Word2), + test_add_word(Token), + test_words_pagination(Token), + test_delete_word(Token, Word1), + test_delete_word(Token, Word2), + + ct:pal("=== All admin banned words tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/banned-words – список всех слов. +-spec test_list_words(binary(), binary(), binary()) -> ok. +test_list_words(Token, Word1, Word2) -> + ct:pal(" TEST: List all banned words"), + Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), + ?assert(is_list(Words)), + ?assert(length(Words) >= 2), + ?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word1 orelse maps:get(<<"word">>, W) =:= Word2 end, Words)), + ct:pal(" OK: ~p words", [length(Words)]). + +%% @doc POST /v1/admin/banned-words – добавление нового слова. +-spec test_add_word(binary()) -> ok. +test_add_word(Token) -> + ct:pal(" TEST: Add a new banned word"), + Unique = integer_to_binary(erlang:system_time()), + NewWord = <<"newbadword", Unique/binary>>, + Result = api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => NewWord}), + ?assertEqual(<<"added">>, maps:get(<<"status">>, Result)), + Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), + ?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= NewWord end, Words)), + ct:pal(" OK"). + +%% @doc DELETE /v1/admin/banned-words/:word – удаление слова. +-spec test_delete_word(binary(), binary()) -> ok. +test_delete_word(Token, Word) -> + ct:pal(" TEST: Delete a banned word"), + Path = <<"/v1/admin/banned-words/", Word/binary>>, + Result = api_test_runner:admin_delete(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Result)), + Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), + ?assertNot(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, Words)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/banned-words?limit=...&offset=... – пагинация. +-spec test_words_pagination(binary()) -> ok. +test_words_pagination(Token) -> + ct:pal(" TEST: Banned words pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/banned-words?limit=1&offset=0">>, Token), + ?assert(length(Page1) >= 1), + Page2 = api_test_runner:admin_get(<<"/v1/admin/banned-words?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 1), + Id1 = maps:get(<<"id">>, hd(Page1)), + Id2 = maps:get(<<"id">>, hd(Page2)), + ?assertNotEqual(Id1, Id2), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_events_tests.erl b/test/api/admins/admin_events_tests.erl new file mode 100644 index 0000000..2634598 --- /dev/null +++ b/test/api/admins/admin_events_tests.erl @@ -0,0 +1,119 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления событиями. +%%% Покрывает эндпоинты: +%%% GET /v1/admin/events +%%% GET /v1/admin/events/:id +%%% PUT /v1/admin/events/:id +%%% DELETE /v1/admin/events/:id +%%% @end +%%%------------------------------------------------------------------- +-module(admin_events_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% EUnit test generator +%%%=================================================================== + +test() -> + ct:pal("=== Admin Events Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + %% Создаём тестовый календарь и два события + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"EventsTestCal">>}), + Event1Id = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"Test Event Alpha">>, + start_time => api_test_runner:future_date(), + duration => 60 + }), + Event2Id = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"Beta Event">>, + start_time => api_test_runner:future_date(), + duration => 60 + }), + + %% Выполняем тесты + test_list_events(Token, CalId, Event1Id, Event2Id), + test_get_event(Token, Event1Id), + test_update_event(Token, Event1Id), + test_delete_event(Token, Event2Id), + test_filter_events(Token, CalId), + test_search_events(Token, CalId), + test_event_pagination(Token, CalId), + test_delete_event(Token, Event1Id), + + ct:pal("=== All admin events tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/events – список событий (базовая проверка). +test_list_events(Token, _CalId, _Event1Id, _Event2Id) -> + ct:pal(" TEST: List all events"), + Events = api_test_runner:admin_get(<<"/v1/admin/events">>, Token), + ?assert(is_list(Events)), + ?assert(length(Events) >= 2), + ct:pal(" OK: ~p events found", [length(Events)]). + +%% @doc GET /v1/admin/events/:id – получение события по ID. +test_get_event(Token, EventId) -> + ct:pal(" TEST: Get event by ID"), + Path = <<"/v1/admin/events/", EventId/binary>>, + Event = api_test_runner:admin_get(Path, Token), + ?assertEqual(EventId, maps:get(<<"id">>, Event)), + ?assertEqual(<<"active">>, maps:get(<<"status">>, Event)), + ct:pal(" OK: ~s", [maps:get(<<"title">>, Event)]). + +%% @doc PUT /v1/admin/events/:id – обновление события. +test_update_event(Token, EventId) -> + ct:pal(" TEST: Update event"), + Path = <<"/v1/admin/events/", EventId/binary>>, + Updates = #{<<"title">> => <<"Updated by admin">>, <<"description">> => <<"Test update">>}, + Updated = api_test_runner:admin_put(Path, Token, Updates), + ?assertEqual(<<"Updated by admin">>, maps:get(<<"title">>, Updated)), + ?assertEqual(<<"Test update">>, maps:get(<<"description">>, Updated)), + ct:pal(" OK"). + +%% @doc DELETE /v1/admin/events/:id – удаление события. +test_delete_event(Token, EventId) -> + ct:pal(" TEST: Delete event"), + Path = <<"/v1/admin/events/", EventId/binary>>, + Deleted = api_test_runner:admin_delete(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/events?status=active – фильтрация по статусу. +test_filter_events(Token, CalId) -> + ct:pal(" TEST: Filter events by status=active"), + Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&status=active">>, + Events = api_test_runner:admin_get(Path, Token), + ?assert(is_list(Events)), + [?assertEqual(<<"active">>, maps:get(<<"status">>, E)) || E <- Events], + ct:pal(" OK: ~p events", [length(Events)]). + +%% @doc GET /v1/admin/events?q=... – поиск по подстроке. +test_search_events(Token, CalId) -> + ct:pal(" TEST: Search events by title substring"), + Path = <<"/v1/admin/events?calendar_id=", CalId/binary, "&q=Updated">>, + Events = api_test_runner:admin_get(Path, Token), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + [?assertNotEqual(nomatch, binary:match(maps:get(<<"title">>, E), <<"Updated">>)) || E <- Events], + ct:pal(" OK: ~p results", [length(Events)]). + +%% @doc GET /v1/admin/events?limit=...&offset=... – пагинация. +test_event_pagination(Token, CalId) -> + ct:pal(" TEST: Event pagination"), + Base = <<"/v1/admin/events?calendar_id=", CalId/binary, "&status=all&sort=title&order=asc">>, + Page1 = api_test_runner:admin_get(<<Base/binary, "&limit=1&offset=0">>, Token), + ?assertEqual(1, length(Page1)), + Page2 = api_test_runner:admin_get(<<Base/binary, "&limit=1&offset=1">>, Token), + ?assertEqual(1, length(Page2)), + Id1 = maps:get(<<"id">>, hd(Page1)), + Id2 = maps:get(<<"id">>, hd(Page2)), + ?assertNotEqual(Id1, Id2), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_me_tests.erl b/test/api/admins/admin_me_tests.erl new file mode 100644 index 0000000..246a28a --- /dev/null +++ b/test/api/admins/admin_me_tests.erl @@ -0,0 +1,36 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для профиля текущего администратора. +%%% Покрывает GET /v1/admin/me и PUT /v1/admin/me. +%%% @end +%%%------------------------------------------------------------------- +-module(admin_me_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Me Tests ==="), + Token = api_test_runner:get_admin_token(), + + test_get_me(Token), + test_update_me(Token), + + ct:pal("=== All admin me tests passed ==="), + ok. + +test_get_me(Token) -> + ct:pal(" TEST: Get current admin profile"), + Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token), + ?assert(is_map(Me)), + ?assert(maps:is_key(<<"id">>, Me)), + ?assert(maps:is_key(<<"email">>, Me)), + ct:pal(" OK: got profile for ~s", [maps:get(<<"email">>, Me)]). + +test_update_me(Token) -> + ct:pal(" TEST: Update current admin profile"), + Updated = api_test_runner:admin_put(<<"/v1/admin/me">>, Token, + #{nickname => <<"TestNick">>, timezone => <<"UTC">>}), + ?assertEqual(<<"TestNick">>, maps:get(<<"nickname">>, Updated)), + ?assertEqual(<<"UTC">>, maps:get(<<"timezone">>, Updated)), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_moderation_tests.erl b/test/api/admins/admin_moderation_tests.erl new file mode 100644 index 0000000..f02d217 --- /dev/null +++ b/test/api/admins/admin_moderation_tests.erl @@ -0,0 +1,133 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для модерации сущностей. +%%% +%%% Покрывает эндпоинты: +%%% PUT /v1/admin/:target_type/:id +%%% +%%% Проверяет: +%%% - заморозку/разморозку календаря +%%% - заморозку/разморозку события +%%% - скрытие/раскрытие отзыва +%%% - блокировку/разблокировку пользователя +%%% @end +%%%------------------------------------------------------------------- +-module(admin_moderation_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Moderation Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ModTestCal">>}), + EventId = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"Event to moderate">>, + start_time => api_test_runner:future_date(), + duration => 60 + }), + + % Создаём пользователя для блокировки + UserEmail = api_test_runner:unique_email(<<"moduser">>), + UserTok = api_test_runner:register_and_login(UserEmail, <<"pass">>), + #{<<"id">> := UserId} = api_test_runner:client_get(<<"/v1/user/me">>, UserTok), + + % Создаём отзыв (требуется участие) + #{<<"id">> := BookingId} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, UserTok, #{}), + api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, UserToken, + #{action => <<"confirm">>}), + #{<<"id">> := ReviewId} = api_test_runner:client_post(<<"/v1/reviews">>, UserTok, #{ + target_type => <<"event">>, + target_id => EventId, + rating => 3, + comment => <<"Moderate me">> + }), + + % Тесты модерации + test_freeze_calendar(Token, CalId), + test_unfreeze_calendar(Token, CalId), + test_freeze_event(Token, EventId), + test_unfreeze_event(Token, EventId), + test_hide_review(Token, ReviewId), + test_unhide_review(Token, ReviewId), + test_block_user(Token, UserId), + test_unblock_user(Token, UserId), + + ct:pal("=== All admin moderation tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +test_freeze_calendar(Token, Id) -> + ct:pal(" TEST: Freeze calendar"), + Path = <<"/v1/admin/calendar/", Id/binary>>, + Body = #{<<"action">> => <<"freeze">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"frozen">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_unfreeze_calendar(Token, Id) -> + ct:pal(" TEST: Unfreeze calendar"), + Path = <<"/v1/admin/calendar/", Id/binary>>, + Body = #{<<"action">> => <<"unfreeze">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_freeze_event(Token, Id) -> + ct:pal(" TEST: Freeze event"), + Path = <<"/v1/admin/event/", Id/binary>>, + Body = #{<<"action">> => <<"freeze">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"frozen">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_unfreeze_event(Token, Id) -> + ct:pal(" TEST: Unfreeze event"), + Path = <<"/v1/admin/event/", Id/binary>>, + Body = #{<<"action">> => <<"unfreeze">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_hide_review(Token, ReviewId) -> + ct:pal(" TEST: Hide review"), + Path = <<"/v1/admin/review/", ReviewId/binary>>, + Body = #{<<"action">> => <<"hide">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"hidden">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_unhide_review(Token, ReviewId) -> + ct:pal(" TEST: Unhide review"), + Path = <<"/v1/admin/review/", ReviewId/binary>>, + Body = #{<<"action">> => <<"unhide">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"visible">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_block_user(Token, UserId) -> + ct:pal(" TEST: Block user"), + Path = <<"/v1/admin/user/", UserId/binary>>, + Body = #{<<"action">> => <<"block">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"blocked">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). + +test_unblock_user(Token, UserId) -> + ct:pal(" TEST: Unblock user"), + Path = <<"/v1/admin/user/", UserId/binary>>, + Body = #{<<"action">> => <<"unblock">>, <<"reason">> => <<"Test">>}, + Resp = api_test_runner:admin_put(Path, Token, Body), + ?assertEqual(<<"active">>, maps:get(<<"status">>, Resp)), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_reports_tests.erl b/test/api/admins/admin_reports_tests.erl new file mode 100644 index 0000000..202066b --- /dev/null +++ b/test/api/admins/admin_reports_tests.erl @@ -0,0 +1,134 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления жалобами. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/reports +%%% GET /v1/admin/reports/:id +%%% PUT /v1/admin/reports/:id +%%% +%%% Проверяет: +%%% - получение списка жалоб +%%% - получение жалобы по ID +%%% - обновление статуса жалобы (review, dismiss) +%%% - фильтрацию по статусу и типу цели +%%% - пагинацию +%%% @end +%%%------------------------------------------------------------------- +-module(admin_reports_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Reports Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + % Создаём тестовые данные: календарь, событие + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReportsTestCal">>}), + EventId = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"Event for reporting">>, + start_time => api_test_runner:future_date(), + duration => 60 + }), + + % Создаём две жалобы от имени пользователя + #{<<"id">> := Report1Id} = api_test_runner:client_post(<<"/v1/reports">>, UserToken, #{ + <<"target_type">> => <<"event">>, + <<"target_id">> => EventId, + <<"reason">> => <<"Inappropriate content">> + }), + #{<<"id">> := Report2Id} = api_test_runner:client_post(<<"/v1/reports">>, UserToken, #{ + <<"target_type">> => <<"event">>, + <<"target_id">> => EventId, + <<"reason">> => <<"Spam">> + }), + + test_list_reports(Token, Report1Id), + test_get_report(Token, Report1Id), + test_review_report(Token, Report1Id), + test_dismiss_report(Token, Report2Id), + test_filter_reports(Token), + test_report_pagination(Token), + + ct:pal("=== All admin reports tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/reports – проверяет получение списка жалоб. +-spec test_list_reports(binary(), binary()) -> ok. +test_list_reports(Token, ReportId) -> + ct:pal(" TEST: List all reports"), + Reports = api_test_runner:admin_get(<<"/v1/admin/reports">>, Token), + ?assert(is_list(Reports)), + ?assert(length(Reports) >= 2), + ?assert(lists:any(fun(R) -> maps:get(<<"id">>, R) =:= ReportId end, Reports)), + ct:pal(" OK: ~p reports", [length(Reports)]). + +%% @doc GET /v1/admin/reports/:id – проверяет получение жалобы по ID. +-spec test_get_report(binary(), binary()) -> ok. +test_get_report(Token, ReportId) -> + ct:pal(" TEST: Get report by ID"), + Path = <<"/v1/admin/reports/", ReportId/binary>>, + Report = api_test_runner:admin_get(Path, Token), + ?assertEqual(ReportId, maps:get(<<"id">>, Report)), + ?assertEqual(<<"pending">>, maps:get(<<"status">>, Report)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/reports/:id – проверяет изменение статуса на reviewed. +-spec test_review_report(binary(), binary()) -> ok. +test_review_report(Token, ReportId) -> + ct:pal(" TEST: Review report (set status to reviewed)"), + Path = <<"/v1/admin/reports/", ReportId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"reviewed">>}), + ?assertEqual(<<"reviewed">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/reports/:id – проверяет изменение статуса на dismissed. +-spec test_dismiss_report(binary(), binary()) -> ok. +test_dismiss_report(Token, ReportId) -> + ct:pal(" TEST: Dismiss report (set status to dismissed)"), + Path = <<"/v1/admin/reports/", ReportId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"dismissed">>}), + ?assertEqual(<<"dismissed">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/reports?status=...&target_type=... – проверяет фильтрацию. +-spec test_filter_reports(binary()) -> ok. +test_filter_reports(Token) -> + ct:pal(" TEST: Filter reports by status=reviewed"), + Reports = api_test_runner:admin_get(<<"/v1/admin/reports?status=reviewed">>, Token), + ?assert(is_list(Reports)), + [?assertEqual(<<"reviewed">>, maps:get(<<"status">>, R)) || R <- Reports], + ct:pal(" OK: ~p reviewed reports", [length(Reports)]), + + ct:pal(" TEST: Filter reports by target_type=event"), + Reports2 = api_test_runner:admin_get(<<"/v1/admin/reports?target_type=event">>, Token), + ?assert(is_list(Reports2)), + [?assertEqual(<<"event">>, maps:get(<<"target_type">>, R)) || R <- Reports2], + ct:pal(" OK: ~p reports for events", [length(Reports2)]). + +%% @doc GET /v1/admin/reports?limit=...&offset=... – проверяет пагинацию. +-spec test_report_pagination(binary()) -> ok. +test_report_pagination(Token) -> + ct:pal(" TEST: Report pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/reports?limit=1&offset=0">>, Token), + ?assert(length(Page1) >= 1), + Page2 = api_test_runner:admin_get(<<"/v1/admin/reports?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 0), + case {Page1, Page2} of + {[First|_], [Second|_]} -> + Id1 = maps:get(<<"id">>, First), + Id2 = maps:get(<<"id">>, Second), + ?assertNotEqual(Id1, Id2); + _ -> ok + end, + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_reviews_tests.erl b/test/api/admins/admin_reviews_tests.erl new file mode 100644 index 0000000..53f345d --- /dev/null +++ b/test/api/admins/admin_reviews_tests.erl @@ -0,0 +1,136 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления отзывами. +%%% Покрывает эндпоинты: +%%% GET /v1/admin/reviews +%%% PATCH /v1/admin/reviews +%%% GET /v1/admin/reviews/:id +%%% PUT /v1/admin/reviews/:id +%%% @end +%%%------------------------------------------------------------------- +-module(admin_reviews_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% EUnit test generator +%%%=================================================================== + +test() -> + ct:pal("=== Admin Reviews Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + %% Создаём тестовые данные: календарь, событие + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReviewsTestCal">>}), + EventId = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"Event for review testing">>, + start_time => api_test_runner:future_date(), + duration => 60 + }), + + %% Создаём двух пользователей для отзывов + User1Email = api_test_runner:unique_email(<<"rev1">>), + User1Token = api_test_runner:register_and_login(User1Email, <<"pass1">>), + User2Email = api_test_runner:unique_email(<<"rev2">>), + User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>), + + %% Записываем пользователей на событие и подтверждаем бронирования + #{<<"id">> := Booking1Id} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, User1Token, #{}), + #{<<"id">> := Booking2Id} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, User2Token, #{}), + + ct:pal(" Confirming bookings as calendar owner..."), + api_test_runner:client_put(<<"/v1/bookings/", Booking1Id/binary>>, UserToken, + #{action => <<"confirm">>}), + api_test_runner:client_put(<<"/v1/bookings/", Booking2Id/binary>>, UserToken, + #{action => <<"confirm">>}), + + %% Оставляем отзывы от имени участников + #{<<"id">> := Review1Id} = api_test_runner:client_post(<<"/v1/reviews">>, User1Token, #{ + target_type => <<"event">>, + target_id => EventId, + rating => 5, + comment => <<"Great!">> + }), + #{<<"id">> := Review2Id} = api_test_runner:client_post(<<"/v1/reviews">>, User2Token, #{ + target_type => <<"event">>, + target_id => EventId, + rating => 4, + comment => <<"Good">> + }), + + %% Запускаем тесты + test_list_reviews(Token), + test_filter_reviews(Token, EventId), + test_bulk_update_status(Token, Review1Id, Review2Id), + test_get_review(Token, Review1Id), + test_update_review(Token, Review1Id), + test_review_pagination(Token), + + ct:pal("=== All admin reviews tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/reviews – список отзывов. +test_list_reviews(Token) -> + ct:pal(" TEST: List all reviews"), + Reviews = api_test_runner:admin_get(<<"/v1/admin/reviews">>, Token), + ?assert(is_list(Reviews)), + ?assert(length(Reviews) >= 2), + ct:pal(" OK: ~p reviews", [length(Reviews)]). + +%% @doc GET /v1/admin/reviews?target_type=event&target_id=... – фильтрация по цели. +test_filter_reviews(Token, EventId) -> + ct:pal(" TEST: Filter reviews by target"), + Path = <<"/v1/admin/reviews?target_type=event&target_id=", EventId/binary>>, + Reviews = api_test_runner:admin_get(Path, Token), + ?assert(is_list(Reviews)), + ?assert(length(Reviews) >= 2), + [?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews], + ct:pal(" OK: ~p reviews for event", [length(Reviews)]). + +%% @doc PATCH /v1/admin/reviews – массовое обновление статусов. +test_bulk_update_status(Token, Review1Id, Review2Id) -> + ct:pal(" TEST: Bulk update review statuses"), + Body = [ + #{<<"id">> => Review1Id, <<"status">> => <<"visible">>}, + #{<<"id">> => Review2Id, <<"status">> => <<"hidden">>} + ], + #{<<"updated_count">> := Count} = api_test_runner:admin_patch(<<"/v1/admin/reviews">>, Token, Body), + ?assertEqual(2, Count), + ct:pal(" OK: updated ~p reviews", [Count]). + +%% @doc GET /v1/admin/reviews/:id – получение отзыва по ID. +test_get_review(Token, ReviewId) -> + ct:pal(" TEST: Get review by ID"), + Path = <<"/v1/admin/reviews/", ReviewId/binary>>, + Review = api_test_runner:admin_get(Path, Token), + ?assertEqual(ReviewId, maps:get(<<"id">>, Review)), + ?assertEqual(<<"visible">>, maps:get(<<"status">>, Review)), + ct:pal(" OK: ~s", [maps:get(<<"comment">>, Review)]). + +%% @doc PUT /v1/admin/reviews/:id – обновление отзыва. +test_update_review(Token, ReviewId) -> + ct:pal(" TEST: Update review"), + Path = <<"/v1/admin/reviews/", ReviewId/binary>>, + Updates = #{<<"comment">> => <<"Updated by admin">>}, + Updated = api_test_runner:admin_put(Path, Token, Updates), + ?assertEqual(<<"Updated by admin">>, maps:get(<<"comment">>, Updated)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/reviews?limit=...&offset=... – пагинация. +test_review_pagination(Token) -> + ct:pal(" TEST: Review pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=0">>, Token), + ?assertEqual(1, length(Page1)), + Page2 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 1), + Id1 = maps:get(<<"id">>, hd(Page1)), + Id2 = maps:get(<<"id">>, hd(Page2)), + ?assertNotEqual(Id1, Id2), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_stats_tests.erl b/test/api/admins/admin_stats_tests.erl new file mode 100644 index 0000000..f596c9b --- /dev/null +++ b/test/api/admins/admin_stats_tests.erl @@ -0,0 +1,77 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для получения статистики. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/stats +%%% +%%% Проверяет: +%%% - получение статистики для всех четырёх ролей администраторов +%%% - для superadmin и admin – наличие ключевых метрик +%%% - для moderator и support – ответ непустой +%%% - работу с фильтром по датам (from, to) +%%% @end +%%%------------------------------------------------------------------- +-module(admin_stats_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Stats Tests ==="), + + SuperToken = api_test_runner:get_superadmin_token(), + AdminToken = api_test_runner:get_admin_token(), + ModerToken = api_test_runner:get_moderator_token(), + SupportToken = api_test_runner:get_support_token(), + + test_stats_for_role("Superadmin", SuperToken, strict), + test_stats_for_role("Admin", AdminToken, strict), + test_stats_for_role("Moderator", ModerToken, loose), + test_stats_for_role("Support", SupportToken, loose), + + test_stats_with_dates(SuperToken), + + ct:pal("=== All admin stats tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Проверяет получение статистики для конкретной роли. +%% strict – ожидаем ключи users_total/users и events_total/events +%% loose – просто убеждаемся, что ответ непустой +-spec test_stats_for_role(string(), binary(), strict | loose) -> ok. +test_stats_for_role(RoleName, Token, Strictness) -> + ct:pal(" TEST: Get stats for role ~s", [RoleName]), + Stats = api_test_runner:admin_get(<<"/v1/admin/stats">>, Token), + ?assert(is_map(Stats)), + case Strictness of + strict -> + HasUsers = maps:is_key(<<"users_total">>, Stats) orelse + maps:is_key(<<"users">>, Stats), + HasEvents = maps:is_key(<<"events_total">>, Stats) orelse + maps:is_key(<<"events">>, Stats), + ?assert(HasUsers orelse HasEvents); + loose -> + ?assert(map_size(Stats) > 0) + end, + ct:pal(" OK: ~p keys", [length(maps:keys(Stats))]). + +%% @doc GET /v1/admin/stats?from=...&to=... – проверяет фильтрацию по датам. +-spec test_stats_with_dates(binary()) -> ok. +test_stats_with_dates(Token) -> + ct:pal(" TEST: Get stats with date range"), + From = <<"2026-01-01T00:00:00Z">>, + To = <<"2026-12-31T23:59:59Z">>, + Path = <<"/v1/admin/stats?from=", From/binary, "&to=", To/binary>>, + Stats = api_test_runner:admin_get(Path, Token), + ?assert(is_map(Stats)), + ?assert(maps:is_key(<<"users_total">>, Stats) orelse + maps:is_key(<<"users">>, Stats)), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_subscriptions_tests.erl b/test/api/admins/admin_subscriptions_tests.erl new file mode 100644 index 0000000..3228122 --- /dev/null +++ b/test/api/admins/admin_subscriptions_tests.erl @@ -0,0 +1,131 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления подписками. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/subscriptions +%%% GET /v1/admin/subscriptions/:id +%%% PUT /v1/admin/subscriptions/:id +%%% DELETE /v1/admin/subscriptions/:id +%%% +%%% Проверяет: +%%% - получение списка подписок +%%% - получение подписки по ID +%%% - обновление подписки (изменение плана, статуса) +%%% - удаление подписки +%%% - фильтрацию по статусу и плану +%%% - пагинацию +%%% @end +%%%------------------------------------------------------------------- +-module(admin_subscriptions_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Subscriptions Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + % Создаём две подписки для разных проверок + #{<<"id">> := Sub1Id} = api_test_runner:client_post(<<"/v1/subscription">>, UserToken, + #{<<"action">> => <<"start_trial">>}), + + % Для второй подписки создаём нового пользователя + User2Email = api_test_runner:unique_email(<<"sub2">>), + User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>), + #{<<"id">> := Sub2Id} = api_test_runner:client_post(<<"/v1/subscription">>, User2Token, + #{<<"action">> => <<"start_trial">>}), + + % Теперь две подписки в системе + test_list_subscriptions(Token, Sub1Id), + test_get_subscription(Token, Sub1Id), + test_update_subscription(Token, Sub1Id), + % После обновления Sub1Id имеет план biannual и статус active + test_filter_subscriptions(Token), + test_subscription_pagination(Token), + % Удаляем вторую подписку, чтобы проверить delete + test_delete_subscription(Token, Sub2Id), + + ct:pal("=== All admin subscriptions tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/subscriptions – список подписок. +-spec test_list_subscriptions(binary(), binary()) -> ok. +test_list_subscriptions(Token, SubId) -> + ct:pal(" TEST: List all subscriptions"), + Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions">>, Token), + ?assert(is_list(Subs)), + ?assert(length(Subs) >= 2), + ?assert(lists:any(fun(S) -> maps:get(<<"id">>, S) =:= SubId end, Subs)), + ct:pal(" OK: ~p subscriptions", [length(Subs)]). + +%% @doc GET /v1/admin/subscriptions/:id – получение подписки по ID. +-spec test_get_subscription(binary(), binary()) -> ok. +test_get_subscription(Token, SubId) -> + ct:pal(" TEST: Get subscription by ID"), + Path = <<"/v1/admin/subscriptions/", SubId/binary>>, + Sub = api_test_runner:admin_get(Path, Token), + ?assertEqual(SubId, maps:get(<<"id">>, Sub)), + ?assert(maps:is_key(<<"plan">>, Sub)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/subscriptions/:id – обновление подписки (план + статус). +-spec test_update_subscription(binary(), binary()) -> ok. +test_update_subscription(Token, SubId) -> + ct:pal(" TEST: Update subscription (change plan)"), + Path = <<"/v1/admin/subscriptions/", SubId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{ + <<"plan">> => <<"biannual">>, + <<"status">> => <<"active">> + }), + ?assertEqual(<<"biannual">>, maps:get(<<"plan">>, Updated)), + ?assertEqual(<<"active">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK"). + +%% @doc DELETE /v1/admin/subscriptions/:id – удаление подписки. +-spec test_delete_subscription(binary(), binary()) -> ok. +test_delete_subscription(Token, SubId) -> + ct:pal(" TEST: Delete subscription"), + Path = <<"/v1/admin/subscriptions/", SubId/binary>>, + Deleted = api_test_runner:admin_delete(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/subscriptions?status=...&plan=... – фильтрация. +-spec test_filter_subscriptions(binary()) -> ok. +test_filter_subscriptions(Token) -> + ct:pal(" TEST: Filter subscriptions by status=active"), + Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions?status=active">>, Token), + ?assert(is_list(Subs)), + ?assert(length(Subs) >= 1), + [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], + ct:pal(" OK: ~p active subscriptions", [length(Subs)]), + + ct:pal(" TEST: Filter subscriptions by plan=biannual"), + Subs2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?plan=biannual">>, Token), + ?assert(is_list(Subs2)), + ?assert(length(Subs2) >= 1), + [?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs2], + ct:pal(" OK: ~p biannual subscriptions", [length(Subs2)]). + +%% @doc GET /v1/admin/subscriptions?limit=...&offset=... – пагинация. +-spec test_subscription_pagination(binary()) -> ok. +test_subscription_pagination(Token) -> + ct:pal(" TEST: Subscription pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=0">>, Token), + ?assert(length(Page1) >= 1), + Page2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 1), + Id1 = maps:get(<<"id">>, hd(Page1)), + Id2 = maps:get(<<"id">>, hd(Page2)), + ?assertNotEqual(Id1, Id2), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_tickets_tests.erl b/test/api/admins/admin_tickets_tests.erl new file mode 100644 index 0000000..4a628b6 --- /dev/null +++ b/test/api/admins/admin_tickets_tests.erl @@ -0,0 +1,153 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления тикетами. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/admin/tickets +%%% GET /v1/admin/tickets/:id +%%% PUT /v1/admin/tickets/:id +%%% DELETE /v1/admin/tickets/:id +%%% +%%% Проверяет: +%%% - получение списка тикетов +%%% - получение тикета по ID +%%% - разрешение (resolve) и закрытие (close) тикета +%%% - назначение исполнителя +%%% - удаление тикета +%%% - фильтрацию по статусу +%%% - пагинацию +%%% @end +%%%------------------------------------------------------------------- +-module(admin_tickets_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Admin Tickets Tests ==="), + Token = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + + % Создаём два тикета для разных проверок + Ticket1 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken, + #{<<"error_message">> => <<"Test bug">>, <<"stacktrace">> => <<"trace">>}), + #{<<"id">> := Ticket1Id} = Ticket1, + + Ticket2 = api_test_runner:client_post(<<"/v1/tickets">>, UserToken, + #{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}), + #{<<"id">> := Ticket2Id} = Ticket2, + + test_list_tickets(Token, Ticket1Id), + test_get_ticket(Token, Ticket1Id), + test_resolve_ticket(Token, Ticket1Id), + test_close_ticket(Token, Ticket1Id), + test_assign_ticket(Token, Ticket1Id), + test_filter_tickets(Token), + test_ticket_pagination(Token, Ticket2Id), + test_delete_ticket(Token, Ticket1Id), + test_delete_ticket(Token, Ticket2Id), + + ct:pal("=== All admin tickets tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов. +%% Убеждается, что список не пуст и содержит созданный тикет. +-spec test_list_tickets(binary(), binary()) -> ok. +test_list_tickets(Token, TicketId) -> + ct:pal(" TEST: List all tickets"), + Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token), + ?assert(is_list(Tickets)), + ?assert(length(Tickets) >= 1), + ?assert(lists:any(fun(T) -> maps:get(<<"id">>, T) =:= TicketId end, Tickets)), + ct:pal(" OK: ~p tickets", [length(Tickets)]). + +%% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID. +%% Убеждается, что статус нового тикета – open. +-spec test_get_ticket(binary(), binary()) -> ok. +test_get_ticket(Token, TicketId) -> + ct:pal(" TEST: Get ticket by ID"), + Path = <<"/v1/admin/tickets/", TicketId/binary>>, + Ticket = api_test_runner:admin_get(Path, Token), + ?assertEqual(TicketId, maps:get(<<"id">>, Ticket)), + ?assertEqual(<<"open">>, maps:get(<<"status">>, Ticket)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/tickets/:id – разрешение тикета (resolve). +%% Ожидается статус resolved после успешного выполнения. +-spec test_resolve_ticket(binary(), binary()) -> ok. +test_resolve_ticket(Token, TicketId) -> + ct:pal(" TEST: Resolve ticket"), + Path = <<"/v1/admin/tickets/", TicketId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{ + <<"status">> => <<"resolved">>, + <<"resolution_note">> => <<"Fixed">> + }), + ?assertEqual(<<"resolved">>, maps:get(<<"status">>, Updated)), + ?assertEqual(<<"Fixed">>, maps:get(<<"resolution_note">>, Updated)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/tickets/:id – закрытие тикета (close). +-spec test_close_ticket(binary(), binary()) -> ok. +test_close_ticket(Token, TicketId) -> + ct:pal(" TEST: Close ticket"), + Path = <<"/v1/admin/tickets/", TicketId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{<<"status">> => <<"closed">>}), + ?assertEqual(<<"closed">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK"). + +%% @doc PUT /v1/admin/tickets/:id – назначение исполнителя. +-spec test_assign_ticket(binary(), binary()) -> ok. +test_assign_ticket(Token, TicketId) -> + ct:pal(" TEST: Assign ticket"), + Me = api_test_runner:admin_get(<<"/v1/admin/me">>, Token), + AdminId = maps:get(<<"id">>, Me), + Path = <<"/v1/admin/tickets/", TicketId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{<<"assigned_to">> => AdminId}), + ?assertEqual(AdminId, maps:get(<<"assigned_to">>, Updated)), + ct:pal(" OK"). + +%% @doc DELETE /v1/admin/tickets/:id – удаление тикета. +-spec test_delete_ticket(binary(), binary()) -> ok. +test_delete_ticket(Token, TicketId) -> + ct:pal(" TEST: Delete ticket"), + Path = <<"/v1/admin/tickets/", TicketId/binary>>, + Deleted = api_test_runner:admin_delete(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), + ct:pal(" OK"). + +%% @doc GET /v1/admin/tickets?status=... – проверяет фильтрацию по статусу open. +%% Использует второй тикет, который всё ещё open. +-spec test_filter_tickets(binary()) -> ok. +test_filter_tickets(Token) -> + ct:pal(" TEST: Filter tickets by status=open"), + Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets?status=open">>, Token), + ?assert(is_list(Tickets)), + ?assert(length(Tickets) >= 1), + [?assertEqual(<<"open">>, maps:get(<<"status">>, T)) || T <- Tickets], + ct:pal(" OK: ~p open tickets", [length(Tickets)]). + +%% @doc GET /v1/admin/tickets?limit=...&offset=... – проверяет пагинацию. +%% Использует второй тикет как гарантированно существующий. +-spec test_ticket_pagination(binary(), binary()) -> ok. +test_ticket_pagination(Token, _TicketId) -> + ct:pal(" TEST: Ticket pagination"), + Page1 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=0">>, Token), + ?assert(length(Page1) >= 1), + Page2 = api_test_runner:admin_get(<<"/v1/admin/tickets?limit=1&offset=1">>, Token), + ?assert(length(Page2) >= 0), + case {Page1, Page2} of + {[First|_], [Second|_]} -> + Id1 = maps:get(<<"id">>, First), + Id2 = maps:get(<<"id">>, Second), + ?assertNotEqual(Id1, Id2); + _ -> ok + end, + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_users_tests.erl b/test/api/admins/admin_users_tests.erl new file mode 100644 index 0000000..7faeea1 --- /dev/null +++ b/test/api/admins/admin_users_tests.erl @@ -0,0 +1,67 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты административного API для управления пользователями. +%%% @end +%%%------------------------------------------------------------------- +-module(admin_users_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +test() -> + ct:pal("=== Admin Users Tests ==="), + Token = api_test_runner:get_admin_token(), + + %% Создаём тестового пользователя + Email = api_test_runner:unique_email(<<"usertest">>), + UserToken = api_test_runner:register_and_login(Email, <<"testpass">>), + Me = api_test_runner:client_get(<<"/v1/user/me">>, UserToken), + UserId = maps:get(<<"id">>, Me), + + test_list_users(Token), + test_get_user(Token, UserId), + test_update_user(Token, UserId), + test_filter_users(Token), + test_delete_user(Token, UserId), + + ct:pal("=== All admin users tests passed ==="), + ok. + +test_list_users(Token) -> + ct:pal(" TEST: List all users"), + Users = api_test_runner:admin_get(<<"/v1/admin/users">>, Token), + ?assert(is_list(Users)), + ?assert(length(Users) >= 1), + ct:pal(" OK: ~p users", [length(Users)]). + +test_get_user(Token, UserId) -> + ct:pal(" TEST: Get user by ID"), + Path = <<"/v1/admin/users/", UserId/binary>>, + User = api_test_runner:admin_get(Path, Token), + ?assertEqual(UserId, maps:get(<<"id">>, User)), + ct:pal(" OK: ~s", [maps:get(<<"email">>, User)]). + +test_update_user(Token, UserId) -> + ct:pal(" TEST: Update user (change role and status)"), + Path = <<"/v1/admin/users/", UserId/binary>>, + Updated = api_test_runner:admin_put(Path, Token, #{ + <<"role">> => <<"bot">>, + <<"status">> => <<"frozen">>, + <<"reason">> => <<"Test freeze">> + }), + ?assertEqual(<<"bot">>, maps:get(<<"role">>, Updated)), + ?assertEqual(<<"frozen">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK"). + +test_filter_users(Token) -> + ct:pal(" TEST: Filter users by status=frozen"), + Users = api_test_runner:admin_get(<<"/v1/admin/users?status=frozen">>, Token), + ?assert(is_list(Users)), + [?assertEqual(<<"frozen">>, maps:get(<<"status">>, U)) || U <- Users], + ct:pal(" OK: ~p frozen users", [length(Users)]). + +test_delete_user(Token, UserId) -> + ct:pal(" TEST: Delete (soft-delete) user"), + Path = <<"/v1/admin/users/", UserId/binary>>, + Deleted = api_test_runner:admin_delete(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), + ct:pal(" OK"). \ No newline at end of file diff --git a/test/api/admins/admin_websocket_tests.erl b/test/api/admins/admin_websocket_tests.erl new file mode 100644 index 0000000..2337bac --- /dev/null +++ b/test/api/admins/admin_websocket_tests.erl @@ -0,0 +1,258 @@ +-module(admin_websocket_tests). +-export([test/0]). + +test() -> + ct:pal("Testing WebSocket API..."), + application:ensure_all_started(gun), + + AdminToken = api_test_runner:get_admin_token(), + UserToken = api_test_runner:get_user_token(), + ct:pal(" AdminToken: ~s...", [binary_part(AdminToken, 0, 30)]), + ct:pal(" UserToken: ~s...", [binary_part(UserToken, 0, 30)]), + + % Создаём календарь и событие через новый api_test_runner + #{<<"id">> := CalId} = api_test_runner:client_post( + <<"/v1/calendars">>, UserToken, + #{title => <<"WS Test Calendar">>, type => <<"commercial">>}), + ct:pal(" CalId: ~s", [CalId]), + + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, UserToken, + #{title => <<"WS Test Event">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + ct:pal(" EventId: ~s", [EventId]), + + WsUrl = api_test_runner:get_base_ws_url() ++ "/ws", + AdminWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws", + + %% TEST 1: Connect to WebSocket with valid token + ct:pal(" TEST 1: Connect WebSocket with valid token..."), + ct:pal(" URL: ~s", [WsUrl]), + ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]), + case test_ws_connect_debug(WsUrl, UserToken) of + {ok, WS} -> + ct:pal(" OK - Connected"), + + %% TEST 2: Subscribe to calendar updates + ct:pal(" TEST 2: Subscribe to calendar..."), + SubMsg = #{action => <<"subscribe">>, + calendar_id => CalId}, + ct:pal(" Sending: ~p", [SubMsg]), + ok = test_ws_send(WS, SubMsg), + case test_ws_recv(WS) of + {ok, #{<<"status">> := <<"subscribed">>}} -> + ct:pal(" OK - Subscribed"); + {ok, Other} -> + ct:pal(" ERROR: Unexpected response: ~p", [Other]), + error({unexpected_response, Other}); + {error, timeout} -> + ct:pal(" ERROR: Timeout waiting for response"), + error(timeout) + end, + + test_ws_close(WS); + {error, Reason} -> + ct:pal(" ERROR: ~p", [Reason]), + error({websocket_connect_failed, Reason}) + end, + + ct:pal("~n✅ WebSocket API tests passed!"), + + %% ============ ТЕСТЫ АДМИНСКОГО WEBSOCKET ============ + ct:pal("~n=== ADMIN WEBSOCKET TESTS ==="), + + %% TEST 6: Admin WebSocket connection + ct:pal(" TEST 6: Admin WebSocket connect..."), + {ok, AdminWS} = test_ws_connect_debug(AdminWsUrl, AdminToken), + ct:pal(" OK - Admin connected"), + + %% TEST 7: Admin subscribe to reports channel + ct:pal(" TEST 7: Admin subscribe to reports channel..."), + ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, + channel => <<"reports">>}), + {ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS), + ct:pal(" OK - Subscribed to reports"), + + %% TEST 8: Admin subscribe to tickets channel + ct:pal(" TEST 8: Admin subscribe to tickets channel..."), + ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, + channel => <<"tickets">>}), + {ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS), + ct:pal(" OK - Subscribed to tickets"), + + %% TEST 9: Admin receives report notification + ct:pal(" TEST 9: Admin receives report notification..."), + api_test_runner:client_post(<<"/v1/reports">>, UserToken, + #{target_type => <<"event">>, + target_id => EventId, + reason => <<"Test report">>}), + {ok, #{<<"type">> := <<"report_created">>}} = test_ws_recv(AdminWS, 5000), + ct:pal(" OK - Received report notification"), + + %% TEST 10: Admin Ping/Pong + ct:pal(" TEST 10: Admin Ping/Pong..."), + ok = test_ws_send(AdminWS, #{action => <<"ping">>}), + {ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(AdminWS), + ct:pal(" OK - Admin Ping/Pong"), + + %% TEST 11: Admin unsubscribe + ct:pal(" TEST 11: Admin unsubscribe from reports..."), + ok = test_ws_send(AdminWS, #{action => <<"unsubscribe">>, + channel => <<"reports">>}), + {ok, #{<<"status">> := <<"unsubscribed">>}} = test_ws_recv(AdminWS), + ct:pal(" OK - Unsubscribed"), + + test_ws_close(AdminWS), + + %% TEST 12: Admin WebSocket with user token (should fail) + ct:pal(" TEST 12: Admin WS with user token..."), + {error, {403, _}} = test_ws_connect_debug(AdminWsUrl, UserToken), + ct:pal(" OK - Rejected"), + + %% TEST 13: Admin WebSocket with invalid token + ct:pal(" TEST 13: Admin WS with invalid token..."), + Chars = <<"abcdefghijklmnopqrstuvwxyz0123456789">>, + InvalidToken = << <<(binary:at(Chars, rand:uniform(byte_size(Chars)) - 1))>> + || _ <- lists:seq(1, 30) >>, + {error, {401, _}} = test_ws_connect_debug(AdminWsUrl, InvalidToken), + ct:pal(" OK - Rejected"), + + ct:pal("~n✅ Admin WebSocket API tests passed!"), + {?MODULE, ok}. + +%% ============ WebSocket хелперы с отладкой ============ +test_ws_connect_debug(Url, Token) -> + Path = case string:split(Url, "://", trailing) of + [_, Rest] -> + case string:split(Rest, "/", leading) of + [_HostPort, WsPath] -> + "/" ++ WsPath ++ "?token=" ++ binary_to_list(Token); + _ -> + "/ws?token=" ++ binary_to_list(Token) + end; + _ -> + "/ws?token=" ++ binary_to_list(Token) + end, + {ok, Port} = extract_port(Url), + {ok, Host} = extract_host(Url), + Opts = case Port of + 443 -> #{protocols => [http], + transport => tls, + tls_opts => [{verify, verify_none}]}; + _ -> #{protocols => [http]} + end, + ct:pal(" Host: ~s", [Host]), + ct:pal(" Port: ~p", [Port]), + ct:pal(" Path: ~s", [Path]), + {ok, ConnPid} = gun:open(Host, Port, Opts), + {ok, http} = gun:await_up(ConnPid, 5000), + Headers = [{<<"host">>, list_to_binary(Host ++ ":" ++ integer_to_list(Port))}], + StreamRef = gun:ws_upgrade(ConnPid, Path, Headers), + receive + {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> + ct:pal(" WebSocket upgrade OK"), + {ok, ConnPid}; + {gun_response, ConnPid, StreamRef, fin, 401, _} -> + ct:pal(" ERROR: HTTP 401 Unauthorized"), + gun:close(ConnPid), + {error, {401, <<"Invalid token">>}}; + {gun_response, ConnPid, StreamRef, fin, 403, _} -> + ct:pal(" ERROR: HTTP 403 Forbidden"), + gun:close(ConnPid), + {error, {403, <<"Admin access required">>}}; + {gun_response, ConnPid, StreamRef, nofin, 403, _} -> + ct:pal(" ERROR: HTTP 403 Forbidden (nofin)"), + gun:close(ConnPid), + {error, {403, <<"Admin access required">>}}; + {gun_response, ConnPid, StreamRef, fin, Status, _} -> + ct:pal(" ERROR: HTTP ~p", [Status]), + gun:close(ConnPid), + {error, {Status, <<"WebSocket upgrade failed">>}}; + {gun_response, ConnPid, StreamRef, nofin, Status, _} -> + ct:pal(" ERROR: HTTP ~p (nofin)", [Status]), + gun:close(ConnPid), + {error, {Status, <<"WebSocket upgrade failed">>}}; + {gun_error, ConnPid, Reason} -> + ct:pal(" ERROR: ~p", [Reason]), + gun:close(ConnPid), + {error, Reason} + after 5000 -> + ct:pal(" ERROR: Timeout"), + gun:close(ConnPid), + {error, timeout} + end. + +test_ws_send(ConnPid, Data) -> + Msg = jsx:encode(Data), + ct:pal(" Sending: ~s", [Msg]), + case catch gun:ws_send(ConnPid, {text, Msg}) of + ok -> ok; + {'EXIT', {undef, _}} -> + gun:ws_send(ConnPid, fin, {text, Msg}); + Other -> + ct:pal(" ERROR sending: ~p", [Other]), + error({ws_send_failed, Other}) + end. + +test_ws_recv(ConnPid) -> + test_ws_recv(ConnPid, 3000). + +test_ws_recv(ConnPid, Timeout) -> + receive + {gun_ws, ConnPid, _StreamRef, {text, Msg}} -> + ct:pal(" Received (with StreamRef): ~s", [Msg]), + {ok, jsx:decode(Msg, [return_maps])}; + {gun_ws, ConnPid, {text, Msg}} -> + ct:pal(" Received: ~s", [Msg]), + {ok, jsx:decode(Msg, [return_maps])}; + {gun_ws, ConnPid, _StreamRef, Frame} -> + ct:pal(" Received frame: ~p", [Frame]), + {ok, Frame}; + {gun_ws, ConnPid, Frame} -> + ct:pal(" Received: ~p", [Frame]), + {ok, Frame}; + {gun_error, ConnPid, Reason} -> + ct:pal(" ERROR: gun_error ~p", [Reason]), + {error, Reason}; + Other -> + ct:pal(" Received unexpected: ~p", [Other]), + test_ws_recv(ConnPid, Timeout) + after Timeout -> + {error, timeout} + end. + +test_ws_close(ConnPid) -> + gun:close(ConnPid). + +%% ========== URL parsing helpers ========== +extract_port(Url) -> + case string:split(Url, "://", trailing) of + [_, Rest] -> + HostPort = case string:split(Rest, "/", leading) of + [H, _] -> H; + [H] -> H + end, + case string:split(HostPort, ":", trailing) of + [_, PortStr] -> {ok, list_to_integer(PortStr)}; + _ -> case string:split(Rest, "://", trailing) of + [_, R] -> extract_port("https://" ++ R); + _ -> {ok, 80} + end + end; + _ -> {ok, 80} + end. + +extract_host(Url) -> + case string:split(Url, "://", trailing) of + [_, Rest] -> + HostPort = case string:split(Rest, "/", leading) of + [H, _] -> H; + [H] -> H + end, + case string:split(HostPort, ":", trailing) of + [Host, _] -> {ok, Host}; + [Host] -> {ok, Host} + end; + _ -> {ok, "localhost"} + end. \ No newline at end of file diff --git a/test/api/api_admin_tests.erl b/test/api/api_admin_tests.erl deleted file mode 100644 index 71f415f..0000000 --- a/test/api/api_admin_tests.erl +++ /dev/null @@ -1,510 +0,0 @@ --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 for role... "), - SuperadminToken = api_test_runner:login_custom_admin(?FALLBACK_ADMIN_SUPER_EMAIL, ?FALLBACK_ADMIN_SUPER_PASSWORD), - ModeratorToken = api_test_runner:login_custom_admin(?FALLBACK_ADMIN_MODER_EMAIL, ?FALLBACK_ADMIN_MODER_PASSWORD), - SupportToken = api_test_runner:login_custom_admin(?FALLBACK_ADMIN_SUPPORT_EMAIL, ?FALLBACK_ADMIN_SUPPORT_PASSWORD), - - ct:pal(" Admin stats (superadmin)... "), - {ok, {{_, 200, _}, _, StatsResp1}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SuperadminToken)}]}, [], []), - Stats1 = jsx:decode(list_to_binary(StatsResp1), [return_maps]), - ct:pal(" OK (Stats 1: ~p)~n", [Stats1]), - true = map_size(Stats1) > 0, - - ct:pal(" Admin stats (admin)... "), - {ok, {{_, 200, _}, _, StatsResp2}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - Stats2 = jsx:decode(list_to_binary(StatsResp2), [return_maps]), - ct:pal(" OK (Stats 1: ~p)~n", [Stats2]), - true = map_size(Stats2) > 0, - - ct:pal(" Admin stats (moderator)... "), - {ok, {{_, 200, _}, _, StatsResp3}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(ModeratorToken)}]}, [], []), - Stats3 = jsx:decode(list_to_binary(StatsResp3), [return_maps]), - ct:pal(" OK (Stats 1: ~p)~n", [Stats3]), - true = map_size(Stats3) > 0, - - ct:pal(" Admin stats (support)... "), - {ok, {{_, 200, _}, _, StatsResp4}} = httpc:request(get, {AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(SupportToken)}]}, [], []), - Stats4 = jsx:decode(list_to_binary(StatsResp4), [return_maps]), - ct:pal(" OK (Stats 1: ~p)~n", [Stats4]), - true = map_size(Stats4) > 0, - - %% 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, {UserURL ++ "/v1/tickets", [{"Authorization", "Bearer " ++ binary_to_list(UserToken)}], "application/json", TicketBody}, [], []), - #{<<"id">> := TicketId} = jsx:decode(list_to_binary(TicketResp), [return_maps]), - ct:pal(" OK (TicketId: ~p)~n", [TicketId]), - 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(#{action => <<"activate">>, plan => <<"monthly">>, payment_info => #{card => <<"4242">>}}), - {ok, {{_, 201, _}, _, SubResp}} = httpc:request(post, {UserURL ++ "/v1/subscription", [{"Authorization", "Bearer " ++ binary_to_list(UserToken)}], "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"), - - %% ======================================================== - %% Admin Events tests - %% ======================================================== - - FutureDate = api_SUITE:future_date(), - FutureDateStr = binary_to_list(FutureDate), - - %% TEST 28: List all events (GET /v1/admin/events) - ct:pal(" TEST 28: List all events... "), - {ok, {{_, 200, _}, _, EventsListResp}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - EventsList = jsx:decode(list_to_binary(EventsListResp), [return_maps]), - true = is_list(EventsList), - ct:pal(" OK (count: ~p)~n", [length(EventsList)]), - - %% TEST 29: List events with date filters - ct:pal(" TEST 29: List events with date filters... "), - FilterEventsURL = AdminURL ++ "/v1/admin/events?from=" ++ FutureDateStr ++ - "&to=" ++ FutureDateStr, - {ok, {{_, 200, _}, _, FilteredEventsResp}} = - httpc:request(get, {FilterEventsURL, - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - FilteredEventsList = jsx:decode(list_to_binary(FilteredEventsResp), [return_maps]), - true = is_list(FilteredEventsList), - ct:pal(" OK (filtered count: ~p)~n", [length(FilteredEventsList)]), - - %% TEST 30: Get event by ID (GET /v1/admin/events/:id) - ct:pal(" TEST 30: Get event by ID... "), - {ok, {{_, 200, _}, _, EventByIdResp}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events/" ++ binary_to_list(EventId), - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - #{<<"id">> := EventId} = jsx:decode(list_to_binary(EventByIdResp), [return_maps]), - ct:pal(" OK (id: ~s)~n", [binary_to_list(EventId)]), - - %% TEST 31: Update event by ID (PUT /v1/admin/events/:id) - ct:pal(" TEST 31: Update event by ID... "), - UpdateEventBody = jsx:encode(#{ - <<"title">> => <<"Updated by admin">>, - <<"description">> => <<"Admin test update">> - }), - {ok, {{_, 200, _}, _, UpdateEventResp}} = - httpc:request(put, {AdminURL ++ "/v1/admin/events/" ++ binary_to_list(EventId), - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], - "application/json", UpdateEventBody}, - [], []), - #{<<"title">> := <<"Updated by admin">>} = - jsx:decode(list_to_binary(UpdateEventResp), [return_maps]), - ct:pal(" OK~n"), - - %% TEST 32: Delete event by ID (DELETE /v1/admin/events/:id) - ct:pal(" TEST 32: Delete event by ID... "), - {ok, {{_, 200, _}, _, DeleteResp}} = - httpc:request(delete, {AdminURL ++ "/v1/admin/events/" ++ binary_to_list(EventId), - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - #{<<"status">> := <<"deleted">>} = jsx:decode(list_to_binary(DeleteResp), [return_maps]), - ct:pal(" OK (status deleted)~n"), - - %% TEST 33: Method not allowed (POST /v1/admin/events → 405) - ct:pal(" TEST 33: POST method not allowed... "), - {ok, {{_, 405, _}, _, _}} = - httpc:request(post, {AdminURL ++ "/v1/admin/events", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], - "application/json", <<"{}">>}, - [], []), - ct:pal("OK~n"), - - %% ======================================================== - %% Extended Admin Events Search & Filter Tests - %% ======================================================== - - %% ── Подготовка изолированных данных ── - ct:pal(" Preparing isolated search test data... "), - UserToken = api_test_runner:get_user_token(), - SearchCalId = api_test_runner:create_calendar(UserToken, #{title => <<"SearchTestCal">>}), - SearchCalIdStr = binary_to_list(SearchCalId), - AlphaId = api_test_runner:create_event(UserToken, SearchCalId, #{ - title => <<"Test Event Alpha">>, - start_time => api_SUITE:future_date(), - duration => 60 - }), - BetaId = api_test_runner:create_event(UserToken, SearchCalId, #{ - title => <<"Beta Event">>, - start_time => api_SUITE:future_date(), - duration => 60 - }), - _AlphaConfId = api_test_runner:create_event(UserToken, SearchCalId, #{ - title => <<"Alpha Conference">>, - start_time => api_SUITE:future_date(), - duration => 60 - }), - % Отменяем BetaId через административный эндпоинт (PUT /v1/admin/events/:id) - ct:pal(" Cancelling Beta Event (admin)... "), - {ok, {{_, 200, _}, _, _}} = - httpc:request(put, {AdminURL ++ "/v1/admin/events/" ++ binary_to_list(BetaId), - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], - "application/json", jsx:encode(#{<<"status">> => <<"cancelled">>})}, [], []), - ct:pal("OK~n"), - - %% ── TEST 34: Filter by status=active ── - ct:pal(" TEST 34: Filter events by status=active... "), - {ok, {{_, 200, _}, _, Body34}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=active", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events34 = jsx:decode(list_to_binary(Body34), [return_maps]), - ct:pal("DEBUG: events34 count = ~p", [length(Events34)]), - ct:pal("DEBUG: events34 = ~p", [Events34]), - true = (length(Events34) >= 2), - Ids34 = [maps:get(<<"id">>, E) || E <- Events34], - ct:pal("OK (count: ~p, ids: ~p)~n", [length(Events34), Ids34]), - - %% ── TEST 35: Filter by status=cancelled ── - ct:pal(" TEST 35: Filter events by status=cancelled... "), - {ok, {{_, 200, _}, _, Body35}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=cancelled", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events35 = jsx:decode(list_to_binary(Body35), [return_maps]), - ct:pal("DEBUG: Events35 count = ~p", [length(Events35)]), - ct:pal("DEBUG: Events35 = ~p", [Events35]), - true = (length(Events35) >= 1), - ct:pal("OK (count: ~p)~n", [length(Events35)]), - - %% ── TEST 36: Filter by status=all ── - ct:pal(" TEST 36: Filter events by status=all... "), - {ok, {{_, 200, _}, _, Body36}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events36 = jsx:decode(list_to_binary(Body36), [return_maps]), - ct:pal("DEBUG: Events36 count = ~p", [length(Events36)]), - ct:pal("DEBUG: Events36 = ~p", [Events36]), - true = (length(Events36) >= 3), - ct:pal("OK (count: ~p)~n", [length(Events36)]), - - %% ── TEST 37: Filter by calendar_id ── - ct:pal(" TEST 37: Filter by calendar_id... "), - {ok, {{_, 200, _}, _, Body37}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr, - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events37 = jsx:decode(list_to_binary(Body37), [return_maps]), - ct:pal("DEBUG: Events37 count = ~p", [length(Events37)]), - ct:pal("DEBUG: Events37 = ~p", [Events37]), - true = (length(Events37) >= 3), - ct:pal("OK (count: ~p)~n", [length(Events37)]), - - %% ── TEST 38: Exact title match ── - ct:pal(" TEST 38: Exact title match... "), - {ok, {{_, 200, _}, _, Body38}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&title=Test%20Event%20Alpha", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events38 = jsx:decode(list_to_binary(Body38), [return_maps]), - ct:pal("DEBUG: Events38 count = ~p", [length(Events38)]), - ct:pal("DEBUG: Events38 = ~p", [Events38]), - 1 = length(Events38), - #{<<"id">> := AlphaId} = hd(Events38), - ct:pal("OK~n"), - - %% ── TEST 39: Substring search (q) ── - ct:pal(" TEST 39: Substring search (q=Alpha)... "), - {ok, {{_, 200, _}, _, Body39}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&q=Alpha", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events39 = jsx:decode(list_to_binary(Body39), [return_maps]), - ct:pal("DEBUG: Events39 count = ~p", [length(Events39)]), - ct:pal("DEBUG: Events39 = ~p", [Events39]), - true = (length(Events39) >= 2), - Titles39 = [maps:get(<<"title">>, E) || E <- Events39], - ct:pal("OK (count: ~p, titles: ~p)~n", [length(Events39), Titles39]), - - %% ── TEST 40: Combined filters (calendar_id + status) ── - ct:pal(" TEST 40: Combined filters (calendar+status)... "), - {ok, {{_, 200, _}, _, Body40}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=active", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events40 = jsx:decode(list_to_binary(Body40), [return_maps]), - ct:pal("DEBUG: Events40 count = ~p", [length(Events40)]), - ct:pal("DEBUG: Events40 = ~p", [Events40]), - true = (length(Events40) >= 2), - ct:pal("OK (count: ~p)~n", [length(Events40)]), - - %% ── TEST 41: Pagination (limit & offset) ── - ct:pal(" TEST 41: Pagination... "), - {ok, {{_, 200, _}, Headers41a, Body41a}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&limit=2&offset=0&sort=title&order=asc", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events41a = jsx:decode(list_to_binary(Body41a), [return_maps]), - ct:pal("DEBUG: Events41a count = ~p", [length(Events41a)]), - ct:pal("DEBUG: Events41a = ~p", [Events41a]), - 2 = length(Events41a), - {"content-range", ContentRange41a} = lists:keyfind("content-range", 1, Headers41a), - ct:pal("page1: ~s; ", [ContentRange41a]), - {ok, {{_, 200, _}, Headers41b, Body41b}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&limit=2&offset=2&sort=title&order=asc", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events41b = jsx:decode(list_to_binary(Body41b), [return_maps]), - ct:pal("DEBUG: Events41b count = ~p", [length(Events41b)]), - ct:pal("DEBUG: Events41b = ~p", [Events41b]), - 1 = length(Events41b), - {"content-range", ContentRange41b} = lists:keyfind("content-range", 1, Headers41b), - ct:pal("page2: ~s~n", [ContentRange41b]), - - %% ── TEST 42: Sorting (order=asc) ── - ct:pal(" TEST 42: Sorting by title ascending... "), - {ok, {{_, 200, _}, _, Body42}} = - httpc:request(get, {AdminURL ++ "/v1/admin/events?calendar_id=" ++ SearchCalIdStr ++ "&status=all&sort=title&order=asc", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, - [], []), - Events42 = jsx:decode(list_to_binary(Body42), [return_maps]), - SortedTitles = [maps:get(<<"title">>, E) || E <- Events42], - SortedTitles = lists:sort(SortedTitles), - ct:pal("OK (titles: ~p)~n", [SortedTitles]), - - ct:pal("~n✅ Admin API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_auth_tests.erl b/test/api/api_auth_tests.erl deleted file mode 100644 index c6ceadd..0000000 --- a/test/api/api_auth_tests.erl +++ /dev/null @@ -1,64 +0,0 @@ --module(api_auth_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing authentication API...~n"), - - Email = api_test_runner:unique_email(<<"auth_test">>), - Password = <<"test123">>, - - % TEST 1: Register - io:format(" TEST 1: Register... "), - RegBody = #{email => Email, password => Password}, - Token = api_test_runner:extract_json( - api_test_runner:http_post("/v1/register", RegBody), <<"token">>), - io:format("OK~n"), - - % TEST 2: Register with existing email - io:format(" TEST 2: Register duplicate... "), - {ok, {{_, 409, _}, _, _}} = api_test_runner:http_post("/v1/register", RegBody), - io:format("OK~n"), - - % TEST 3: Login with correct credentials - io:format(" TEST 3: Login... "), - LoginBody = #{email => Email, password => Password}, - RefreshToken = api_test_runner:extract_json( - api_test_runner:http_post("/v1/login", LoginBody), <<"refresh_token">>), - io:format("OK~n"), - - % TEST 4: Login with wrong password - io:format(" TEST 4: Login wrong password... "), - {ok, {{_, 401, _}, _, _}} = api_test_runner:http_post("/v1/login", #{email => Email, password => <<"wrong">>}), - io:format("OK~n"), - - % TEST 5: Get profile with valid token - io:format(" TEST 5: Get profile... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/user/me", Token), - io:format("OK~n"), - - % TEST 6: Get profile with invalid token - io:format(" TEST 6: Get profile invalid token... "), - {ok, {{_, 401, _}, _, _}} = api_test_runner:http_get("/v1/user/me", <<"invalid">>), - io:format("OK~n"), - - % TEST 7: Refresh token - io:format(" TEST 7: Refresh token... "), - RefreshBody = #{refresh_token => RefreshToken}, - NewToken = api_test_runner:extract_json( - api_test_runner:http_post("/v1/refresh", RefreshBody), <<"token">>), - io:format("OK~n"), - - % TEST 8: Refresh with used token (should fail) - io:format(" TEST 8: Refresh with used token... "), - {ok, {{_, 401, _}, _, _}} = api_test_runner:http_post("/v1/refresh", RefreshBody), - io:format("OK~n"), - - % TEST 9: Use new token - io:format(" TEST 9: Use new token... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/user/me", NewToken), - io:format("OK~n"), - - io:format("~n✅ Authentication API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_booking_tests.erl b/test/api/api_booking_tests.erl deleted file mode 100644 index c9b2234..0000000 --- a/test/api/api_booking_tests.erl +++ /dev/null @@ -1,83 +0,0 @@ --module(api_booking_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing booking API...~n"), - - OwnerEmail = api_test_runner:unique_email(<<"book_owner">>), - ParticipantEmail = api_test_runner:unique_email(<<"book_part">>), - - OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"owner123">>), - ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"part123">>), - - % Используем COMMERCIAL календари - AutoCalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"Auto">>, type => <<"commercial">>, confirmation => <<"auto">>}, OwnerToken), <<"id">>), - ManualCalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"Manual">>, type => <<"commercial">>, confirmation => <<"manual">>}, OwnerToken), <<"id">>), - - % Создаём события - AutoEventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(AutoCalId) ++ "/events", - #{title => <<"Auto Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, OwnerToken), <<"id">>), - ManualEventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(ManualCalId) ++ "/events", - #{title => <<"Manual Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, OwnerToken), <<"id">>), - - % TEST 1: Auto booking - io:format(" TEST 1: Auto booking... "), - AutoBookingId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/events/" ++ binary_to_list(AutoEventId) ++ "/bookings", #{}, ParticipantToken), <<"id">>), - timer:sleep(200), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/bookings/" ++ binary_to_list(AutoBookingId), ParticipantToken), - io:format("OK~n"), - - % TEST 2: Manual booking - io:format(" TEST 2: Manual booking... "), - ManualBookingId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/events/" ++ binary_to_list(ManualEventId) ++ "/bookings", #{}, ParticipantToken), <<"id">>), - io:format("OK~n"), - - % TEST 3: Duplicate booking - io:format(" TEST 3: Duplicate booking... "), - {ok, {{_, 409, _}, _, _}} = api_test_runner:http_post("/v1/events/" ++ binary_to_list(AutoEventId) ++ "/bookings", #{}, ParticipantToken), - io:format("OK~n"), - - % TEST 4: Owner confirms booking - io:format(" TEST 4: Owner confirms booking... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/bookings/" ++ binary_to_list(ManualBookingId), - #{action => <<"confirm">>}, OwnerToken), - io:format("OK~n"), - - % TEST 5: List event bookings - io:format(" TEST 5: List event bookings... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/events/" ++ binary_to_list(ManualEventId) ++ "/bookings", OwnerToken), - io:format("OK~n"), - - % TEST 6: List user bookings - io:format(" TEST 6: List user bookings... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/user/bookings", ParticipantToken), - io:format("OK~n"), - - % TEST 7: Cancel booking - io:format(" TEST 7: Cancel booking... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/bookings/" ++ binary_to_list(AutoBookingId), ParticipantToken), - io:format("OK~n"), - - % TEST 8: Owner declines booking (новое событие) - io:format(" TEST 8: Owner declines booking... "), - NewEventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(ManualCalId) ++ "/events", - #{title => <<"Decline Event">>, start_time => <<"2026-06-02T10:00:00Z">>, duration => 60}, OwnerToken), <<"id">>), - NewBookingId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/events/" ++ binary_to_list(NewEventId) ++ "/bookings", #{}, ParticipantToken), <<"id">>), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/bookings/" ++ binary_to_list(NewBookingId), - #{action => <<"decline">>}, OwnerToken), - io:format("OK~n"), - - io:format("~n✅ Booking API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_calendar_tests.erl b/test/api/api_calendar_tests.erl deleted file mode 100644 index 1747e7f..0000000 --- a/test/api/api_calendar_tests.erl +++ /dev/null @@ -1,61 +0,0 @@ --module(api_calendar_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing calendar API...~n"), - - OwnerEmail = api_test_runner:unique_email(<<"cal_owner">>), - OtherEmail = api_test_runner:unique_email(<<"cal_other">>), - - OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"owner123">>), - OtherToken = api_test_runner:register_and_login(OtherEmail, <<"other123">>), - - % TEST 1: Create personal calendar - io:format(" TEST 1: Create personal calendar... "), - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"Personal">>, type => <<"personal">>}, OwnerToken), <<"id">>), - io:format("OK~n"), - - % TEST 2: Create commercial calendar - io:format(" TEST 2: Create commercial calendar... "), - CommId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"Commercial">>, type => <<"commercial">>}, OwnerToken), <<"id">>), - io:format("OK~n"), - - % TEST 3: List calendars - io:format(" TEST 3: List calendars... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/calendars", OwnerToken), - io:format("OK~n"), - - % TEST 4: Get personal calendar (owner) - io:format(" TEST 4: Get personal calendar... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/calendars/" ++ binary_to_list(CalId), OwnerToken), - io:format("OK~n"), - - % TEST 5: Get personal calendar (other - denied) - io:format(" TEST 5: Get personal calendar (other)... "), - {ok, {{_, 403, _}, _, _}} = api_test_runner:http_get("/v1/calendars/" ++ binary_to_list(CalId), OtherToken), - io:format("OK~n"), - - % TEST 6: Get commercial calendar (other - allowed) - io:format(" TEST 6: Get commercial calendar (other)... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/calendars/" ++ binary_to_list(CommId), OtherToken), - io:format("OK~n"), - - % TEST 7: Update calendar - io:format(" TEST 7: Update calendar... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/calendars/" ++ binary_to_list(CalId), - #{title => <<"Updated">>}, OwnerToken), - io:format("OK~n"), - - % TEST 8: Delete calendar - io:format(" TEST 8: Delete calendar... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/calendars/" ++ binary_to_list(CalId), OwnerToken), - io:format("OK~n"), - - io:format("~n✅ Calendar API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_event_tests.erl b/test/api/api_event_tests.erl deleted file mode 100644 index c5f36b8..0000000 --- a/test/api/api_event_tests.erl +++ /dev/null @@ -1,70 +0,0 @@ --module(api_event_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing event API...~n"), - - OwnerEmail = api_test_runner:unique_email(<<"ev_owner">>), - OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"owner123">>), - - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", #{title => <<"Test">>}, OwnerToken), <<"id">>), - - % TEST 1: Create single event - io:format(" TEST 1: Create single event... "), - EventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Test Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, OwnerToken), <<"id">>), - io:format("OK~n"), - - % TEST 2: Create event in past (should fail) - io:format(" TEST 2: Create past event... "), - {ok, {{_, 400, _}, _, _}} = api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Past Event">>, start_time => <<"2020-01-01T10:00:00Z">>, duration => 60}, OwnerToken), - io:format("OK~n"), - - % TEST 3: Create recurring event - io:format(" TEST 3: Create recurring event... "), - RecurringId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Weekly Meeting">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60, - recurrence => #{freq => <<"WEEKLY">>, interval => 1}}, OwnerToken), <<"id">>), - io:format("OK~n"), - - % TEST 4: List events - io:format(" TEST 4: List events... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", OwnerToken), - io:format("OK~n"), - - % TEST 5: Get event - io:format(" TEST 5: Get event... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/events/" ++ binary_to_list(EventId), OwnerToken), - io:format("OK~n"), - - % TEST 6: Update event - io:format(" TEST 6: Update event... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/events/" ++ binary_to_list(EventId), - #{title => <<"Updated Event">>}, OwnerToken), - io:format("OK~n"), - - % TEST 7: Get occurrences - io:format(" TEST 7: Get occurrences... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get( - "/v1/events/" ++ binary_to_list(RecurringId) ++ "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z", OwnerToken), - io:format("OK~n"), - - % TEST 8: Cancel occurrence - io:format(" TEST 8: Cancel occurrence... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete( - "/v1/events/" ++ binary_to_list(RecurringId) ++ "/occurrences/2026-06-08T10:00:00Z", OwnerToken), - io:format("OK~n"), - - % TEST 9: Delete event - io:format(" TEST 9: Delete event... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/events/" ++ binary_to_list(EventId), OwnerToken), - io:format("OK~n"), - - io:format("~n✅ Event API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_moderation_tests.erl b/test/api/api_moderation_tests.erl deleted file mode 100644 index 94dfcf3..0000000 --- a/test/api/api_moderation_tests.erl +++ /dev/null @@ -1,72 +0,0 @@ --module(api_moderation_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). --define(ADMIN_BASE_URL, api_test_runner:get_admin_url()). - -test() -> - io:format("Testing moderation API...~n"), - AdminToken = api_test_runner:get_admin_token(), - UserToken = api_test_runner:get_user_token(), - - %% Создаём календарь и событие - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken), - <<"id">>), - EventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Mod Event">>, - start_time => <<"2026-06-01T10:00:00Z">>, - duration => 60}, - UserToken), - <<"id">>), - - %% TEST 1: Create report - io:format(" TEST 1: Create report... "), - ReportId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/reports", - #{target_type => <<"event">>, - target_id => EventId, - reason => <<"Inappropriate">>}, - UserToken), - <<"id">>), - io:format("OK~n"), - - %% TEST 2: Admin views reports - io:format(" TEST 2: Admin views reports... "), - {ok, {{_, 200, _}, _, _}} = httpc:request(get, - {?ADMIN_BASE_URL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format("OK~n"), - - %% TEST 3: Admin resolves report с reason - io:format(" TEST 3: Admin resolves report... "), - {ok, {{_, 200, _}, _, _}} = httpc:request(put, - {?ADMIN_BASE_URL ++ "/v1/admin/reports/" ++ binary_to_list(ReportId), - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], - "application/json", - jsx:encode(#{status => <<"reviewed">>, reason => <<"Resolved by moderator">>})}, [], []), - io:format("OK~n"), - - %% TEST 4: Add banned word - io:format(" TEST 4: Add banned word... "), - {ok, {{_, 201, _}, _, _}} = httpc:request(post, - {?ADMIN_BASE_URL ++ "/v1/admin/banned-words", - [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], - "application/json", - jsx:encode(#{<<"word">> => <<"badword">>})}, [], []), - io:format("OK~n"), - - %% TEST 5: List banned words - io:format(" TEST 5: List banned words... "), - {ok, {{_, 200, _}, _, _}} = httpc:request(get, - {?ADMIN_BASE_URL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format("OK~n"), - - %% TEST 6: Remove banned word - io:format(" TEST 6: Remove banned word... "), - {ok, {{_, 200, _}, _, _}} = httpc:request(delete, - {?ADMIN_BASE_URL ++ "/v1/admin/banned-words/badword", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), - io:format("OK~n"), - - io:format("~n✅ Moderation API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_reviews_tests.erl b/test/api/api_reviews_tests.erl deleted file mode 100644 index f54c0e9..0000000 --- a/test/api/api_reviews_tests.erl +++ /dev/null @@ -1,58 +0,0 @@ --module(api_reviews_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing reviews API...~n"), - - OwnerEmail = api_test_runner:unique_email(<<"rev_owner">>), - ParticipantEmail = api_test_runner:unique_email(<<"rev_part">>), - - OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"owner123">>), - ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"part123">>), - - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", #{title => <<"Review Cal">>}, OwnerToken), <<"id">>), - - EventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Review Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, OwnerToken), <<"id">>), - - % Создаём и подтверждаем бронирование - BookingId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/events/" ++ binary_to_list(EventId) ++ "/bookings", #{}, ParticipantToken), <<"id">>), - api_test_runner:http_put("/v1/bookings/" ++ binary_to_list(BookingId), #{action => <<"confirm">>}, OwnerToken), - - % TEST 1: Create review - io:format(" TEST 1: Create review... "), - ReviewId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/reviews", - #{target_type => <<"event">>, target_id => EventId, rating => 5, comment => <<"Great!">>}, - ParticipantToken), <<"id">>), - io:format("OK~n"), - - % TEST 2: Duplicate review - io:format(" TEST 2: Duplicate review... "), - {ok, {{_, 409, _}, _, _}} = api_test_runner:http_post("/v1/reviews", - #{target_type => <<"event">>, target_id => EventId, rating => 4, comment => <<"Again">>}, ParticipantToken), - io:format("OK~n"), - - % TEST 3: Get event reviews - io:format(" TEST 3: Get event reviews... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/reviews?target_type=event&target_id=" ++ binary_to_list(EventId), ParticipantToken), - io:format("OK~n"), - - % TEST 4: Update review - io:format(" TEST 4: Update review... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/reviews/" ++ binary_to_list(ReviewId), - #{rating => 4}, ParticipantToken), - io:format("OK~n"), - - % TEST 5: Delete review - io:format(" TEST 5: Delete review... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/reviews/" ++ binary_to_list(ReviewId), ParticipantToken), - io:format("OK~n"), - - io:format("~n✅ Reviews API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_search_tests.erl b/test/api/api_search_tests.erl deleted file mode 100644 index bde21c9..0000000 --- a/test/api/api_search_tests.erl +++ /dev/null @@ -1,54 +0,0 @@ --module(api_search_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing search API...~n"), - - OwnerEmail = api_test_runner:unique_email(<<"search_owner">>), - OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"owner123">>), - - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", #{title => <<"Search Cal">>}, OwnerToken), <<"id">>), - - % Создаём события с тегами - api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"Python Workshop">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60, - tags => [<<"python">>, <<"workshop">>]}, OwnerToken), <<"id">>), - - api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"JavaScript">>, start_time => <<"2026-06-15T10:00:00Z">>, duration => 60, - tags => [<<"javascript">>]}, OwnerToken), <<"id">>), - - timer:sleep(500), - - % TEST 1: Text search - io:format(" TEST 1: Text search... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/search?type=event&q=Python", OwnerToken), - io:format("OK~n"), - - % TEST 2: Tag search - io:format(" TEST 2: Tag search... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/search?type=event&tags=workshop", OwnerToken), - io:format("OK~n"), - - % TEST 3: Combined search - io:format(" TEST 3: Combined search... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/search?type=event&q=Python&tags=workshop", OwnerToken), - io:format("OK~n"), - - % TEST 4: Pagination - io:format(" TEST 4: Pagination... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/search?type=event&limit=2", OwnerToken), - io:format("OK~n"), - - % TEST 5: Search calendars - io:format(" TEST 5: Search calendars... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/search?type=calendar", OwnerToken), - io:format("OK~n"), - - io:format("~n✅ Search API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_subscription_tests.erl b/test/api/api_subscription_tests.erl deleted file mode 100644 index 7407dce..0000000 --- a/test/api/api_subscription_tests.erl +++ /dev/null @@ -1,36 +0,0 @@ --module(api_subscription_tests). --export([test/0]). - --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing subscription API...~n"), - - UserEmail = api_test_runner:unique_email(<<"sub_user">>), - UserToken = api_test_runner:register_and_login(UserEmail, <<"user123">>), - - % TEST 1: Get subscription (free) - io:format(" TEST 1: Get subscription (free)... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/subscription", UserToken), - io:format("OK~n"), - - % TEST 2: Create commercial calendar (auto-activates trial) - io:format(" TEST 2: Create commercial calendar... "), - api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"Commercial">>, type => <<"commercial">>}, UserToken), <<"id">>), - io:format("OK~n"), - - % TEST 3: Get subscription (trial) - io:format(" TEST 3: Get subscription (trial)... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/subscription", UserToken), - io:format("OK~n"), - - % TEST 4: Activate paid subscription - io:format(" TEST 4: Activate paid subscription... "), - {ok, {{_, 201, _}, _, _}} = api_test_runner:http_post("/v1/subscription", - #{action => <<"activate">>, plan => <<"monthly">>, payment_info => #{card => <<"4242">>}}, UserToken), - io:format("OK~n"), - - io:format("~n✅ Subscription API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/api_test_runner.erl b/test/api/api_test_runner.erl index 53b2ca5..bf31203 100644 --- a/test/api/api_test_runner.erl +++ b/test/api/api_test_runner.erl @@ -1,277 +1,302 @@ +%%%------------------------------------------------------------------- +%%% @doc Централизованный модуль для запуска API-тестов. +%%% Предоставляет функции для выполнения HTTP-запросов +%%% к административному и клиентскому API с автоматическим +%%% логированием, проверкой статусов и конфигурацией +%%% через стандартные переменные окружения. +%%% @end +%%%------------------------------------------------------------------- -module(api_test_runner). --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]). --export([unique_email/1, register_and_login/2, create_calendar/2, create_event/3]). --export([get_admin_token/0, get_admin_id/0, get_user_token/0, get_user_id/0, get_admin_url/0, get_base_url/0, get_admin_ws_url/0, get_base_ws_url/0, login_admin/2, login_custom_admin/2]). --export([wait_for_server/0]). --export([format_datetime/1]). --define(BASE_URL, base_url()). --define(ADMIN_URL, admin_base_url()). +-export([ + get_admin_url/0, + get_base_url/0, + get_base_ws_url/0, + get_admin_ws_url/0, + get_admin_token/0, + get_superadmin_token/0, + get_moderator_token/0, + get_support_token/0, + get_user_token/0, + unique_email/1, + future_date/0, + register_and_login/2, + create_calendar/2, + create_event/3 +]). +-export([ + admin_request/3, + admin_request/4, + client_request/3, + client_request/4 +]). +-export([ + admin_get/2, + admin_post/3, + admin_put/3, + admin_delete/2, + client_get/2, + client_post/3, + client_put/3, + client_delete/2, + admin_patch/3]). -%% Учётные данные по умолчанию (используются в локальном режиме, если словарь пуст) --define(FALLBACK_ADMIN_EMAIL, <<"admin@eventhub.local">>). --define(FALLBACK_ADMIN_PASSWORD, <<"123456">>). --define(USER_EMAIL, <<"global_user@test.com">>). --define(USER_PASSWORD, <<"user123">>). +%%%=================================================================== +%%% Конфигурация окружения (CT_MODE, ...) +%%%=================================================================== -%% ------------------------------------------------------------------ -%% Выбор базовых URL в зависимости от режима запуска -%% ------------------------------------------------------------------ -base_url() -> - case os:getenv("CT_MODE", "local") of +-spec ct_mode() -> string(). +ct_mode() -> + os:getenv("CT_MODE", "local"). + +-spec get_base_url() -> string(). +get_base_url() -> + case ct_mode() of "remote" -> os:getenv("API_HOST", "http://localhost:8080"); _ -> "http://localhost:8080" end. -base_ws_url() -> - case os:getenv("CT_MODE", "local") of - "remote" -> os:getenv("WS_HOST", "ws://localhost:8081"); - _ -> "ws://localhost:8081" - end. - -admin_base_url() -> - case os:getenv("CT_MODE", "local") of +-spec get_admin_url() -> string(). +get_admin_url() -> + case ct_mode() of "remote" -> os:getenv("ADMIN_API_HOST", "http://localhost:8445"); _ -> "http://localhost:8445" end. -admin_ws_url() -> - case os:getenv("CT_MODE", "local") of +-spec get_base_ws_url() -> string(). +get_base_ws_url() -> + case ct_mode() of + "remote" -> os:getenv("WS_HOST", "ws://localhost:8081"); + _ -> "ws://localhost:8081" + end. + +-spec get_admin_ws_url() -> string(). +get_admin_ws_url() -> + case ct_mode() of "remote" -> os:getenv("ADMIN_WS_HOST", "ws://localhost:8446"); _ -> "ws://localhost:8446" end. -%% ------------------------------------------------------------------ -%% Инициализация глобальных тестовых пользователей -%% ------------------------------------------------------------------ -init_global_urls() -> - put(admin_url, admin_base_url()), - put(admin_ws_url, admin_ws_url()), - put(base_url, base_url()), - put(base_ws_url, base_ws_url()). +%%%=================================================================== +%%% Учётные данные администраторов (из переменных окружения) +%%%=================================================================== -init_global_users() -> - case get(admin_token) of - undefined -> - ct:pal("~n=== Initializing global test users ===~n"), +-spec admin_super_email() -> binary(). +admin_super_email() -> + list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")). - %% 1. Администратор - AdminEmail = get(admin_super_email), - AdminPassword = get(admin_super_password), - AdminToken = - if - AdminEmail =/= undefined, AdminPassword =/= undefined -> - %% Учётные данные переданы из api_SUITE (remote‑режим) – просто логинимся - login_admin(AdminEmail, AdminPassword); - true -> - %% Локальный режим: админы уже есть, логинимся под суперадмином - login_admin(?FALLBACK_ADMIN_EMAIL, ?FALLBACK_ADMIN_PASSWORD) - end, +-spec admin_super_password() -> binary(). +admin_super_password() -> + list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")). - %% Получаем ID администратора через /v1/admin/me - MeUrl = ?ADMIN_URL ++ "/v1/admin/me", - {ok, {{_, 200, _}, _, MeBody}} = httpc:request(get, - {MeUrl, [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, ssl_opts(), []), - #{<<"id">> := AdminId} = jsx:decode(list_to_binary(MeBody), [return_maps]), +-spec admin_email() -> binary(). +admin_email() -> + list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")). - put(admin_token, AdminToken), - put(admin_id, AdminId), +-spec admin_password() -> binary(). +admin_password() -> + list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")). - %% 2. Обычный пользователь - UserToken = register_and_login(?USER_EMAIL, ?USER_PASSWORD), - {ok, {{_, 200, _}, _, UserMeBody}} = http_get("/v1/user/me", UserToken), - #{<<"id">> := UserId} = jsx:decode(list_to_binary(UserMeBody), [return_maps]), +-spec admin_moder_email() -> binary(). +admin_moder_email() -> + list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")). - put(user_token, UserToken), - put(user_id, UserId), +-spec admin_moder_password() -> binary(). +admin_moder_password() -> + list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")). - ct:pal("Admin ID: ~s, User ID: ~s~n", [AdminId, UserId]), - ct:pal("=== Global users initialized ===~n~n"), - ok; +-spec admin_support_email() -> binary(). +admin_support_email() -> + list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")). + +-spec admin_support_password() -> binary(). +admin_support_password() -> + list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")). + +%%%=================================================================== +%%% Получение токенов (с кешированием в persistent_term) +%%%=================================================================== + +-spec get_admin_token() -> binary(). +get_admin_token() -> + get_or_login(admin, admin_email(), admin_password()). + +-spec get_superadmin_token() -> binary(). +get_superadmin_token() -> + get_or_login(superadmin, admin_super_email(), admin_super_password()). + +-spec get_moderator_token() -> binary(). +get_moderator_token() -> + get_or_login(moderator, admin_moder_email(), admin_moder_password()). + +-spec get_support_token() -> binary(). +get_support_token() -> + get_or_login(support, admin_support_email(), admin_support_password()). + +-spec get_or_login(atom(), binary(), binary()) -> binary(). +get_or_login(Role, Email, Password) -> + Key = {?MODULE, admin_token, Role}, + case persistent_term:get(Key, undefined) of + Token when is_binary(Token) -> Token; _ -> - ct:pal("Global users already initialized.~n"), - ok + Token = login_admin(Email, Password), + persistent_term:put(Key, Token), + timer:apply_after(5 * 60 * 1000, fun() -> persistent_term:erase(Key) end), + Token end. -%% ------------------------------------------------------------------ -%% Вход администратора (используется, когда учётки уже известны) -%% ------------------------------------------------------------------ -login_admin(Email, Password) -> - ct:pal("Admin url: ~s~n", [?ADMIN_URL]), - ct:pal("Admin: ~s, password: ~s~n", [Email, Password]), - LoginBody = jsx:encode(#{<<"email">> => Email, <<"password">> => Password}), - ct:pal("url: ~s, body: ~s~n", [?ADMIN_URL ++ "/v1/admin/login", LoginBody]), - {ok, {{_, _, _}, _, LoginResp}} = httpc:request(post, - {?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, ssl_opts(), []), - ct:pal("LoginResp: ~s~n", [LoginResp]), - #{<<"token">> := Token} = jsx:decode(list_to_binary(LoginResp), [return_maps]), +%% @doc Возвращает JWT-токен обычного пользователя. +%% При каждом вызове создаёт нового уникального пользователя, +%% чтобы избежать конфликтов состояния в тестах. +-spec get_user_token() -> binary(). +get_user_token() -> + Email = unique_email(<<"testuser">>), + register_and_login(Email, <<"testpass">>). + +%%%=================================================================== +%%% HTTP-клиент (логирование, заголовки) +%%%=================================================================== + +-spec admin_request(atom(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. +admin_request(Method, Path, Token) -> + admin_request(Method, Path, Token, <<>>). + +-spec admin_request(atom(), binary(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. +admin_request(Method, Path, Token, Body) -> + request(get_admin_url(), Method, Path, Token, Body, "ADMIN"). + +-spec client_request(atom(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. +client_request(Method, Path, Token) -> + client_request(Method, Path, Token, <<>>). + +-spec client_request(atom(), binary(), binary(), binary()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. +client_request(Method, Path, Token, Body) -> + request(get_base_url(), Method, Path, Token, Body, "CLIENT"). + +%%%=================================================================== +%%% Внутренняя реализация HTTP-запроса +%%%=================================================================== + +-spec request(string(), atom(), binary(), binary(), binary(), string()) -> {ok, integer(), proplists:proplist(), binary()} | {error, term()}. +request(BaseUrl, Method, Path, Token, Body, Prefix) -> + URL = BaseUrl ++ binary_to_list(Path), + Headers0 = [], + Headers = case Token of + <<>> -> Headers0; % пустой токен – не добавляем Authorization + _ -> [{"Authorization", "Bearer " ++ binary_to_list(Token)}] + end, + ct:pal("~s REQUEST: ~s ~s", [Prefix, Method, URL]), + RequestArg = case Method of + get -> {URL, Headers}; + delete -> {URL, Headers}; + _ -> {URL, Headers, "application/json", Body} + end, + Response = httpc:request(Method, RequestArg, [], []), + case Response of + {ok, {{_, Status, _}, RespHeaders, RespBody}} -> + ct:pal("~s RESPONSE: ~p ~s", [Prefix, Status, RespBody]), + {ok, Status, RespHeaders, RespBody}; + _ -> + ct:pal("~s REQUEST ERROR: ~p", [Prefix, Response]), + {error, Response} + end. + +%%%=================================================================== +%%% Высокоуровневые обёртки (GET/POST/PUT/DELETE) +%%%=================================================================== + +-spec admin_get(binary(), binary()) -> jsx:json_term(). +admin_get(Path, Token) -> + {ok, 200, _, Body} = admin_request(get, Path, Token), + jsx:decode(list_to_binary(Body), [return_maps]). + +-spec admin_post(binary(), binary(), map()) -> jsx:json_term(). +admin_post(Path, Token, BodyMap) -> + Body = jsx:encode(BodyMap), + {ok, 201, _, RespBody} = admin_request(post, Path, Token, Body), + jsx:decode(list_to_binary(RespBody), [return_maps]). + +-spec admin_put(binary(), binary(), map()) -> jsx:json_term(). +admin_put(Path, Token, BodyMap) -> + Body = jsx:encode(BodyMap), + {ok, 200, _, RespBody} = admin_request(put, Path, Token, Body), + jsx:decode(list_to_binary(RespBody), [return_maps]). + +%% В api_test_runner.erl добавить в блок высокоуровневых обёрток: +-spec admin_patch(binary(), binary(), [map()]) -> jsx:json_term(). +admin_patch(Path, Token, BodyList) -> + Body = jsx:encode(BodyList), + {ok, 200, _, RespBody} = admin_request(patch, Path, Token, Body), + jsx:decode(list_to_binary(RespBody), [return_maps]). + +-spec admin_delete(binary(), binary()) -> jsx:json_term(). +admin_delete(Path, Token) -> + {ok, 200, _, Body} = admin_request(delete, Path, Token), + jsx:decode(list_to_binary(Body), [return_maps]). + +-spec client_get(binary(), binary()) -> jsx:json_term(). +client_get(Path, Token) -> + {ok, 200, _, Body} = client_request(get, Path, Token), + jsx:decode(list_to_binary(Body), [return_maps]). + +-spec client_post(binary(), binary(), map()) -> jsx:json_term(). +client_post(Path, Token, BodyMap) -> + Body = jsx:encode(BodyMap), + {ok, 201, _, RespBody} = client_request(post, Path, Token, Body), + jsx:decode(list_to_binary(RespBody), [return_maps]). + +-spec client_put(binary(), binary(), map()) -> jsx:json_term(). +client_put(Path, Token, BodyMap) -> + Body = jsx:encode(BodyMap), + {ok, 200, _, RespBody} = client_request(put, Path, Token, Body), + jsx:decode(list_to_binary(RespBody), [return_maps]). + +-spec client_delete(binary(), binary()) -> jsx:json_term(). +client_delete(Path, Token) -> + {ok, 200, _, Body} = client_request(delete, Path, Token), + jsx:decode(list_to_binary(Body), [return_maps]). + +%%%=================================================================== +%%% Фикстуры (создание тестовых данных) +%%%=================================================================== + +-spec unique_email(binary()) -> binary(). +unique_email(Prefix) -> + Unique = integer_to_binary(erlang:system_time()), + <<Prefix/binary, "_", Unique/binary, "@test.local">>. + +-spec future_date() -> calendar:datetime(). +future_date() -> + Seconds = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 86400, + calendar:gregorian_seconds_to_datetime(Seconds). + +-spec register_and_login(binary(), binary()) -> binary(). +register_and_login(Email, Password) -> + Resp = client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + {ok, 201, _, Body} = Resp, + #{<<"token">> := Token} = jsx:decode(list_to_binary(Body), [return_maps]), Token. -%% ------------------------------------------------------------------ -%% Остальные функции (без изменений, только используют ?BASE_URL / ?ADMIN_URL) -%% ------------------------------------------------------------------ -get_admin_url() -> - init_global_urls(), - get(admin_url). - -get_admin_ws_url() -> - init_global_urls(), - get(admin_ws_url). - -get_base_url() -> - init_global_urls(), - get(base_url). - -get_base_ws_url() -> - init_global_urls(), - get(base_ws_url). - -get_admin_token() -> - init_global_users(), - get(admin_token). - -get_admin_id() -> - init_global_users(), - get(admin_id). - -get_user_token() -> - init_global_users(), - get(user_token). - -get_user_id() -> - init_global_users(), - get(user_id). - -run_all() -> - inets:start(), - ssl:start(), - - case wait_for_server() of - ok -> ok; - {error, _} -> ct:pal("❌ Server is not running!~n"), exit(server_not_running) - end, - - init_global_users(), - - ct:pal("Starting API tests...~n"), - Modules = [ - api_auth_tests, - api_calendar_tests, - api_event_tests, - api_booking_tests, - api_search_tests, - api_reviews_tests, - api_moderation_tests, - api_tickets_tests, - api_subscription_tests, - api_admin_tests - ], - lists:foreach(fun(M) -> M:test() end, Modules). - -run(Module) -> - inets:start(), - ssl:start(), - init_global_users(), - Module:test(). - -%% ── HTTP‑запросы ───────────────────────────────────────── -ssl_opts() -> - [{ssl, [{verify, verify_none}]}]. - -http_post(Url, Body) -> http_post(Url, Body, undefined). -http_post(Url, Body, Token) -> - Headers = case Token of - undefined -> [{"Content-Type", "application/json"}]; - _ -> [{"Content-Type", "application/json"}, {"Authorization", "Bearer " ++ binary_to_list(Token)}] - end, - httpc:request(post, {?BASE_URL ++ Url, Headers, "application/json", jsx:encode(Body)}, ssl_opts(), []). - -http_get(Url) -> http_get(Url, undefined). -http_get(Url, Token) -> - Headers = case Token of - undefined -> []; - _ -> [{"Authorization", "Bearer " ++ binary_to_list(Token)}] - end, - httpc:request(get, {?BASE_URL ++ Url, Headers}, ssl_opts(), []). - -http_put(Url, Body, Token) -> - Headers = [{"Content-Type", "application/json"}, {"Authorization", "Bearer " ++ binary_to_list(Token)}], - httpc:request(put, {?BASE_URL ++ Url, Headers, "application/json", jsx:encode(Body)}, ssl_opts(), []). - -http_delete(Url, Token) -> - Headers = [{"Authorization", "Bearer " ++ binary_to_list(Token)}], - httpc:request(delete, {?BASE_URL ++ Url, Headers}, ssl_opts(), []). - -%% ── Вспомогательные функции ────────────────────────────── -extract_json({ok, {{_, 200, _}, _, Body}}, Field) -> - Map = jsx:decode(list_to_binary(Body), [return_maps]), - maps:get(Field, Map); -extract_json({ok, {{_, 201, _}, _, Body}}, Field) -> - Map = jsx:decode(list_to_binary(Body), [return_maps]), - maps:get(Field, Map); -extract_json(Response, _Field) -> - error({unexpected_response, Response}). - -extract_json(Response, Field, ExpectedStatus) -> - case Response of - {ok, {{_, ExpectedStatus, _}, _, Body}} -> - Map = jsx:decode(list_to_binary(Body), [return_maps]), - maps:get(Field, Map); - _ -> - error({unexpected_response, Response}) - end. - -assert_status(Status, {ok, {{_, Status, _}, _, _}}) -> ok; -assert_status(Expected, {ok, {{_, Got, _}, _, _}}) -> - error({expected_status, Expected, got, Got}). - -unique_email(Prefix) -> - list_to_binary([Prefix, "_", integer_to_binary(os:system_time(millisecond)), "@test.com"]). - -register_and_login(Email, Password) -> - RegBody = #{email => Email, password => Password}, - case http_post("/v1/register", RegBody) of - {ok, {{_, 201, _}, _, RegResp}} -> - Map = jsx:decode(list_to_binary(RegResp), [return_maps]), - maps:get(<<"token">>, Map); - {ok, {{_, 409, _}, _, _}} -> - LoginBody = #{email => Email, password => Password}, - {ok, {{_, 200, _}, _, LoginResp}} = http_post("/v1/login", LoginBody), - Map = jsx:decode(list_to_binary(LoginResp), [return_maps]), - maps:get(<<"token">>, Map) - end. - -login_custom_admin(Email, Password) -> -%% LoginBody = #{email => Email, password => Password}, - LoginBody = jsx:encode(#{<<"email">> => Email, <<"password">> => Password}), - {ok, {{_, _, _}, _, LoginResp}} = httpc:request(post, - {?ADMIN_URL ++ "/v1/admin/login", [], "application/json", LoginBody}, ssl_opts(), []), - Map = jsx:decode(list_to_binary(LoginResp), [return_maps]), - maps:get(<<"token">>, Map). - +-spec create_calendar(binary(), map()) -> binary(). create_calendar(Token, Params) -> - Response = http_post("/v1/calendars", Params, Token), - ct:pal(" create_calendar Response: ~p~n", [Response]), - Id = extract_json(Response, <<"id">>), - Id. + #{<<"id">> := CalId} = client_post(<<"/v1/calendars">>, Token, Params), + CalId. +-spec create_event(binary(), binary(), map()) -> binary(). create_event(Token, CalId, Params) -> - Url = "/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - Id = extract_json(http_post(Url, Params, Token), <<"id">>), - Id. + Path = <<"/v1/calendars/", CalId/binary, "/events">>, + #{<<"id">> := EventId} = client_post(Path, Token, Params), + EventId. -wait_for_server() -> wait_for_server(30). -wait_for_server(0) -> {error, timeout}; -wait_for_server(Attempts) -> - case httpc:request(get, {?BASE_URL ++ "/health", []}, ssl_opts(), [{timeout, 1000}]) of - {ok, {{_, 200, _}, _, _}} -> ok; - _ -> timer:sleep(1000), wait_for_server(Attempts - 1) - end. +%%%=================================================================== +%%% Внутренние функции +%%%=================================================================== -format_datetime({{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]) - ). \ No newline at end of file +-spec login_admin(binary(), binary()) -> binary(). +login_admin(Email, Password) -> + BodyMap = #{<<"email">> => Email, <<"password">> => Password}, + Body = jsx:encode(BodyMap), + {ok, 200, _, RespBody} = admin_request(post, <<"/v1/admin/login">>, <<>>, Body), + #{<<"token">> := Token} = jsx:decode(list_to_binary(RespBody), [return_maps]), + Token. \ No newline at end of file diff --git a/test/api/api_tickets_tests.erl b/test/api/api_tickets_tests.erl deleted file mode 100644 index 6f28107..0000000 --- a/test/api/api_tickets_tests.erl +++ /dev/null @@ -1,72 +0,0 @@ --module(api_tickets_tests). --export([test/0]). - --define(ADMIN_BASE_URL, api_test_runner:get_admin_url()). --define(BASE_URL, api_test_runner:get_base_url()). - -test() -> - io:format("Testing tickets API...~n"), - Token = api_test_runner:get_user_token(), - AdminToken = api_test_runner:get_admin_token(), - - %% TEST 1: Create ticket (user) - 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">>), - ct:pal(" OK (TicketId: ~p)~n", [TicketId]), - io:format("OK~n"), - - %% TEST 2: Get my tickets (user) - io:format(" TEST 2: Get my tickets... "), - {ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/tickets", Token), - io:format("OK~n"), - - %% TEST 3: Get single ticket (user) - 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"), - - %% TEST 4: Admin lists all tickets - 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"), - - %% TEST 5: Admin updates ticket status - 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"), - - %% TEST 6: Admin assigns ticket - 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"), - - %% TEST 7: Admin views ticket stats - 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"), - - %% TEST 8: Admin deletes ticket - 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("~n✅ Tickets API tests passed!~n"), - {?MODULE, ok}. \ No newline at end of file diff --git a/test/api/users/user_bookings_tests.erl b/test/api/users/user_bookings_tests.erl new file mode 100644 index 0000000..8622793 --- /dev/null +++ b/test/api/users/user_bookings_tests.erl @@ -0,0 +1,115 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для бронирований. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/events/:id/bookings +%%% PUT /v1/bookings/:id +%%% DELETE /v1/bookings/:id +%%% +%%% Проверяет: +%%% - создание бронирования участником +%%% - подтверждение бронирования владельцем календаря +%%% - отмену бронирования участником +%%% - ошибку при повторном бронировании +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_bookings_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Bookings Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + ParticipantEmail = api_test_runner:unique_email(<<"participant">>), + ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"BookingTest">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Event to book">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + test_create_booking(ParticipantToken, EventId), + test_confirm_booking(OwnerToken, EventId), + test_cancel_booking(ParticipantToken, EventId), + test_duplicate_booking(ParticipantToken, EventId), + test_booking_unauthorized(EventId), + + ct:pal("=== All user bookings tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное создание бронирования: 201 Created. +-spec test_create_booking(binary(), binary()) -> ok. +test_create_booking(Token, EventId) -> + ct:pal(" TEST: Create booking"), + Path = <<"/v1/events/", EventId/binary, "/bookings">>, + Resp = api_test_runner:client_request(post, Path, Token, <<"{}">>), + {ok, 201, _, Body} = Resp, + #{<<"id">> := BookingId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(BookingId)), + ?assertEqual(<<"pending">>, Status), + ct:pal(" OK: booking ~s created", [BookingId]). + +%% @doc Подтверждение бронирования владельцем: 200 OK. +-spec test_confirm_booking(binary(), binary()) -> ok. +test_confirm_booking(OwnerToken, EventId) -> + ct:pal(" TEST: Confirm booking as owner"), + % Создаём новое бронирование, которое ещё не подтверждено + Participant2 = api_test_runner:register_and_login( + api_test_runner:unique_email(<<"part2">>), <<"pass">>), + Path = <<"/v1/events/", EventId/binary, "/bookings">>, + #{<<"id">> := BookingId} = api_test_runner:client_post(Path, Participant2, #{}), + % Подтверждаем + ConfirmPath = <<"/v1/bookings/", BookingId/binary>>, + Updated = api_test_runner:client_put(ConfirmPath, OwnerToken, + #{action => <<"confirm">>}), + ?assertEqual(<<"confirmed">>, maps:get(<<"status">>, Updated)), + ct:pal(" OK: booking ~s confirmed", [BookingId]). + +%% @doc Отмена бронирования участником: 200 OK. +-spec test_cancel_booking(binary(), binary()) -> ok. +test_cancel_booking(OwnerToken, EventId) -> + ct:pal(" TEST: Cancel booking as participant"), + % Создаём нового участника, у которого нет бронирований + CancelUserEmail = api_test_runner:unique_email(<<"canceluser">>), + CancelUserToken = api_test_runner:register_and_login(CancelUserEmail, <<"pass">>), + Path = <<"/v1/events/", EventId/binary, "/bookings">>, + #{<<"id">> := BookingId} = api_test_runner:client_post(Path, CancelUserToken, #{}), + CancelPath = <<"/v1/bookings/", BookingId/binary>>, + Resp = api_test_runner:client_request(delete, CancelPath, CancelUserToken), + ?assertMatch({ok, 200, _, _}, Resp), + ct:pal(" OK: booking ~s cancelled", [BookingId]). + +%% @doc Повторное бронирование того же события: 409 Conflict. +-spec test_duplicate_booking(binary(), binary()) -> ok. +test_duplicate_booking(ParticipantToken, EventId) -> + ct:pal(" TEST: Duplicate booking"), + Path = <<"/v1/events/", EventId/binary, "/bookings">>, + % Первый раз должно быть 201 (или 200, если уже есть pending) + _ = api_test_runner:client_request(post, Path, ParticipantToken, <<"{}">>), + % Второй раз – уже booked + Resp2 = api_test_runner:client_request(post, Path, ParticipantToken, <<"{}">>), + {ok, 409, _, _} = Resp2, + ct:pal(" OK: got 409 conflict"). + +%% @doc Запрос без токена: 401 Unauthorized. +-spec test_booking_unauthorized(binary()) -> ok. +test_booking_unauthorized(EventId) -> + ct:pal(" TEST: Booking without token"), + Path = <<"/v1/events/", EventId/binary, "/bookings">>, + Resp = api_test_runner:client_request(post, Path, <<>>, <<"{}">>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_calendar_by_id_tests.erl b/test/api/users/user_calendar_by_id_tests.erl new file mode 100644 index 0000000..f3aa268 --- /dev/null +++ b/test/api/users/user_calendar_by_id_tests.erl @@ -0,0 +1,87 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для работы с конкретным календарём. +%%% Покрывает GET, PUT, DELETE /v1/calendars/:id. +%%% @end +%%%------------------------------------------------------------------- +-module(user_calendar_by_id_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +-spec test() -> ok. +test() -> + ct:pal("=== User Calendar By ID Tests ==="), + Token = api_test_runner:get_user_token(), + OtherToken = api_test_runner:register_and_login( + api_test_runner:unique_email(<<"other">>), <<"pass">>), + + % Создаём календарь для тестов + #{<<"id">> := CalId} = api_test_runner:client_post(<<"/v1/calendars">>, Token, + #{title => <<"TestCal">>, type => <<"personal">>}), + + test_get_calendar(Token, CalId), + test_get_calendar_unauthorized(CalId), + test_get_calendar_not_found(Token), + test_update_calendar(Token, CalId), + test_update_calendar_forbidden(OtherToken, CalId), + test_delete_calendar(Token, CalId), + test_delete_calendar_forbidden(OtherToken, CalId), + + ct:pal("=== All user calendar by id tests passed ==="), + ok. + +test_get_calendar(Token, CalId) -> + ct:pal(" TEST: Get calendar by ID"), + Path = <<"/v1/calendars/", CalId/binary>>, + Cal = api_test_runner:client_get(Path, Token), + ?assertEqual(CalId, maps:get(<<"id">>, Cal)), + ?assert(maps:is_key(<<"title">>, Cal)), + ct:pal(" OK: ~s", [maps:get(<<"title">>, Cal)]). + +test_get_calendar_unauthorized(CalId) -> + ct:pal(" TEST: Get calendar without token (401)"), + Path = <<"/v1/calendars/", CalId/binary>>, + Resp = api_test_runner:client_request(get, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). + +test_get_calendar_not_found(Token) -> + ct:pal(" TEST: Get non-existent calendar (404)"), + Resp = api_test_runner:client_request(get, <<"/v1/calendars/fakeid">>, Token), + ?assertMatch({ok, 404, _, _}, Resp), + ct:pal(" OK: got 404"). + +test_update_calendar(Token, CalId) -> + ct:pal(" TEST: Update calendar"), + Path = <<"/v1/calendars/", CalId/binary>>, + Updated = api_test_runner:client_put(Path, Token, + #{title => <<"Updated">>, description => <<"New desc">>}), + ?assertEqual(<<"Updated">>, maps:get(<<"title">>, Updated)), + ?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)), + ct:pal(" OK"). + +test_update_calendar_forbidden(OtherToken, CalId) -> + ct:pal(" TEST: Update calendar by non-owner (403)"), + Path = <<"/v1/calendars/", CalId/binary>>, + Resp = api_test_runner:client_request(put, Path, OtherToken, + jsx:encode(#{title => <<"fail">>})), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +test_delete_calendar(Token, CalId) -> + ct:pal(" TEST: Delete calendar (soft-delete)"), + Path = <<"/v1/calendars/", CalId/binary>>, + Resp = api_test_runner:client_request(delete, Path, Token), + ?assertMatch({ok, 200, _, _}, Resp), + ct:pal(" OK: deleted"). + +test_delete_calendar_forbidden(OtherToken, CalId) -> + % Первый раз мы уже удалили, но проверим на другом календаре + ct:pal(" TEST: Delete calendar by non-owner (403)"), + % Создадим новый календарь владельцем Token, попробуем удалить OtherToken + #{<<"id">> := NewCalId} = api_test_runner:client_post(<<"/v1/calendars">>, api_test_runner:get_user_token(), + #{title => <<"ForbiddenDel">>, type => <<"personal">>}), + Path = <<"/v1/calendars/", NewCalId/binary>>, + Resp = api_test_runner:client_request(delete, Path, OtherToken), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). \ No newline at end of file diff --git a/test/api/users/user_calendar_view_tests.erl b/test/api/users/user_calendar_view_tests.erl new file mode 100644 index 0000000..cdbed28 --- /dev/null +++ b/test/api/users/user_calendar_view_tests.erl @@ -0,0 +1,61 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для HTML-представления календаря. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/calendars/:calendar_id/view +%%% +%%% Проверяет: +%%% - успешное получение HTML-страницы (200, text/html) +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_calendar_view_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Calendar View Tests ==="), + Token = api_test_runner:get_user_token(), + + % Создаём календарь + CalId = api_test_runner:create_calendar(Token, #{title => <<"ViewCal">>}), + + test_get_calendar_view(Token, CalId), + test_get_calendar_view_unauthorized(CalId), + + ct:pal("=== All user calendar view tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешный запрос HTML-представления: 200 OK, тип text/html. +-spec test_get_calendar_view(binary(), binary()) -> ok. +test_get_calendar_view(Token, CalId) -> + ct:pal(" TEST: Get calendar HTML view"), + Path = <<"/v1/calendars/", CalId/binary, "/view?month=2026-06">>, + Resp = api_test_runner:client_request(get, Path, Token), + {ok, 200, Headers, Body} = Resp, + ?assert(lists:keymember("content-type", 1, Headers)), + {"content-type", CT} = lists:keyfind("content-type", 1, Headers), + ?assert(string:str(CT, "text/html") > 0), + % Body может быть строкой или binary, приводим к binary и проверяем непустоту + BodyBin = iolist_to_binary(Body), + ?assert(byte_size(BodyBin) > 0), + ct:pal(" OK: got HTML of ~p bytes", [byte_size(BodyBin)]). + +%% @doc Запрос без токена: 401 Unauthorized. +-spec test_get_calendar_view_unauthorized(binary()) -> ok. +test_get_calendar_view_unauthorized(CalId) -> + ct:pal(" TEST: Get calendar view without token"), + Path = <<"/v1/calendars/", CalId/binary, "/view?month=2026-06">>, + Resp = api_test_runner:client_request(get, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_calendars_tests.erl b/test/api/users/user_calendars_tests.erl new file mode 100644 index 0000000..8bf3694 --- /dev/null +++ b/test/api/users/user_calendars_tests.erl @@ -0,0 +1,51 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для управления календарями. +%%% Покрывает POST /v1/calendars (создание) и GET /v1/calendars (список). +%%% @end +%%%------------------------------------------------------------------- +-module(user_calendars_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +-spec test() -> ok. +test() -> + ct:pal("=== User Calendars Tests ==="), + Token = api_test_runner:get_user_token(), + + % Создаём один календарь для тестов + #{<<"id">> := CalId} = api_test_runner:client_post(<<"/v1/calendars">>, Token, + #{title => <<"TestCal">>, type => <<"personal">>}), + + test_create_calendar(Token), + test_list_calendars(Token), + test_list_calendars_unauthorized(), + + ct:pal("=== All user calendars tests passed ==="), + ok. + +test_create_calendar(Token) -> + ct:pal(" TEST: Create a new calendar"), + Resp = api_test_runner:client_request(post, <<"/v1/calendars">>, Token, + jsx:encode(#{title => <<"NewCal">>, type => <<"personal">>})), + {ok, 201, _, Body} = Resp, + #{<<"id">> := Id, <<"title">> := Title} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(Id)), + ?assertEqual(<<"NewCal">>, Title), + ct:pal(" OK: created calendar ~s", [Id]). + +test_list_calendars(Token) -> + ct:pal(" TEST: List user calendars"), + Calendars = api_test_runner:client_get(<<"/v1/calendars">>, Token), + ?assert(is_list(Calendars)), + ?assert(length(Calendars) >= 1), + First = hd(Calendars), + ?assert(maps:is_key(<<"id">>, First)), + ?assert(maps:is_key(<<"title">>, First)), + ct:pal(" OK: ~p calendars found", [length(Calendars)]). + +test_list_calendars_unauthorized() -> + ct:pal(" TEST: List calendars without token (401)"), + Resp = api_test_runner:client_request(get, <<"/v1/calendars">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_event_by_id_tests.erl b/test/api/users/user_event_by_id_tests.erl new file mode 100644 index 0000000..5a2265e --- /dev/null +++ b/test/api/users/user_event_by_id_tests.erl @@ -0,0 +1,114 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для работы с конкретным событием. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/events/:id +%%% PUT /v1/events/:id +%%% DELETE /v1/events/:id +%%% +%%% Проверяет: +%%% - получение события по ID +%%% - обновление события (владельцем) +%%% - мягкое удаление события (статус становится deleted) +%%% - ошибку 403 при попытке изменения чужого события +%%% - ошибку 404 для несуществующего события +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_event_by_id_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Event By ID Tests ==="), + Token = api_test_runner:get_user_token(), + OtherToken = api_test_runner:register_and_login( + api_test_runner:unique_email(<<"other">>), <<"pass">>), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(Token, #{title => <<"EvtById">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, Token, + #{title => <<"Test Event">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + test_get_event(Token, EventId), + test_update_event(Token, EventId), + test_update_event_forbidden(OtherToken, EventId), + test_delete_event(Token, EventId), + test_get_event_not_found(Token), + test_get_event_unauthorized(EventId), + + ct:pal("=== All user event by id tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/events/:id – получение события. +-spec test_get_event(binary(), binary()) -> ok. +test_get_event(Token, EventId) -> + ct:pal(" TEST: Get event by ID"), + Path = <<"/v1/events/", EventId/binary>>, + Event = api_test_runner:client_get(Path, Token), + ?assertEqual(EventId, maps:get(<<"id">>, Event)), + ?assertEqual(<<"Test Event">>, maps:get(<<"title">>, Event)), + ct:pal(" OK: got event ~s", [EventId]). + +%% @doc PUT /v1/events/:id – обновление события. +-spec test_update_event(binary(), binary()) -> ok. +test_update_event(Token, EventId) -> + ct:pal(" TEST: Update event"), + Path = <<"/v1/events/", EventId/binary>>, + Updated = api_test_runner:client_put(Path, Token, + #{title => <<"Updated Event">>, description => <<"New desc">>}), + ?assertEqual(<<"Updated Event">>, maps:get(<<"title">>, Updated)), + ?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)), + ct:pal(" OK"). + +%% @doc PUT /v1/events/:id – попытка обновления чужим пользователем (403). +-spec test_update_event_forbidden(binary(), binary()) -> ok. +test_update_event_forbidden(OtherToken, EventId) -> + ct:pal(" TEST: Update event by non-owner"), + Path = <<"/v1/events/", EventId/binary>>, + Resp = api_test_runner:client_request(put, Path, OtherToken, + jsx:encode(#{title => <<"fail">>})), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +%% @doc DELETE /v1/events/:id – мягкое удаление события. +-spec test_delete_event(binary(), binary()) -> ok. +test_delete_event(Token, EventId) -> + ct:pal(" TEST: Soft-delete event"), + Path = <<"/v1/events/", EventId/binary>>, + % Удаляем событие + {ok, 200, _, _} = api_test_runner:client_request(delete, Path, Token), + % Проверяем, что событие доступно, но его статус = deleted + Event = api_test_runner:client_get(Path, Token), + ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Event)), + ct:pal(" OK: event soft-deleted"). + +%% @doc GET /v1/events/:id – несуществующее событие (404). +-spec test_get_event_not_found(binary()) -> ok. +test_get_event_not_found(Token) -> + ct:pal(" TEST: Get non-existent event"), + Resp = api_test_runner:client_request(get, <<"/v1/events/fakeid">>, Token), + ?assertMatch({ok, 404, _, _}, Resp), + ct:pal(" OK: got 404"). + +%% @doc GET /v1/events/:id – без токена (401). +-spec test_get_event_unauthorized(binary()) -> ok. +test_get_event_unauthorized(EventId) -> + ct:pal(" TEST: Get event without token"), + Path = <<"/v1/events/", EventId/binary>>, + Resp = api_test_runner:client_request(get, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_events_tests.erl b/test/api/users/user_events_tests.erl new file mode 100644 index 0000000..10c625e --- /dev/null +++ b/test/api/users/user_events_tests.erl @@ -0,0 +1,80 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для событий. +%%% Покрывает POST /v1/calendars/:id/events и GET /v1/events/:id/occurrences +%%% @end +%%%------------------------------------------------------------------- +-module(user_events_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +-spec test() -> ok. +test() -> + ct:pal("=== User Events Tests ==="), + Token = api_test_runner:get_user_token(), + + % Создаём календарь + CalId = api_test_runner:create_calendar(Token, #{title => <<"EventsTest">>}), + + % Тесты + test_create_single_event(Token, CalId), + test_list_events_with_dates(Token, CalId), + test_create_recurring_event(Token, CalId), + + ct:pal("=== All user events tests passed ==="), + ok. + +%% @doc POST /v1/calendars/:calendar_id/events – одиночное событие. +test_create_single_event(Token, CalId) -> + ct:pal(" TEST: Create single event"), + Path = <<"/v1/calendars/", CalId/binary, "/events">>, + Body = jsx:encode(#{ + title => <<"Single Event">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60 + }), + Resp = api_test_runner:client_request(post, Path, Token, Body), + {ok, 201, _, RespBody} = Resp, + #{<<"id">> := EventId, <<"title">> := Title} = jsx:decode(list_to_binary(RespBody), [return_maps]), + ?assert(is_binary(EventId)), + ?assertEqual(<<"Single Event">>, Title), + ct:pal(" OK: created event ~s", [EventId]). + +%% @doc GET /v1/calendars/:calendar_id/events?from=...&to=... – список с фильтром. +test_list_events_with_dates(Token, CalId) -> + ct:pal(" TEST: List events with date range"), + Path = <<"/v1/calendars/", CalId/binary, "/events?from=2026-05-01T00:00:00Z&to=2026-07-01T00:00:00Z">>, + Events = api_test_runner:client_get(Path, Token), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + First = hd(Events), + ?assert(maps:is_key(<<"id">>, First)), + ?assert(maps:is_key(<<"title">>, First)), + ct:pal(" OK: ~p events found", [length(Events)]). + +%% @doc POST /v1/calendars/:calendar_id/events – повторяющееся событие и проверка вхождений. +test_create_recurring_event(Token, CalId) -> + ct:pal(" TEST: Create recurring event and check occurrences"), + Path = <<"/v1/calendars/", CalId/binary, "/events">>, + Body = jsx:encode(#{ + title => <<"Weekly Meeting">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60, + recurrence => #{ + freq => <<"WEEKLY">>, + interval => 1 + } + }), + Resp = api_test_runner:client_request(post, Path, Token, Body), + {ok, 201, _, RespBody} = Resp, + #{<<"id">> := RecurringId} = jsx:decode(list_to_binary(RespBody), [return_maps]), + ct:pal(" Created recurring event ~s", [RecurringId]), + + % Запрашиваем вхождения на месяц + OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>, + Occs = api_test_runner:client_get(OccPath, Token), + ?assert(is_list(Occs)), + ?assert(length(Occs) >= 4), % минимум 4 недели в июне + FirstOcc = hd(Occs), + ?assert(maps:is_key(<<"start_time">>, FirstOcc)), + ct:pal(" OK: ~p occurrences found", [length(Occs)]). \ No newline at end of file diff --git a/test/api/users/user_login_tests.erl b/test/api/users/user_login_tests.erl new file mode 100644 index 0000000..9ab441b --- /dev/null +++ b/test/api/users/user_login_tests.erl @@ -0,0 +1,90 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для входа пользователей. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/login +%%% +%%% Проверяет: +%%% - успешный вход с правильными email и паролем +%%% - ошибку при неверном пароле +%%% - ошибку при несуществующем email +%%% - ошибку при отсутствии обязательных полей +%%% @end +%%%------------------------------------------------------------------- +-module(user_login_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Client Login Tests ==="), + Email = api_test_runner:unique_email(<<"login">>), + Password = <<"StrongPass1!">>, + + % Создаём пользователя для тестов входа + api_test_runner:register_and_login(Email, Password), + + test_successful_login(Email, Password), + test_wrong_password(Email), + test_nonexistent_email(), + test_missing_fields(), + + ct:pal("=== All client login tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешный вход: 200 OK, возвращает токен и данные пользователя. +-spec test_successful_login(binary(), binary()) -> ok. +test_successful_login(Email, Password) -> + ct:pal(" TEST: Successful login"), + Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + {ok, 200, _, Body} = Resp, + #{<<"token">> := Token, <<"user">> := User} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(Token)), + ?assertEqual(Email, maps:get(<<"email">>, User)), + ct:pal(" OK: user ~s logged in", [maps:get(<<"id">>, User)]). + +%% @doc Неверный пароль: 401 Unauthorized. +-spec test_wrong_password(binary()) -> ok. +test_wrong_password(Email) -> + ct:pal(" TEST: Wrong password"), + Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{email => Email, password => <<"WrongPass1">>})), + {ok, 401, _, Body} = Resp, + #{<<"error">> := Msg} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assertEqual(<<"Invalid credentials">>, Msg), + ct:pal(" OK: got 401 unauthorized"). + +%% @doc Несуществующий email: 401 Unauthorized. +-spec test_nonexistent_email() -> ok. +test_nonexistent_email() -> + ct:pal(" TEST: Nonexistent email"), + Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{email => <<"no@such.user">>, password => <<"Anything1">>})), + {ok, 401, _, Body} = Resp, + #{<<"error">> := Msg} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assertEqual(<<"Invalid credentials">>, Msg), + ct:pal(" OK: got 401 unauthorized"). + +%% @doc Отсутствие обязательных полей: 400 Bad Request. +-spec test_missing_fields() -> ok. +test_missing_fields() -> + ct:pal(" TEST: Missing required fields"), + Resp1 = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{email => <<"a@b.com">>})), + ?assertMatch({ok, 400, _, _}, Resp1), + + Resp2 = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{password => <<"NoEmail1">>})), + ?assertMatch({ok, 400, _, _}, Resp2), + + ct:pal(" OK: 400 on missing fields"). \ No newline at end of file diff --git a/test/api/users/user_me_tests.erl b/test/api/users/user_me_tests.erl new file mode 100644 index 0000000..989b95b --- /dev/null +++ b/test/api/users/user_me_tests.erl @@ -0,0 +1,55 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для получения профиля текущего пользователя. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/user/me +%%% +%%% Проверяет: +%%% - успешное получение профиля с валидным токеном +%%% - ошибку 401 при отсутствии токена +%%% - наличие ключевых полей в ответе +%%% @end +%%%------------------------------------------------------------------- +-module(user_me_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Profile (me) Tests ==="), + Token = api_test_runner:get_user_token(), + + test_get_me_success(Token), + test_get_me_unauthorized(), + + ct:pal("=== All user me tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное получение профиля: 200 OK, возвращает данные пользователя. +-spec test_get_me_success(binary()) -> ok. +test_get_me_success(Token) -> + ct:pal(" TEST: Get current user profile"), + User = api_test_runner:client_get(<<"/v1/user/me">>, Token), + ?assert(is_map(User)), + ?assert(maps:is_key(<<"id">>, User)), + ?assert(maps:is_key(<<"email">>, User)), + ?assert(maps:is_key(<<"role">>, User)), + ?assert(maps:is_key(<<"status">>, User)), + ct:pal(" OK: got profile for ~s", [maps:get(<<"email">>, User)]). + +%% @doc Отсутствие токена: 401 Unauthorized. +-spec test_get_me_unauthorized() -> ok. +test_get_me_unauthorized() -> + ct:pal(" TEST: Get profile without token"), + Resp = api_test_runner:client_request(get, <<"/v1/user/me">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401 unauthorized"). \ No newline at end of file diff --git a/test/api/users/user_my_bookings_tests.erl b/test/api/users/user_my_bookings_tests.erl new file mode 100644 index 0000000..486b26c --- /dev/null +++ b/test/api/users/user_my_bookings_tests.erl @@ -0,0 +1,69 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для получения своих бронирований. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/user/bookings +%%% +%%% Проверяет: +%%% - получение списка бронирований текущего пользователя +%%% - что бронирование, созданное пользователем, присутствует в ответе +%%% - ошибку 401 при отсутствии токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_my_bookings_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User My Bookings Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + ParticipantEmail = api_test_runner:unique_email(<<"mybooker">>), + ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyBookTest">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Event for my booking">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + % Бронируем событие от имени участника и подтверждаем + #{<<"id">> := BookingId} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}), + api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken, + #{action => <<"confirm">>}), + + test_get_my_bookings(ParticipantToken, BookingId), + test_get_my_bookings_unauthorized(), + + ct:pal("=== All user my bookings tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное получение своих бронирований: 200 OK, содержит бронирование. +-spec test_get_my_bookings(binary(), binary()) -> ok. +test_get_my_bookings(Token, ExpectedBookingId) -> + ct:pal(" TEST: Get my bookings"), + Bookings = api_test_runner:client_get(<<"/v1/user/bookings">>, Token), + ?assert(is_list(Bookings)), + ?assert(length(Bookings) >= 1), + ?assert(lists:any(fun(B) -> maps:get(<<"id">>, B) =:= ExpectedBookingId end, Bookings)), + ct:pal(" OK: booking ~s found in list", [ExpectedBookingId]). + +%% @doc Запрос без токена: 401 Unauthorized. +-spec test_get_my_bookings_unauthorized() -> ok. +test_get_my_bookings_unauthorized() -> + ct:pal(" TEST: Get my bookings without token"), + Resp = api_test_runner:client_request(get, <<"/v1/user/bookings">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_my_reviews_tests.erl b/test/api/users/user_my_reviews_tests.erl new file mode 100644 index 0000000..7ec37d5 --- /dev/null +++ b/test/api/users/user_my_reviews_tests.erl @@ -0,0 +1,75 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для получения своих отзывов. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/user/reviews +%%% +%%% Проверяет: +%%% - получение списка отзывов текущего пользователя +%%% - наличие созданного отзыва в ответе +%%% - ошибку 401 при отсутствии токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_my_reviews_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User My Reviews Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + ParticipantEmail = api_test_runner:unique_email(<<"myreviewer">>), + ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyRevTest">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Event for my review">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + % Бронируем, подтверждаем, оставляем отзыв + #{<<"id">> := BookingId} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}), + api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken, + #{action => <<"confirm">>}), + #{<<"id">> := ReviewId} = api_test_runner:client_post( + <<"/v1/reviews">>, ParticipantToken, + #{target_type => <<"event">>, + target_id => EventId, + rating => 4, + comment => <<"Nice event!">>}), + + test_get_my_reviews(ParticipantToken, ReviewId), + test_get_my_reviews_unauthorized(), + + ct:pal("=== All user my reviews tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное получение своих отзывов: 200 OK, содержит отзыв. +-spec test_get_my_reviews(binary(), binary()) -> ok. +test_get_my_reviews(Token, ExpectedReviewId) -> + ct:pal(" TEST: Get my reviews"), + Reviews = api_test_runner:client_get(<<"/v1/user/reviews">>, Token), + ?assert(is_list(Reviews)), + ?assert(length(Reviews) >= 1), + ?assert(lists:any(fun(R) -> maps:get(<<"id">>, R) =:= ExpectedReviewId end, Reviews)), + ct:pal(" OK: review ~s found in list", [ExpectedReviewId]). + +%% @doc Запрос без токена: 401 Unauthorized. +-spec test_get_my_reviews_unauthorized() -> ok. +test_get_my_reviews_unauthorized() -> + ct:pal(" TEST: Get my reviews without token"), + Resp = api_test_runner:client_request(get, <<"/v1/user/reviews">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_occurrence_cancel_tests.erl b/test/api/users/user_occurrence_cancel_tests.erl new file mode 100644 index 0000000..56c15ad --- /dev/null +++ b/test/api/users/user_occurrence_cancel_tests.erl @@ -0,0 +1,99 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для отмены вхождения повторяющегося события. +%%% +%%% Покрывает эндпоинты: +%%% DELETE /v1/events/:id/occurrences/:start_time +%%% +%%% Проверяет: +%%% - успешную отмену конкретного вхождения +%%% - ошибку 400 для не-recurring события +%%% - ошибку 403 при попытке отмены чужим пользователем +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_occurrence_cancel_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Occurrence Cancel Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + OtherToken = api_test_runner:register_and_login( + api_test_runner:unique_email(<<"other">>), <<"pass">>), + + % Создаём календарь и повторяющееся событие + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"OccCancel">>}), + #{<<"id">> := RecurringId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Weekly Standup">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 30, + recurrence => #{freq => <<"WEEKLY">>, interval => 1}}), + + % Получаем вхождения (ответ – список карт) + OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>, + Occurrences = api_test_runner:client_get(OccPath, OwnerToken), + ?assert(is_list(Occurrences)), + ?assert(length(Occurrences) >= 1), + #{<<"start_time">> := FirstStart} = hd(Occurrences), + + test_cancel_occurrence(OwnerToken, RecurringId, FirstStart), + test_cancel_occurrence_on_single_event(OwnerToken, CalId), + test_cancel_occurrence_forbidden(OtherToken, RecurringId, FirstStart), + test_cancel_occurrence_unauthorized(RecurringId, FirstStart), + + ct:pal("=== All user occurrence cancel tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешная отмена вхождения: 200 OK. +-spec test_cancel_occurrence(binary(), binary(), binary()) -> ok. +test_cancel_occurrence(Token, EventId, StartTime) -> + ct:pal(" TEST: Cancel occurrence"), + Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>, + Resp = api_test_runner:client_request(delete, Path, Token), + {ok, 200, _, Body} = Resp, + #{<<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assertEqual(<<"cancelled">>, Status), + ct:pal(" OK: occurrence cancelled"). + +%% @doc Попытка отменить вхождение для одиночного события: 400. +-spec test_cancel_occurrence_on_single_event(binary(), binary()) -> ok. +test_cancel_occurrence_on_single_event(Token, CalId) -> + ct:pal(" TEST: Cancel occurrence on non-recurring event"), + #{<<"id">> := SingleId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, Token, + #{title => <<"Single">>, + start_time => <<"2026-06-02T10:00:00Z">>, + duration => 30}), + Path = <<"/v1/events/", SingleId/binary, "/occurrences/2026-06-02T10:00:00Z">>, + Resp = api_test_runner:client_request(delete, Path, Token), + ?assertMatch({ok, 400, _, _}, Resp), + ct:pal(" OK: got 400"). + +%% @doc Попытка отмены чужим пользователем: 403. +-spec test_cancel_occurrence_forbidden(binary(), binary(), binary()) -> ok. +test_cancel_occurrence_forbidden(OtherToken, EventId, StartTime) -> + ct:pal(" TEST: Cancel occurrence by non-owner"), + Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>, + Resp = api_test_runner:client_request(delete, Path, OtherToken), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +%% @doc Запрос без токена: 401. +-spec test_cancel_occurrence_unauthorized(binary(), binary()) -> ok. +test_cancel_occurrence_unauthorized(EventId, StartTime) -> + ct:pal(" TEST: Cancel occurrence without token"), + Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>, + Resp = api_test_runner:client_request(delete, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_refresh_tests.erl b/test/api/users/user_refresh_tests.erl new file mode 100644 index 0000000..414fd33 --- /dev/null +++ b/test/api/users/user_refresh_tests.erl @@ -0,0 +1,88 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для обновления токена. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/refresh +%%% +%%% Проверяет: +%%% - успешное обновление токена по валидному refresh_token +%%% - ошибку 401 при невалидном refresh_token +%%% - ошибку 400 при отсутствии refresh_token в теле запроса +%%% @end +%%%------------------------------------------------------------------- +-module(user_refresh_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Refresh Tests ==="), + Token = api_test_runner:get_user_token(), + + % Получаем refresh_token через логин (или регистрацию) + Email = api_test_runner:unique_email(<<"refresh">>), + Password = <<"StrongPass1!">>, + #{<<"refresh_token">> := RefreshToken} = register_and_get_refresh(Email, Password), + + test_successful_refresh(RefreshToken), + test_invalid_refresh_token(), + test_missing_refresh_token(), + + ct:pal("=== All user refresh tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное обновление: 200 OK, возвращает новую пару токенов. +-spec test_successful_refresh(binary()) -> ok. +test_successful_refresh(RefreshToken) -> + ct:pal(" TEST: Successful token refresh"), + Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>, + jsx:encode(#{refresh_token => RefreshToken})), + {ok, 200, _, Body} = Resp, + #{<<"token">> := NewToken, <<"refresh_token">> := NewRefresh} = + jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(NewToken)), + ?assert(is_binary(NewRefresh)), + ?assertNotEqual(RefreshToken, NewRefresh), + ct:pal(" OK: got new token pair"). + +%% @doc Невалидный refresh_token: 401 Unauthorized. +-spec test_invalid_refresh_token() -> ok. +test_invalid_refresh_token() -> + ct:pal(" TEST: Invalid refresh token"), + Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>, + jsx:encode(#{refresh_token => <<"invalid_token_here">>})), + {ok, 401, _, _} = Resp, + ct:pal(" OK: got 401"). + +%% @doc Отсутствие refresh_token в теле: 400 Bad Request. +-spec test_missing_refresh_token() -> ok. +test_missing_refresh_token() -> + ct:pal(" TEST: Missing refresh_token field"), + Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>, + jsx:encode(#{})), + ?assertMatch({ok, 400, _, _}, Resp), + ct:pal(" OK: got 400"). + +%%%=================================================================== +%%% Вспомогательные функции +%%%=================================================================== + +%% @doc Регистрирует пользователя, выполняет логин и возвращает refresh_token. +-spec register_and_get_refresh(binary(), binary()) -> map(). +register_and_get_refresh(Email, Password) -> + % Регистрируем + _ = api_test_runner:client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + % Логинимся, чтобы получить refresh_token + {ok, 200, _, Body} = api_test_runner:client_request(post, <<"/v1/login">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + jsx:decode(list_to_binary(Body), [return_maps]). \ No newline at end of file diff --git a/test/api/users/user_register_tests.erl b/test/api/users/user_register_tests.erl new file mode 100644 index 0000000..c31ba6b --- /dev/null +++ b/test/api/users/user_register_tests.erl @@ -0,0 +1,76 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для регистрации пользователей. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/register +%%% +%%% Проверяет: +%%% - успешную регистрацию нового пользователя +%%% - возврат JWT токена и данных пользователя +%%% - ошибку при повторной регистрации с тем же email +%%% - ошибку при отсутствии обязательных полей +%%% @end +%%%------------------------------------------------------------------- +-module(user_register_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== Client Register Tests ==="), + Email = api_test_runner:unique_email(<<"register">>), + Password = <<"StrongPass1!">>, + + test_successful_register(Email, Password), + test_duplicate_register(Email, Password), + test_missing_fields(), + + ct:pal("=== All client register tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешная регистрация: 201 Created, возвращает токен и пользователя. +-spec test_successful_register(binary(), binary()) -> ok. +test_successful_register(Email, Password) -> + ct:pal(" TEST: Successful registration"), + Resp = api_test_runner:client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + {ok, 201, _, Body} = Resp, + #{<<"token">> := Token, <<"user">> := User} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(Token)), + ?assert(maps:is_key(<<"id">>, User)), + ?assertEqual(Email, maps:get(<<"email">>, User)), + ct:pal(" OK: user ~s created", [maps:get(<<"id">>, User)]). + +%% @doc Повторная регистрация с тем же email: 409 Conflict. +-spec test_duplicate_register(binary(), binary()) -> ok. +test_duplicate_register(Email, Password) -> + ct:pal(" TEST: Duplicate registration"), + Resp = api_test_runner:client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{email => Email, password => Password})), + {ok, 409, _, Body} = Resp, + #{<<"error">> := ErrorMsg} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assertEqual(<<"Email already exists">>, ErrorMsg), + ct:pal(" OK: got 409 conflict"). + +%% @doc Отсутствие обязательных полей: 400 Bad Request. +-spec test_missing_fields() -> ok. +test_missing_fields() -> + ct:pal(" TEST: Missing required fields"), + Resp1 = api_test_runner:client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{email => <<"missing@test.local">>})), + ?assertMatch({ok, 400, _, _}, Resp1), + + Resp2 = api_test_runner:client_request(post, <<"/v1/register">>, <<>>, + jsx:encode(#{password => <<"NoEmail1">>})), + ?assertMatch({ok, 400, _, _}, Resp2), + + ct:pal(" OK: 400 on missing fields"). \ No newline at end of file diff --git a/test/api/users/user_reports_tests.erl b/test/api/users/user_reports_tests.erl new file mode 100644 index 0000000..7fec350 --- /dev/null +++ b/test/api/users/user_reports_tests.erl @@ -0,0 +1,101 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для работы с жалобами. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/reports +%%% GET /v1/reports (требует прав администратора) +%%% +%%% Проверяет: +%%% - успешное создание жалобы (201 Created) +%%% - ошибку 400 при отсутствии обязательных полей +%%% - ошибку 401 при создании без токена +%%% - ошибку 403 при попытке получения списка обычным пользователем +%%% @end +%%%------------------------------------------------------------------- +-module(user_reports_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Reports Tests ==="), + UserToken = api_test_runner:get_user_token(), + + % Создаём событие, на которое можно пожаловаться + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReportTest">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, UserToken, + #{title => <<"Event to report">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + test_create_report(UserToken, EventId), + test_create_report_missing_fields(UserToken), + test_create_report_unauthorized(EventId), + test_list_reports_forbidden(UserToken), + + ct:pal("=== All user reports tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное создание жалобы: 201 Created. +-spec test_create_report(binary(), binary()) -> ok. +test_create_report(Token, EventId) -> + ct:pal(" TEST: Create a report"), + Resp = api_test_runner:client_request(post, <<"/v1/reports">>, Token, + jsx:encode(#{ + target_type => <<"event">>, + target_id => EventId, + reason => <<"Inappropriate content">> + })), + {ok, 201, _, Body} = Resp, + #{<<"id">> := ReportId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(ReportId)), + ?assertEqual(<<"pending">>, Status), + ct:pal(" OK: report ~s created", [ReportId]). + +%% @doc Отсутствие обязательных полей: 400 Bad Request. +-spec test_create_report_missing_fields(binary()) -> ok. +test_create_report_missing_fields(Token) -> + ct:pal(" TEST: Create report with missing fields"), + Resp1 = api_test_runner:client_request(post, <<"/v1/reports">>, Token, + jsx:encode(#{target_id => <<"id">>, reason => <<"text">>})), + ?assertMatch({ok, 400, _, _}, Resp1), + + Resp2 = api_test_runner:client_request(post, <<"/v1/reports">>, Token, + jsx:encode(#{target_type => <<"event">>, reason => <<"text">>})), + ?assertMatch({ok, 400, _, _}, Resp2), + + Resp3 = api_test_runner:client_request(post, <<"/v1/reports">>, Token, + jsx:encode(#{target_type => <<"event">>, target_id => <<"id">>})), + ?assertMatch({ok, 400, _, _}, Resp3), + ct:pal(" OK: got 400"). + +%% @doc Создание жалобы без токена: 401 Unauthorized. +-spec test_create_report_unauthorized(binary()) -> ok. +test_create_report_unauthorized(EventId) -> + ct:pal(" TEST: Create report without token"), + Resp = api_test_runner:client_request(post, <<"/v1/reports">>, <<>>, + jsx:encode(#{ + target_type => <<"event">>, + target_id => EventId, + reason => <<"test">> + })), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). + +%% @doc GET /v1/reports для обычного пользователя должен вернуть 403. +-spec test_list_reports_forbidden(binary()) -> ok. +test_list_reports_forbidden(Token) -> + ct:pal(" TEST: List reports as regular user"), + Resp = api_test_runner:client_request(get, <<"/v1/reports">>, Token), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). \ No newline at end of file diff --git a/test/api/users/user_review_by_id_tests.erl b/test/api/users/user_review_by_id_tests.erl new file mode 100644 index 0000000..40c99d1 --- /dev/null +++ b/test/api/users/user_review_by_id_tests.erl @@ -0,0 +1,126 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для работы с конкретным отзывом. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/reviews/:id +%%% PUT /v1/reviews/:id +%%% DELETE /v1/reviews/:id +%%% +%%% Проверяет: +%%% - получение отзыва по ID +%%% - обновление отзыва (автором) +%%% - удаление отзыва (автором) +%%% - ошибку 403 при попытке изменения чужим пользователем +%%% - ошибку 404 для несуществующего отзыва +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_review_by_id_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Review By ID Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + ParticipantEmail = api_test_runner:unique_email(<<"reviewer">>), + ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>), + StrangerEmail = api_test_runner:unique_email(<<"stranger">>), + StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>), + + % Создаём календарь, событие, бронирование и отзыв + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"RevById">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Event for review">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + #{<<"id">> := BookingId} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}), + api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken, + #{action => <<"confirm">>}), + #{<<"id">> := ReviewId} = api_test_runner:client_post( + <<"/v1/reviews">>, ParticipantToken, + #{target_type => <<"event">>, + target_id => EventId, + rating => 5, + comment => <<"Excellent!">>}), + + test_get_review(ParticipantToken, ReviewId), + test_update_review(ParticipantToken, ReviewId), + test_update_review_forbidden(StrangerToken, ReviewId), + test_delete_review(ParticipantToken, ReviewId), + test_get_review_not_found(ParticipantToken), + test_get_review_unauthorized(ReviewId), + + ct:pal("=== All user review by id tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/reviews/:id – получение отзыва. +-spec test_get_review(binary(), binary()) -> ok. +test_get_review(Token, ReviewId) -> + ct:pal(" TEST: Get review by ID"), + Path = <<"/v1/reviews/", ReviewId/binary>>, + Review = api_test_runner:client_get(Path, Token), + ?assertEqual(ReviewId, maps:get(<<"id">>, Review)), + ?assertEqual(<<"Excellent!">>, maps:get(<<"comment">>, Review)), + ct:pal(" OK: got review"). + +%% @doc PUT /v1/reviews/:id – обновление отзыва автором. +-spec test_update_review(binary(), binary()) -> ok. +test_update_review(Token, ReviewId) -> + ct:pal(" TEST: Update review"), + Path = <<"/v1/reviews/", ReviewId/binary>>, + Updated = api_test_runner:client_put(Path, Token, + #{comment => <<"Updated comment">>, rating => 4}), + ?assertEqual(<<"Updated comment">>, maps:get(<<"comment">>, Updated)), + ?assertEqual(4, maps:get(<<"rating">>, Updated)), + ct:pal(" OK"). + +%% @doc PUT /v1/reviews/:id – попытка обновления чужим пользователем (403). +-spec test_update_review_forbidden(binary(), binary()) -> ok. +test_update_review_forbidden(StrangerToken, ReviewId) -> + ct:pal(" TEST: Update review by non-author"), + Path = <<"/v1/reviews/", ReviewId/binary>>, + Resp = api_test_runner:client_request(put, Path, StrangerToken, + jsx:encode(#{comment => <<"fail">>})), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +%% @doc DELETE /v1/reviews/:id – удаление отзыва автором. +-spec test_delete_review(binary(), binary()) -> ok. +test_delete_review(Token, ReviewId) -> + ct:pal(" TEST: Delete review"), + Path = <<"/v1/reviews/", ReviewId/binary>>, + % Удаляем + {ok, 200, _, _} = api_test_runner:client_request(delete, Path, Token), + % Проверяем, что отзыв больше недоступен + GetResp = api_test_runner:client_request(get, Path, Token), + ?assertMatch({ok, 404, _, _}, GetResp), + ct:pal(" OK: review deleted"). + +%% @doc GET /v1/reviews/:id – несуществующий отзыв (404). +-spec test_get_review_not_found(binary()) -> ok. +test_get_review_not_found(Token) -> + ct:pal(" TEST: Get non-existent review"), + Resp = api_test_runner:client_request(get, <<"/v1/reviews/fakeid">>, Token), + ?assertMatch({ok, 404, _, _}, Resp), + ct:pal(" OK: got 404"). + +%% @doc GET /v1/reviews/:id – без токена (401). +-spec test_get_review_unauthorized(binary()) -> ok. +test_get_review_unauthorized(ReviewId) -> + ct:pal(" TEST: Get review without token"), + Path = <<"/v1/reviews/", ReviewId/binary>>, + Resp = api_test_runner:client_request(get, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_reviews_tests.erl b/test/api/users/user_reviews_tests.erl new file mode 100644 index 0000000..57ef93b --- /dev/null +++ b/test/api/users/user_reviews_tests.erl @@ -0,0 +1,114 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для отзывов. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/reviews +%%% GET /v1/reviews +%%% +%%% Проверяет: +%%% - создание отзыва участником подтверждённого бронирования +%%% - ошибку при повторном отзыве на ту же цель +%%% - ошибку при отзыве без участия (403) +%%% - получение списка отзывов для цели +%%% @end +%%%------------------------------------------------------------------- +-module(user_reviews_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Reviews Tests ==="), + OwnerToken = api_test_runner:get_user_token(), + ParticipantEmail = api_test_runner:unique_email(<<"reviewer">>), + ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>), + StrangerEmail = api_test_runner:unique_email(<<"stranger">>), + StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>), + + % Создаём календарь и событие + CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"ReviewTest">>}), + #{<<"id">> := EventId} = api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken, + #{title => <<"Event for review">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60}), + + % Бронируем и подтверждаем участие + #{<<"id">> := BookingId} = api_test_runner:client_post( + <<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}), + api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken, + #{action => <<"confirm">>}), + + test_create_review(ParticipantToken, EventId), + test_duplicate_review(ParticipantToken, EventId), + test_review_without_booking(StrangerToken, EventId), + test_list_reviews(OwnerToken, EventId), + + ct:pal("=== All user reviews tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное создание отзыва: 201 Created. +-spec test_create_review(binary(), binary()) -> ok. +test_create_review(Token, EventId) -> + ct:pal(" TEST: Create a review"), + Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, Token, + jsx:encode(#{ + target_type => <<"event">>, + target_id => EventId, + rating => 5, + comment => <<"Excellent!">> + })), + {ok, 201, _, Body} = Resp, + #{<<"id">> := ReviewId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(ReviewId)), + ?assertEqual(<<"visible">>, Status), + ct:pal(" OK: review ~s created", [ReviewId]). + +%% @doc Повторный отзыв на ту же цель: 409 Conflict. +-spec test_duplicate_review(binary(), binary()) -> ok. +test_duplicate_review(Token, EventId) -> + ct:pal(" TEST: Duplicate review"), + Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, Token, + jsx:encode(#{ + target_type => <<"event">>, + target_id => EventId, + rating => 3, + comment => <<"Second try">> + })), + {ok, 409, _, _} = Resp, + ct:pal(" OK: got 409 conflict"). + +%% @doc Попытка оставить отзыв без бронирования: 403 Forbidden. +-spec test_review_without_booking(binary(), binary()) -> ok. +test_review_without_booking(StrangerToken, EventId) -> + ct:pal(" TEST: Review without booking"), + Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, StrangerToken, + jsx:encode(#{ + target_type => <<"event">>, + target_id => EventId, + rating => 1, + comment => <<"Not allowed">> + })), + {ok, 403, _, _} = Resp, + ct:pal(" OK: got 403 forbidden"). + +%% @doc GET /v1/reviews?target_type=event&target_id=... – список отзывов. +-spec test_list_reviews(binary(), binary()) -> ok. +test_list_reviews(_, EventId) -> + ct:pal(" TEST: List reviews for event"), + Path = <<"/v1/reviews?target_type=event&target_id=", EventId/binary>>, + Reviews = api_test_runner:client_get(Path, api_test_runner:get_user_token()), + ?assert(is_list(Reviews)), + ?assert(length(Reviews) >= 1), + First = hd(Reviews), + ?assertEqual(<<"Excellent!">>, maps:get(<<"comment">>, First)), + ct:pal(" OK: ~p reviews found", [length(Reviews)]). \ No newline at end of file diff --git a/test/api/users/user_search_tests.erl b/test/api/users/user_search_tests.erl new file mode 100644 index 0000000..3b0de5b --- /dev/null +++ b/test/api/users/user_search_tests.erl @@ -0,0 +1,126 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для поиска. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/search +%%% +%%% Проверяет: +%%% - полнотекстовый поиск по названиям событий +%%% - поиск с фильтрацией по типу (event/calendar) +%%% - поиск с фильтрацией по тегам +%%% - поиск с фильтрацией по датам (from/to) +%%% - геопоиск (lat, lon, radius) +%%% - пагинацию результатов +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_search_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Search Tests ==="), + Token = api_test_runner:get_user_token(), + + % Создаём тестовые данные: календарь и несколько событий с тегами + CalId = api_test_runner:create_calendar(Token, #{title => <<"SearchTestCal">>}), + api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, Token, + #{title => <<"Python Workshop">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60, + tags => [<<"python">>, <<"workshop">>]}), + api_test_runner:client_post( + <<"/v1/calendars/", CalId/binary, "/events">>, Token, + #{title => <<"JavaScript Meetup">>, + start_time => <<"2026-06-15T10:00:00Z">>, + duration => 60, + tags => [<<"javascript">>]}), + + test_basic_search(Token), + test_type_filter(Token), + test_tag_filter(Token), + test_date_filter(Token), + test_geo_search(Token), + test_search_pagination(Token), + test_search_unauthorized(), + + ct:pal("=== All user search tests passed ==="), + ok. + +%%%=================================================================== +%%% Вспомогательная функция +%%%=================================================================== + +%% @private Извлекает список событий из результата поиска. +%% Ожидает ответ вида {"results": {"events": [...], "calendars": [...]}}. +-spec extract_events(map()) -> list(). +extract_events(#{<<"results">> := #{<<"events">> := Events}}) -> Events. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +test_basic_search(Token) -> + ct:pal(" TEST: Basic search"), + Events = extract_events( + api_test_runner:client_get(<<"/v1/search?q=Python">>, Token)), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + ct:pal(" OK: ~p events found", [length(Events)]). + +test_type_filter(Token) -> + ct:pal(" TEST: Search with type filter"), + Events = extract_events( + api_test_runner:client_get(<<"/v1/search?q=Python&type=event">>, Token)), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + ct:pal(" OK: ~p events", [length(Events)]). + +test_tag_filter(Token) -> + ct:pal(" TEST: Search with tag filter"), + Events = extract_events( + api_test_runner:client_get(<<"/v1/search?tags=python">>, Token)), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + ct:pal(" OK: ~p events", [length(Events)]). + +test_date_filter(Token) -> + ct:pal(" TEST: Search with date range"), + Events = extract_events( + api_test_runner:client_get( + <<"/v1/search?from=2026-06-01T00:00:00Z&to=2026-06-15T23:59:59Z">>, Token)), + ?assert(is_list(Events)), + ?assert(length(Events) >= 1), + ct:pal(" OK: ~p events", [length(Events)]). + +test_geo_search(Token) -> + ct:pal(" TEST: Geo search"), + Events = extract_events( + api_test_runner:client_get( + <<"/v1/search?lat=55.75&lon=37.61&radius=1">>, Token)), + ?assert(is_list(Events)), + ?assert(length(Events) >= 0), + ct:pal(" OK: ~p events", [length(Events)]). + +test_search_pagination(Token) -> + ct:pal(" TEST: Search pagination"), + Events1 = extract_events( + api_test_runner:client_get(<<"/v1/search?limit=1&offset=0">>, Token)), + ?assertEqual(1, length(Events1)), + Events2 = extract_events( + api_test_runner:client_get(<<"/v1/search?limit=1&offset=1">>, Token)), + ?assert(length(Events2) >= 0), + ct:pal(" OK"). + +test_search_unauthorized() -> + ct:pal(" TEST: Search without token"), + Resp = api_test_runner:client_request(get, <<"/v1/search?q=test">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_subscription_tests.erl b/test/api/users/user_subscription_tests.erl new file mode 100644 index 0000000..341ede0 --- /dev/null +++ b/test/api/users/user_subscription_tests.erl @@ -0,0 +1,78 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для управления подпиской. +%%% +%%% Покрывает эндпоинты: +%%% GET /v1/subscription +%%% POST /v1/subscription +%%% +%%% Проверяет: +%%% - получение информации о подписке (200) +%%% - активацию пробного периода (start_trial, 201) +%%% - ошибку 409 при повторной активации +%%% - ошибку 401 без токена +%%% @end +%%%------------------------------------------------------------------- +-module(user_subscription_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Subscription Tests ==="), + % Создаём уникального пользователя – у него точно нет активной подписки + Email = api_test_runner:unique_email(<<"subtest">>), + Token = api_test_runner:register_and_login(Email, <<"StrongPass1!">>), + + test_get_subscription(Token), + test_start_trial(Token), + test_start_trial_duplicate(Token), + test_get_subscription_unauthorized(), + + ct:pal("=== All user subscription tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc GET /v1/subscription – получение подписки (может быть пустой). +-spec test_get_subscription(binary()) -> ok. +test_get_subscription(Token) -> + ct:pal(" TEST: Get subscription"), + Sub = api_test_runner:client_get(<<"/v1/subscription">>, Token), + ?assert(is_map(Sub)), + ct:pal(" OK: subscription info received"). + +%% @doc POST /v1/subscription – успешная активация пробного периода. +-spec test_start_trial(binary()) -> ok. +test_start_trial(Token) -> + ct:pal(" TEST: Start trial"), + Resp = api_test_runner:client_request(post, <<"/v1/subscription">>, Token, + jsx:encode(#{action => <<"start_trial">>})), + {ok, 201, _, Body} = Resp, + #{<<"status">> := Status, <<"plan">> := Plan} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assertEqual(<<"active">>, Status), + ?assert(is_binary(Plan)), + ct:pal(" OK: trial activated with plan ~s", [Plan]). + +%% @doc POST /v1/subscription – повторная активация (409). +-spec test_start_trial_duplicate(binary()) -> ok. +test_start_trial_duplicate(Token) -> + ct:pal(" TEST: Start trial again (duplicate)"), + Resp = api_test_runner:client_request(post, <<"/v1/subscription">>, Token, + jsx:encode(#{action => <<"start_trial">>})), + ?assertMatch({ok, 409, _, _}, Resp), + ct:pal(" OK: got 409 conflict"). + +%% @doc GET /v1/subscription без токена (401). +-spec test_get_subscription_unauthorized() -> ok. +test_get_subscription_unauthorized() -> + ct:pal(" TEST: Get subscription without token"), + Resp = api_test_runner:client_request(get, <<"/v1/subscription">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/users/user_tickets_tests.erl b/test/api/users/user_tickets_tests.erl new file mode 100644 index 0000000..a790863 --- /dev/null +++ b/test/api/users/user_tickets_tests.erl @@ -0,0 +1,137 @@ +%%%------------------------------------------------------------------- +%%% @doc Тесты клиентского API для работы с тикетами. +%%% +%%% Покрывает эндпоинты: +%%% POST /v1/tickets +%%% GET /v1/tickets +%%% GET /v1/tickets/:id +%%% +%%% Проверяет: +%%% - успешное создание тикета +%%% - получение списка своих тикетов +%%% - получение конкретного тикета по ID +%%% - ошибку 400 при отсутствии обязательных полей +%%% - ошибку 401 без токена +%%% - ошибку 403 при попытке доступа к чужому тикету +%%% @end +%%%------------------------------------------------------------------- +-module(user_tickets_tests). +-include_lib("eunit/include/eunit.hrl"). + +-export([test/0]). + +%%%=================================================================== +%%% Главная тестовая функция +%%%=================================================================== + +-spec test() -> ok. +test() -> + ct:pal("=== User Tickets Tests ==="), + Token = api_test_runner:get_user_token(), + StrangerEmail = api_test_runner:unique_email(<<"stranger">>), + StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>), + + % Создаём тикет + #{<<"id">> := TicketId} = api_test_runner:client_post(<<"/v1/tickets">>, Token, + #{error_message => <<"Something broke">>, stacktrace => <<"line 42">>}), + + test_create_ticket(Token), + test_create_ticket_missing_fields(Token), + test_create_ticket_unauthorized(), + test_list_tickets(Token, TicketId), + test_list_tickets_unauthorized(), + test_get_ticket(Token, TicketId), + test_get_ticket_forbidden(StrangerToken, TicketId), + test_get_ticket_not_found(Token), + test_get_ticket_unauthorized(TicketId), + + ct:pal("=== All user tickets tests passed ==="), + ok. + +%%%=================================================================== +%%% Тестовые функции +%%%=================================================================== + +%% @doc Успешное создание тикета: 201 Created. +-spec test_create_ticket(binary()) -> ok. +test_create_ticket(Token) -> + ct:pal(" TEST: Create a ticket"), + Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token, + jsx:encode(#{error_message => <<"Test bug">>, stacktrace => <<"trace">>})), + {ok, 201, _, Body} = Resp, + #{<<"id">> := Id, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_binary(Id)), + ?assertEqual(<<"open">>, Status), + ct:pal(" OK: ticket ~s created", [Id]). + +%% @doc Отсутствие обязательного поля error_message: 400 Bad Request. +-spec test_create_ticket_missing_fields(binary()) -> ok. +test_create_ticket_missing_fields(Token) -> + ct:pal(" TEST: Create ticket without error_message"), + Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token, + jsx:encode(#{stacktrace => <<"trace">>})), + ?assertMatch({ok, 400, _, _}, Resp), + ct:pal(" OK: got 400"). + +%% @doc Создание тикета без токена: 401 Unauthorized. +-spec test_create_ticket_unauthorized() -> ok. +test_create_ticket_unauthorized() -> + ct:pal(" TEST: Create ticket without token"), + Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, <<>>, + jsx:encode(#{error_message => <<"bug">>})), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). + +%% @doc GET /v1/tickets – получение списка своих тикетов. +-spec test_list_tickets(binary(), binary()) -> ok. +test_list_tickets(Token, ExpectedTicketId) -> + ct:pal(" TEST: List my tickets"), + Tickets = api_test_runner:client_get(<<"/v1/tickets">>, Token), + ?assert(is_list(Tickets)), + ?assert(length(Tickets) >= 1), + ?assert(lists:any(fun(T) -> maps:get(<<"id">>, T) =:= ExpectedTicketId end, Tickets)), + ct:pal(" OK: my ticket found"). + +%% @doc GET /v1/tickets без токена: 401 Unauthorized. +-spec test_list_tickets_unauthorized() -> ok. +test_list_tickets_unauthorized() -> + ct:pal(" TEST: List tickets without token"), + Resp = api_test_runner:client_request(get, <<"/v1/tickets">>, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). + +%% @doc GET /v1/tickets/:id – получение своего тикета. +-spec test_get_ticket(binary(), binary()) -> ok. +test_get_ticket(Token, TicketId) -> + ct:pal(" TEST: Get my ticket by ID"), + Path = <<"/v1/tickets/", TicketId/binary>>, + Ticket = api_test_runner:client_get(Path, Token), + ?assertEqual(TicketId, maps:get(<<"id">>, Ticket)), + ?assertEqual(<<"Something broke">>, maps:get(<<"error_message">>, Ticket)), + ct:pal(" OK: got my ticket"). + +%% @doc GET /v1/tickets/:id – попытка доступа к чужому тикету (403). +-spec test_get_ticket_forbidden(binary(), binary()) -> ok. +test_get_ticket_forbidden(StrangerToken, TicketId) -> + ct:pal(" TEST: Get ticket that is not mine"), + Path = <<"/v1/tickets/", TicketId/binary>>, + Resp = api_test_runner:client_request(get, Path, StrangerToken), + ?assertMatch({ok, 403, _, _}, Resp), + ct:pal(" OK: got 403"). + +%% @doc GET /v1/tickets/:id – несуществующий тикет (404). +-spec test_get_ticket_not_found(binary()) -> ok. +test_get_ticket_not_found(Token) -> + ct:pal(" TEST: Get non-existent ticket"), + Resp = api_test_runner:client_request(get, <<"/v1/tickets/fakeid">>, Token), + ?assertMatch({ok, 404, _, _}, Resp), + ct:pal(" OK: got 404"). + +%% @doc GET /v1/tickets/:id – без токена (401). +-spec test_get_ticket_unauthorized(binary()) -> ok. +test_get_ticket_unauthorized(TicketId) -> + ct:pal(" TEST: Get ticket without token"), + Path = <<"/v1/tickets/", TicketId/binary>>, + Resp = api_test_runner:client_request(get, Path, <<>>), + ?assertMatch({ok, 401, _, _}, Resp), + ct:pal(" OK: got 401"). \ No newline at end of file diff --git a/test/api/api_websocket_tests.erl b/test/api/users/user_websocket_tests.erl similarity index 75% rename from test/api/api_websocket_tests.erl rename to test/api/users/user_websocket_tests.erl index 60837f4..2a396b8 100644 --- a/test/api/api_websocket_tests.erl +++ b/test/api/users/user_websocket_tests.erl @@ -1,53 +1,42 @@ --module(api_websocket_tests). +-module(user_websocket_tests). -export([test/0]). --define(BASE_URL, api_test_runner:get_base_url()). --define(WS_URL, api_test_runner:get_base_ws_url() ++ "/ws"). --define(ADMIN_WS_URL, api_test_runner:get_admin_ws_url() ++ "/admin/ws"). - test() -> ct:pal("Testing WebSocket API..."), - - % Запускаем gun application:ensure_all_started(gun), - % Используем глобальных пользователей AdminToken = api_test_runner:get_admin_token(), - UserToken = api_test_runner:get_user_token(), - + UserToken = api_test_runner:get_user_token(), ct:pal(" AdminToken: ~s...", [binary_part(AdminToken, 0, 30)]), ct:pal(" UserToken: ~s...", [binary_part(UserToken, 0, 30)]), - % Создаём календарь и событие для тестов - CalId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars", - #{title => <<"WS Test Calendar">>, type => <<"commercial">>}, - UserToken), <<"id">>, 201), + % Создаём календарь и событие через новый api_test_runner + CalId = api_test_runner:create_calendar(UserToken, #{title => <<"WS Test Calendar">>, type => <<"commercial">>}), ct:pal(" CalId: ~s", [CalId]), - EventId = api_test_runner:extract_json( - api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events", - #{title => <<"WS Test Event">>, - start_time => <<"2026-06-01T10:00:00Z">>, - duration => 60}, - UserToken), <<"id">>, 201), + EventId = api_test_runner:create_event(UserToken, CalId, #{ + title => <<"WS Test Event">>, + start_time => <<"2026-06-01T10:00:00Z">>, + duration => 60 + }), ct:pal(" EventId: ~s", [EventId]), - % TEST 1: Connect to WebSocket with valid token - ct:pal(" TEST 1: Connect WebSocket with valid token..."), - ct:pal(" URL: ~s", [?WS_URL]), - ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]), + WsUrl = api_test_runner:get_base_ws_url() ++ "/ws", + AdminWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws", - case test_ws_connect_debug(?WS_URL, UserToken) of + %% TEST 1: Connect to WebSocket with valid token + ct:pal(" TEST 1: Connect WebSocket with valid token..."), + ct:pal(" URL: ~s", [WsUrl]), + ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]), + case test_ws_connect_debug(WsUrl, UserToken) of {ok, WS} -> ct:pal(" OK - Connected"), - % TEST 2: Subscribe to calendar updates + %% TEST 2: Subscribe to calendar updates ct:pal(" TEST 2: Subscribe to calendar..."), SubMsg = #{action => <<"subscribe">>, calendar_id => CalId}, ct:pal(" Sending: ~p", [SubMsg]), ok = test_ws_send(WS, SubMsg), - case test_ws_recv(WS) of {ok, #{<<"status">> := <<"subscribed">>}} -> ct:pal(" OK - Subscribed"); @@ -59,7 +48,6 @@ test() -> error(timeout) end, - % Закрываем соединение test_ws_close(WS); {error, Reason} -> ct:pal(" ERROR: ~p", [Reason]), @@ -68,42 +56,40 @@ test() -> ct:pal("~n✅ WebSocket API tests passed!"), -% ============ ТЕСТЫ АДМИНСКОГО WEBSOCKET ============ + %% ============ ТЕСТЫ АДМИНСКОГО WEBSOCKET ============ ct:pal("~n=== ADMIN WEBSOCKET TESTS ==="), - % TEST 6: Admin WebSocket connection + %% TEST 6: Admin WebSocket connection ct:pal(" TEST 6: Admin WebSocket connect..."), - {ok, AdminWS} = test_ws_connect_debug(?ADMIN_WS_URL, AdminToken), + {ok, AdminWS} = test_ws_connect_debug(AdminWsUrl, AdminToken), ct:pal(" OK - Admin connected"), - % TEST 7: Admin subscribe to reports channel + %% TEST 7: Admin subscribe to reports channel ct:pal(" TEST 7: Admin subscribe to reports channel..."), ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"reports">>}), {ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS), ct:pal(" OK - Subscribed to reports"), - % TEST 8: Admin subscribe to tickets channel + %% TEST 8: Admin subscribe to tickets channel ct:pal(" TEST 8: Admin subscribe to tickets channel..."), ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"tickets">>}), {ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS), ct:pal(" OK - Subscribed to tickets"), - % TEST 9: Admin receives report notification + %% TEST 9: Admin receives report notification ct:pal(" TEST 9: Admin receives report notification..."), - % Создаём жалобу через HTTP - api_test_runner:http_post("/v1/reports", - #{target_type => <<"event">>, target_id => EventId, reason => <<"Test report">>}, - UserToken), + api_test_runner:client_post(<<"/v1/reports">>, UserToken, + #{target_type => <<"event">>, target_id => EventId, reason => <<"Test report">>}), {ok, #{<<"type">> := <<"report_created">>}} = test_ws_recv(AdminWS, 5000), ct:pal(" OK - Received report notification"), - % TEST 10: Admin Ping/Pong + %% TEST 10: Admin Ping/Pong ct:pal(" TEST 10: Admin Ping/Pong..."), ok = test_ws_send(AdminWS, #{action => <<"ping">>}), {ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(AdminWS), ct:pal(" OK - Admin Ping/Pong"), - % TEST 11: Admin unsubscribe + %% TEST 11: Admin unsubscribe ct:pal(" TEST 11: Admin unsubscribe from reports..."), ok = test_ws_send(AdminWS, #{action => <<"unsubscribe">>, channel => <<"reports">>}), {ok, #{<<"status">> := <<"unsubscribed">>}} = test_ws_recv(AdminWS), @@ -111,23 +97,23 @@ test() -> test_ws_close(AdminWS), - % TEST 12: Admin WebSocket with user token (should fail) + %% TEST 12: Admin WebSocket with user token (should fail) ct:pal(" TEST 12: Admin WS with user token..."), - {error, {403, _}} = test_ws_connect_debug(?ADMIN_WS_URL, UserToken), + {error, {403, _}} = test_ws_connect_debug(AdminWsUrl, UserToken), ct:pal(" OK - Rejected"), - % TEST 13: Admin WebSocket with invalid token + %% TEST 13: Admin WebSocket with invalid token ct:pal(" TEST 13: Admin WS with invalid token..."), Chars = <<"abcdefghijklmnopqrstuvwxyz0123456789">>, - InvalidToken = << <<(binary:at(Chars, rand:uniform(byte_size(Chars)) - 1))>> || _ <- lists:seq(1, 30) >>, - {error, {401, _}} = test_ws_connect_debug(?ADMIN_WS_URL, InvalidToken), + InvalidToken = << <<(binary:at(Chars, rand:uniform(byte_size(Chars)) - 1))>> + || _ <- lists:seq(1, 30) >>, + {error, {401, _}} = test_ws_connect_debug(AdminWsUrl, InvalidToken), ct:pal(" OK - Rejected"), ct:pal("~n✅ Admin WebSocket API tests passed!"), {?MODULE, ok}. %% ============ WebSocket хелперы с отладкой ============ - test_ws_connect_debug(Url, Token) -> Path = case string:split(Url, "://", trailing) of [_, Rest] -> @@ -140,33 +126,21 @@ test_ws_connect_debug(Url, Token) -> _ -> "/ws?token=" ++ binary_to_list(Token) end, - {ok, Port} = extract_port(Url), {ok, Host} = extract_host(Url), - Opts = case Port of - 443 -> - #{ - protocols => [http], - transport => tls, - tls_opts => [{verify, verify_none}] - }; - _ -> #{ protocols => [http] } + 443 -> #{protocols => [http], + transport => tls, + tls_opts => [{verify, verify_none}]}; + _ -> #{protocols => [http]} end, - ct:pal(" Host: ~s", [Host]), ct:pal(" Port: ~p", [Port]), ct:pal(" Path: ~s", [Path]), - {ok, ConnPid} = gun:open(Host, Port, Opts), - {ok, http} = gun:await_up(ConnPid, 5000), - - Headers = [ - {<<"host">>, list_to_binary(Host ++ ":" ++ integer_to_list(Port))} - ], + {ok, http} = gun:await_up(ConnPid, 5000), + Headers = [{<<"host">>, list_to_binary(Host ++ ":" ++ integer_to_list(Port))}], StreamRef = gun:ws_upgrade(ConnPid, Path, Headers), - - % Ожидаем ответ receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> ct:pal(" WebSocket upgrade OK"), @@ -207,7 +181,6 @@ test_ws_send(ConnPid, Data) -> case catch gun:ws_send(ConnPid, {text, Msg}) of ok -> ok; {'EXIT', {undef, _}} -> - % Пробуем альтернативный синтаксис gun:ws_send(ConnPid, fin, {text, Msg}); Other -> ct:pal(" ERROR sending: ~p", [Other]), @@ -245,7 +218,6 @@ test_ws_close(ConnPid) -> gun:close(ConnPid). %% ========== URL parsing helpers ========== - normalize_scheme(S) when is_binary(S) -> S; normalize_scheme(S) when is_list(S) -> list_to_binary(S); normalize_scheme(S) when is_atom(S) -> atom_to_binary(S, utf8); diff --git a/test/api_SUITE.erl b/test/api_SUITE.erl deleted file mode 100644 index 88877a3..0000000 --- a/test/api_SUITE.erl +++ /dev/null @@ -1,130 +0,0 @@ --module(api_SUITE). --include_lib("common_test/include/ct.hrl"). - --export([all/0, init_per_suite/1, end_per_suite/1]). --export([auth_test/1, calendar_test/1, event_test/1, booking_test/1, - search_test/1, reviews_test/1, moderation_test/1, tickets_test/1, - subscription_test/1, admin_test/1, websocket_test/1]). --export([future_date/0]). - -all() -> - [ - auth_test, - calendar_test, - event_test, - booking_test, - search_test, - reviews_test, - moderation_test, - tickets_test, - subscription_test, - admin_test, - websocket_test - ]. - -init_per_suite(Config) -> - ct:pal("Start Api Testing ~n"), - Mode = os:getenv("CT_MODE", "local"), - ct:pal(" Mode: ~s", [Mode]), - AdminURL = os:getenv("ADMIN_API_HOST"), - ct:pal(" AdminURL: ~s", [AdminURL]), - AdminWsURL = os:getenv("ADMIN_WS_HOST"), - ct:pal(" AdminWsURL: ~s", [AdminWsURL]), - UserURL = os:getenv("API_HOST"), - ct:pal(" UserURL: ~s", [UserURL]), - UserWsURL = os:getenv("WS_HOST"), - ct:pal(" UserWsURL: ~s", [UserWsURL]), - - case Mode of - "remote" -> - inets:start(), - ssl:start(), - % Отключаем авто-редирект и проверку сертификатов - httpc:set_options([ - {autoredirect, false}, - {ssl, [{verify, verify_none}]} - ]), - wait_for_server(), - timer:sleep(1000), - % Извлекаем учётные данные администраторов из переменных окружения - % и сохраняем их в словаре процесса для api_test_runner - put(admin_super_email, - list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local"))), - put(admin_super_password, - list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456"))), - put(admin_moder_email, - list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local"))), - put(admin_moder_password, - list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456"))), - put(admin_support_email, - list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local"))), - put(admin_support_password, - list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456"))), - Config; - _ -> - application:ensure_all_started(eventhub), - timer:sleep(3000), - check_admins(), - Config - end. - -end_per_suite(Config) -> - Mode = os:getenv("CT_MODE", "local"), - case Mode of - "remote" -> - ok; - _ -> - application:stop(eventhub) - end, - Config. - -%% ── Тестовые обёртки ────────────────────────────────── -auth_test(_) -> api_auth_tests:test(). -calendar_test(_) -> api_calendar_tests:test(). -event_test(_) -> api_event_tests:test(). -booking_test(_) -> api_booking_tests:test(). -search_test(_) -> api_search_tests:test(). -reviews_test(_) -> api_reviews_tests:test(). -moderation_test(_) -> api_moderation_tests:test(). -tickets_test(_) -> api_tickets_tests:test(). -subscription_test(_) -> api_subscription_tests:test(). -admin_test(_) -> api_admin_tests:test(). -websocket_test(_) -> api_websocket_tests:test(). - -%% @doc Проверка наличия администраторов (только в remote‑режиме) -%% Если таблица admin пуста – роняем тест явно, чтобы не гадать. -check_admins() -> - case core_admin:list_all() of - [] -> - ct:fail("No admins found in remote cluster. Run init_default_admins first."); - Admins -> - ct:pal("Admins present: ~p", [length(Admins)]) - end. - -%% @doc Ожидание доступности healthcheck-эндпоинта (/health) -wait_for_server() -> - URL = case os:getenv("API_HOST") of - false -> "http://localhost:8080/health"; - Host -> Host ++ "/health" - end, - wait_for_server(URL, 30). - -wait_for_server(URL, 0) -> - ct:fail("Healthcheck ~s not responding after 30 seconds", [URL]); -wait_for_server(URL, Attempts) -> - case httpc:request(get, {URL, []}, [{timeout, 2000}, {ssl, [{verify, verify_none}]}], []) of - {ok, {{_, 200, _}, _, _}} -> - ct:pal("Healthcheck OK", []); - _ -> - timer:sleep(1000), - wait_for_server(URL, Attempts - 1) - end. - -future_date() -> - Now = calendar:universal_time(), - Tomorrow = calendar:gregorian_seconds_to_datetime( - calendar:datetime_to_gregorian_seconds(Now) + 86400 - ), - {{Y, M, D}, {H, Min, S}} = Tomorrow, - iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", - [Y, M, D, H, Min, S])). \ No newline at end of file diff --git a/test/api_admins_SUITE.erl b/test/api_admins_SUITE.erl new file mode 100644 index 0000000..4c12144 --- /dev/null +++ b/test/api_admins_SUITE.erl @@ -0,0 +1,143 @@ +%%%------------------------------------------------------------------- +%%% @doc Common Test Suite для административного API. +%%% +%%% Поддерживает два режима запуска: +%%% - Локальный (CT_MODE=local или не задан): автоматически +%%% стартует EventHub, выполняет тесты и останавливает приложение. +%%% - Удалённый (CT_MODE=remote): подключается к уже работающему +%%% кластеру по URL, определённым в переменных окружения +%%% (API_HOST, ADMIN_API_HOST и т.д.). +%%% +%%% Запуск: +%%% rebar3 ct --suite=api_admin_SUITE +%%% или CT_MODE=remote rebar3 ct --suite=api_admin_SUITE +%%% @end +%%%------------------------------------------------------------------- +-module(api_admins_SUITE). +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%%=================================================================== +%%% Common Test callbacks +%%%=================================================================== + +%% @doc Возвращает список тестовых кейсов. +all() -> + [ + admin_test_events, + admin_test_reviews, + admin_test_users, + admin_test_tickets, + admin_test_reports, + admin_test_subscriptions, + admin_test_banned_words, + admin_test_moderation, + admin_test_audit, + admin_test_stats, + admin_test_me, + admin_test_admins, + admin_test_websocket + ]. + +%% @doc Инициализация сьюта. +%% В локальном режиме запускает приложение EventHub. +init_per_suite(Config) -> + case os:getenv("CT_MODE", "local") of + "remote" -> + ct:pal("Remote mode: assuming application is already running"), + wait_for_remote(), + [{started_by_us, false} | Config]; + _ -> + case lists:keymember(eventhub, 1, application:which_applications()) of + true -> + ct:pal("Local mode: application already running"), + [{started_by_us, false} | Config]; + false -> + ct:pal("Local mode: starting application..."), +%% ok = application:load(eventhub), + {ok, _} = application:ensure_all_started(eventhub), + timer:sleep(1000), + [{started_by_us, true} | Config] + end + end. + +%% @doc Завершение сьюта. В локальном режиме останавливает приложение. +end_per_suite(Config) -> + case proplists:get_value(started_by_us, Config, false) andalso + os:getenv("CT_MODE", "local") =/= "remote" of + true -> + application:stop(eventhub); + false -> + ok + end, + Config. + +%%%=================================================================== +%%% Тестовые кейсы +%%%=================================================================== + +%% @doc Тесты для событий. +admin_test_events(_Config) -> + admin_events_tests:test(). + +admin_test_reviews(_Config) -> + admin_reviews_tests:test(). + +admin_test_users(_Config) -> + admin_users_tests:test(). + +admin_test_tickets(_Config) -> + admin_tickets_tests:test(). + +admin_test_reports(_Config) -> + admin_reports_tests:test(). + +admin_test_subscriptions(_Config) -> + admin_subscriptions_tests:test(). + +admin_test_banned_words(_Config) -> + admin_banned_words_tests:test(). + +admin_test_moderation(_Config) -> + admin_moderation_tests:test(). + +admin_test_audit(_Config) -> + admin_audit_tests:test(). + +admin_test_stats(_Config) -> + admin_stats_tests:test(). + +admin_test_me(_Config) -> + admin_me_tests:test(). + +admin_test_admins(_Config) -> + admin_admins_tests:test(). + +admin_test_websocket(_Config) -> + admin_websocket_tests:test(). + +%%%=================================================================== +%%% Внутренние функции +%%%=================================================================== + +-spec ct_mode() -> string(). +ct_mode() -> + os:getenv("CT_MODE", "local"). + +%% @private Ожидание доступности удалённого API. +-spec wait_for_remote() -> ok. +wait_for_remote() -> + URL = os:getenv("ADMIN_API_HOST", "http://localhost:8445") ++ "/admin/health", + ct:pal("Waiting for remote API: ~s", [URL]), + wait_for_health(URL, 30). + +wait_for_health(_URL, 0) -> + ct:fail("Remote API did not start within 30 seconds"); +wait_for_health(URL, Retries) -> + case httpc:request(get, {URL, []}, [], []) of + {ok, {{_, 200, _}, _, _}} -> ok; + _ -> + timer:sleep(1000), + wait_for_health(URL, Retries - 1) + end. \ No newline at end of file diff --git a/test/api_users_SUITE.erl b/test/api_users_SUITE.erl new file mode 100644 index 0000000..cdb39af --- /dev/null +++ b/test/api_users_SUITE.erl @@ -0,0 +1,171 @@ +%%%------------------------------------------------------------------- +%%% @doc Common Test Suite для клиентского API. +%%% +%%% Поддерживает два режима запуска: +%%% - Локальный (CT_MODE=local или не задан): автоматически +%%% стартует EventHub, выполняет тесты и останавливает приложение. +%%% - Удалённый (CT_MODE=remote): подключается к уже работающему +%%% кластеру по URL, определённым в переменных окружения +%%% (API_HOST, ADMIN_API_HOST и т.д.). +%%% +%%% Запуск: +%%% rebar3 ct --suite=api_client_SUITE +%%% или CT_MODE=remote rebar3 ct --suite=api_client_SUITE +%%% @end +%%%------------------------------------------------------------------- +-module(api_users_SUITE). +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%%=================================================================== +%%% Common Test callbacks +%%%=================================================================== + +%% @doc Возвращает список тестовых кейсов. +all() -> + [ + user_test_register, + user_test_login, + user_test_user_me, + user_test_calendars, + user_test_calendar_by_id, + user_test_calendar_view, + user_test_event_by_id, + user_test_events, + user_test_occurrence_cancel, + user_test_bookings, + user_test_my_bookings, + user_test_reviews, + user_test_review_by_id, + user_test_my_reviews, + user_test_search, + user_test_refresh, + user_test_reports, + user_test_tickets, + user_test_subscription, + user_test_websocket + ]. + +%% @doc Инициализация сьюта. +%% В локальном режиме запускает приложение EventHub. +init_per_suite(Config) -> + case os:getenv("CT_MODE", "local") of + "remote" -> + ct:pal("Remote mode: assuming application is already running"), + wait_for_remote(), + [{started_by_us, false} | Config]; + _ -> + case lists:keymember(eventhub, 1, application:which_applications()) of + true -> + ct:pal("Local mode: application already running"), + [{started_by_us, false} | Config]; + false -> + ct:pal("Local mode: starting application..."), +%% ok = application:load(eventhub), + {ok, _} = application:ensure_all_started(eventhub), + timer:sleep(1000), + [{started_by_us, true} | Config] + end + end. + +%% @doc Завершение сьюта. В локальном режиме останавливает приложение. +end_per_suite(Config) -> + case proplists:get_value(started_by_us, Config, false) andalso + os:getenv("CT_MODE", "local") =/= "remote" of + true -> + application:stop(eventhub); + false -> + ok + end, + Config. + +%%%=================================================================== +%%% Тестовые кейсы +%%%=================================================================== + +%% @doc Тесты регистрации. +user_test_register(_Config) -> + user_register_tests:test(). + +user_test_login(_Config) -> + user_login_tests:test(). + +user_test_user_me(_Config) -> + user_me_tests:test(). + +user_test_calendars(_Config) -> + user_calendars_tests:test(). + +user_test_calendar_by_id(_Config) -> + user_calendar_by_id_tests:test(). + +user_test_calendar_view(_Config) -> + user_calendar_view_tests:test(). + +user_test_events(_Config) -> + user_events_tests:test(). + +user_test_event_by_id(_Config) -> + user_event_by_id_tests:test(). + +user_test_occurrence_cancel(_Config) -> + user_occurrence_cancel_tests:test(). + +user_test_bookings(_Config) -> + user_bookings_tests:test(). + +user_test_my_bookings(_Config) -> + user_my_bookings_tests:test(). + +user_test_reviews(_Config) -> + user_reviews_tests:test(). + +user_test_review_by_id(_Config) -> + user_review_by_id_tests:test(). + +user_test_my_reviews(_Config) -> + user_my_reviews_tests:test(). + +user_test_search(_Config) -> + user_search_tests:test(). + +user_test_refresh(_Config) -> + user_refresh_tests:test(). + +user_test_reports(_Config) -> + user_reports_tests:test(). + +user_test_tickets(_Config) -> + user_tickets_tests:test(). + +user_test_subscription(_Config) -> + user_subscription_tests:test(). + +user_test_websocket(_Config) -> + user_websocket_tests:test(). + +%%%=================================================================== +%%% Внутренние функции +%%%=================================================================== + +-spec ct_mode() -> string(). +ct_mode() -> + os:getenv("CT_MODE", "local"). + +%% @private Ожидание доступности удалённого API. +-spec wait_for_remote() -> ok. +wait_for_remote() -> + URL = os:getenv("API_HOST", "http://localhost:8080") ++ "/health", + ct:pal("Waiting for remote API: ~s", [URL]), + wait_for_health(URL, 30). + +wait_for_health(_URL, 0) -> + ct:fail("Remote API did not start within 30 seconds"); +wait_for_health(URL, Retries) -> + case httpc:request(get, {URL, []}, [], []) of + {ok, {{_, 200, _}, _, _}} -> ok; + _ -> + timer:sleep(1000), + wait_for_health(URL, Retries - 1) + end. \ No newline at end of file diff --git a/test/scripts/run_tests.sh b/test/scripts/run_tests.sh deleted file mode 100644 index 2630d34..0000000 --- a/test/scripts/run_tests.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$PROJECT_ROOT" - -echo "============================================================" -echo " FULL TEST CYCLE" -echo "============================================================" - -# Остановка старых процессов -echo "[1/4] Stopping old servers..." -pkill -f "beam.*eventhub" 2>/dev/null || true -rm -rf Mnesia.* -sleep 2 - -# Запуск сервера в фоне -echo "[2/4] Starting server..." -./test/scripts/start_server_bg.sh -if [ $? -ne 0 ]; then - echo "Failed to start server" - exit 1 -fi - -# Запуск тестов -echo "[3/4] Running tests..." -chmod +x test/scripts/*.sh -cd test/scripts - -if [ -n "$1" ]; then - ./test_runner.sh -s "$1" -else - ./test_runner.sh -s -fi - -TEST_RESULT=$? - -# Остановка сервера -echo "" -echo "[4/4] Stopping server..." -pkill -f "beam.*eventhub" 2>/dev/null || true - -echo "============================================================" -if [ $TEST_RESULT -eq 0 ]; then - echo "🎉 ALL TESTS PASSED!" -else - echo "❌ TESTS FAILED" -fi -echo "============================================================" - -exit $TEST_RESULT \ No newline at end of file diff --git a/test/scripts/start_server_bg.sh b/test/scripts/start_server_bg.sh deleted file mode 100644 index 658d4aa..0000000 --- a/test/scripts/start_server_bg.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$PROJECT_ROOT" -echo "PROJECT_ROOT: $PROJECT_ROOT" -# Очистка -echo "Stopping old processes..." -pkill beam 2>/dev/null || true -rm -rf Mnesia.* -sleep 2 - -# Компиляция -echo "Compiling..." -rebar3 compile > /dev/null 2>&1 - -# Запуск в фоне через erl -echo "Starting server in background..." -erl -sname eventhub_test \ - -pa _build/default/lib/*/ebin \ - -eval "application:ensure_all_started(eventhub)" \ - -noshell \ - -detached - -# Ждём запуска -echo "Waiting for server..." -for i in {1..30}; do - if curl -s http://localhost:8080/health 2>/dev/null | grep -q "ok"; then - echo "✓ Server ready at http://localhost:8080" - pgrep -f "beam.*eventhub_test" - exit 0 - fi - echo -n "." - sleep 1 -done - -echo "" -echo "✗ Server failed to start" -pkill beam 2>/dev/null || true -exit 1 \ No newline at end of file diff --git a/test/scripts/test_admin_api.sh b/test/scripts/test_admin_api.sh deleted file mode 100644 index d296261..0000000 --- a/test/scripts/test_admin_api.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/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 diff --git a/test/scripts/test_all.sh b/test/scripts/test_all.sh deleted file mode 100644 index 412f37e..0000000 --- a/test/scripts/test_all.sh +++ /dev/null @@ -1,298 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPTS_DIR/../.." && pwd)" -BASE_URL="http://localhost:8080" - -PASSED=0 -FAILED=0 -SKIPPED=0 -SERVER_STARTED=false -SERVER_PID="" - -# ============================================================================ -# Вспомогательные функции -# ============================================================================ -log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } -log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } -log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } -log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } - -# Очистка при выходе -cleanup() { - echo "" - log_info "Cleaning up..." - - if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then - log_info "Stopping server (PID: $SERVER_PID)..." - kill "$SERVER_PID" 2>/dev/null - wait "$SERVER_PID" 2>/dev/null - - # Убеждаемся, что все beam процессы остановлены - pkill -f "beam.*eventhub" 2>/dev/null || true - log_success "Server stopped" - fi - - # Удаляем временные файлы - rm -f /tmp/eventhub_test_*.log 2>/dev/null -} - -# Обработчик сигналов -trap cleanup EXIT INT TERM - -# Проверка порта -check_port() { - local port=$1 - if lsof -i ":$port" > /dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":$port "; then - return 0 - fi - return 1 -} - -# Ожидание запуска сервера -wait_for_server() { - local max_attempts=30 - local attempt=0 - - log_info "Waiting for server to start..." - - while [ $attempt -lt $max_attempts ]; do - if curl -s "$BASE_URL/health" | grep -q "ok"; then - log_success "Server is ready (took $attempt seconds)" - return 0 - fi - sleep 1 - ((attempt++)) - echo -n "." - done - echo "" - - log_error "Server failed to start within $max_attempts seconds" - return 1 -} - -# Запуск сервера -start_server() { - echo -e "${CYAN}[STEP]${NC} Starting EventHub server..." - - cd "$PROJECT_ROOT" - - if [ ! -f "rebar.config" ]; then - echo -e "${RED}[ERROR]${NC} rebar.config not found in $(pwd)" - return 1 - fi - - echo -e "${BLUE}[INFO]${NC} Project root: $(pwd)" - - # Очищаем старые данные - rm -rf Mnesia.* 2>/dev/null - pkill -f "beam.*eventhub_test" 2>/dev/null || true - sleep 1 - - # Компилируем - echo -e "${BLUE}[INFO]${NC} Compiling..." - rebar3 compile > /dev/null 2>&1 - - # Запускаем сервер через erl (более надёжно для фона) - LOG_FILE="/tmp/eventhub_test_server.log" - echo -e "${BLUE}[INFO]${NC} Starting server..." - - # Запускаем в фоне с перенаправлением вывода - rebar3 shell --sname eventhub_test </dev/null > "$LOG_FILE" 2>&1 & - SERVER_PID=$! - - # Даём процессу время запуститься - sleep 3 - - # Проверяем, жив ли процесс - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - echo -e "${RED}[ERROR]${NC} Server process died immediately" - echo -e "${YELLOW}[INFO]${NC} Check log: $LOG_FILE" - cat "$LOG_FILE" - return 1 - fi - - echo -e "${BLUE}[INFO]${NC} Server PID: $SERVER_PID" - - # Ждём готовности - for i in {1..30}; do - echo -n "." - if curl -s "http://localhost:8080/health" 2>/dev/null | grep -q "ok"; then - echo "" - echo -e "${GREEN}[SUCCESS]${NC} Server ready at http://localhost:8080" - return 0 - fi - - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - echo "" - echo -e "${RED}[ERROR]${NC} Server died during startup" - echo -e "${YELLOW}[INFO]${NC} Last lines of log:" - tail -30 "$LOG_FILE" - return 1 - fi - - sleep 1 - done - - echo "" - echo -e "${RED}[ERROR]${NC} Server failed to respond" - return 1 -} - -# Остановка сервера -stop_server() { - if [ "$SERVER_STARTED" = true ] && [ -n "$SERVER_PID" ]; then - log_step "Stopping EventHub server..." - - # Останавливаем нашу ноду - if kill -0 "$SERVER_PID" 2>/dev/null; then - kill "$SERVER_PID" 2>/dev/null - wait "$SERVER_PID" 2>/dev/null - fi - - # Останавливаем все связанные beam процессы - pkill -f "beam.*eventhub_test" 2>/dev/null || true - - SERVER_STARTED=false - log_success "Server stopped" - sleep 2 - fi -} - -# Проверка, запущен ли сервер -is_server_running() { - curl -s "$BASE_URL/health" | grep -q "ok" -} - -# Запуск одного тестового скрипта -run_test_script() { - local script_path=$1 - local script_name=$(basename "$script_path") - - echo "" - echo "============================================================" - echo -e "${CYAN}[RUNNING]${NC} $script_name" - echo "============================================================" - - # Даём скрипту права на выполнение - chmod +x "$script_path" 2>/dev/null - - # Запускаем тест - if bash "$script_path"; then - echo "" - echo -e "${GREEN}[PASSED]${NC} $script_name" - return 0 - else - echo "" - echo -e "${RED}[FAILED]${NC} $script_name" - return 1 - fi -} - -# ============================================================================ -# Главная логика -# ============================================================================ -main() { - echo "============================================================" - echo " EVENTHUB FULL API TEST SUITE" - echo "============================================================" - echo "" - - # Проверяем, не запущен ли уже сервер - if is_server_running; then - log_warning "Server is already running on port 8080" - read -p "Use existing server? [Y/n]: " USE_EXISTING - if [[ "$USE_EXISTING" =~ ^[Nn] ]]; then - log_error "Please stop the existing server first: make stop" - exit 1 - fi - log_info "Using existing server" - else - # Проверяем, свободен ли порт - if check_port 8080; then - log_error "Port 8080 is in use by another process" - log_info "Please free the port or stop the other process" - exit 1 - fi - - # Запускаем сервер - if ! start_server; then - log_error "Failed to start server" - exit 1 - fi - fi - - echo "" - log_info "Server is ready at $BASE_URL" - - # Получаем список всех тестовых скриптов - TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort) - - if [ -z "$TEST_SCRIPTS" ]; then - log_warning "No test scripts found in $SCRIPTS_DIR" - exit 0 - fi - - echo "" - log_info "Found test scripts:" - for script in $TEST_SCRIPTS; do - echo " - $(basename "$script")" - done - - # Счётчики времени - START_TIME=$(date +%s) - - # Запускаем все тесты - for script in $TEST_SCRIPTS; do - if run_test_script "$script"; then - ((PASSED++)) - else - ((FAILED++)) - fi - done - - END_TIME=$(date +%s) - DURATION=$((END_TIME - START_TIME)) - - # Останавливаем сервер, если мы его запускали - if [ "$SERVER_STARTED" = true ]; then - echo "" - stop_server - fi - - # Итоговый отчёт - echo "" - echo "============================================================" - echo " TEST SUMMARY" - echo "============================================================" - echo -e "Total scripts: $((PASSED + FAILED))" - echo -e "${GREEN}Passed: $PASSED${NC}" - echo -e "${RED}Failed: $FAILED${NC}" - if [ $SKIPPED -gt 0 ]; then - echo -e "${YELLOW}Skipped: $SKIPPED${NC}" - fi - echo -e "Duration: ${DURATION}s" - echo "============================================================" - - if [ $FAILED -eq 0 ]; then - echo "" - echo -e "${GREEN}🎉 ALL TESTS PASSED!${NC}" - return 0 - else - echo "" - echo -e "${RED}❌ SOME TESTS FAILED${NC}" - return 1 - fi -} - -# Запуск -main "$@" -exit $? \ No newline at end of file diff --git a/test/scripts/test_auth_api.sh b/test/scripts/test_auth_api.sh deleted file mode 100644 index bd4fe9b..0000000 --- a/test/scripts/test_auth_api.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/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"; } -log_warning() { echo -e "${YELLOW}[WARNING]${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 -} - -echo "============================================================" -echo " EVENTHUB AUTHENTICATION 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 "TEST 1: Healthcheck" -log_info "============================================================" - -response=$(http_get "$BASE_URL/health" "") -if echo "$response" | grep -q "ok"; then - log_success "Healthcheck passed: $response" -else - log_error "Healthcheck failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Register new user" -log_info "============================================================" - -TEST_EMAIL="test_auth_$(date +%s)@example.com" -TEST_PASSWORD="testpass123" - -log_info "Registering $TEST_EMAIL..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") - -if echo "$response" | grep -q "token"; then - TOKEN=$(extract_json "$response" "token") - USER_ID=$(extract_json "$response" "id") - log_success "Registration successful" - log_info "User ID: $USER_ID" - log_info "Token: ${TOKEN:0:30}..." -else - log_error "Registration failed: $response" - exit 1 -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Register with existing email (should fail)" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") -if echo "$response" | grep -q "already exists"; then - log_success "Duplicate registration correctly rejected" -else - log_error "Duplicate registration not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: Login with correct credentials" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") - -if echo "$response" | grep -q "token"; then - LOGIN_TOKEN=$(extract_json "$response" "token") - REFRESH_TOKEN=$(extract_json "$response" "refresh_token") - log_success "Login successful" - log_info "Refresh token received: ${REFRESH_TOKEN:0:30}..." -else - log_error "Login failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 5: Login with wrong password (should fail)" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$TEST_EMAIL\",\"password\":\"wrongpassword\"}" "") -if echo "$response" | grep -q "Invalid credentials"; then - log_success "Wrong password correctly rejected" -else - log_error "Wrong password not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Get user profile with valid token" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/user/me" "$TOKEN") -if echo "$response" | grep -q "$TEST_EMAIL"; then - log_success "Profile retrieved successfully" - log_info "Response: $response" -else - log_error "Profile retrieval failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 7: Get user profile with invalid token" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/user/me" "invalid.token.here") -if echo "$response" | grep -q "Invalid token"; then - log_success "Invalid token correctly rejected" -else - log_error "Invalid token not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Get user profile without token" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/user/me" "") -if echo "$response" | grep -q "Missing or invalid Authorization"; then - log_success "Missing token correctly rejected" -else - log_error "Missing token not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Refresh token" -log_info "============================================================" - -if [ -n "$REFRESH_TOKEN" ]; then - response=$(http_post "$BASE_URL/v1/refresh" "{\"refresh_token\":\"$REFRESH_TOKEN\"}" "") - if echo "$response" | grep -q "token"; then - NEW_TOKEN=$(extract_json "$response" "token") - NEW_REFRESH=$(extract_json "$response" "refresh_token") - log_success "Token refreshed successfully" - log_info "New token: ${NEW_TOKEN:0:30}..." - log_info "New refresh token: ${NEW_REFRESH:0:30}..." - else - log_error "Token refresh failed: $response" - fi - - log_info "Trying to reuse old refresh token (should fail)..." - response=$(http_post "$BASE_URL/v1/refresh" "{\"refresh_token\":\"$REFRESH_TOKEN\"}" "") - if echo "$response" | grep -q "Invalid refresh token"; then - log_success "Old refresh token correctly rejected" - else - log_warning "Old refresh token not rejected: $response" - fi -else - log_warning "No refresh token to test" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Access protected endpoint with new token" -log_info "============================================================" - -if [ -n "$NEW_TOKEN" ]; then - response=$(http_get "$BASE_URL/v1/user/me" "$NEW_TOKEN") - if echo "$response" | grep -q "$TEST_EMAIL"; then - log_success "Protected endpoint accessible with new token" - else - log_error "Protected endpoint not accessible: $response" - fi -fi - -echo "" -echo "============================================================" -log_success "AUTHENTICATION TESTS COMPLETED!" -echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_booking_api.sh b/test/scripts/test_booking_api.sh deleted file mode 100644 index 37e0230..0000000 --- a/test/scripts/test_booking_api.sh +++ /dev/null @@ -1,265 +0,0 @@ -#!/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 BOOKING 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 "============================================================" - -OWNER_EMAIL="owner_test@example.com" -OWNER_PASSWORD="owner123" -PARTICIPANT_EMAIL="participant_test@example.com" -PARTICIPANT_PASSWORD="participant123" - -# Пробуем зарегистрировать владельца -log_info "Creating calendar owner..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "") - -if echo "$response" | grep -q "token"; then - OWNER_TOKEN=$(extract_json "$response" "token") - OWNER_ID=$(extract_json "$response" "id") - log_success "Owner registered: $OWNER_EMAIL" -else - log_info "Owner exists, trying login..." - response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "") - OWNER_TOKEN=$(extract_json "$response" "token") - OWNER_ID=$(extract_json "$response" "id") -fi - -if [ -z "$OWNER_TOKEN" ]; then - log_error "Failed to get owner token" - echo "$response" - exit 1 -fi -log_success "Owner ready (ID: $OWNER_ID)" - -# Пробуем зарегистрировать участника -log_info "Creating participant..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$PARTICIPANT_EMAIL\",\"password\":\"$PARTICIPANT_PASSWORD\"}" "") - -if echo "$response" | grep -q "token"; then - PARTICIPANT_TOKEN=$(extract_json "$response" "token") - PARTICIPANT_ID=$(extract_json "$response" "id") - log_success "Participant registered: $PARTICIPANT_EMAIL" -else - log_info "Participant exists, trying login..." - response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$PARTICIPANT_EMAIL\",\"password\":\"$PARTICIPANT_PASSWORD\"}" "") - PARTICIPANT_TOKEN=$(extract_json "$response" "token") - PARTICIPANT_ID=$(extract_json "$response" "id") -fi - -if [ -z "$PARTICIPANT_TOKEN" ]; then - log_error "Failed to get participant token" - echo "$response" - exit 1 -fi -log_success "Participant ready (ID: $PARTICIPANT_ID)" - -echo "" -log_info "============================================================" -log_info "STEP 2: Create calendars" -log_info "============================================================" - -log_info "Creating AUTO calendar..." -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Auto Calendar\",\"confirmation\":\"auto\"}" "$OWNER_TOKEN") -AUTO_CALENDAR_ID=$(extract_json "$response" "id") -log_success "Auto calendar: $AUTO_CALENDAR_ID" - -log_info "Creating MANUAL calendar..." -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Manual Calendar\",\"confirmation\":\"manual\"}" "$OWNER_TOKEN") -MANUAL_CALENDAR_ID=$(extract_json "$response" "id") -log_success "Manual calendar: $MANUAL_CALENDAR_ID" - -echo "" -log_info "============================================================" -log_info "STEP 3: Create events" -log_info "============================================================" - -EVENT_START="2026-05-01T10:00:00Z" - -log_info "Creating event in AUTO calendar..." -response=$(http_post "$BASE_URL/v1/calendars/$AUTO_CALENDAR_ID/events" \ - "{\"title\":\"Auto Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") -AUTO_EVENT_ID=$(extract_json "$response" "id") -log_success "Auto event: $AUTO_EVENT_ID" - -log_info "Creating event in MANUAL calendar..." -response=$(http_post "$BASE_URL/v1/calendars/$MANUAL_CALENDAR_ID/events" \ - "{\"title\":\"Manual Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") -MANUAL_EVENT_ID=$(extract_json "$response" "id") -log_success "Manual event: $MANUAL_EVENT_ID" - -echo "" -log_info "============================================================" -log_info "STEP 4: Test AUTO confirmation" -log_info "============================================================" - -log_info "Participant booking AUTO event..." -response=$(http_post "$BASE_URL/v1/events/$AUTO_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") -echo "Response: $response" -AUTO_BOOKING_STATUS=$(extract_json "$response" "status") - -if [ "$AUTO_BOOKING_STATUS" = "confirmed" ]; then - log_success "Auto-booking confirmed immediately" -else - log_error "Auto-booking status: $AUTO_BOOKING_STATUS" -fi - -# Сохраняем ID авто-бронирования -AUTO_BOOKING_ID=$(extract_json "$response" "id") - -echo "" -log_info "============================================================" -log_info "STEP 5: Test MANUAL confirmation" -log_info "============================================================" - -log_info "Participant booking MANUAL event..." -response=$(http_post "$BASE_URL/v1/events/$MANUAL_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") -MANUAL_BOOKING_ID=$(extract_json "$response" "id") -MANUAL_BOOKING_STATUS=$(extract_json "$response" "status") - -if [ "$MANUAL_BOOKING_STATUS" = "pending" ]; then - log_success "Manual-booking is pending: $MANUAL_BOOKING_ID" -else - log_error "Manual-booking status: $MANUAL_BOOKING_STATUS" -fi - -log_info "Owner confirming booking..." -response=$(http_put "$BASE_URL/v1/bookings/$MANUAL_BOOKING_ID" "{\"action\":\"confirm\"}" "$OWNER_TOKEN") -CONFIRMED_STATUS=$(extract_json "$response" "status") - -if [ "$CONFIRMED_STATUS" = "confirmed" ]; then - log_success "Booking confirmed by owner" -else - log_error "Confirmation failed" -fi - -echo "" -log_info "============================================================" -log_info "STEP 6: Test booking lists" -log_info "============================================================" - -log_info "Owner viewing event bookings..." -response=$(http_get "$BASE_URL/v1/events/$MANUAL_EVENT_ID/bookings" "$OWNER_TOKEN") -echo "Response: $response" - -log_info "Participant viewing their bookings..." -response=$(http_get "$BASE_URL/v1/user/bookings" "$PARTICIPANT_TOKEN") -echo "Response: $response" - -echo "" -log_info "============================================================" -log_info "STEP 7: Test booking cancellation" -log_info "============================================================" - -# Используем первое бронирование для отмены -if [ -n "$AUTO_BOOKING_ID" ]; then - CANCEL_BOOKING_ID="$AUTO_BOOKING_ID" - log_info "Using auto-booking for cancellation: $CANCEL_BOOKING_ID" -else - # Создаём новое событие для теста отмены - log_info "Creating new event for cancellation test..." - response=$(http_post "$BASE_URL/v1/calendars/$MANUAL_CALENDAR_ID/events" \ - "{\"title\":\"Cancel Test Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") - CANCEL_EVENT_ID=$(extract_json "$response" "id") - log_info "Event created: $CANCEL_EVENT_ID" - - log_info "Creating booking to cancel..." - response=$(http_post "$BASE_URL/v1/events/$CANCEL_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") - CANCEL_BOOKING_ID=$(extract_json "$response" "id") - log_info "Created: $CANCEL_BOOKING_ID" -fi - -if [ -n "$CANCEL_BOOKING_ID" ]; then - log_info "Cancelling booking $CANCEL_BOOKING_ID..." - response=$(http_delete "$BASE_URL/v1/bookings/$CANCEL_BOOKING_ID" "$PARTICIPANT_TOKEN") - CANCELLED_STATUS=$(extract_json "$response" "status") - - if [ "$CANCELLED_STATUS" = "cancelled" ]; then - log_success "Booking cancelled" - else - log_error "Cancellation failed: $response" - fi -else - log_error "No booking to cancel" -fi - -echo "" -echo "============================================================" -log_success "TESTS COMPLETED!" -echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_calendar_api.sh b/test/scripts/test_calendar_api.sh deleted file mode 100644 index 585e911..0000000 --- a/test/scripts/test_calendar_api.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -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 CALENDAR API TEST SCRIPT" -echo "============================================================" -echo "" - -log_info "Setting up test users..." - -# Создаём двух пользователей -OWNER_EMAIL="calendar_owner_$(date +%s)@example.com" -OWNER_PASS="owner123" -OTHER_EMAIL="calendar_other_$(date +%s)@example.com" -OTHER_PASS="other123" - -# Владелец -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASS\"}" "") -OWNER_TOKEN=$(extract_json "$response" "token") -OWNER_ID=$(extract_json "$response" "id") -log_success "Owner created: $OWNER_ID" - -# Другой пользователь -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OTHER_EMAIL\",\"password\":\"$OTHER_PASS\"}" "") -OTHER_TOKEN=$(extract_json "$response" "token") -OTHER_ID=$(extract_json "$response" "id") -log_success "Other user created: $OTHER_ID" - -echo "" -log_info "============================================================" -log_info "TEST 1: Create calendar" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"My Personal Calendar\",\"description\":\"Test description\"}" "$OWNER_TOKEN") -CALENDAR_ID=$(extract_json "$response" "id") - -if [ -n "$CALENDAR_ID" ]; then - log_success "Calendar created: $CALENDAR_ID" -else - log_error "Calendar creation failed: $response" - exit 1 -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Create commercial calendar" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Commercial Calendar\",\"type\":\"commercial\"}" "$OWNER_TOKEN") -COMMERCIAL_ID=$(extract_json "$response" "id") -log_success "Commercial calendar created: $COMMERCIAL_ID" - -echo "" -log_info "============================================================" -log_info "TEST 3: List calendars (owner)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars" "$OWNER_TOKEN") -COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) -log_success "Owner sees $COUNT calendars" - -echo "" -log_info "============================================================" -log_info "TEST 4: List calendars (other user - empty)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars" "$OTHER_TOKEN") -COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) -log_success "Other user sees $COUNT calendars" - -echo "" -log_info "============================================================" -log_info "TEST 5: Get calendar by ID (owner)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "My Personal Calendar"; then - log_success "Owner can access personal calendar" -else - log_error "Owner cannot access calendar: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Get personal calendar (other user - denied)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OTHER_TOKEN") -if echo "$response" | grep -q "Access denied"; then - log_success "Other user correctly denied access to personal calendar" -else - log_error "Access control failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 7: Get commercial calendar (other user - allowed)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars/$COMMERCIAL_ID" "$OTHER_TOKEN") -if echo "$response" | grep -q "Commercial Calendar"; then - log_success "Other user can access commercial calendar" -else - log_error "Other user cannot access commercial calendar: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Update calendar (owner)" -log_info "============================================================" - -response=$(http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"title\":\"Updated Calendar\"}" "$OWNER_TOKEN") -if echo "$response" | grep -q "Updated Calendar"; then - log_success "Calendar updated successfully" -else - log_error "Calendar update failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Update calendar (other user - denied)" -log_info "============================================================" - -response=$(http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"title\":\"Hacked\"}" "$OTHER_TOKEN") -if echo "$response" | grep -q "Access denied"; then - log_success "Other user correctly denied update" -else - log_error "Access control failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Delete calendar (owner)" -log_info "============================================================" - -response=$(http_delete "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "deleted"; then - log_success "Calendar deleted" -else - log_error "Calendar deletion failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 11: Get deleted calendar (should be denied)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "Access denied"; then - log_success "Deleted calendar not accessible" -else - log_error "Deleted calendar still accessible: $response" -fi - -echo "" -echo "============================================================" -log_success "CALENDAR API TESTS COMPLETED!" -echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_event_api.sh b/test/scripts/test_event_api.sh deleted file mode 100644 index 54a5b32..0000000 --- a/test/scripts/test_event_api.sh +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -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 EVENT API TEST SCRIPT" -echo "============================================================" -echo "" - -log_info "Setting up test users and calendar..." - -OWNER_EMAIL="event_owner_$(date +%s)@example.com" -OWNER_PASS="owner123" - -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASS\"}" "") -OWNER_TOKEN=$(extract_json "$response" "token") -OWNER_ID=$(extract_json "$response" "id") -log_success "Owner created" - -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Test Calendar\"}" "$OWNER_TOKEN") -CALENDAR_ID=$(extract_json "$response" "id") -log_success "Calendar created: $CALENDAR_ID" - -echo "" -log_info "============================================================" -log_info "TEST 1: Create single event" -log_info "============================================================" - -EVENT_START="2026-06-01T10:00:00Z" -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"Single Event\",\"start_time\":\"$EVENT_START\",\"duration\":60}" "$OWNER_TOKEN") -EVENT_ID=$(extract_json "$response" "id") - -if [ -n "$EVENT_ID" ]; then - log_success "Single event created: $EVENT_ID" -else - log_error "Event creation failed: $response" - exit 1 -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Create event with capacity" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"Capacity Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") -CAPACITY_EVENT_ID=$(extract_json "$response" "id") -log_success "Event with capacity created: $CAPACITY_EVENT_ID" - -echo "" -log_info "============================================================" -log_info "TEST 3: Create event in past (should fail)" -log_info "============================================================" - -PAST_START="2020-01-01T10:00:00Z" -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"Past Event\",\"start_time\":\"$PAST_START\",\"duration\":60}" "$OWNER_TOKEN") -if echo "$response" | grep -q "past"; then - log_success "Past event correctly rejected" -else - log_error "Past event not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: List events" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID/events" "$OWNER_TOKEN") -COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) -log_success "Found $COUNT events" - -echo "" -log_info "============================================================" -log_info "TEST 5: Get event by ID" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "Single Event"; then - log_success "Event retrieved successfully" -else - log_error "Event retrieval failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Update event" -log_info "============================================================" - -response=$(http_put "$BASE_URL/v1/events/$EVENT_ID" "{\"title\":\"Updated Event\"}" "$OWNER_TOKEN") -if echo "$response" | grep -q "Updated Event"; then - log_success "Event updated" -else - log_error "Event update failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 7: Delete event" -log_info "============================================================" - -response=$(http_delete "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "deleted"; then - log_success "Event deleted" -else - log_error "Event deletion failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Get deleted event (should fail)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -if echo "$response" | grep -q "not found"; then - log_success "Deleted event not found" -else - log_error "Deleted event still accessible: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Create recurring event" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"Weekly Meeting\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"recurrence\":{\"freq\":\"WEEKLY\",\"interval\":1}}" "$OWNER_TOKEN") -RECURRING_ID=$(extract_json "$response" "id") - -if [ -n "$RECURRING_ID" ]; then - log_success "Recurring event created: $RECURRING_ID" -else - log_error "Recurring event creation failed: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Get occurrences" -log_info "============================================================" - -FROM="2026-06-01T00:00:00Z" -TO="2026-06-30T00:00:00Z" -response=$(http_get "$BASE_URL/v1/events/$RECURRING_ID/occurrences?from=$FROM&to=$TO" "$OWNER_TOKEN") -if [ -n "$response" ] && [ "$response" != "[]" ]; then - log_success "Occurrences retrieved" -else - log_error "Occurrences retrieval failed: $response" -fi - -echo "" -echo "============================================================" -log_success "EVENT API TESTS COMPLETED!" -echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_moderation_api.sh b/test/scripts/test_moderation_api.sh deleted file mode 100644 index 3387bc7..0000000 --- a/test/scripts/test_moderation_api.sh +++ /dev/null @@ -1,370 +0,0 @@ -#!/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 diff --git a/test/scripts/test_reviews_api.sh b/test/scripts/test_reviews_api.sh deleted file mode 100644 index 9863273..0000000 --- a/test/scripts/test_reviews_api.sh +++ /dev/null @@ -1,454 +0,0 @@ -#!/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/\"$//" -} - -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 REVIEWS 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="admin_$(date +%s)@example.com" -ADMIN_PASSWORD="admin123" - -log_info "Creating admin user (first 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_EMAIL" - -# Владелец календаря -OWNER_EMAIL="review_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: $OWNER_EMAIL" - -# Участник 1 -PARTICIPANT1_EMAIL="review_p1_$(date +%s)@example.com" -PARTICIPANT1_PASSWORD="p1_123" - -log_info "Creating participant 1..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$PARTICIPANT1_EMAIL\",\"password\":\"$PARTICIPANT1_PASSWORD\"}" "") -PARTICIPANT1_TOKEN=$(extract_json "$response" "token") -PARTICIPANT1_ID=$(extract_json "$response" "id") -log_success "Participant 1 created" - -# Участник 2 -PARTICIPANT2_EMAIL="review_p2_$(date +%s)@example.com" -PARTICIPANT2_PASSWORD="p2_123" - -log_info "Creating participant 2..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$PARTICIPANT2_EMAIL\",\"password\":\"$PARTICIPANT2_PASSWORD\"}" "") -PARTICIPANT2_TOKEN=$(extract_json "$response" "token") -PARTICIPANT2_ID=$(extract_json "$response" "id") -log_success "Participant 2 created" - -# Сторонний пользователь (без бронирований) -OTHER_EMAIL="review_other_$(date +%s)@example.com" -OTHER_PASSWORD="other123" - -log_info "Creating other user (no bookings)..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OTHER_EMAIL\",\"password\":\"$OTHER_PASSWORD\"}" "") -OTHER_TOKEN=$(extract_json "$response" "token") -OTHER_ID=$(extract_json "$response" "id") -log_success "Other user created" - -echo "" -log_info "============================================================" -log_info "STEP 2: Create calendar and events" -log_info "============================================================" - -log_info "Creating calendar..." -response=$(http_post "$BASE_URL/v1/calendars" \ - "{\"title\":\"Review Test Calendar\",\"description\":\"Calendar for review tests\"}" "$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,\"capacity\":10}" "$OWNER_TOKEN") -EVENT_ID=$(extract_json "$response" "id") -log_success "Event created: $EVENT_ID" - -log_info "Creating second event..." -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"Test Event 2\",\"start_time\":\"$EVENT_START\",\"duration\":60}" "$OWNER_TOKEN") -EVENT2_ID=$(extract_json "$response" "id") -log_success "Second event created: $EVENT2_ID" - -echo "" -log_info "============================================================" -log_info "STEP 3: Create bookings" -log_info "============================================================" - -log_info "Participant 1 booking event..." -response=$(http_post "$BASE_URL/v1/events/$EVENT_ID/bookings" "" "$PARTICIPANT1_TOKEN") -BOOKING1_ID=$(extract_json "$response" "id") -log_success "Booking created: $BOOKING1_ID" - -log_info "Owner confirming participant 1 booking..." -response=$(http_put "$BASE_URL/v1/bookings/$BOOKING1_ID" "{\"action\":\"confirm\"}" "$OWNER_TOKEN") -log_success "Booking confirmed" - -log_info "Participant 2 booking event..." -response=$(http_post "$BASE_URL/v1/events/$EVENT_ID/bookings" "" "$PARTICIPANT2_TOKEN") -BOOKING2_ID=$(extract_json "$response" "id") -log_success "Booking created: $BOOKING2_ID" - -log_info "Owner confirming participant 2 booking..." -response=$(http_put "$BASE_URL/v1/bookings/$BOOKING2_ID" "{\"action\":\"confirm\"}" "$OWNER_TOKEN") -log_success "Booking confirmed" - -echo "" -log_info "============================================================" -log_info "TEST 1: Create review for event (participant)" -log_info "============================================================" - -log_info "Participant 1 creating review..." -response=$(http_post "$BASE_URL/v1/reviews" \ - "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"rating\":5,\"comment\":\"Excellent event!\"}" "$PARTICIPANT1_TOKEN") -REVIEW1_ID=$(extract_json "$response" "id") - -if [ -n "$REVIEW1_ID" ]; then - log_success "Review created: $REVIEW1_ID" -else - log_error "Failed to create review: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Create review for event (second participant)" -log_info "============================================================" - -log_info "Participant 2 creating review..." -response=$(http_post "$BASE_URL/v1/reviews" \ - "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"rating\":3,\"comment\":\"It was okay\"}" "$PARTICIPANT2_TOKEN") -REVIEW2_ID=$(extract_json "$response" "id") - -if [ -n "$REVIEW2_ID" ]; then - log_success "Review created: $REVIEW2_ID" -else - log_error "Failed to create review: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Cannot review twice" -log_info "============================================================" - -log_info "Participant 1 trying to review again..." -response=$(http_post "$BASE_URL/v1/reviews" \ - "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"rating\":4,\"comment\":\"Trying again\"}" "$PARTICIPANT1_TOKEN") - -if echo "$response" | grep -q "Already reviewed"; then - log_success "Duplicate review correctly rejected" -else - log_error "Duplicate review not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: Cannot review without booking" -log_info "============================================================" - -log_info "Other user trying to review event..." -response=$(http_post "$BASE_URL/v1/reviews" \ - "{\"target_type\":\"event\",\"target_id\":\"$EVENT_ID\",\"rating\":5,\"comment\":\"Wasn't there\"}" "$OTHER_TOKEN") - -if echo "$response" | grep -q "Cannot review"; then - log_success "Review without booking correctly rejected" -else - log_error "Review without booking not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 5: Create review for calendar" -log_info "============================================================" - -log_info "Other user creating calendar review..." -response=$(http_post "$BASE_URL/v1/reviews" \ - "{\"target_type\":\"calendar\",\"target_id\":\"$CALENDAR_ID\",\"rating\":4,\"comment\":\"Nice calendar!\"}" "$OTHER_TOKEN") -CALENDAR_REVIEW_ID=$(extract_json "$response" "id") - -if [ -n "$CALENDAR_REVIEW_ID" ]; then - log_success "Calendar review created: $CALENDAR_REVIEW_ID" -else - log_error "Failed to create calendar review: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Get reviews for event" -log_info "============================================================" - -log_info "Getting reviews for event..." -response=$(http_get "$BASE_URL/v1/reviews?target_type=event&target_id=$EVENT_ID" "$PARTICIPANT1_TOKEN") -REVIEW_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$REVIEW_COUNT" -eq 2 ]; then - log_success "Found $REVIEW_COUNT reviews for event" -else - log_error "Expected 2 reviews, found $REVIEW_COUNT" -fi - -sleep 1 - -echo "" -log_info "============================================================" -log_info "TEST 7: Check event rating updated" -log_info "============================================================" - -log_info "Checking event rating..." -response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -RATING_AVG=$(extract_json_number "$response" "rating_avg") -RATING_COUNT=$(extract_json_number "$response" "rating_count") - -if [ "$RATING_AVG" = "4.0" ] && [ "$RATING_COUNT" = "2" ]; then - log_success "Event rating updated: $RATING_AVG ($RATING_COUNT reviews)" -else - log_error "Event rating incorrect: avg=$RATING_AVG, count=$RATING_COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Update own review" -log_info "============================================================" - -log_info "Participant 1 updating review..." -response=$(http_put "$BASE_URL/v1/reviews/$REVIEW1_ID" \ - "{\"rating\":4,\"comment\":\"Updated: Very good!\"}" "$PARTICIPANT1_TOKEN") - -if echo "$response" | grep -q "\"id\""; then - log_success "Review updated" -else - log_error "Review update failed: $response" -fi - -sleep 1 - -log_info "Checking event rating after update..." -response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -NEW_RATING_AVG=$(extract_json_number "$response" "rating_avg") - -if [ "$NEW_RATING_AVG" = "3.5" ]; then - log_success "Event rating updated to $NEW_RATING_AVG" -else - log_error "Event rating incorrect: $NEW_RATING_AVG (expected 3.5)" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Cannot update others review" -log_info "============================================================" - -log_info "Participant 2 trying to update participant 1 review..." -response=$(http_put "$BASE_URL/v1/reviews/$REVIEW1_ID" \ - "{\"rating\":1,\"comment\":\"Hacked!\"}" "$PARTICIPANT2_TOKEN") - -if echo "$response" | grep -q "Access denied"; then - log_success "Update others review correctly rejected" -else - log_error "Update others review not rejected: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Get user reviews" -log_info "============================================================" - -log_info "Getting participant 1 reviews..." -response=$(http_get "$BASE_URL/v1/user/reviews" "$PARTICIPANT1_TOKEN") -USER_REVIEW_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$USER_REVIEW_COUNT" -ge 1 ]; then - log_success "Found $USER_REVIEW_COUNT reviews for user" -else - log_error "User reviews not found" -fi - -echo "" -log_info "============================================================" -log_info "TEST 11: Admin hides review" -log_info "============================================================" - -log_info "Admin hiding review $REVIEW2_ID..." -response=$(http_put "$BASE_URL/v1/admin/reviews/$REVIEW2_ID" \ - "{\"action\":\"hide\"}" "$ADMIN_TOKEN") - -HIDDEN_STATUS=$(extract_json "$response" "status") -if [ "$HIDDEN_STATUS" = "hidden" ]; then - log_success "Review hidden by admin" -else - log_error "Failed to hide review: $response" -fi - -log_info "Participant 1 getting event reviews (hidden should not appear)..." -response=$(http_get "$BASE_URL/v1/reviews?target_type=event&target_id=$EVENT_ID" "$PARTICIPANT1_TOKEN") -VISIBLE_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$VISIBLE_COUNT" -eq 1 ]; then - log_success "Only 1 review visible (hidden filtered out)" -else - log_error "Expected 1 visible review, found $VISIBLE_COUNT" -fi - -log_info "Admin getting event reviews (should see all)..." -response=$(http_get "$BASE_URL/v1/reviews?target_type=event&target_id=$EVENT_ID" "$ADMIN_TOKEN") -ADMIN_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$ADMIN_COUNT" -eq 2 ]; then - log_success "Admin sees all 2 reviews" -else - log_error "Admin should see 2 reviews, found $ADMIN_COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 12: Admin unhides review" -log_info "============================================================" - -log_info "Admin unhiding review..." -response=$(http_put "$BASE_URL/v1/admin/reviews/$REVIEW2_ID" \ - "{\"action\":\"unhide\"}" "$ADMIN_TOKEN") - -UNHIDDEN_STATUS=$(extract_json "$response" "status") -if [ "$UNHIDDEN_STATUS" = "visible" ]; then - log_success "Review unhidden by admin" -else - log_error "Failed to unhide review: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 13: Delete own review" -log_info "============================================================" - -log_info "Participant 1 deleting review..." -response=$(http_delete "$BASE_URL/v1/reviews/$REVIEW1_ID" "$PARTICIPANT1_TOKEN") - -if echo "$response" | grep -q "deleted"; then - log_success "Review deleted" -else - log_error "Failed to delete review: $response" -fi - -sleep 1 - -log_info "Checking event rating after deletion..." -response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") -FINAL_RATING_AVG=$(extract_json_number "$response" "rating_avg") -FINAL_RATING_COUNT=$(extract_json_number "$response" "rating_count") - -if [ "$FINAL_RATING_AVG" = "3.0" ] && [ "$FINAL_RATING_COUNT" = "1" ]; then - log_success "Event rating updated: $FINAL_RATING_AVG ($FINAL_RATING_COUNT review)" -else - log_error "Event rating incorrect: avg=$FINAL_RATING_AVG, count=$FINAL_RATING_COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 14: Get deleted review (should fail)" -log_info "============================================================" - -log_info "Trying to get deleted review..." -response=$(http_get "$BASE_URL/v1/reviews/$REVIEW1_ID" "$PARTICIPANT1_TOKEN") - -if echo "$response" | grep -q "not found"; then - log_success "Deleted review not found" -else - log_error "Deleted review still accessible: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 15: Calendar rating updated" -log_info "============================================================" - -log_info "Checking calendar rating..." -response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") -CAL_RATING_AVG=$(extract_json_number "$response" "rating_avg") -CAL_RATING_COUNT=$(extract_json_number "$response" "rating_count") - -if [ "$CAL_RATING_AVG" = "4.0" ] && [ "$CAL_RATING_COUNT" = "1" ]; then - log_success "Calendar rating: $CAL_RATING_AVG ($CAL_RATING_COUNT review)" -else - log_error "Calendar rating incorrect: avg=$CAL_RATING_AVG, count=$CAL_RATING_COUNT" -fi - -echo "" -echo "============================================================" -log_success "REVIEWS API TESTS COMPLETED!" -echo "============================================================" -echo "" -echo "Summary of created resources:" -echo " Admin: $ADMIN_EMAIL" -echo " Owner: $OWNER_EMAIL" -echo " Participant 1: $PARTICIPANT1_EMAIL" -echo " Participant 2: $PARTICIPANT2_EMAIL" -echo " Calendar: $CALENDAR_ID" -echo " Event: $EVENT_ID" -echo "" \ No newline at end of file diff --git a/test/scripts/test_runner.sh b/test/scripts/test_runner.sh deleted file mode 100644 index 1aacb2a..0000000 --- a/test/scripts/test_runner.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPTS_DIR/../.." && pwd)" -BASE_URL="http://localhost:8080" - -SERVER_STARTED=false -SERVER_PID="" - -# ============================================================================ -# Функции -# ============================================================================ -cleanup() { - if [ "$SERVER_STARTED" = true ] && [ -n "$SERVER_PID" ]; then - echo "" - echo -e "${BLUE}[INFO]${NC} Stopping server..." - kill "$SERVER_PID" 2>/dev/null - wait "$SERVER_PID" 2>/dev/null - pkill -f "beam.*eventhub_test" 2>/dev/null || true - fi -} - -trap cleanup EXIT INT TERM - -usage() { - echo "Usage: $0 [options] [pattern]" - echo "" - echo "Options:" - echo " -h, --help Show this help" - echo " -l, --list List available test scripts" - echo " -v, --verbose Verbose output" - echo " -s, --server Use existing server (don't start/stop)" - echo "" - echo "Examples:" - echo " $0 Run all tests" - echo " $0 auth Run tests matching 'auth'" - echo " $0 booking Run tests matching 'booking'" - echo " $0 -l List all test scripts" - echo " $0 -s Use already running server" -} - -list_scripts() { - echo "Available test scripts:" - find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort | while read script; do - name=$(basename "$script") - echo " - $name" - done -} - -start_server() { - echo -e "${CYAN}[STEP]${NC} Starting EventHub server..." - - # Переходим в корень проекта - cd "$PROJECT_ROOT" - - # Проверяем, что мы в правильной директории - if [ ! -f "rebar.config" ]; then - echo -e "${RED}[ERROR]${NC} rebar.config not found in $(pwd)" - return 1 - fi - - echo -e "${BLUE}[INFO]${NC} Project root: $(pwd)" - - # Компилируем если нужно - if [ ! -d "_build" ]; then - echo -e "${BLUE}[INFO]${NC} Compiling project..." - rebar3 compile - fi - - # Запускаем сервер - LOG_FILE="/tmp/eventhub_test_server.log" - echo -e "${BLUE}[INFO]${NC} Starting server, log: $LOG_FILE" - - rebar3 shell --sname eventhub_test > "$LOG_FILE" 2>&1 & - SERVER_PID=$! - - echo -e "${BLUE}[INFO]${NC} Server PID: $SERVER_PID" - - # Ждём готовности - for i in {1..30}; do - echo -n "." - if curl -s "http://localhost:8080/health" 2>/dev/null | grep -q "ok"; then - echo "" - echo -e "${GREEN}[SUCCESS]${NC} Server ready (took $i seconds)" - return 0 - fi - - # Проверяем, не умер ли процесс - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - echo "" - echo -e "${RED}[ERROR]${NC} Server process died" - echo -e "${YELLOW}[INFO]${NC} Last 20 lines of log:" - tail -20 "$LOG_FILE" - return 1 - fi - - sleep 1 - done - - echo "" - echo -e "${RED}[ERROR]${NC} Server failed to start within 30 seconds" - echo -e "${YELLOW}[INFO]${NC} Last 20 lines of log:" - tail -20 "$LOG_FILE" - return 1 -} - -# ============================================================================ -# Парсинг аргументов -# ============================================================================ -VERBOSE=false -USE_EXISTING=false -PATTERN="" - -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - usage - exit 0 - ;; - -l|--list) - list_scripts - exit 0 - ;; - -v|--verbose) - VERBOSE=true - shift - ;; - -s|--server) - USE_EXISTING=true - shift - ;; - *) - PATTERN="$1" - shift - ;; - esac -done - -# ============================================================================ -# Главная логика -# ============================================================================ -echo "============================================================" -echo " EVENTHUB TEST RUNNER" -echo "============================================================" -echo "" - -if [ "$USE_EXISTING" = false ]; then - if ! start_server; then - exit 1 - fi - SERVER_STARTED=true -else - if ! curl -s "$BASE_URL/health" | grep -q "ok"; then - echo -e "${RED}[ERROR]${NC} Server is not running" - exit 1 - fi - echo -e "${GREEN}[SUCCESS]${NC} Using existing server" -fi - -echo "" - -# Находим тесты -if [ -n "$PATTERN" ]; then - TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*${PATTERN}*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort) -else - TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort) -fi - -if [ -z "$TEST_SCRIPTS" ]; then - echo -e "${YELLOW}[WARNING]${NC} No test scripts found" - exit 0 -fi - -echo -e "${BLUE}[INFO]${NC} Running tests:" -for script in $TEST_SCRIPTS; do - echo " - $(basename "$script")" -done -echo "" - -PASSED=0 -FAILED=0 - -for script in $TEST_SCRIPTS; do - script_name=$(basename "$script") - echo "============================================================" - echo -e "${CYAN}[RUNNING]${NC} $script_name" - echo "============================================================" - - if $VERBOSE; then - bash "$script" - EXIT_CODE=$? - else - bash "$script" - EXIT_CODE=$? - fi - - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[PASSED]${NC} $script_name" - ((PASSED++)) - else - echo -e "${RED}[FAILED]${NC} $script_name" - ((FAILED++)) - fi - echo "" -done - -echo "============================================================" -echo " TEST SUMMARY" -echo "============================================================" -echo -e "Scripts run: $((PASSED + FAILED))" -echo -e "${GREEN}Passed: $PASSED${NC}" -echo -e "${RED}Failed: $FAILED${NC}" -echo "============================================================" - -if [ $FAILED -eq 0 ]; then - echo -e "${GREEN}🎉 ALL TESTS PASSED!${NC}" - exit 0 -else - echo -e "${RED}❌ SOME TESTS FAILED${NC}" - exit 1 -fi \ No newline at end of file diff --git a/test/scripts/test_search_api.sh b/test/scripts/test_search_api.sh deleted file mode 100644 index 8e22f36..0000000 --- a/test/scripts/test_search_api.sh +++ /dev/null @@ -1,393 +0,0 @@ -#!/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"; } -log_warning() { echo -e "${YELLOW}[WARNING]${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" -} - -url_encode() { - echo -n "$1" | sed 's/ /%20/g;s/,/%2C/g' -} - -echo "============================================================" -echo " EVENTHUB SEARCH 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 "============================================================" - -OWNER_EMAIL="search_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") - -if [ -z "$OWNER_TOKEN" ]; then - log_error "Failed to create owner" - exit 1 -fi -log_success "Owner created: $OWNER_EMAIL" - -echo "" -log_info "============================================================" -log_info "STEP 2: Create calendar with tags" -log_info "============================================================" - -log_info "Creating calendar..." -response=$(http_post "$BASE_URL/v1/calendars" \ - "{\"title\":\"Tech Events Calendar\",\"description\":\"Calendar for technology events and workshops\",\"tags\":[\"tech\",\"programming\",\"workshop\"]}" "$OWNER_TOKEN") -CALENDAR_ID=$(extract_json "$response" "id") -log_success "Calendar created with tags: $CALENDAR_ID" - -# Добавляем теги через обновление -http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"tags\":[\"tech\",\"programming\",\"workshop\"]}" "$OWNER_TOKEN" > /dev/null -log_success "Calendar created with tags: $CALENDAR_ID" - -echo "" -log_info "============================================================" -log_info "STEP 3: Create events with different properties" -log_info "============================================================" - -# Функция для создания события -create_event() { - local title=$1 - local description=$2 - local start_time=$3 - local tags=$4 - local lat=$5 - local lon=$6 - local address=$7 - - local location_json="null" - if [ -n "$lat" ] && [ -n "$lon" ]; then - location_json="{\"address\":\"$address\",\"lat\":$lat,\"lon\":$lon}" - fi - - local tags_json="[]" - if [ -n "$tags" ]; then - tags_json="$tags" - fi - - local data="{\"title\":\"$title\",\"description\":\"$description\",\"start_time\":\"$start_time\",\"duration\":60,\"tags\":$tags_json" - if [ "$location_json" != "null" ]; then - data="$data,\"location\":$location_json" - fi - data="$data}" - - response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" "$data" "$OWNER_TOKEN") - local event_id=$(extract_json "$response" "id") - - echo "$event_id" -} - -log_info "Creating Python Workshop event..." -WORKSHOP_ID=$(create_event "Python Workshop" "Learn Python programming basics" "2026-06-01T10:00:00Z" \ - "[\"python\",\"workshop\",\"programming\"]" "55.7558" "37.6173" "Moscow, Russia") -log_success "Created: $WORKSHOP_ID" - -log_info "Creating JavaScript Conference event..." -JS_ID=$(create_event "JavaScript Conference" "Annual JS conference for developers" "2026-06-15T09:00:00Z" \ - "[\"javascript\",\"conference\",\"web\"]" "55.7558" "37.6173" "Moscow, Russia") -log_success "Created: $JS_ID" - -log_info "Creating Yoga Class event (no tags)..." -YOGA_ID=$(create_event "Yoga Class" "Morning yoga session" "2026-06-10T08:00:00Z" \ - "" "" "" "") -log_success "Created: $YOGA_ID" - -log_info "Creating Tech Meetup in another city..." -MEETUP_ID=$(create_event "Tech Meetup" "Networking for tech professionals" "2026-06-20T18:00:00Z" \ - "[\"networking\",\"tech\"]" "59.9343" "30.3351" "Saint Petersburg, Russia") -log_success "Created: $MEETUP_ID" - -log_info "Creating past event..." -PAST_ID=$(create_event "Past Event" "This event already happened" "2020-01-01T10:00:00Z" \ - "[\"past\"]" "" "" "") -log_success "Created: $PAST_ID" - -echo "" -log_info "============================================================" -log_info "TEST 1: Search by text query" -log_info "============================================================" - -log_info "Searching for 'Python'..." -response=$(http_get "$BASE_URL/v1/search?type=event&q=Python" "$OWNER_TOKEN") -if echo "$response" | grep -q "Python Workshop"; then - log_success "Found Python Workshop" -else - log_error "Python Workshop not found" -fi - -log_info "Searching for 'conference'..." -response=$(http_get "$BASE_URL/v1/search?type=event&q=conference" "$OWNER_TOKEN") -if echo "$response" | grep -q "JavaScript Conference"; then - log_success "Found JavaScript Conference" -else - log_error "JavaScript Conference not found" -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Search by tags" -log_info "============================================================" - -log_info "Searching for events with tag 'python'..." -response=$(http_get "$BASE_URL/v1/search?type=event&tags=python" "$OWNER_TOKEN") -if echo "$response" | grep -q "Python Workshop"; then - log_success "Found Python Workshop by tag" -else - log_error "Python Workshop not found by tag" -fi - -log_info "Searching for multiple tags 'tech,workshop'..." -response=$(http_get "$BASE_URL/v1/search?type=event&tags=tech,workshop" "$OWNER_TOKEN") -log_success "Multiple tag search completed" - -log_info "Searching for tag 'yoga' (should be empty)..." -response=$(http_get "$BASE_URL/v1/search?type=event&tags=yoga" "$OWNER_TOKEN") -if echo "$response" | grep -q '"total":0'; then - log_success "Yoga tag correctly returned no results (no tags on event)" -else - log_warning "Yoga event has no tags, but might appear in results" -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Search by date range" -log_info "============================================================" - -log_info "Searching events in June 2026..." -FROM="2026-06-01T00:00:00Z" -TO="2026-06-30T23:59:59Z" -response=$(http_get "$BASE_URL/v1/search?type=event&from=$FROM&to=$TO" "$OWNER_TOKEN") - -if echo "$response" | grep -q "Python Workshop"; then - log_success "Found June events" -else - log_error "June events not found" -fi - -log_info "Searching past events only..." -FROM="2019-01-01T00:00:00Z" -TO="2021-01-01T00:00:00Z" -response=$(http_get "$BASE_URL/v1/search?type=event&from=$FROM&to=$TO" "$OWNER_TOKEN") -if echo "$response" | grep -q "Past Event"; then - log_success "Found past event" -else - log_error "Past event not found" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: Geo-location search" -log_info "============================================================" - -log_info "Searching events within 5km of Moscow center..." -response=$(http_get "$BASE_URL/v1/search?type=event&lat=55.7558&lon=37.6173&radius=5" "$OWNER_TOKEN") - -if echo "$response" | grep -q "Python Workshop"; then - log_success "Found Moscow events" -else - log_error "Moscow events not found" -fi - -log_info "Searching events within 1km of Moscow (should find fewer)..." -response=$(http_get "$BASE_URL/v1/search?type=event&lat=55.7558&lon=37.6173&radius=1" "$OWNER_TOKEN") -log_success "Radius search completed" - -log_info "Searching events in Saint Petersburg..." -response=$(http_get "$BASE_URL/v1/search?type=event&lat=59.9343&lon=30.3351&radius=10" "$OWNER_TOKEN") -if echo "$response" | grep -q "Tech Meetup"; then - log_success "Found Saint Petersburg event" -else - log_error "Saint Petersburg event not found" -fi - -echo "" -log_info "============================================================" -log_info "TEST 5: Combined search" -log_info "============================================================" - -log_info "Search: text 'Python' + tag 'workshop'..." -response=$(http_get "$BASE_URL/v1/search?type=event&q=Python&tags=workshop" "$OWNER_TOKEN") -if echo "$response" | grep -q "Python Workshop"; then - log_success "Combined text+tag search successful" -else - log_error "Combined search failed" -fi - -log_info "Search: tag 'javascript' + date range..." -FROM="2026-06-01T00:00:00Z" -TO="2026-06-30T23:59:59Z" -response=$(http_get "$BASE_URL/v1/search?type=event&tags=javascript&from=$FROM&to=$TO" "$OWNER_TOKEN") -if echo "$response" | grep -q "JavaScript Conference"; then - log_success "Combined tag+date search successful" -else - log_error "Combined search failed" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Pagination" -log_info "============================================================" - -log_info "Search with limit=2..." -response=$(http_get "$BASE_URL/v1/search?type=event&limit=2" "$OWNER_TOKEN") -COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) -if [ "$COUNT" -le 2 ]; then - log_success "Pagination limit works (got $COUNT results)" -else - log_error "Pagination limit failed (got $COUNT results)" -fi - -log_info "Search with offset=2..." -response=$(http_get "$BASE_URL/v1/search?type=event&limit=2&offset=2" "$OWNER_TOKEN") -log_success "Pagination offset works" - -echo "" -log_info "============================================================" -log_info "TEST 7: Sorting" -log_info "============================================================" - -log_info "Sort by start_time ascending..." -response=$(http_get "$BASE_URL/v1/search?type=event&sort=start_time&order=asc" "$OWNER_TOKEN") -log_success "Sort ascending completed" - -log_info "Sort by start_time descending..." -response=$(http_get "$BASE_URL/v1/search?type=event&sort=start_time&order=desc" "$OWNER_TOKEN") -log_success "Sort descending completed" - -echo "" -log_info "============================================================" -log_info "TEST 8: Calendar search" -log_info "============================================================" - -log_info "Searching calendars by text..." -response=$(http_get "$BASE_URL/v1/search?type=calendar&q=Tech" "$OWNER_TOKEN") -if echo "$response" | grep -q "Tech Events Calendar"; then - log_success "Found calendar by text" -else - log_error "Calendar not found by text" -fi - -log_info "Searching calendars by tag..." -response=$(http_get "$BASE_URL/v1/search?type=calendar&tags=tech" "$OWNER_TOKEN") -if echo "$response" | grep -q "Tech Events Calendar"; then - log_success "Found calendar by tag" -else - log_error "Calendar not found by tag" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Search all (events + calendars)" -log_info "============================================================" - -log_info "Searching all (no type specified)..." -response=$(http_get "$BASE_URL/v1/search?q=Tech" "$OWNER_TOKEN") -if echo "$response" | grep -q "events" && echo "$response" | grep -q "calendars"; then - log_success "All search returned both events and calendars" -else - log_warning "All search may not have returned both types" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Empty search results" -log_info "============================================================" - -log_info "Searching for non-existent text..." -response=$(http_get "$BASE_URL/v1/search?type=event&q=nonexistenttext12345" "$OWNER_TOKEN") -if echo "$response" | grep -q '"total":0'; then - log_success "Empty search handled correctly" -else - log_error "Empty search not handled correctly" -fi - -echo "" -log_info "============================================================" -log_info "TEST 11: Commercial calendar visibility" -log_info "============================================================" - -log_info "Creating commercial calendar..." -response=$(http_post "$BASE_URL/v1/calendars" \ - "{\"title\":\"Public Commercial Calendar\",\"type\":\"commercial\"}" "$OWNER_TOKEN") -COMMERCIAL_ID=$(extract_json "$response" "id") -log_success "Commercial calendar created: $COMMERCIAL_ID" - -log_info "Creating event in commercial calendar..." -response=$(http_post "$BASE_URL/v1/calendars/$COMMERCIAL_ID/events" \ - "{\"title\":\"Public Event\",\"start_time\":\"2026-06-01T10:00:00Z\",\"duration\":60}" "$OWNER_TOKEN") -log_success "Public event created" - -log_info "Creating another user to test visibility..." -OTHER_EMAIL="search_other_$(date +%s)@example.com" -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OTHER_EMAIL\",\"password\":\"test123\"}" "") -OTHER_TOKEN=$(extract_json "$response" "token") - -log_info "Other user searching for public event..." -response=$(http_get "$BASE_URL/v1/search?type=event&q=Public" "$OTHER_TOKEN") -if echo "$response" | grep -q "Public Event"; then - log_success "Other user can see public event in commercial calendar" -else - log_error "Other user cannot see public event" -fi - -echo "" -echo "============================================================" -log_success "SEARCH API TESTS COMPLETED!" -echo "============================================================" -echo "" -echo "Summary of created resources:" -echo " Owner: $OWNER_EMAIL" -echo " Calendar: $CALENDAR_ID" -echo " Commercial Calendar: $COMMERCIAL_ID" -echo " Events created: 5" -echo "" \ No newline at end of file diff --git a/test/scripts/test_subscription_api.sh b/test/scripts/test_subscription_api.sh deleted file mode 100644 index 5463254..0000000 --- a/test/scripts/test_subscription_api.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -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 -} - -echo "============================================================" -echo " EVENTHUB SUBSCRIPTION 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 "============================================================" - -# Пользователь 1 (будет использовать пробный период через commercial календарь) -USER1_EMAIL="sub_user1_$(date +%s)@example.com" -USER1_PASSWORD="user123" - -log_info "Creating user 1..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER1_EMAIL\",\"password\":\"$USER1_PASSWORD\"}" "") -USER1_TOKEN=$(extract_json "$response" "token") -USER1_ID=$(extract_json "$response" "id") -log_success "User 1 created" - -# Пользователь 2 (для проверки, что пробный период используется один раз) -USER2_EMAIL="sub_user2_$(date +%s)@example.com" -USER2_PASSWORD="user123" - -log_info "Creating user 2..." -response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$USER2_EMAIL\",\"password\":\"$USER2_PASSWORD\"}" "") -USER2_TOKEN=$(extract_json "$response" "token") -USER2_ID=$(extract_json "$response" "id") -log_success "User 2 created" - -echo "" -log_info "============================================================" -log_info "TEST 1: Get subscription (free)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/subscription" "$USER1_TOKEN") -if echo "$response" | grep -q "free"; then - log_success "User has free subscription" -else - log_error "Expected free subscription: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Create personal calendar (should work with free)" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Personal Calendar\",\"type\":\"personal\"}" "$USER1_TOKEN") -PERSONAL_CALENDAR_ID=$(extract_json "$response" "id") - -if [ -n "$PERSONAL_CALENDAR_ID" ]; then - log_success "Personal calendar created (free subscription allows this)" -else - log_error "Failed to create personal calendar: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Create commercial calendar (auto-activates trial)" -log_info "============================================================" - -log_info "User 1 creating commercial calendar (should auto-start trial)..." -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Commercial Calendar\",\"type\":\"commercial\"}" "$USER1_TOKEN") -COMMERCIAL_CALENDAR_ID=$(extract_json "$response" "id") - -if [ -n "$COMMERCIAL_CALENDAR_ID" ]; then - log_success "Commercial calendar created - trial auto-activated" -else - log_error "Failed to create commercial calendar: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: Get subscription (should be trial now)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/subscription" "$USER1_TOKEN") -if echo "$response" | grep -q "trial"; then - log_success "User now has trial subscription" -else - log_error "Expected trial subscription: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 5: Create second commercial calendar (should still work)" -log_info "============================================================" - -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Second Commercial\",\"type\":\"commercial\"}" "$USER1_TOKEN") -SECOND_COMMERCIAL_ID=$(extract_json "$response" "id") - -if [ -n "$SECOND_COMMERCIAL_ID" ]; then - log_success "Second commercial calendar created" -else - log_error "Failed to create second commercial calendar: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: User 2 creates commercial calendar (gets trial)" -log_info "============================================================" - -log_info "User 2 creating commercial calendar..." -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"User2 Commercial\",\"type\":\"commercial\"}" "$USER2_TOKEN") -USER2_CALENDAR_ID=$(extract_json "$response" "id") - -if [ -n "$USER2_CALENDAR_ID" ]; then - log_success "User 2 commercial calendar created - trial activated" -else - log_error "User 2 failed: $response" -fi - -response=$(http_get "$BASE_URL/v1/subscription" "$USER2_TOKEN") -if echo "$response" | grep -q "trial"; then - log_success "User 2 has trial subscription" -else - log_error "User 2 subscription: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 7: Activate paid subscription" -log_info "============================================================" - -log_info "User 1 activating paid subscription..." -response=$(http_post "$BASE_URL/v1/subscription" "{\"action\":\"activate\",\"plan\":\"monthly\",\"payment_info\":{\"card\":\"4242424242424242\"}}" "$USER1_TOKEN") -if echo "$response" | grep -q "monthly"; then - log_success "Paid subscription activated" -else - log_error "Failed to activate: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Get subscription (should be active paid)" -log_info "============================================================" - -response=$(http_get "$BASE_URL/v1/subscription" "$USER1_TOKEN") -if echo "$response" | grep -q "active" && echo "$response" | grep -q "monthly"; then - log_success "User has active paid subscription" -else - log_error "Expected active paid subscription: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: User 3 (free) tries to use already consumed trial" -log_info "============================================================" - -# Создаём пользователя 3, который сначала использует trial, потом отменяет подписку -USER3_EMAIL="sub_user3_$(date +%s)@example.com" -USER3_PASSWORD="user123" - -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" - -log_info "User 3 creating commercial calendar (uses trial)..." -response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"User3 Commercial\",\"type\":\"commercial\"}" "$USER3_TOKEN") -USER3_CALENDAR_ID=$(extract_json "$response" "id") -log_success "Commercial calendar created" - -log_info "Simulating trial expiration (requires admin or time travel - skipped)" - -echo "" -echo "============================================================" -log_success "SUBSCRIPTION API TESTS COMPLETED!" -echo "============================================================" -echo "" -echo "Summary:" -echo " User 1: $USER1_EMAIL (trial -> paid)" -echo " User 2: $USER2_EMAIL (trial)" -echo " User 3: $USER3_EMAIL (trial)" -echo "" \ No newline at end of file diff --git a/test/scripts/test_tickets_api.sh b/test/scripts/test_tickets_api.sh deleted file mode 100644 index 73f3a81..0000000 --- a/test/scripts/test_tickets_api.sh +++ /dev/null @@ -1,282 +0,0 @@ -#!/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/\"$//" -} - -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" -} - -echo "============================================================" -echo " EVENTHUB TICKETS 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="ticket_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" - -# Обычный пользователь -USER_EMAIL="ticket_user_$(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" - -echo "" -log_info "============================================================" -log_info "TEST 1: Report error (user)" -log_info "============================================================" - -log_info "User reporting error..." -response=$(http_post "$BASE_URL/v1/tickets" \ - "{\"error_message\":\"Test error occurred\",\"stacktrace\":\"line 1\\nline 2\",\"context\":{\"user_id\":\"$USER_ID\"}}" "$USER_TOKEN") -TICKET1_ID=$(extract_json "$response" "id") - -if [ -n "$TICKET1_ID" ]; then - log_success "Ticket created: $TICKET1_ID" -else - log_error "Failed to create ticket: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 2: Report same error again (should increment count)" -log_info "============================================================" - -log_info "User reporting same error..." -response=$(http_post "$BASE_URL/v1/tickets" \ - "{\"error_message\":\"Test error occurred\",\"stacktrace\":\"line 1\\nline 2\"}" "$USER_TOKEN") -COUNT=$(extract_json_number "$response" "count") - -if [ "$COUNT" -eq 2 ]; then - log_success "Ticket count incremented to $COUNT" -else - log_error "Count should be 2, got $COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Report different error" -log_info "============================================================" - -log_info "User reporting different error..." -response=$(http_post "$BASE_URL/v1/tickets" \ - "{\"error_message\":\"Another error\"}" "$USER_TOKEN") -TICKET2_ID=$(extract_json "$response" "id") - -if [ -n "$TICKET2_ID" ] && [ "$TICKET2_ID" != "$TICKET1_ID" ]; then - log_success "New ticket created: $TICKET2_ID" -else - log_error "Failed to create new ticket" -fi - -echo "" -log_info "============================================================" -log_info "TEST 4: Admin views all tickets" -log_info "============================================================" - -log_info "Admin getting all tickets..." -response=$(http_get "$BASE_URL/v1/admin/tickets" "$ADMIN_TOKEN") -TICKET_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$TICKET_COUNT" -eq 2 ]; then - log_success "Admin sees $TICKET_COUNT tickets" -else - log_error "Admin should see 2 tickets, found $TICKET_COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 5: User cannot view tickets" -log_info "============================================================" - -log_info "User trying to view tickets..." -response=$(http_get "$BASE_URL/v1/admin/tickets" "$USER_TOKEN") - -if echo "$response" | grep -q "Admin access required"; then - log_success "User correctly denied access" -else - log_error "User should be denied: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 6: Admin views tickets by status" -log_info "============================================================" - -log_info "Admin getting open tickets..." -response=$(http_get "$BASE_URL/v1/admin/tickets?status=open" "$ADMIN_TOKEN") -OPEN_COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) - -if [ "$OPEN_COUNT" -eq 2 ]; then - log_success "Found $OPEN_COUNT open tickets" -else - log_error "Should find 2 open tickets, found $OPEN_COUNT" -fi - -echo "" -log_info "============================================================" -log_info "TEST 7: Admin updates ticket status" -log_info "============================================================" - -log_info "Admin marking ticket as in_progress..." -response=$(http_put "$BASE_URL/v1/admin/tickets/$TICKET1_ID" \ - "{\"action\":\"status\",\"status\":\"in_progress\"}" "$ADMIN_TOKEN") - -STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') -if [ "$STATUS" = "in_progress" ]; then - log_success "Ticket status updated to in_progress" -else - log_error "Failed to update status: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 8: Admin assigns ticket" -log_info "============================================================" - -log_info "Admin assigning ticket..." -response=$(http_put "$BASE_URL/v1/admin/tickets/$TICKET1_ID" \ - "{\"action\":\"assign\",\"admin_id\":\"$ADMIN_ID\"}" "$ADMIN_TOKEN") - -ASSIGNED=$(echo "$response" | grep -o "\"assigned_to\":\"[^\"]*\"" | sed 's/"assigned_to":"//;s/"//') -if [ "$ASSIGNED" = "$ADMIN_ID" ]; then - log_success "Ticket assigned to admin" -else - log_error "Failed to assign ticket: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 9: Admin resolves ticket" -log_info "============================================================" - -log_info "Admin resolving ticket..." -response=$(http_put "$BASE_URL/v1/admin/tickets/$TICKET1_ID" \ - "{\"action\":\"resolve\",\"note\":\"Fixed in version 1.0\"}" "$ADMIN_TOKEN") - -STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') -NOTE=$(echo "$response" | grep -o "\"resolution_note\":\"[^\"]*\"" | sed 's/"resolution_note":"//;s/"//') - -if [ "$STATUS" = "resolved" ] && [ "$NOTE" = "Fixed in version 1.0" ]; then - log_success "Ticket resolved with note" -else - log_error "Failed to resolve ticket: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 10: Admin closes ticket" -log_info "============================================================" - -log_info "Admin closing ticket..." -response=$(http_put "$BASE_URL/v1/admin/tickets/$TICKET1_ID" \ - "{\"action\":\"close\"}" "$ADMIN_TOKEN") - -STATUS=$(echo "$response" | grep -o "\"status\":\"[^\"]*\"" | sed 's/"status":"//;s/"//') -if [ "$STATUS" = "closed" ]; then - log_success "Ticket closed" -else - log_error "Failed to close ticket: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 11: Admin views statistics" -log_info "============================================================" - -log_info "Admin getting statistics..." -response=$(http_get "$BASE_URL/v1/admin/tickets/stats" "$ADMIN_TOKEN") - -TOTAL=$(extract_json_number "$response" "total_tickets") -OPEN=$(extract_json_number "$response" "open") -CLOSED=$(extract_json_number "$response" "closed") - -if [ "$TOTAL" -eq 2 ] && [ "$OPEN" -eq 1 ] && [ "$CLOSED" -eq 1 ]; then - log_success "Statistics: total=$TOTAL, open=$OPEN, closed=$CLOSED" -else - log_error "Statistics incorrect: $response" -fi - -echo "" -log_info "============================================================" -log_info "TEST 12: Get single ticket" -log_info "============================================================" - -log_info "Admin getting ticket $TICKET2_ID..." -response=$(http_get "$BASE_URL/v1/admin/tickets/$TICKET2_ID" "$ADMIN_TOKEN") - -if echo "$response" | grep -q "$TICKET2_ID"; then - log_success "Ticket retrieved" -else - log_error "Failed to get ticket: $response" -fi - -echo "" -echo "============================================================" -log_success "TICKETS API TESTS COMPLETED!" -echo "============================================================" -echo "" -echo "Summary of created resources:" -echo " Admin: $ADMIN_EMAIL" -echo " User: $USER_EMAIL" -echo " Tickets: $TICKET1_ID, $TICKET2_ID" -echo "" \ No newline at end of file diff --git a/test/scripts/test_websocket_api.sh b/test/scripts/test_websocket_api.sh deleted file mode 100644 index 355cb69..0000000 --- a/test/scripts/test_websocket_api.sh +++ /dev/null @@ -1,313 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -BASE_URL="http://localhost:8080" -WS_URL="ws://localhost:8081/ws" -ADMIN_WS_URL="ws://localhost:8446/admin/ws" - -DEBUG=${DEBUG:-false} - -log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } -log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } -log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } -log_debug() { - if [ "$DEBUG" = "true" ]; then - echo -e "${CYAN}[DEBUG]${NC} $1" - fi -} - -extract_json() { - echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" -} - -http_post() { - local url=$1; local data=$2; local token=$3 - log_debug "POST $url" - 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 - log_debug "GET $url" - 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 - log_debug "PUT $url" - curl -s -X PUT "$url" -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d "$data" -} - -# Проверка curl WebSocket (ручная проверка заголовков) -test_ws_curl() { - local url=$1 - local token=$2 - local full_url="${url}?token=${token}" - - log_debug "Testing WebSocket with curl: $full_url" - - # Используем --include для заголовков, --no-buffer для немедленного вывода - response=$(curl -s --include --no-buffer \ - -H "Connection: Upgrade" \ - -H "Upgrade: websocket" \ - -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \ - -H "Sec-WebSocket-Version: 13" \ - --max-time 2 \ - "$full_url" 2>&1) - - log_debug "Response: $(echo "$response" | head -5)" - - if echo "$response" | grep -q "101"; then - log_debug "Got 101 Switching Protocols" - return 0 - elif echo "$response" | grep -q "401"; then - log_debug "Got 401 Unauthorized" - return 1 - elif echo "$response" | grep -q "403"; then - log_debug "Got 403 Forbidden" - return 2 - elif echo "$response" | grep -q "404"; then - log_debug "Got 404 Not Found" - return 4 - elif echo "$response" | grep -q "Invalid token"; then - log_debug "Got 'Invalid token' message" - return 1 - else - log_debug "Unknown response" - return 3 - fi -} - -# Проверка наличия websocat -check_websocat() { - if ! command -v websocat &> /dev/null; then - log_warning "websocat не установлен" - echo "Установите websocat:" - echo " cargo install websocat" - echo " или скачайте с https://github.com/vi/websocat/releases" - return 1 - fi - log_debug "websocat found: $(which websocat)" - return 0 -} - -echo "============================================================" -echo " EVENTHUB WEBSOCKET API TEST SCRIPT" -echo "============================================================" -echo "" - -if [ "$DEBUG" = "true" ]; then - log_info "DEBUG MODE ENABLED" -fi - -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 "http://localhost:8445/admin/health" | grep -q "ok"; then - log_warning "Admin server is not running on port 8445" -else - log_success "Admin server is running" -fi - -echo "" -log_info "============================================================" -log_info "STEP 1: Create test users" -log_info "============================================================" - -# Админ -ADMIN_EMAIL="ws_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\"}" "") -log_debug "Register response: $response" -ADMIN_TOKEN=$(extract_json "$response" "token") -ADMIN_ID=$(extract_json "$response" "id") -log_success "Admin created: $ADMIN_EMAIL" -log_debug "Admin token: ${ADMIN_TOKEN:0:30}..." - -# Обычный пользователь -USER_EMAIL="ws_user_$(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_EMAIL" -log_debug "User token: ${USER_TOKEN:0:30}..." - -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\":\"WS Test Calendar\"}" "$USER_TOKEN") -CALENDAR_ID=$(extract_json "$response" "id") -log_success "Calendar created: $CALENDAR_ID" -log_debug "Calendar ID: $CALENDAR_ID" - -log_info "Creating event..." -EVENT_START="2026-06-01T10:00:00Z" -response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ - "{\"title\":\"WS Test Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$USER_TOKEN") -EVENT_ID=$(extract_json "$response" "id") -log_success "Event created: $EVENT_ID" -log_debug "Event ID: $EVENT_ID" - -echo "" -log_info "============================================================" -log_info "TEST 1: Connect to WebSocket with valid token (curl test)" -log_info "============================================================" - -test_ws_curl "$WS_URL" "$USER_TOKEN" -CURL_RESULT=$? - -case $CURL_RESULT in - 0) - log_success "WebSocket upgrade successful (101 Switching Protocols)" - ;; - 1) - log_error "WebSocket authentication failed (401 Unauthorized)" - log_debug "Token might be invalid or expired" - ;; - 2) - log_error "WebSocket access denied (403 Forbidden)" - ;; - *) - log_error "WebSocket connection failed (unknown error)" - log_debug "Check if WebSocket server is running on port 8081" - ;; -esac - -echo "" -log_info "============================================================" -log_info "TEST 2: Connect with invalid token (curl test)" -log_info "============================================================" - -test_ws_curl "$WS_URL" "invalid.token.here" -CURL_RESULT=$? - -if [ $CURL_RESULT -eq 1 ]; then - log_success "Invalid token correctly rejected (401 Unauthorized)" -else - log_error "Invalid token should be rejected with 401" -fi - -echo "" -log_info "============================================================" -log_info "TEST 3: Admin WebSocket with valid token (curl test)" -log_info "============================================================" - -test_ws_curl "$ADMIN_WS_URL" "$ADMIN_TOKEN" -CURL_RESULT=$? - -case $CURL_RESULT in - 0) - log_success "Admin WebSocket upgrade successful" - ;; - 1) - log_error "Admin WebSocket authentication failed" - ;; - 2) - log_error "Admin WebSocket access denied (not admin)" - log_debug "Check if token has admin role" - ;; - *) - log_error "Admin WebSocket connection failed" - ;; -esac - -echo "" -log_info "============================================================" -log_info "TEST 4: Admin WebSocket with user token (should fail)" -log_info "============================================================" - -test_ws_curl "$ADMIN_WS_URL" "$USER_TOKEN" -CURL_RESULT=$? - -if [ $CURL_RESULT -eq 2 ]; then - log_success "User token correctly rejected for admin WebSocket (403 Forbidden)" -elif [ $CURL_RESULT -eq 1 ]; then - log_warning "User token rejected with 401 instead of 403" -else - log_error "User token should be rejected" -fi - -echo "" -log_info "============================================================" -log_info "WEBSOCKET API TESTS (curl validation) COMPLETED!" -log_info "============================================================" - -# Опциональные тесты с websocat -if check_websocat; then - echo "" - log_info "============================================================" - log_info "OPTIONAL: Testing with websocat" - log_info "============================================================" - - WS_URL_WITH_TOKEN="${WS_URL}?token=${USER_TOKEN}" - log_debug "WebSocket URL: $WS_URL_WITH_TOKEN" - - TEMP_FILE=$(mktemp) - log_debug "Temp file: $TEMP_FILE" - - # Запускаем websocat в фоне - log_info "Connecting with websocat..." - timeout 3 websocat "$WS_URL_WITH_TOKEN" > "$TEMP_FILE" 2>&1 & - WS_PID=$! - log_debug "WebSocket PID: $WS_PID" - sleep 1 - - if kill -0 $WS_PID 2>/dev/null; then - log_success "WebSocket connection established with websocat" - - # Отправляем ping - echo '{"action":"ping"}' | timeout 2 websocat "$WS_URL_WITH_TOKEN" > "$TEMP_FILE" 2>&1 - if grep -q "pong" "$TEMP_FILE"; then - log_success "Ping-pong successful" - fi - - kill $WS_PID 2>/dev/null - else - log_warning "Websocket connection failed" - fi - - rm -f "$TEMP_FILE" -fi - -echo "" -echo "============================================================" -log_success "ALL WEBSOCKET TESTS COMPLETED!" -echo "============================================================" -echo "" -echo "Summary:" -echo " Admin: $ADMIN_EMAIL" -echo " User: $USER_EMAIL" -echo " Calendar: $CALENDAR_ID" -echo " Event: $EVENT_ID" -echo "" -echo "Run with DEBUG=true for more details:" -echo " DEBUG=true ./test/scripts/test_websocket_api.sh" -echo "" \ No newline at end of file