Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 1

This commit is contained in:
2026-04-27 15:54:48 +03:00
parent 62bc62f990
commit 4ed6a961ab
40 changed files with 3573 additions and 800 deletions

View File

@@ -0,0 +1,158 @@
-module(admin_handler_banned_words_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_banned_words, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_banned_words),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_banned_words_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/banned-words success", fun test_list/0},
{"GET /admin/banned-words forbidden", fun test_list_forbidden/0},
{"POST /admin/banned-words success", fun test_add/0},
{"POST /admin/banned-words missing field", fun test_add_missing/0},
{"POST /admin/banned-words already exists", fun test_add_exists/0},
{"DELETE /admin/banned-words/:word success", fun test_delete/0},
{"DELETE /admin/banned-words/:word not found", fun test_delete_not_found/0},
{"PUT /admin/banned-words/:word success", fun test_update/0},
{"PUT /admin/banned-words/:word not found", fun test_update_not_found/0},
{"PATCH /admin/banned-words method not allowed", fun test_wrong_method/0}
]}.
test_list() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
Words = [
#banned_word{id = <<"bw1">>, word = <<"badword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}},
#banned_word{id = <<"bw2">>, word = <<"spamword">>, added_by = <<"adm2">>, added_at = {{2026,4,27},{12,30,0}}}
],
ok = meck:expect(core_banned_words, list_banned_words, fun() -> Words end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
Result = jsx:decode(RespBody, [return_maps]),
?assertEqual(2, length(Result)),
First = hd(Result),
?assertEqual(<<"badword">>, maps:get(<<"word">>, First)),
?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, First)).
test_list_forbidden() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
test_add() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"banned">>}), Req} end),
NewBW = #banned_word{id = <<"bw_new">>, word = <<"banned">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{13,0,0}}},
ok = meck:expect(core_banned_words, add_banned_word, fun(<<"banned">>, <<"adm1">>) -> {ok, NewBW} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status),
#{<<"word">> := <<"banned">>, <<"added_by">> := <<"adm1">>} = jsx:decode(RespBody, [return_maps]).
test_add_missing() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_add_exists() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"already">>}), Req} end),
ok = meck:expect(core_banned_words, add_banned_word, fun(_, _) -> {error, already_exists} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(409, Status).
test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"badword">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_banned_words, remove_banned_word, fun(<<"badword">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"unknown">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_banned_words, remove_banned_word, fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_update() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"oldword">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end),
UpdatedBW = #banned_word{id = <<"bw1">>, word = <<"newword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}},
ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {ok, UpdatedBW} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
Resp = jsx:decode(RespBody, [return_maps]),
?assertEqual(<<"newword">>, maps:get(<<"word">>, Resp)),
?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, Resp)).
test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end),
ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,49 @@
-module(admin_handler_health_tests).
-include_lib("eunit/include/eunit.hrl").
%% ------------------------------------------------------------------
%% Фикстуры
%% ------------------------------------------------------------------
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok.
cleanup(_) ->
meck:unload(cowboy_req).
%% ------------------------------------------------------------------
%% Тесты
%% ------------------------------------------------------------------
admin_handler_health_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/health returns 200 with status ok",
fun test_health_get/0},
{"POST /admin/health returns 405 Method not allowed",
fun test_health_post/0}
]}.
%% ── Успешный GET ─────────────────────────────────────────────
test_health_get() ->
% Мокаем method → <<"GET">>
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
% reply/4 будем перехватывать
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_health:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
?assertEqual(#{<<"status">> => <<"ok">>}, jsx:decode(RespBody, [return_maps])).
%% ── Метод не разрешён ───────────────────────────────────────
test_health_post() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_health:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])).

View File

@@ -0,0 +1,104 @@
-module(admin_handler_login_tests).
-include_lib("eunit/include/eunit.hrl").
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
setup() ->
ok = meck:new(logic_auth, [non_strict]),
ok = meck:new(cowboy_req, [non_strict]),
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose).
cleanup(_) ->
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose),
meck:unload(cowboy_req),
meck:unload(logic_auth),
ok.
admin_handler_login_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Valid admin login returns 200 and token", fun test_valid_admin_login/0},
{"Invalid credentials return 401", fun test_invalid_credentials/0},
{"Nonadmin role returns 403", fun test_insufficient_permissions/0},
{"Malformed JSON returns 400", fun test_malformed_json/0},
{"Missing body returns 400", fun test_missing_body/0},
{"Wrong HTTP method returns 405", fun test_wrong_method/0}
]}.
%% ── Вспомогательная функция для создания запроса и ожидания reply ──
prepare_req(Method, HasBody, Body) ->
ok = meck:expect(cowboy_req, method, fun(_) -> Method end),
ok = meck:expect(cowboy_req, has_body, fun(_) -> HasBody end),
case {HasBody, Body} of
{true, undefined} -> ok;
{true, _} ->
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, Body, Req} end);
{false, _} -> ok
end,
% Устанавливаем мок на reply, который сохраняет ответ в словаре процесса
meck:expect(cowboy_req, reply,
fun(Code, Headers, RespBody, Req) ->
put(test_reply, {Code, Headers, RespBody}),
Req
end),
req.
%% ── Тесты ────────────────────────────────────────────────────
test_valid_admin_login() ->
UserMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>},
ok = meck:expect(logic_auth, authenticate_user,
fun(<<"admin@test.com">>, <<"pass">>) -> {ok, UserMap} end),
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"admin@test.com">>, password => <<"pass">>})),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, Headers, Body} = get(test_reply),
?assertEqual(200, Code),
?assertEqual(<<"application/json">>, maps:get(<<"content-type">>, Headers)),
Resp = jsx:decode(Body, [return_maps]),
?assert(is_map_key(<<"token">>, Resp)),
?assertEqual(<<"superadmin">>, maps:get(<<"role">>, maps:get(<<"user">>, Resp))).
test_invalid_credentials() ->
ok = meck:expect(logic_auth, authenticate_user,
fun(_, _) -> {error, bad_credentials} end),
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"bad@test.com">>, password => <<"wrong">>})),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, _, Body} = get(test_reply),
?assertEqual(401, Code),
#{<<"error">> := <<"bad_credentials">>} = jsx:decode(Body, [return_maps]).
test_insufficient_permissions() ->
UserMap = #{id => <<"user1">>, email => <<"user@test.com">>, role => <<"user">>},
ok = meck:expect(logic_auth, authenticate_user,
fun(_, _) -> {ok, UserMap} end),
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"user@test.com">>, password => <<"pass">>})),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, _, Body} = get(test_reply),
?assertEqual(403, Code),
#{<<"error">> := <<"insufficient_permissions">>} = jsx:decode(Body, [return_maps]).
test_malformed_json() ->
Req0 = prepare_req(<<"POST">>, true, <<"not a json">>),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, _, Body} = get(test_reply),
?assertEqual(400, Code),
#{<<"error">> := <<"invalid_request">>} = jsx:decode(Body, [return_maps]).
test_missing_body() ->
Req0 = prepare_req(<<"POST">>, false, undefined),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, _, Body} = get(test_reply),
?assertEqual(400, Code),
#{<<"error">> := <<"Missing request body">>} = jsx:decode(Body, [return_maps]).
test_wrong_method() ->
Req0 = prepare_req(<<"GET">>, false, undefined),
{ok, _, _} = admin_handler_login:init(Req0, []),
{Code, _, Body} = get(test_reply),
?assertEqual(405, Code),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(Body, [return_maps]).

View File

@@ -0,0 +1,189 @@
-module(admin_handler_moderation_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_calendar, [non_strict]),
ok = meck:new(core_event, [non_strict]),
ok = meck:new(core_review, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_review),
meck:unload(core_event),
meck:unload(core_calendar),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_moderation_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Freeze calendar success", fun test_freeze_calendar/0},
{"Freeze calendar not found", fun test_freeze_calendar_not_found/0},
{"Unfreeze calendar success", fun test_unfreeze_calendar/0},
{"Freeze event success", fun test_freeze_event/0},
{"Unfreeze event success", fun test_unfreeze_event/0},
{"Hide review success", fun test_hide_review/0},
{"Show review success", fun test_show_review/0},
{"Block user success", fun test_block_user/0},
{"Unblock user success", fun test_unblock_user/0},
{"Invalid target type", fun test_invalid_target/0},
{"Invalid action", fun test_invalid_action/0},
{"Missing action field", fun test_missing_action/0},
{"Forbidden non admin", fun test_forbidden/0},
{"Wrong method POST", fun test_wrong_method/0}
]}.
%% ── Вспомогательные функции ──────────────────────────────
prepare_req(TargetType, TargetId, Action) ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(target_type, _) -> TargetType;
(id, _) -> TargetId
end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"action">> => Action}), Req} end).
%% ── Календари ───────────────────────────────────────────
test_freeze_calendar() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"freeze">>),
Frozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = frozen},
ok = meck:expect(core_calendar, freeze, fun(<<"c1">>) -> {ok, Frozen} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
test_freeze_calendar_not_found() ->
prepare_req(<<"calendar">>, <<"c99">>, <<"freeze">>),
ok = meck:expect(core_calendar, freeze, fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_unfreeze_calendar() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"unfreeze">>),
Unfrozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = active},
ok = meck:expect(core_calendar, unfreeze, fun(<<"c1">>) -> {ok, Unfrozen} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── События ─────────────────────────────────────────────
test_freeze_event() ->
prepare_req(<<"event">>, <<"e1">>, <<"freeze">>),
FrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = frozen},
ok = meck:expect(core_event, freeze, fun(<<"e1">>) -> {ok, FrozenE} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
test_unfreeze_event() ->
prepare_req(<<"event">>, <<"e1">>, <<"unfreeze">>),
UnfrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = active},
ok = meck:expect(core_event, unfreeze, fun(<<"e1">>) -> {ok, UnfrozenE} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Отзывы ──────────────────────────────────────────────
test_hide_review() ->
prepare_req(<<"review">>, <<"r1">>, <<"hide">>),
Hidden = #review{id = <<"r1">>, status = hidden},
ok = meck:expect(core_review, hide, fun(<<"r1">>) -> {ok, Hidden} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"hidden">>} = jsx:decode(RespBody, [return_maps]).
test_show_review() ->
prepare_req(<<"review">>, <<"r1">>, <<"show">>),
Visible = #review{id = <<"r1">>, status = active},
ok = meck:expect(core_review, show, fun(<<"r1">>) -> {ok, Visible} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Пользователи ────────────────────────────────────────
test_block_user() ->
prepare_req(<<"user">>, <<"u1">>, <<"block">>),
Blocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = frozen},
ok = meck:expect(core_user, block, fun(<<"u1">>) -> {ok, Blocked} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
test_unblock_user() ->
prepare_req(<<"user">>, <<"u1">>, <<"unblock">>),
Unblocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = active},
ok = meck:expect(core_user, unblock, fun(<<"u1">>) -> {ok, Unblocked} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Ошибки ──────────────────────────────────────────────
test_invalid_target() ->
prepare_req(<<"bad_type">>, <<"x">>, <<"freeze">>),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_invalid_action() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"delete">>),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_missing_action() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(target_type, _) -> <<"calendar">>;
(id, _) -> <<"c1">>
end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,145 @@
-module(admin_handler_report_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_report, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_report),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_report_by_id_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/reports/:id success", fun test_get_report/0},
{"GET /admin/reports/:id not found", fun test_get_report_not_found/0},
{"GET /admin/reports/:id forbidden", fun test_get_report_forbidden/0},
{"PUT /admin/reports/:id success", fun test_update_report/0},
{"PUT /admin/reports/:id not found", fun test_update_report_not_found/0},
{"PUT /admin/reports/:id bad JSON", fun test_update_report_bad_json/0},
{"DELETE /admin/reports/:id method not allowed", fun test_wrong_method/0}
]}.
%% GET успех
test_get_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
Report = #report{
id = <<"r1">>,
reporter_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = <<"new">>,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
ok = meck:expect(core_report, get_by_id,
fun(<<"r1">>) -> {ok, Report} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"r1">>, <<"status">> := <<"new">>} = jsx:decode(RespBody, [return_maps]).
%% GET не найдено
test_get_report_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r99">> end),
ok = meck:expect(core_report, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% GET запрещён
test_get_report_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% PUT успех
test_update_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = <<"reviewed">>},
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
%% PUT не найдено
test_update_report_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT невалидный JSON
test_update_report_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% Неверный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,121 @@
-module(admin_handler_reports_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_report, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_report),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_reports_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/reports success", fun test_list_reports/0},
{"GET /admin/reports forbidden", fun test_list_reports_forbidden/0},
{"PUT /admin/reports/:id success", fun test_update_report/0},
{"PUT /admin/reports/:id missing status", fun test_update_report_bad_json/0},
{"PUT /admin/reports/:id not found", fun test_update_report_not_found/0},
{"POST /admin/reports method not allowed", fun test_wrong_method/0}
]}.
test_list_reports() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Report = #report{
id = <<"r1">>,
reporter_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = <<"new">>,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
ok = meck:expect(core_report, list_reports, fun() -> [Report] end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
[#{<<"id">> := <<"r1">>, <<"target_type">> := <<"event">>, <<"status">> := <<"new">>}]
= jsx:decode(RespBody, [return_maps]).
test_list_reports_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(403, Status),
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
test_update_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = <<"reviewed">>},
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
test_update_report_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
?assertEqual(400, Status).
test_update_report_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
?assertEqual(404, Status).
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,146 @@
-module(admin_handler_reviews_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_review, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_review),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_reviews_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/reviews/:id success", fun test_get_review/0},
{"GET /admin/reviews/:id not found", fun test_get_review_not_found/0},
{"GET /admin/reviews/:id forbidden", fun test_get_review_forbidden/0},
{"PUT /admin/reviews/:id success", fun test_update_review/0},
{"PUT /admin/reviews/:id not found", fun test_update_review_not_found/0},
{"PUT /admin/reviews/:id bad JSON", fun test_update_review_bad_json/0},
{"DELETE /admin/reviews/:id method not allowed", fun test_wrong_method/0}
]}.
%% GET успех
test_get_review() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv1">> end),
Review = #review{
id = <<"rv1">>,
user_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
rating = 5,
comment = <<"Great!">>,
status = <<"active">>,
created_at = {{2026,4,26},{12,0,0}},
updated_at = {{2026,4,26},{12,0,0}}
},
ok = meck:expect(core_review, get_by_id,
fun(<<"rv1">>) -> {ok, Review} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"rv1">>, <<"comment">> := <<"Great!">>, <<"rating">> := 5} = jsx:decode(RespBody, [return_maps]).
%% GET не найдено
test_get_review_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv99">> end),
ok = meck:expect(core_review, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% GET запрещён
test_get_review_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% PUT успех
test_update_review() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end),
Updated = #review{id = <<"rv1">>, status = <<"hidden">>},
ok = meck:expect(core_review, update_status,
fun(<<"rv1">>, <<"hidden">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"hidden">>} = jsx:decode(RespBody, [return_maps]).
%% PUT не найдено
test_update_review_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end),
ok = meck:expect(core_review, update_status,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT невалидный JSON
test_update_review_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% Неверный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -2,106 +2,88 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
setup() ->
mnesia:start(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]),
mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]),
mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
mnesia:create_table(subscription, [{attributes, record_info(fields, subscription)}, {ram_copies, [node()]}]),
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(mnesia, [non_strict]),
ok = meck:expect(mnesia, dirty_match_object, fun(_) -> [] end),
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose),
ok.
cleanup(_) ->
mnesia:delete_table(subscription),
mnesia:delete_table(ticket),
mnesia:delete_table(report),
mnesia:delete_table(review),
mnesia:delete_table(booking),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose),
meck:unload(mnesia),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_stats_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Count users", fun test_count_users/0},
{"Count calendars", fun test_count_calendars/0},
{"Count events", fun test_count_events/0},
{"Count bookings", fun test_count_bookings/0},
{"Count reviews", fun test_count_reviews/0},
{"Count reports", fun test_count_reports/0},
{"Count tickets", fun test_count_tickets/0},
{"Count subscriptions", fun test_count_subscriptions/0}
]}.
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/stats with admin role returns 200 and dashboard data",
fun test_stats_admin/0},
{"GET /admin/stats with non-admin role returns 403",
fun test_stats_forbidden/0},
{"POST /admin/stats returns 405",
fun test_stats_wrong_method/0},
{"Count functions return 0 with empty DB",
fun test_count_functions/0}
]}.
create_test_user() ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = user, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
%% ── Успешный GET с ролью админа ────────────────────────────
test_stats_admin() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
% Администратор с ролью superadmin
AdminUser = #user{id = <<"adm1">>, role = superadmin, _ = '_'},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
Stats = jsx:decode(RespBody, [return_maps]),
?assert(is_map_key(<<"users">>, Stats)),
?assert(is_map_key(<<"events">>, Stats)).
test_count_users() ->
%% ── Обычный пользователь получает 403 ─────────────────────
test_stats_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(403, Status),
?assertEqual(#{<<"error">> => <<"Admin access required">>}, jsx:decode(RespBody, [return_maps])).
%% ── Неверный метод ──────────────────────────────────────
test_stats_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])).
%% ── Функции подсчёта (мок mnesia) ──────────────────────
test_count_functions() ->
?assertEqual(0, admin_handler_stats:count_users()),
create_test_user(),
create_test_user(),
?assertEqual(2, admin_handler_stats:count_users()).
test_count_calendars() ->
?assertEqual(0, admin_handler_stats:count_calendars()),
UserId = create_test_user(),
core_calendar:create(UserId, <<"Cal1">>, <<"">>, manual),
core_calendar:create(UserId, <<"Cal2">>, <<"">>, auto),
?assertEqual(2, admin_handler_stats:count_calendars()).
test_count_events() ->
?assertEqual(0, admin_handler_stats:count_events()),
UserId = create_test_user(),
{ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual),
core_event:create(Cal#calendar.id, <<"Ev1">>, {{2026,6,1},{10,0,0}}, 60),
core_event:create(Cal#calendar.id, <<"Ev2">>, {{2026,6,2},{10,0,0}}, 60),
?assertEqual(2, admin_handler_stats:count_events()).
test_count_bookings() ->
?assertEqual(0, admin_handler_stats:count_bookings()),
UserId = create_test_user(),
ParticipantId = create_test_user(),
{ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual),
{ok, Ev} = core_event:create(Cal#calendar.id, <<"Ev">>, {{2026,6,1},{10,0,0}}, 60),
core_booking:create(Ev#event.id, ParticipantId),
core_booking:create(Ev#event.id, ParticipantId),
?assertEqual(2, admin_handler_stats:count_bookings()).
test_count_reviews() ->
?assertEqual(0, admin_handler_stats:count_reviews()),
UserId = create_test_user(),
core_review:create(UserId, calendar, <<"cal1">>, 5, <<"Great">>),
core_review:create(UserId, event, <<"ev1">>, 4, <<"Good">>),
?assertEqual(2, admin_handler_stats:count_reviews()).
test_count_reports() ->
?assertEqual(0, admin_handler_stats:count_reports()),
UserId = create_test_user(),
core_report:create(UserId, event, <<"ev1">>, <<"Bad">>),
core_report:create(UserId, calendar, <<"cal1">>, <<"Spam">>),
?assertEqual(2, admin_handler_stats:count_reports()).
test_count_tickets() ->
?assertEqual(0, admin_handler_stats:count_tickets()),
core_ticket:create_or_update(<<"Error1">>, <<"">>, #{}),
core_ticket:create_or_update(<<"Error2">>, <<"">>, #{}),
?assertEqual(2, admin_handler_stats:count_tickets()).
test_count_subscriptions() ->
?assertEqual(0, admin_handler_stats:count_subscriptions()),
UserId = create_test_user(),
core_subscription:create(UserId, trial, false),
core_subscription:create(UserId, monthly, true),
?assertEqual(2, admin_handler_stats:count_subscriptions()).
?assertEqual(0, admin_handler_stats:count_events()).

View File

@@ -0,0 +1,219 @@
-module(admin_handler_subscriptions_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_subscription, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_subscription),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_subscriptions_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/subscriptions success", fun test_list/0},
{"GET /admin/subscriptions forbidden", fun test_list_forbidden/0},
{"POST /admin/subscriptions success", fun test_create/0},
{"POST /admin/subscriptions missing user_id", fun test_create_missing/0},
{"GET /admin/subscriptions/:id success", fun test_get/0},
{"GET /admin/subscriptions/:id not found", fun test_get_not_found/0},
{"PUT /admin/subscriptions/:id success", fun test_update/0},
{"PUT /admin/subscriptions/:id not found", fun test_update_not_found/0},
{"DELETE /admin/subscriptions/:id success", fun test_delete/0},
{"DELETE /admin/subscriptions/:id not found", fun test_delete_not_found/0},
{"PATCH /admin/subscriptions method not allowed", fun test_wrong_method/0}
]}.
%% GET список
test_list() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Sub1 = #subscription{
id = <<"s1">>,
user_id = <<"u1">>,
plan = monthly,
status = active,
trial_used = false,
started_at = {{2026,4,27},{12,0,0}},
expires_at = {{2026,5,27},{12,0,0}},
created_at = {{2026,4,27},{12,0,0}},
updated_at = {{2026,4,27},{12,0,0}}
},
ok = meck:expect(core_subscription, list_subscriptions, fun() -> [Sub1] end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
[#{<<"id">> := <<"s1">>, <<"plan">> := <<"monthly">>, <<"status">> := <<"active">>}] =
jsx:decode(RespBody, [return_maps]).
test_list_forbidden() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% POST создание
test_create() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
BodyMap = #{<<"user_id">> => <<"u1">>, <<"plan">> => <<"yearly">>},
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
Created = #subscription{
id = <<"s_new">>, user_id = <<"u1">>, plan = yearly, status = active,
trial_used = false,
started_at = {{2026,4,27},{14,0,0}}, expires_at = {{2027,4,27},{14,0,0}},
created_at = {{2026,4,27},{14,0,0}}, updated_at = {{2026,4,27},{14,0,0}}
},
ok = meck:expect(core_subscription, create_subscription, fun(_) -> {ok, Created} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status),
#{<<"plan">> := <<"yearly">>, <<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
test_create_missing() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"plan">> => <<"monthly">>}), Req} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% GET по ID
test_get() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Sub = #subscription{
id = <<"s1">>, user_id = <<"u1">>, plan = monthly, status = active,
trial_used = false,
started_at = {{2026,4,27},{12,0,0}}, expires_at = {{2026,5,27},{12,0,0}},
created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{12,0,0}}
},
ok = meck:expect(core_subscription, get_by_id,
fun(<<"s1">>) -> {ok, Sub} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"s1">>, <<"plan">> := <<"monthly">>} = jsx:decode(RespBody, [return_maps]).
test_get_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT обновление
test_update() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end),
Updated = #subscription{id = <<"s1">>, status = cancelled},
ok = meck:expect(core_subscription, update_subscription,
fun(<<"s1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"cancelled">>} = jsx:decode(RespBody, [return_maps]).
test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end),
ok = meck:expect(core_subscription, update_subscription,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% DELETE
test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, delete_subscription,
fun(<<"s1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, delete_subscription,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неверный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,187 @@
-module(admin_handler_ticket_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_ticket, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_ticket),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_ticket_by_id_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets/:id success", fun test_get/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0},
{"GET /admin/tickets/:id forbidden", fun test_get_forbidden/0},
{"PUT /admin/tickets/:id success", fun test_update/0},
{"PUT /admin/tickets/:id not found", fun test_update_not_found/0},
{"PUT /admin/tickets/:id bad JSON", fun test_update_bad_json/0},
{"DELETE /admin/tickets/:id success", fun test_delete/0},
{"DELETE /admin/tickets/:id not found", fun test_delete_not_found/0},
{"POST /admin/tickets/:id method not allowed", fun test_wrong_method/0}
]}.
%% GET успех
test_get() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"hash">>,
error_message = <<"msg">>,
stacktrace = <<"trace">>,
context = <<"ctx">>,
count = 5,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{13,0,0}},
status = open,
assigned_to = <<"dev1">>,
resolution_note = undefined
},
ok = meck:expect(core_ticket, get_by_id,
fun(<<"t1">>) -> {ok, Ticket} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"t1">>, <<"error_message">> := <<"msg">>, <<"count">> := 5} = jsx:decode(RespBody, [return_maps]).
%% GET не найдено
test_get_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(core_ticket, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% GET запрещён
test_get_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% PUT успех
test_update() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>, <<"assigned_to">> => <<"adm2">>}), Req} end),
Updated = #ticket{id = <<"t1">>, status = closed, assigned_to = <<"adm2">>},
ok = meck:expect(core_ticket, update_ticket,
fun(<<"t1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"closed">>, <<"assigned_to">> := <<"adm2">>} = jsx:decode(RespBody, [return_maps]).
%% PUT не найдено
test_update_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_ticket, update_ticket,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT невалидный JSON
test_update_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"not json">>, Req} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% DELETE успех
test_delete() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
ok = meck:expect(core_ticket, delete_ticket,
fun(<<"t1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
%% DELETE не найдено
test_delete_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(core_ticket, delete_ticket,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неверный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,61 @@
-module(admin_handler_ticket_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_ticket, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_ticket),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_ticket_stats_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets/stats success", fun test_stats/0},
{"GET /admin/tickets/stats forbidden", fun test_stats_forbidden/0},
{"POST /admin/tickets/stats method not allowed", fun test_wrong_method/0}
]}.
test_stats() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
StatsData = #{
open => 5,
in_progress => 3,
resolved => 12,
closed => 20
},
ok = meck:expect(core_ticket, stats, fun() -> StatsData end),
{ok, _, _} = admin_handler_ticket_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"open">> := 5, <<"resolved">> := 12} = jsx:decode(RespBody, [return_maps]).
test_stats_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_ticket_stats:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
{ok, _, _} = admin_handler_ticket_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -0,0 +1,203 @@
-module(admin_handler_tickets_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok = meck:new(core_ticket, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok.
cleanup(_) ->
meck:unload(core_ticket),
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_tickets_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets success", fun test_list/0},
{"GET /admin/tickets forbidden", fun test_list_forbidden/0},
{"POST /admin/tickets success", fun test_create/0},
{"POST /admin/tickets missing error_message", fun test_create_missing/0},
{"GET /admin/tickets/:id success", fun test_get/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0},
{"PUT /admin/tickets/:id success", fun test_update/0},
{"PUT /admin/tickets/:id not found", fun test_update_not_found/0},
{"DELETE /admin/tickets/:id success", fun test_delete/0},
{"DELETE /admin/tickets/:id not found", fun test_delete_not_found/0},
{"PATCH /admin/tickets method not allowed", fun test_wrong_method/0}
]}.
test_list() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"abc123">>,
error_message = <<"Ooops">>,
stacktrace = <<"trace">>,
context = <<"ctx">>,
count = 3,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{13,0,0}},
status = open,
assigned_to = <<"adm2">>,
resolution_note = undefined
},
ok = meck:expect(core_ticket, list_tickets, fun() -> [Ticket] end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Ooops">>, <<"status">> := <<"open">>}] =
jsx:decode(RespBody, [return_maps]).
test_list_forbidden() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
test_create() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
BodyMap = #{<<"error_message">> => <<"New bug">>, <<"stacktrace">> => <<"trace">>},
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
Created = #ticket{
id = <<"t_new">>,
error_hash = <<"hash">>,
error_message = <<"New bug">>,
stacktrace = <<"trace">>,
context = <<>>,
count = 1,
first_seen = {{2026,4,27},{14,0,0}},
last_seen = {{2026,4,27},{14,0,0}},
status = open,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, create_ticket, fun(Data) ->
true = maps:is_key(<<"error_message">>, Data),
{ok, Created}
end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status),
#{<<"error_message">> := <<"New bug">>, <<"status">> := <<"open">>} = jsx:decode(RespBody, [return_maps]).
test_create_missing() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"desc">> => <<"no msg">>}), Req} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_get() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"abc">>,
error_message = <<"msg">>,
stacktrace = <<>>,
context = <<>>,
count = 1,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{12,0,0}},
status = open,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, get_by_id, fun(<<"t1">>) -> {ok, Ticket} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"t1">>} = jsx:decode(RespBody, [return_maps]).
test_get_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, get_by_id, fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_update() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
Updated = #ticket{id = <<"t1">>, status = closed},
ok = meck:expect(core_ticket, update_ticket, fun(<<"t1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"closed">>} = jsx:decode(RespBody, [return_maps]).
test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_ticket, update_ticket, fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket, fun(<<"t1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket, fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).

View File

@@ -3,25 +3,187 @@
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]),
ok = meck:new(core_user, [non_strict]),
ok.
cleanup(_) ->
mnesia:delete_table(user),
mnesia:stop(),
ok.
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_user_by_id_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Convert updates test", fun test_convert_updates/0}
]}.
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/users/:id success", fun test_get_user/0},
{"GET /admin/users/:id not found", fun test_get_user_not_found/0},
{"GET /admin/users/:id forbidden", fun test_get_user_forbidden/0},
{"PUT /admin/users/:id success", fun test_update_user/0},
{"PUT /admin/users/:id not found", fun test_update_user_not_found/0},
{"DELETE /admin/users/:id success", fun test_delete_user/0},
{"DELETE /admin/users/:id not found", fun test_delete_user_not_found/0},
{"POST /admin/users/:id method not allowed", fun test_wrong_method/0},
{"convert_updates/1", fun test_convert_updates/0}
]}.
%% ── GET success ─────────────────────────────────────────
test_get_user() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser};
(<<"user1">>) -> {ok, #user{id = <<"user1">>, email = <<"u@t.com">>,
role = user, status = active,
created_at = {{2026,4,27},{12,0,0}},
updated_at = {{2026,4,27},{12,0,0}}}}
end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"user1">>, <<"email">> := <<"u@t.com">>, <<"role">> := <<"user">>}
= jsx:decode(RespBody, [return_maps]).
%% ── GET not found ───────────────────────────────────────
test_get_user_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser};
(_) -> {error, not_found}
end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(404, Status),
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% ── GET forbidden ───────────────────────────────────────
test_get_user_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(403, Status),
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
%% ── PUT success ─────────────────────────────────────────
test_update_user() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser} end),
User = #user{id = <<"user1">>, email = <<"u@t.com">>, role = user, status = frozen,
created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{13,0,0}}},
ok = meck:expect(core_user, update, fun(<<"user1">>, _) -> {ok, User} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{status => <<"frozen">>}), Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
%% ── PUT not found ──────────────────────────────────────
test_update_user_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_user, update, fun(_, _) -> {error, not_found} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, <<"{}">>, Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(404, Status),
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% ── DELETE success ─────────────────────────────────────
test_delete_user() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_user, delete, fun(<<"user1">>) -> {ok, deleted} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
%% ── DELETE not found ───────────────────────────────────
test_delete_user_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_user, delete, fun(_) -> {error, not_found} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(404, Status),
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% ── Wrong method ─────────────────────────────────────────
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).
%% ── convert_updates/1 ────────────────────────────────────
test_convert_updates() ->
Updates = [{<<"status">>, <<"frozen">>}, {<<"role">>, <<"admin">>}, {<<"email">>, <<"test@test.com">>}],
Updates = [
{<<"status">>, <<"frozen">>},
{<<"role">>, <<"admin">>},
{<<"email">>, <<"test@test.com">>}
],
Converted = admin_handler_user_by_id:convert_updates(Updates),
?assertEqual({status, frozen}, lists:keyfind(status, 1, Converted)),
?assertEqual({role, admin}, lists:keyfind(role, 1, Converted)),

View File

@@ -1,45 +1,65 @@
-module(admin_handler_users_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
ok = meck:new(cowboy_req, [non_strict]),
ok = meck:new(handler_auth, [non_strict]), % вместо auth
ok = meck:new(core_user, [non_strict]),
ok.
cleanup(_) ->
mnesia:delete_table(user),
mnesia:stop(),
ok.
meck:unload(core_user),
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_users_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"User to JSON conversion", fun test_user_to_json/0},
{"Is admin check", fun test_is_admin/0}
]}.
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/users with valid admin token returns 200 and list of users", fun test_list_users/0},
{"GET /admin/users with non-admin token returns 403", fun test_list_users_forbidden/0},
{"POST /admin/users returns 405", fun test_wrong_method/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
test_list_users() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
User = #{
id => <<"user1">>,
email => <<"user@test.com">>,
role => <<"user">>,
status => <<"active">>,
created_at => {{2025,4,27},{12,0,0}},
updated_at => {{2025,4,27},{12,30,0}}
},
ok = meck:expect(core_user, list_users, fun() -> {ok, [User]} end),
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_users:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
Users = jsx:decode(RespBody, [return_maps]),
?assertEqual(1, length(Users)),
?assertEqual(<<"user1">>, maps:get(<<"id">>, hd(Users))).
test_user_to_json() ->
UserId = create_test_user(user),
{ok, User} = core_user:get_by_id(UserId),
Json = admin_handler_user_by_id:user_to_json(User),
?assert(is_map(Json)),
?assertEqual(UserId, maps:get(id, Json)),
?assertEqual(user, maps:get(role, Json)),
?assertEqual(active, maps:get(status, Json)).
test_list_users_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_users:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(403, Status),
?assertEqual(#{<<"error">> => <<"Admin access required">>}, jsx:decode(RespBody, [return_maps])).
test_is_admin() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
?assert(admin_handler_stats:is_admin(AdminId)),
?assertNot(admin_handler_stats:is_admin(UserId)),
?assertNot(admin_handler_stats:is_admin(<<"nonexistent">>)).
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_users:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])).

139
test/unit/auth_test.erl Normal file
View File

@@ -0,0 +1,139 @@
-module(auth_test).
-include_lib("eunit/include/eunit.hrl").
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
%% ------------------------------------------------------------------
%% EUnit фикстуры запуск и остановка моков
%% ------------------------------------------------------------------
setup() ->
ok = meck:new(logic_auth, [non_strict]),
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose),
ok.
cleanup(_) ->
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose),
meck:unload(logic_auth).
%% ------------------------------------------------------------------
%% Тесты генерации токенов
%% ------------------------------------------------------------------
generate_user_token_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Generate user token returns a binary",
fun() ->
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
?assert(is_binary(Token)),
?assert(size(Token) > 0)
end},
{"Generated user token can be verified",
fun() ->
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
{ok, UserId, Role} = auth:verify_user_token(Token),
?assertEqual(<<"user123">>, UserId),
?assertEqual(<<"user">>, Role)
end},
{"Generate admin token with superadmin role",
fun() ->
Token = auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
{ok, UserId, Role} = auth:verify_admin_token(Token),
?assertEqual(<<"admin1">>, UserId),
?assertEqual(<<"superadmin">>, Role)
end}
]}.
%% ------------------------------------------------------------------
%% Тесты верификации токенов (граничные случаи)
%% ------------------------------------------------------------------
verify_token_errors_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Invalid token signature returns error",
fun() ->
FakeToken = <<"not.a.valid.token">>,
?assertEqual({error, invalid_token}, auth:verify_user_token(FakeToken)),
?assertEqual({error, invalid_token}, auth:verify_admin_token(FakeToken))
end},
{"User token rejected by admin verifier (different secret)",
fun() ->
Token = auth:generate_user_token(<<"x">>, <<"user">>),
% Разные секреты → подпись недействительна для admin JWK
?assertEqual({error, invalid_signature}, auth:verify_admin_token(Token))
end},
{"Admin token rejected by user verifier (different secret)",
fun() ->
Token = auth:generate_admin_token(<<"x">>, <<"admin">>),
?assertEqual({error, invalid_signature}, auth:verify_user_token(Token))
end}
]}.
%% ------------------------------------------------------------------
%% Тесты для authenticate_user_request/3
%% ------------------------------------------------------------------
authenticate_user_request_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Successful user login returns token and user data",
fun() ->
UserMap = #{id => <<"user1">>, email => <<"u@test.com">>, role => <<"user">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
Req = undefined,
{ok, Token, ReturnedUser} = auth:authenticate_user_request(Req, <<"u@test.com">>, <<"pass">>),
?assert(is_binary(Token)),
?assertEqual(UserMap, ReturnedUser),
{ok, UserId, Role} = auth:verify_user_token(Token),
?assertEqual(<<"user1">>, UserId),
?assertEqual(<<"user">>, Role)
end},
{"User login failure propagates error from logic_auth",
fun() ->
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {error, bad_credentials} end),
Req = undefined,
?assertEqual({error, bad_credentials}, auth:authenticate_user_request(Req, <<"bad">>, <<"pwd">>))
end}
]}.
%% ------------------------------------------------------------------
%% Тесты для authenticate_admin_request/3
%% ------------------------------------------------------------------
authenticate_admin_request_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Successful admin login returns admin token",
fun() ->
AdminMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, AdminMap} end),
Req = undefined,
{ok, Token, ReturnedUser} = auth:authenticate_admin_request(Req, <<"admin@test.com">>, <<"pass">>),
?assert(is_binary(Token)),
?assertEqual(AdminMap, ReturnedUser),
{ok, UserId, Role} = auth:verify_admin_token(Token),
?assertEqual(<<"adm1">>, UserId),
?assertEqual(<<"superadmin">>, Role)
end},
{"Non-admin role is rejected with insufficient_permissions",
fun() ->
UserMap = #{id => <<"simpleuser">>, email => <<"u@test.com">>, role => <<"user">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
Req = undefined,
?assertEqual({error, insufficient_permissions},
auth:authenticate_admin_request(Req, <<"u@test.com">>, <<"pwd">>))
end},
{"Moderator role is accepted as admin",
fun() ->
ModMap = #{id => <<"moder1">>, email => <<"mod@test.com">>, role => <<"moderator">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, ModMap} end),
Req = undefined,
{ok, Token, _} = auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
{ok, _, Role} = auth:verify_admin_token(Token),
?assertEqual(<<"moderator">>, Role)
end}
]}.
%% ------------------------------------------------------------------
%% Тест generate_refresh_token/1
%% ------------------------------------------------------------------
generate_refresh_token_test() ->
{_, _} = auth:generate_refresh_token(<<"anyuser">>).

View File

@@ -3,78 +3,61 @@
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(banned_word, [
{atomic, ok} = mnesia:start(), % правильное значение
ok = mnesia:create_table(banned_word, [
{attributes, record_info(fields, banned_word)},
{disc_copies, []},
{ram_copies, [node()]}
]),
ok.
]).
cleanup(_) ->
mnesia:delete_table(banned_word),
mnesia:stop(),
ok.
mnesia:stop().
core_banned_word_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Add banned word test", fun test_add_word/0},
{"Add duplicate word test", fun test_add_duplicate/0},
{"Remove banned word test", fun test_remove_word/0},
{"List banned words test", fun test_list_words/0},
{"Is banned test", fun test_is_banned/0},
{"Check text test", fun test_check_text/0},
{"Filter text test", fun test_filter_text/0}
]}.
{setup, fun setup/0, fun cleanup/1, [
{"Add banned word success", fun test_add_success/0},
{"Add banned word already exists", fun test_add_already_exists/0},
{"Remove banned word success", fun test_remove_success/0},
{"Remove banned word not found", fun test_remove_not_found/0},
{"Update banned word success", fun test_update_success/0},
{"Update banned word not found", fun test_update_not_found/0},
{"List banned words returns all records", fun test_list/0}
]}.
test_add_word() ->
Word = <<"badword">>,
{ok, BannedWord} = core_banned_word:add(Word),
?assertEqual(Word, BannedWord#banned_word.word),
?assert(is_binary(BannedWord#banned_word.id)).
test_add_success() ->
{ok, BW} = core_banned_words:add_banned_word(<<"badword">>, <<"admin1">>),
?assertEqual(<<"badword">>, BW#banned_word.word),
?assertEqual(<<"admin1">>, BW#banned_word.added_by),
?assert(is_binary(BW#banned_word.id)),
?assert(size(BW#banned_word.id) > 0),
?assertEqual(1, length(core_banned_words:list_banned_words())).
test_add_duplicate() ->
Word = <<"badword">>,
{ok, _} = core_banned_word:add(Word),
{error, already_exists} = core_banned_word:add(Word),
{error, already_exists} = core_banned_word:add(<<"BADWORD">>). % case insensitive
test_add_already_exists() ->
{ok, _} = core_banned_words:add_banned_word(<<"spam">>, <<"admin1">>),
{error, already_exists} = core_banned_words:add_banned_word(<<"spam">>, <<"admin2">>).
test_remove_word() ->
Word = <<"badword">>,
{ok, _} = core_banned_word:add(Word),
{ok, removed} = core_banned_word:remove(Word),
{error, not_found} = core_banned_word:remove(<<"nonexistent">>).
test_remove_success() ->
{ok, _} = core_banned_words:add_banned_word(<<"badword">>, <<"admin1">>),
{ok, deleted} = core_banned_words:remove_banned_word(<<"badword">>),
?assertEqual([], core_banned_words:list_banned_words()).
test_list_words() ->
{ok, _} = core_banned_word:add(<<"word1">>),
{ok, _} = core_banned_word:add(<<"word2">>),
{ok, _} = core_banned_word:add(<<"word3">>),
test_remove_not_found() ->
?assertEqual({error, not_found}, core_banned_words:remove_banned_word(<<"unknown">>)).
{ok, Words} = core_banned_word:list_all(),
?assertEqual(3, length(Words)),
?assert(lists:member(<<"word1">>, Words)).
test_update_success() ->
{ok, _} = core_banned_words:add_banned_word(<<"oldword">>, <<"admin1">>),
{ok, BW} = core_banned_words:update_banned_word(<<"oldword">>, <<"newword">>),
?assertEqual(<<"newword">>, BW#banned_word.word),
?assertEqual([<<"newword">>], [W#banned_word.word || W <- core_banned_words:list_banned_words()]).
test_is_banned() ->
Word = <<"badword">>,
?assertNot(core_banned_word:is_banned(Word)),
{ok, _} = core_banned_word:add(Word),
?assert(core_banned_word:is_banned(Word)),
?assert(core_banned_word:is_banned(<<"BADWORD">>)). % case insensitive
test_update_not_found() ->
?assertEqual({error, not_found}, core_banned_words:update_banned_word(<<"unknown">>, <<"newword">>)).
test_check_text() ->
{ok, _} = core_banned_word:add(<<"bad">>),
{ok, _} = core_banned_word:add(<<"spam">>),
?assertNot(core_banned_word:check_text(<<"Hello world">>)),
?assert(core_banned_word:check_text(<<"This is bad">>)),
?assert(core_banned_word:check_text(<<"This is SPAM">>)).
test_filter_text() ->
{ok, _} = core_banned_word:add(<<"bad">>),
{ok, _} = core_banned_word:add(<<"spam">>),
?assertEqual(<<"Hello world">>, core_banned_word:filter_text(<<"Hello world">>)),
?assertEqual(<<"This is ***">>, core_banned_word:filter_text(<<"This is bad">>)),
?assertEqual(<<"*** and ***">>, core_banned_word:filter_text(<<"bad and spam">>)).
test_list() ->
{ok, _} = core_banned_words:add_banned_word(<<"word1">>, <<"adm1">>),
{ok, _} = core_banned_words:add_banned_word(<<"word2">>, <<"adm2">>),
List = core_banned_words:list_banned_words(),
?assertEqual(2, length(List)),
?assert(lists:any(fun(W) -> W#banned_word.word == <<"word1">> end, List)),
?assert(lists:any(fun(W) -> W#banned_word.word == <<"word2">> end, List)).

View File

@@ -1,14 +1,37 @@
-module(logic_auth_tests).
-include_lib("eunit/include/eunit.hrl").
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
%% ------------------------------------------------------------------
%% Фикстуры
%% ------------------------------------------------------------------
setup() ->
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose),
ok.
cleanup(_) ->
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose).
%% ------------------------------------------------------------------
%% Тесты
%% ------------------------------------------------------------------
logic_auth_test_() ->
[
{"Password hash test", fun test_password_hash/0},
{"JWT generate and verify test", fun test_jwt/0},
{"JWT expired test", fun test_jwt_expired/0},
{"Refresh token test", fun test_refresh_token/0}
{setup, fun setup/0, fun cleanup/1, [
{"JWT generate and verify test", fun test_jwt/0},
{"JWT expired test", fun test_jwt_expired/0},
{"Refresh token test", fun test_refresh_token/0}
]}
].
%% ── Хеширование паролей (остаётся в logic_auth) ──────────────────
test_password_hash() ->
Password = <<"secret123">>,
{ok, Hash} = logic_auth:hash_password(Password),
@@ -16,31 +39,27 @@ test_password_hash() ->
{ok, true} = logic_auth:verify_password(Password, Hash),
{ok, false} = logic_auth:verify_password(<<"wrong">>, Hash).
%% ── JWT тесты (перенесены в auth) ─────────────────────────────────
test_jwt() ->
UserId = <<"user123">>,
Role = user,
Token = logic_auth:generate_jwt(UserId, Role),
Role = <<"user">>,
Token = auth:generate_user_token(UserId, Role),
?assert(is_binary(Token)),
{ok, Claims} = logic_auth:verify_jwt(Token),
?assertEqual(UserId, maps:get(<<"user_id">>, Claims)),
?assertEqual(<<"user">>, maps:get(<<"role">>, Claims)),
?assert(maps:is_key(<<"exp">>, Claims)),
?assert(maps:is_key(<<"iat">>, Claims)),
{ok, ReturnedUserId, ReturnedRole} = auth:verify_user_token(Token),
?assertEqual(UserId, ReturnedUserId),
?assertEqual(Role, ReturnedRole),
% Проверка невалидного токена
{error, invalid_token} = logic_auth:verify_jwt(<<"invalid.token.here">>).
{error, invalid_token} = auth:verify_user_token(<<"invalid.token.here">>).
test_jwt_expired() ->
% Пропускаем для простоты, так как требует мока времени
% Тест на истечение срока пока пропущен, так как требует мока времени
ok.
%% ── Refresh token (перенесён в auth) ────────────────────────────
test_refresh_token() ->
{Token, ExpiresAt} = logic_auth:generate_refresh_token(<<"user123">>),
{Token, ExpiresAt} = auth:generate_refresh_token(<<"user123">>),
?assert(is_binary(Token)),
?assert(size(Token) >= 32),
?assert(is_tuple(ExpiresAt)),
% Проверяем, что срок действия в будущем
Now = calendar:universal_time(),
?assert(is_integer(ExpiresAt)),
Now = os:system_time(second),
?assert(ExpiresAt > Now).