Рефакторинг обработчиков. Часть 3 #21
This commit is contained in:
180
test/api/admins/admin_admins_tests.erl
Normal file
180
test/api/admins/admin_admins_tests.erl
Normal file
@@ -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]).
|
||||
79
test/api/admins/admin_audit_tests.erl
Normal file
79
test/api/admins/admin_audit_tests.erl
Normal file
@@ -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").
|
||||
94
test/api/admins/admin_banned_words_tests.erl
Normal file
94
test/api/admins/admin_banned_words_tests.erl
Normal file
@@ -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").
|
||||
119
test/api/admins/admin_events_tests.erl
Normal file
119
test/api/admins/admin_events_tests.erl
Normal file
@@ -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").
|
||||
36
test/api/admins/admin_me_tests.erl
Normal file
36
test/api/admins/admin_me_tests.erl
Normal file
@@ -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").
|
||||
133
test/api/admins/admin_moderation_tests.erl
Normal file
133
test/api/admins/admin_moderation_tests.erl
Normal file
@@ -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").
|
||||
134
test/api/admins/admin_reports_tests.erl
Normal file
134
test/api/admins/admin_reports_tests.erl
Normal file
@@ -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").
|
||||
136
test/api/admins/admin_reviews_tests.erl
Normal file
136
test/api/admins/admin_reviews_tests.erl
Normal file
@@ -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").
|
||||
77
test/api/admins/admin_stats_tests.erl
Normal file
77
test/api/admins/admin_stats_tests.erl
Normal file
@@ -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").
|
||||
131
test/api/admins/admin_subscriptions_tests.erl
Normal file
131
test/api/admins/admin_subscriptions_tests.erl
Normal file
@@ -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").
|
||||
153
test/api/admins/admin_tickets_tests.erl
Normal file
153
test/api/admins/admin_tickets_tests.erl
Normal file
@@ -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").
|
||||
67
test/api/admins/admin_users_tests.erl
Normal file
67
test/api/admins/admin_users_tests.erl
Normal file
@@ -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").
|
||||
258
test/api/admins/admin_websocket_tests.erl
Normal file
258
test/api/admins/admin_websocket_tests.erl
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user