Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 1
This commit is contained in:
@@ -118,7 +118,9 @@
|
||||
|
||||
-record(banned_word, {
|
||||
id :: binary(),
|
||||
word :: binary()
|
||||
word :: binary(),
|
||||
added_by :: binary() | undefined, % id администратора, добавившего слово
|
||||
added_at :: calendar:datetime() | undefined
|
||||
}).
|
||||
|
||||
%% ------------------- Баг-трекер --------------------------------------
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
-module(core_banned_word).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([add/1, remove/1, list_all/0, is_banned/1]).
|
||||
-export([check_text/1, filter_text/1]).
|
||||
|
||||
%% Добавить слово в бан-лист
|
||||
add(Word) when is_binary(Word) ->
|
||||
WordLower = string:lowercase(Word),
|
||||
case is_banned(WordLower) of
|
||||
true -> {error, already_exists};
|
||||
false ->
|
||||
BannedWord = #banned_word{
|
||||
id = generate_id(),
|
||||
word = WordLower
|
||||
},
|
||||
F = fun() ->
|
||||
mnesia:write(BannedWord),
|
||||
{ok, BannedWord}
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end
|
||||
end.
|
||||
|
||||
%% Удалить слово из бан-листа
|
||||
remove(Word) when is_binary(Word) ->
|
||||
WordLower = string:lowercase(Word),
|
||||
Match = #banned_word{word = WordLower, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> {error, not_found};
|
||||
[BannedWord] ->
|
||||
F = fun() ->
|
||||
mnesia:delete_object(BannedWord),
|
||||
{ok, removed}
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end
|
||||
end.
|
||||
|
||||
%% Список всех запрещённых слов
|
||||
list_all() ->
|
||||
Match = #banned_word{_ = '_'},
|
||||
Words = mnesia:dirty_match_object(Match),
|
||||
{ok, [W#banned_word.word || W <- Words]}.
|
||||
|
||||
%% Проверить, является ли слово запрещённым
|
||||
is_banned(Word) when is_binary(Word) ->
|
||||
WordLower = string:lowercase(Word),
|
||||
Match = #banned_word{word = WordLower, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> false;
|
||||
_ -> true
|
||||
end.
|
||||
|
||||
%% Проверить текст на наличие запрещённых слов
|
||||
check_text(Text) when is_binary(Text) ->
|
||||
TextLower = string:lowercase(Text),
|
||||
Words = string:split(TextLower, " ", all),
|
||||
{ok, BannedWords} = list_all(),
|
||||
lists:any(fun(W) -> lists:member(W, BannedWords) end, Words).
|
||||
|
||||
%% Отфильтровать запрещённые слова (заменить на ***)
|
||||
filter_text(Text) when is_binary(Text) ->
|
||||
{ok, BannedWords} = list_all(),
|
||||
Words = binary:split(Text, <<" ">>, [global]),
|
||||
Filtered = lists:map(fun(W) ->
|
||||
case lists:member(string:lowercase(W), BannedWords) of
|
||||
true -> <<"***">>;
|
||||
false -> W
|
||||
end
|
||||
end, Words),
|
||||
iolist_to_binary(join_binary(Filtered, <<" ">>)).
|
||||
|
||||
join_binary([], _) -> [];
|
||||
join_binary([H], _) -> [H];
|
||||
join_binary([H|T], Sep) ->
|
||||
[H, Sep | join_binary(T, Sep)].
|
||||
|
||||
%% Внутренние функции
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||
54
src/core/core_banned_words.erl
Normal file
54
src/core/core_banned_words.erl
Normal file
@@ -0,0 +1,54 @@
|
||||
-module(core_banned_words).
|
||||
-export([list_banned_words/0,
|
||||
add_banned_word/2,
|
||||
remove_banned_word/1,
|
||||
update_banned_word/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
list_banned_words() ->
|
||||
mnesia:dirty_match_object(#banned_word{_ = '_'}).
|
||||
|
||||
add_banned_word(Word, AddedBy) ->
|
||||
Id = generate_id(),
|
||||
Now = calendar:universal_time(),
|
||||
BW = #banned_word{id = Id, word = Word, added_by = AddedBy, added_at = Now},
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:match_object(#banned_word{word = Word, _ = '_'}) of
|
||||
[] -> mnesia:write(BW), {ok, BW};
|
||||
_ -> mnesia:abort(already_exists)
|
||||
end
|
||||
end) of
|
||||
{atomic, {ok, BWRec}} -> {ok, BWRec};
|
||||
{aborted, already_exists} -> {error, already_exists};
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
remove_banned_word(Word) ->
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:match_object(#banned_word{word = Word, _ = '_'}) of
|
||||
[Rec] -> mnesia:delete_object(Rec), {ok, deleted};
|
||||
[] -> mnesia:abort(not_found)
|
||||
end
|
||||
end) of
|
||||
{atomic, {ok, deleted}} -> {ok, deleted};
|
||||
{aborted, not_found} -> {error, not_found}
|
||||
end.
|
||||
|
||||
update_banned_word(OldWord, NewWord) ->
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:match_object(#banned_word{word = OldWord, _ = '_'}) of
|
||||
[Rec] ->
|
||||
Updated = Rec#banned_word{word = NewWord},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated};
|
||||
[] ->
|
||||
mnesia:abort(not_found)
|
||||
end
|
||||
end) of
|
||||
{atomic, {ok, UpdatedRec}} -> {ok, UpdatedRec};
|
||||
{aborted, not_found} -> {error, not_found}
|
||||
end.
|
||||
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(9)).
|
||||
@@ -4,6 +4,7 @@
|
||||
-export([create/2, get_by_id/1, get_by_email/1, update/2, delete/1]).
|
||||
-export([email_exists/1]).
|
||||
-export([generate_id/0]).
|
||||
-export([list_users/0]).
|
||||
|
||||
%% Создание пользователя
|
||||
create(Email, Password) ->
|
||||
@@ -86,6 +87,22 @@ update(Id, Updates) ->
|
||||
delete(Id) ->
|
||||
update(Id, [{status, deleted}]).
|
||||
|
||||
list_users() ->
|
||||
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||
ActiveUsers = [U || U <- Users, U#user.status =/= deleted],
|
||||
{ok, [user_to_map(U) || U <- ActiveUsers]}.
|
||||
|
||||
user_to_map(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
password_hash => User#user.password_hash,
|
||||
role => User#user.role,
|
||||
status => User#user.status,
|
||||
created_at => User#user.created_at,
|
||||
updated_at => User#user.updated_at
|
||||
}.
|
||||
|
||||
%% Внутренние функции
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
-module(eventhub_app).
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
pg:start_link(),
|
||||
application:ensure_all_started(mnesia),
|
||||
application:ensure_all_started(cowboy),
|
||||
|
||||
case infra_sup:start_link() of
|
||||
{ok, Pid} ->
|
||||
ok = infra_mnesia:init_tables(),
|
||||
ok = infra_mnesia:wait_for_tables(),
|
||||
connect_nodes(),
|
||||
start_http(),
|
||||
start_admin_http(),
|
||||
% Запускаем сборщик метрик Prometheus
|
||||
start_http(), % Пользовательский API (8080)
|
||||
start_admin_http(), % Административный API (8445)
|
||||
application:ensure_all_started(prometheus),
|
||||
application:ensure_all_started(prometheus_cowboy),
|
||||
{ok, Pid};
|
||||
@@ -23,12 +20,13 @@ start(_StartType, _StartArgs) ->
|
||||
Error
|
||||
end.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
stop(_State) -> ok.
|
||||
|
||||
%% ===================================================================
|
||||
%% Пользовательский HTTP (порт 8080) — только публичные эндпоинты
|
||||
%% ===================================================================
|
||||
start_http() ->
|
||||
Port = application:get_env(eventhub, http_port, 8080),
|
||||
|
||||
Dispatch = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/metrics/[:registry]", prometheus_cowboy2_handler, []},
|
||||
@@ -52,83 +50,66 @@ start_http() ->
|
||||
{"/v1/reviews/:id", handler_review_by_id, []},
|
||||
{"/v1/reports", handler_reports, []},
|
||||
{"/v1/tickets", handler_tickets, []},
|
||||
{"/v1/subscription", handler_subscription, []},
|
||||
|
||||
% Админские маршруты - более конкретные ПЕРЕД общими
|
||||
{"/v1/admin/reports", handler_reports, []},
|
||||
{"/v1/admin/reports/:id", handler_report_by_id, []},
|
||||
{"/v1/admin/reviews/:id", handler_admin_reviews, []},
|
||||
{"/v1/admin/banned-words", handler_banned_words, []},
|
||||
{"/v1/admin/banned-words/:word", handler_banned_words, []},
|
||||
{"/v1/admin/tickets/stats", handler_ticket_stats, []},
|
||||
{"/v1/admin/tickets/:id", handler_ticket_by_id, []},
|
||||
{"/v1/admin/tickets", handler_tickets, []},
|
||||
{"/v1/admin/subscriptions", handler_admin_subscriptions, []},
|
||||
{"/v1/admin/subscriptions/:id", handler_admin_subscriptions, []},
|
||||
|
||||
% Общий маршрут для заморозки (должен быть последним)
|
||||
{"/v1/admin/:target_type/:id", handler_admin_moderation, []}
|
||||
{"/v1/subscription", handler_subscription, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
Middlewares = [
|
||||
cowboy_router,
|
||||
cowboy_handler
|
||||
],
|
||||
|
||||
Middlewares = [cowboy_router, cowboy_handler],
|
||||
Env = #{dispatch => Dispatch},
|
||||
|
||||
cowboy:start_clear(http, [{port, Port}], #{
|
||||
env => Env,
|
||||
middlewares => Middlewares
|
||||
}),
|
||||
|
||||
cowboy:start_clear(http, [{port, Port}], #{env => Env, middlewares => Middlewares}),
|
||||
io:format("HTTP server started on port ~p~n", [Port]).
|
||||
|
||||
%% ===================================================================
|
||||
%% Административный HTTP (порт 8445) — все админские эндпоинты
|
||||
%% ===================================================================
|
||||
start_admin_http() ->
|
||||
Port = application:get_env(eventhub, admin_http_port, 8445),
|
||||
|
||||
Dispatch = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/admin/health", admin_handler_health, []},
|
||||
{"/admin/stats", admin_handler_stats, []},
|
||||
{"/admin/users", admin_handler_users, []},
|
||||
{"/admin/users/:id", admin_handler_user_by_id, []}
|
||||
% ================== БАЗОВЫЕ ==================
|
||||
{"/v1/admin/health", admin_handler_health, []},
|
||||
{"/v1/admin/stats", admin_handler_stats, []},
|
||||
{"/v1/admin/login", admin_handler_login, []},
|
||||
% ================== ПОЛЬЗОВАТЕЛИ ==================
|
||||
{"/v1/admin/users", admin_handler_users, []},
|
||||
{"/v1/admin/users/:id", admin_handler_user_by_id, []},
|
||||
% ================== ОТЧЁТЫ ==================
|
||||
{"/v1/admin/reports", admin_handler_reports, []},
|
||||
{"/v1/admin/reports/:id", admin_handler_report_by_id, []},
|
||||
% ================== ОТЗЫВЫ ==================
|
||||
{"/v1/admin/reviews/:id", admin_handler_reviews, []},
|
||||
% ================== БАН-СЛОВА ==================
|
||||
{"/v1/admin/banned-words", admin_handler_banned_words, []},
|
||||
{"/v1/admin/banned-words/:word", admin_handler_banned_words, []},
|
||||
% ================== ТИКЕТЫ ==================
|
||||
{"/v1/admin/tickets/stats", admin_handler_ticket_stats, []},
|
||||
{"/v1/admin/tickets/:id", admin_handler_ticket_by_id, []},
|
||||
{"/v1/admin/tickets", admin_handler_tickets, []},
|
||||
% ================== ПОДПИСКИ ==================
|
||||
{"/v1/admin/subscriptions", admin_handler_subscriptions, []},
|
||||
{"/v1/admin/subscriptions/:id", admin_handler_subscriptions, []},
|
||||
% ================== МОДЕРАЦИЯ (общий маршрут) ==================
|
||||
{"/v1/admin/:target_type/:id", admin_handler_moderation, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
Middlewares = [
|
||||
cowboy_router,
|
||||
cowboy_handler
|
||||
],
|
||||
|
||||
Middlewares = [cowboy_router, cowboy_handler],
|
||||
Env = #{dispatch => Dispatch},
|
||||
|
||||
cowboy:start_clear(admin_http, [{port, Port}], #{
|
||||
env => Env,
|
||||
middlewares => Middlewares
|
||||
}),
|
||||
|
||||
cowboy:start_clear(admin_http, [{port, Port}], #{env => Env, middlewares => Middlewares}),
|
||||
io:format("Admin HTTP server started on port ~p~n", [Port]),
|
||||
|
||||
% WebSocket для пользователей
|
||||
WsDispatch = cowboy_router:compile([
|
||||
{'_', [{"/ws", ws_handler, []}]}
|
||||
]),
|
||||
cowboy:start_clear(ws, [{port, 8081}], #{
|
||||
env => #{dispatch => WsDispatch}
|
||||
}),
|
||||
WsDispatch = cowboy_router:compile([{'_', [{"/ws", ws_handler, []}]}]),
|
||||
cowboy:start_clear(ws, [{port, 8081}], #{env => #{dispatch => WsDispatch}}),
|
||||
|
||||
% WebSocket для админов
|
||||
AdminWsDispatch = cowboy_router:compile([
|
||||
{'_', [{"/admin/ws", admin_ws_handler, []}]}
|
||||
]),
|
||||
cowboy:start_clear(admin_ws, [{port, 8446}], #{
|
||||
env => #{dispatch => AdminWsDispatch}
|
||||
}),
|
||||
AdminWsDispatch = cowboy_router:compile([{'_', [{"/admin/ws", admin_ws_handler, []}]}]),
|
||||
cowboy:start_clear(admin_ws, [{port, 8446}], #{env => #{dispatch => AdminWsDispatch}}),
|
||||
|
||||
io:format("WebSocket started on ports 8081 (user) and 8446 (admin)~n").
|
||||
|
||||
%% ===================================================================
|
||||
%% Ручное подключение к нодам кластера (запасной вариант)
|
||||
%% ===================================================================
|
||||
connect_nodes() ->
|
||||
case os:getenv("JOIN_NODES") of
|
||||
false -> ok;
|
||||
|
||||
136
src/handlers/admin/admin_handler_banned_words.erl
Normal file
136
src/handlers/admin/admin_handler_banned_words.erl
Normal file
@@ -0,0 +1,136 @@
|
||||
-module(admin_handler_banned_words).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:binding(word, Req) of
|
||||
undefined -> handle_collection(Req);
|
||||
Word -> handle_item(Word, Req)
|
||||
end.
|
||||
|
||||
handle_collection(Req) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_banned_words(Req);
|
||||
<<"POST">> -> add_banned_word(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
handle_item(Word, Req) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"DELETE">> -> delete_banned_word(Word, Req);
|
||||
<<"PUT">> -> update_banned_word(Word, Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
list_banned_words(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Words = core_banned_words:list_banned_words(),
|
||||
send_json(Req1, 200, [banned_word_to_json(W) || W <- Words]);
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
add_banned_word(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"word">> := NewWord} ->
|
||||
case core_banned_words:add_banned_word(NewWord, AdminId) of
|
||||
{ok, WordRec} ->
|
||||
send_json(Req2, 201, banned_word_to_json(WordRec));
|
||||
{error, already_exists} ->
|
||||
send_error(Req2, 409, <<"Word already exists">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing 'word' field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
delete_banned_word(Word, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
case core_banned_words:remove_banned_word(Word) of
|
||||
{ok, deleted} ->
|
||||
send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Word not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_banned_word(Word, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"word">> := NewWord} ->
|
||||
case core_banned_words:update_banned_word(Word, NewWord) of
|
||||
{ok, WordRec} ->
|
||||
send_json(Req2, 200, banned_word_to_json(WordRec));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Word not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing 'word' field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
banned_word_to_json(BW) ->
|
||||
#{
|
||||
id => BW#banned_word.id,
|
||||
word => BW#banned_word.word,
|
||||
added_by => BW#banned_word.added_by,
|
||||
added_at => datetime_to_iso8601(BW#banned_word.added_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
51
src/handlers/admin/admin_handler_login.erl
Normal file
51
src/handlers/admin/admin_handler_login.erl
Normal file
@@ -0,0 +1,51 @@
|
||||
-module(admin_handler_login).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
init(Req0, State) ->
|
||||
case cowboy_req:method(Req0) of
|
||||
<<"POST">> ->
|
||||
case cowboy_req:has_body(Req0) of
|
||||
true ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req0),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case auth:authenticate_admin_request(Req1, Email, Password) of
|
||||
{ok, Token, User} ->
|
||||
Resp = jsx:encode(#{
|
||||
<<"token">> => Token,
|
||||
<<"user">> => #{
|
||||
<<"id">> => maps:get(id, User),
|
||||
<<"email">> => maps:get(email, User),
|
||||
<<"role">> => maps:get(role, User)
|
||||
}
|
||||
}),
|
||||
Req2 = cowboy_req:reply(200, #{
|
||||
<<"content-type">> => <<"application/json">>,
|
||||
<<"access-control-allow-origin">> => <<"*">>
|
||||
}, Resp, Req1),
|
||||
{ok, Req2, State};
|
||||
{error, insufficient_permissions} ->
|
||||
error_response(403, insufficient_permissions, Req1, State);
|
||||
{error, Reason} ->
|
||||
error_response(401, Reason, Req1, State)
|
||||
end;
|
||||
_ ->
|
||||
error_response(400, <<"invalid_request">>, Req1, State)
|
||||
catch
|
||||
_:_ -> error_response(400, <<"invalid_request">>, Req1, State)
|
||||
end;
|
||||
false ->
|
||||
error_response(400, <<"Missing request body">>, Req0, State)
|
||||
end;
|
||||
_ ->
|
||||
error_response(405, <<"Method not allowed">>, Req0, State)
|
||||
end.
|
||||
|
||||
error_response(Code, Reason, Req, State) ->
|
||||
Body = jsx:encode(#{<<"error">> => Reason}),
|
||||
Req2 = cowboy_req:reply(Code, #{
|
||||
<<"content-type">> => <<"application/json">>,
|
||||
<<"access-control-allow-origin">> => <<"*">>
|
||||
}, Body, Req),
|
||||
{ok, Req2, State}.
|
||||
154
src/handlers/admin/admin_handler_moderation.erl
Normal file
154
src/handlers/admin/admin_handler_moderation.erl
Normal file
@@ -0,0 +1,154 @@
|
||||
-module(admin_handler_moderation).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
-define(VALID_TARGETS, [<<"calendar">>, <<"event">>, <<"review">>, <<"user">>]).
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"PUT">> -> moderate(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
moderate(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
TargetType = cowboy_req:binding(target_type, Req1),
|
||||
TargetId = cowboy_req:binding(id, Req1),
|
||||
case lists:member(TargetType, ?VALID_TARGETS) of
|
||||
true ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"action">> := Action} ->
|
||||
apply_moderation(TargetType, TargetId, Action, Req2);
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing 'action' field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 400, <<"Invalid target_type">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
apply_moderation(<<"calendar">>, Id, Action, Req) ->
|
||||
handle_calendar(Id, Action, Req);
|
||||
apply_moderation(<<"event">>, Id, Action, Req) ->
|
||||
handle_event(Id, Action, Req);
|
||||
apply_moderation(<<"review">>, Id, Action, Req) ->
|
||||
handle_review(Id, Action, Req);
|
||||
apply_moderation(<<"user">>, Id, Action, Req) ->
|
||||
handle_user(Id, Action, Req).
|
||||
|
||||
handle_calendar(Id, <<"freeze">>, Req) ->
|
||||
case core_calendar:freeze(Id) of
|
||||
{ok, Calendar} -> send_json(Req, 200, calendar_to_json(Calendar));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Calendar not found">>)
|
||||
end;
|
||||
handle_calendar(Id, <<"unfreeze">>, Req) ->
|
||||
case core_calendar:unfreeze(Id) of
|
||||
{ok, Calendar} -> send_json(Req, 200, calendar_to_json(Calendar));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Calendar not found">>)
|
||||
end;
|
||||
handle_calendar(_Id, _Action, Req) ->
|
||||
send_error(Req, 400, <<"Invalid action for calendar">>).
|
||||
|
||||
handle_event(Id, <<"freeze">>, Req) ->
|
||||
case core_event:freeze(Id) of
|
||||
{ok, Event} -> send_json(Req, 200, event_to_json(Event));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Event not found">>)
|
||||
end;
|
||||
handle_event(Id, <<"unfreeze">>, Req) ->
|
||||
case core_event:unfreeze(Id) of
|
||||
{ok, Event} -> send_json(Req, 200, event_to_json(Event));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Event not found">>)
|
||||
end;
|
||||
handle_event(_Id, _Action, Req) ->
|
||||
send_error(Req, 400, <<"Invalid action for event">>).
|
||||
|
||||
handle_review(Id, <<"hide">>, Req) ->
|
||||
case core_review:hide(Id) of
|
||||
{ok, Review} -> send_json(Req, 200, review_to_json(Review));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Review not found">>)
|
||||
end;
|
||||
handle_review(Id, <<"show">>, Req) ->
|
||||
case core_review:show(Id) of
|
||||
{ok, Review} -> send_json(Req, 200, review_to_json(Review));
|
||||
{error, not_found} -> send_error(Req, 404, <<"Review not found">>)
|
||||
end;
|
||||
handle_review(_Id, _Action, Req) ->
|
||||
send_error(Req, 400, <<"Invalid action for review">>).
|
||||
|
||||
handle_user(Id, <<"block">>, Req) ->
|
||||
case core_user:block(Id) of
|
||||
{ok, User} -> send_json(Req, 200, user_to_json(User));
|
||||
{error, not_found} -> send_error(Req, 404, <<"User not found">>)
|
||||
end;
|
||||
handle_user(Id, <<"unblock">>, Req) ->
|
||||
case core_user:unblock(Id) of
|
||||
{ok, User} -> send_json(Req, 200, user_to_json(User));
|
||||
{error, not_found} -> send_error(Req, 404, <<"User not found">>)
|
||||
end;
|
||||
handle_user(_Id, _Action, Req) ->
|
||||
send_error(Req, 400, <<"Invalid action for user">>).
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
calendar_to_json(C) ->
|
||||
#{
|
||||
id => C#calendar.id,
|
||||
title => C#calendar.title,
|
||||
status => atom_to_binary(C#calendar.status, utf8)
|
||||
}.
|
||||
|
||||
event_to_json(E) ->
|
||||
#{
|
||||
id => E#event.id,
|
||||
title => E#event.title,
|
||||
status => atom_to_binary(E#event.status, utf8)
|
||||
}.
|
||||
|
||||
review_to_json(R) ->
|
||||
#{
|
||||
id => R#review.id,
|
||||
status => atom_to_binary(R#review.status, utf8)
|
||||
}.
|
||||
|
||||
user_to_json(U) ->
|
||||
#{
|
||||
id => U#user.id,
|
||||
email => U#user.email,
|
||||
status => atom_to_binary(U#user.status, utf8)
|
||||
}.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
96
src/handlers/admin/admin_handler_report_by_id.erl
Normal file
96
src/handlers/admin/admin_handler_report_by_id.erl
Normal file
@@ -0,0 +1,96 @@
|
||||
-module(admin_handler_report_by_id).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_report(Req);
|
||||
<<"PUT">> -> update_report(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
get_report(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
ReportId = cowboy_req:binding(id, Req1),
|
||||
case core_report:get_by_id(ReportId) of
|
||||
{ok, Report} ->
|
||||
send_json(Req1, 200, report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Report not found">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_report(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
ReportId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"status">> := NewStatus} ->
|
||||
case core_report:update_status(ReportId, NewStatus) of
|
||||
{ok, Report} ->
|
||||
send_json(Req2, 200, report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Report not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing status field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
report_to_json(R) ->
|
||||
#{
|
||||
id => R#report.id,
|
||||
reporter_id => R#report.reporter_id,
|
||||
target_type => R#report.target_type,
|
||||
target_id => R#report.target_id,
|
||||
reason => R#report.reason,
|
||||
status => R#report.status,
|
||||
created_at => datetime_to_iso8601(R#report.created_at),
|
||||
resolved_at => datetime_to_iso8601(R#report.resolved_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
91
src/handlers/admin/admin_handler_reports.erl
Normal file
91
src/handlers/admin/admin_handler_reports.erl
Normal file
@@ -0,0 +1,91 @@
|
||||
-module(admin_handler_reports).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl"). %% ← обязательно для #user{} и #report{}
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_reports(Req);
|
||||
<<"PUT">> -> update_report(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
list_reports(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
Reports = core_report:list_reports(),
|
||||
send_json(Req1, 200, [report_to_json(R) || R <- Reports]);
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_report(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
ReportId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"status">> := NewStatus} ->
|
||||
case core_report:update_status(ReportId, NewStatus) of
|
||||
{ok, Report} ->
|
||||
send_json(Req2, 200, report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Report not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing status field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
report_to_json(R) ->
|
||||
#{
|
||||
id => R#report.id,
|
||||
reporter_id => R#report.reporter_id,
|
||||
target_type => R#report.target_type,
|
||||
target_id => R#report.target_id,
|
||||
reason => R#report.reason,
|
||||
status => R#report.status,
|
||||
created_at => datetime_to_iso8601(R#report.created_at),
|
||||
resolved_at => datetime_to_iso8601(R#report.resolved_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
97
src/handlers/admin/admin_handler_reviews.erl
Normal file
97
src/handlers/admin/admin_handler_reviews.erl
Normal file
@@ -0,0 +1,97 @@
|
||||
-module(admin_handler_reviews).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_review(Req);
|
||||
<<"PUT">> -> update_review(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
get_review(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
ReviewId = cowboy_req:binding(id, Req1),
|
||||
case core_review:get_by_id(ReviewId) of
|
||||
{ok, Review} ->
|
||||
send_json(Req1, 200, review_to_json(Review));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Review not found">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_review(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
ReviewId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"status">> := NewStatus} ->
|
||||
case core_review:update_status(ReviewId, NewStatus) of
|
||||
{ok, Review} ->
|
||||
send_json(Req2, 200, review_to_json(Review));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Review not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing status field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
review_to_json(R) ->
|
||||
#{
|
||||
id => R#review.id,
|
||||
user_id => R#review.user_id,
|
||||
target_type => R#review.target_type,
|
||||
target_id => R#review.target_id,
|
||||
rating => R#review.rating,
|
||||
comment => R#review.comment,
|
||||
status => R#review.status,
|
||||
created_at => datetime_to_iso8601(R#review.created_at),
|
||||
updated_at => datetime_to_iso8601(R#review.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
@@ -1,6 +1,5 @@
|
||||
-module(admin_handler_stats).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
-export([count_users/0, count_calendars/0, count_events/0, count_bookings/0,
|
||||
count_reviews/0, count_reports/0, count_tickets/0, count_subscriptions/0]).
|
||||
@@ -41,33 +40,21 @@ get_stats(Req) ->
|
||||
%% Вспомогательные функции
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} -> User#user.role =:= admin;
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
count_users() ->
|
||||
length(mnesia:dirty_match_object(#user{_ = '_'})).
|
||||
|
||||
count_calendars() ->
|
||||
length(mnesia:dirty_match_object(#calendar{_ = '_'})).
|
||||
|
||||
count_events() ->
|
||||
length(mnesia:dirty_match_object(#event{is_instance = false, _ = '_'})).
|
||||
|
||||
count_bookings() ->
|
||||
length(mnesia:dirty_match_object(#booking{_ = '_'})).
|
||||
|
||||
count_reviews() ->
|
||||
length(mnesia:dirty_match_object(#review{_ = '_'})).
|
||||
|
||||
count_reports() ->
|
||||
length(mnesia:dirty_match_object(#report{_ = '_'})).
|
||||
|
||||
count_tickets() ->
|
||||
length(mnesia:dirty_match_object(#ticket{_ = '_'})).
|
||||
|
||||
count_subscriptions() ->
|
||||
length(mnesia:dirty_match_object(#subscription{_ = '_'})).
|
||||
count_users() -> length(mnesia:dirty_match_object(#user{_ = '_'})).
|
||||
count_calendars() -> length(mnesia:dirty_match_object(#calendar{_ = '_'})).
|
||||
count_events() -> length(mnesia:dirty_match_object(#event{is_instance = false, _ = '_'})).
|
||||
count_bookings() -> length(mnesia:dirty_match_object(#booking{_ = '_'})).
|
||||
count_reviews() -> length(mnesia:dirty_match_object(#review{_ = '_'})).
|
||||
count_reports() -> length(mnesia:dirty_match_object(#report{_ = '_'})).
|
||||
count_tickets() -> length(mnesia:dirty_match_object(#ticket{_ = '_'})).
|
||||
count_subscriptions() -> length(mnesia:dirty_match_object(#subscription{_ = '_'})).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
|
||||
160
src/handlers/admin/admin_handler_subscriptions.erl
Normal file
160
src/handlers/admin/admin_handler_subscriptions.erl
Normal file
@@ -0,0 +1,160 @@
|
||||
-module(admin_handler_subscriptions).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:binding(id, Req) of
|
||||
undefined ->
|
||||
handle_collection(Req);
|
||||
_SubscriptionId ->
|
||||
handle_item(Req)
|
||||
end.
|
||||
|
||||
handle_collection(Req) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_subscriptions(Req);
|
||||
<<"POST">> -> create_subscription(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
handle_item(Req) ->
|
||||
SubscriptionId = cowboy_req:binding(id, Req),
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_subscription(SubscriptionId, Req);
|
||||
<<"PUT">> -> update_subscription(SubscriptionId, Req);
|
||||
<<"DELETE">> -> delete_subscription(SubscriptionId, Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
list_subscriptions(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Subscriptions = core_subscription:list_subscriptions(),
|
||||
send_json(Req1, 200, [subscription_to_json(S) || S <- Subscriptions]);
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
create_subscription(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"user_id">> := _UserId} = Data ->
|
||||
SubscriptionData = maps:merge(#{
|
||||
<<"status">> => <<"active">>,
|
||||
<<"trial_used">> => false
|
||||
}, Data),
|
||||
case core_subscription:create_subscription(SubscriptionData) of
|
||||
{ok, Subscription} ->
|
||||
send_json(Req2, 201, subscription_to_json(Subscription));
|
||||
{error, Reason} ->
|
||||
send_error(Req2, 500, Reason)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing 'user_id' field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
get_subscription(SubscriptionId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
case core_subscription:get_by_id(SubscriptionId) of
|
||||
{ok, Subscription} ->
|
||||
send_json(Req1, 200, subscription_to_json(Subscription));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Subscription not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_subscription(SubscriptionId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
UpdatesMap when is_map(UpdatesMap) ->
|
||||
case core_subscription:update_subscription(SubscriptionId, UpdatesMap) of
|
||||
{ok, Subscription} ->
|
||||
send_json(Req2, 200, subscription_to_json(Subscription));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Subscription not found">>);
|
||||
{error, Reason} ->
|
||||
send_error(Req2, 500, Reason)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
delete_subscription(SubscriptionId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
case core_subscription:delete_subscription(SubscriptionId) of
|
||||
{ok, deleted} ->
|
||||
send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Subscription not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
subscription_to_json(S) ->
|
||||
#{
|
||||
id => S#subscription.id,
|
||||
user_id => S#subscription.user_id,
|
||||
plan => atom_to_binary(S#subscription.plan, utf8),
|
||||
status => atom_to_binary(S#subscription.status, utf8),
|
||||
trial_used => S#subscription.trial_used,
|
||||
started_at => datetime_to_iso8601(S#subscription.started_at),
|
||||
expires_at => datetime_to_iso8601(S#subscription.expires_at),
|
||||
created_at => datetime_to_iso8601(S#subscription.created_at),
|
||||
updated_at => datetime_to_iso8601(S#subscription.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
115
src/handlers/admin/admin_handler_ticket_by_id.erl
Normal file
115
src/handlers/admin/admin_handler_ticket_by_id.erl
Normal file
@@ -0,0 +1,115 @@
|
||||
-module(admin_handler_ticket_by_id).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_ticket(Req);
|
||||
<<"PUT">> -> update_ticket(Req);
|
||||
<<"DELETE">> -> delete_ticket(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
get_ticket(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
case core_ticket:get_by_id(TicketId) of
|
||||
{ok, Ticket} ->
|
||||
send_json(Req1, 200, ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_ticket(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
UpdatesMap when is_map(UpdatesMap) ->
|
||||
case core_ticket:update_ticket(TicketId, UpdatesMap) of
|
||||
{ok, Ticket} ->
|
||||
send_json(Req2, 200, ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Ticket not found">>);
|
||||
{error, Reason} ->
|
||||
send_error(Req2, 500, Reason)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
delete_ticket(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
case core_ticket:delete_ticket(TicketId) of
|
||||
{ok, deleted} ->
|
||||
send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
ticket_to_json(T) ->
|
||||
#{
|
||||
id => T#ticket.id,
|
||||
error_hash => T#ticket.error_hash,
|
||||
error_message => T#ticket.error_message,
|
||||
stacktrace => T#ticket.stacktrace,
|
||||
context => T#ticket.context,
|
||||
count => T#ticket.count,
|
||||
first_seen => datetime_to_iso8601(T#ticket.first_seen),
|
||||
last_seen => datetime_to_iso8601(T#ticket.last_seen),
|
||||
status => T#ticket.status,
|
||||
assigned_to => T#ticket.assigned_to,
|
||||
resolution_note => T#ticket.resolution_note
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
50
src/handlers/admin/admin_handler_ticket_stats.erl
Normal file
50
src/handlers/admin/admin_handler_ticket_stats.erl
Normal file
@@ -0,0 +1,50 @@
|
||||
-module(admin_handler_ticket_stats).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl"). % ← добавлено
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_stats(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
get_stats(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Stats = core_ticket:stats(),
|
||||
send_json(Req1, 200, Stats);
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
160
src/handlers/admin/admin_handler_tickets.erl
Normal file
160
src/handlers/admin/admin_handler_tickets.erl
Normal file
@@ -0,0 +1,160 @@
|
||||
-module(admin_handler_tickets).
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:binding(id, Req) of
|
||||
undefined -> handle_collection(Req);
|
||||
TicketId -> handle_item(TicketId, Req)
|
||||
end.
|
||||
|
||||
handle_collection(Req) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_tickets(Req);
|
||||
<<"POST">> -> create_ticket(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
handle_item(TicketId, Req) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_ticket(TicketId, Req);
|
||||
<<"PUT">> -> update_ticket(TicketId, Req);
|
||||
<<"DELETE">> -> delete_ticket(TicketId, Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
list_tickets(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Tickets = core_ticket:list_tickets(),
|
||||
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]);
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
create_ticket(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"error_message">> := _} = Data ->
|
||||
% Администратор может указать error_hash, stacktrace, context, status
|
||||
TicketData = maps:merge(#{
|
||||
<<"status">> => <<"open">>,
|
||||
<<"assigned_to">> => undefined
|
||||
}, Data),
|
||||
case core_ticket:create_ticket(TicketData) of
|
||||
{ok, Ticket} ->
|
||||
send_json(Req2, 201, ticket_to_json(Ticket));
|
||||
{error, Reason} ->
|
||||
send_error(Req2, 500, Reason)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing 'error_message' field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
get_ticket(TicketId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
case core_ticket:get_by_id(TicketId) of
|
||||
{ok, Ticket} ->
|
||||
send_json(Req1, 200, ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
update_ticket(TicketId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
UpdatesMap when is_map(UpdatesMap) ->
|
||||
case core_ticket:update_ticket(TicketId, UpdatesMap) of
|
||||
{ok, Ticket} ->
|
||||
send_json(Req2, 200, ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Ticket not found">>);
|
||||
{error, Reason} ->
|
||||
send_error(Req2, 500, Reason)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
delete_ticket(TicketId, Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
case core_ticket:delete_ticket(TicketId) of
|
||||
{ok, deleted} ->
|
||||
send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
auth_admin(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true -> {ok, AdminId, Req1};
|
||||
false -> {error, 403, <<"Admin access required">>, Req1}
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
{error, Code, Message, Req1}
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Role = User#user.role,
|
||||
Role =:= admin orelse Role =:= superadmin orelse
|
||||
Role =:= moderator orelse Role =:= support;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
ticket_to_json(T) ->
|
||||
#{
|
||||
id => T#ticket.id,
|
||||
error_hash => T#ticket.error_hash,
|
||||
error_message => T#ticket.error_message,
|
||||
stacktrace => T#ticket.stacktrace,
|
||||
context => T#ticket.context,
|
||||
count => T#ticket.count,
|
||||
first_seen => datetime_to_iso8601(T#ticket.first_seen),
|
||||
last_seen => datetime_to_iso8601(T#ticket.last_seen),
|
||||
status => T#ticket.status,
|
||||
assigned_to => T#ticket.assigned_to,
|
||||
resolution_note => T#ticket.resolution_note
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
@@ -1,6 +1,5 @@
|
||||
-module(admin_handler_user_by_id).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
-export([user_to_json/1, convert_updates/1]).
|
||||
|
||||
@@ -9,10 +8,10 @@ init(Req, Opts) ->
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_user(Req);
|
||||
<<"PUT">> -> update_user(Req);
|
||||
<<"GET">> -> get_user(Req);
|
||||
<<"PUT">> -> update_user(Req);
|
||||
<<"DELETE">> -> delete_user(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
get_user(Req) ->
|
||||
@@ -22,11 +21,10 @@ get_user(Req) ->
|
||||
true ->
|
||||
UserId = cowboy_req:binding(id, Req1),
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} when User#user.status =:= deleted ->
|
||||
send_error(Req1, 404, <<"User not found">>);
|
||||
{ok, User} ->
|
||||
case User#user.status of
|
||||
deleted -> send_error(Req1, 404, <<"User not found">>);
|
||||
_ -> send_json(Req1, 200, user_to_json(User))
|
||||
end;
|
||||
send_json(Req1, 200, user_to_json(User));
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"User not found">>)
|
||||
end;
|
||||
@@ -47,9 +45,8 @@ update_user(Req) ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
Decoded when is_map(Decoded) ->
|
||||
Updates = maps:to_list(Decoded),
|
||||
% Преобразуем бинарные значения в атомы где нужно
|
||||
ConvertedUpdates = convert_updates(Updates),
|
||||
case core_user:update(UserId, ConvertedUpdates) of
|
||||
Converted = convert_updates(Updates),
|
||||
case core_user:update(UserId, Converted) of
|
||||
{ok, User} ->
|
||||
send_json(Req2, 200, user_to_json(User));
|
||||
{error, not_found} ->
|
||||
@@ -69,16 +66,6 @@ update_user(Req) ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
convert_updates(Updates) ->
|
||||
lists:map(fun
|
||||
({<<"status">>, <<"active">>}) -> {status, active};
|
||||
({<<"status">>, <<"frozen">>}) -> {status, frozen};
|
||||
({<<"status">>, <<"deleted">>}) -> {status, deleted};
|
||||
({<<"role">>, <<"user">>}) -> {role, user};
|
||||
({<<"role">>, <<"admin">>}) -> {role, admin};
|
||||
(Other) -> Other
|
||||
end, Updates).
|
||||
|
||||
delete_user(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
@@ -106,10 +93,10 @@ is_admin(UserId) ->
|
||||
|
||||
user_to_json(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role,
|
||||
status => User#user.status,
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role,
|
||||
status => User#user.status,
|
||||
created_at => datetime_to_iso8601(User#user.created_at),
|
||||
updated_at => datetime_to_iso8601(User#user.updated_at)
|
||||
}.
|
||||
@@ -118,6 +105,13 @@ datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
convert_updates(Updates) ->
|
||||
lists:map(fun
|
||||
({<<"status">>, Value}) -> {status, binary_to_existing_atom(Value)};
|
||||
({<<"role">>, Value}) -> {role, binary_to_existing_atom(Value)};
|
||||
(Other) -> Other
|
||||
end, Updates).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
|
||||
@@ -1,47 +1,30 @@
|
||||
-module(admin_handler_users).
|
||||
-include("records.hrl").
|
||||
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_users(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
list_users(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||
ActiveUsers = [U || U <- Users, U#user.status =/= deleted],
|
||||
Response = [user_to_json(U) || U <- ActiveUsers],
|
||||
<<"GET">> ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, _UserId, Req1} ->
|
||||
{ok, Users} = core_user:list_users(),
|
||||
Response = [user_to_map(U) || U <- Users],
|
||||
send_json(Req1, 200, Response);
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
_ ->
|
||||
send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} -> User#user.role =:= admin;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
user_to_json(User) ->
|
||||
user_to_map(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role,
|
||||
status => User#user.status,
|
||||
created_at => datetime_to_iso8601(User#user.created_at),
|
||||
updated_at => datetime_to_iso8601(User#user.updated_at)
|
||||
id => maps:get(id, User),
|
||||
email => maps:get(email, User),
|
||||
role => maps:get(role, User),
|
||||
status => maps:get(status, User),
|
||||
created_at => datetime_to_iso8601(maps:get(created_at, User)),
|
||||
updated_at => datetime_to_iso8601(maps:get(updated_at, User))
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
-module(handler_admin_moderation).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"PUT">> -> moderate(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% PUT /v1/admin/:target_type/:id - заморозка/разморозка
|
||||
moderate(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
TargetTypeBin = cowboy_req:binding(target_type, Req1),
|
||||
TargetId = cowboy_req:binding(id, Req1),
|
||||
TargetType = parse_target_type(TargetTypeBin),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"action">> := Action} ->
|
||||
case {TargetType, Action} of
|
||||
{calendar, <<"freeze">>} ->
|
||||
case logic_moderation:freeze_calendar(AdminId, TargetId) of
|
||||
{ok, Calendar} ->
|
||||
send_json(Req2, 200, calendar_to_json(Calendar));
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Calendar not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{calendar, <<"unfreeze">>} ->
|
||||
case logic_moderation:unfreeze_calendar(AdminId, TargetId) of
|
||||
{ok, Calendar} ->
|
||||
send_json(Req2, 200, calendar_to_json(Calendar));
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Calendar not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{event, <<"freeze">>} ->
|
||||
case logic_moderation:freeze_event(AdminId, TargetId) of
|
||||
{ok, Event} ->
|
||||
send_json(Req2, 200, event_to_json(Event));
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Event not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{event, <<"unfreeze">>} ->
|
||||
case logic_moderation:unfreeze_event(AdminId, TargetId) of
|
||||
{ok, Event} ->
|
||||
send_json(Req2, 200, event_to_json(Event));
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Event not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid target_type or action">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing action field">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
parse_target_type(<<"calendars">>) -> calendar;
|
||||
parse_target_type(<<"events">>) -> event;
|
||||
parse_target_type(_) -> undefined.
|
||||
|
||||
calendar_to_json(Calendar) ->
|
||||
#{
|
||||
id => Calendar#calendar.id,
|
||||
owner_id => Calendar#calendar.owner_id,
|
||||
title => Calendar#calendar.title,
|
||||
description => Calendar#calendar.description,
|
||||
type => Calendar#calendar.type,
|
||||
tags => Calendar#calendar.tags,
|
||||
status => Calendar#calendar.status,
|
||||
rating_avg => Calendar#calendar.rating_avg,
|
||||
rating_count => Calendar#calendar.rating_count,
|
||||
created_at => datetime_to_iso8601(Calendar#calendar.created_at),
|
||||
updated_at => datetime_to_iso8601(Calendar#calendar.updated_at)
|
||||
}.
|
||||
|
||||
event_to_json(Event) ->
|
||||
#{
|
||||
id => Event#event.id,
|
||||
calendar_id => Event#event.calendar_id,
|
||||
title => Event#event.title,
|
||||
description => Event#event.description,
|
||||
event_type => Event#event.event_type,
|
||||
start_time => datetime_to_iso8601(Event#event.start_time),
|
||||
duration => Event#event.duration,
|
||||
status => Event#event.status,
|
||||
tags => Event#event.tags,
|
||||
rating_avg => Event#event.rating_avg,
|
||||
rating_count => Event#event.rating_count,
|
||||
created_at => datetime_to_iso8601(Event#event.created_at),
|
||||
updated_at => datetime_to_iso8601(Event#event.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
@@ -1,93 +0,0 @@
|
||||
-module(handler_admin_reviews).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"PUT">> -> moderate_review(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% PUT /v1/admin/reviews/:id - скрыть/раскрыть отзыв
|
||||
moderate_review(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
% Проверим роль
|
||||
case core_user:get_by_id(AdminId) of
|
||||
{ok, User} ->
|
||||
io:format("User ~p role: ~p~n", [AdminId, User#user.role]);
|
||||
_ -> ok
|
||||
end,
|
||||
ReviewId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"action">> := Action} ->
|
||||
case Action of
|
||||
<<"hide">> ->
|
||||
case logic_review:hide_review(AdminId, ReviewId) of
|
||||
{ok, Review} ->
|
||||
Response = review_to_json(Review),
|
||||
send_json(Req2, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Review not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
<<"unhide">> ->
|
||||
case logic_review:unhide_review(AdminId, ReviewId) of
|
||||
{ok, Review} ->
|
||||
Response = review_to_json(Review),
|
||||
send_json(Req2, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Review not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid action. Use 'hide' or 'unhide'">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing action field">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
review_to_json(Review) ->
|
||||
#{
|
||||
id => Review#review.id,
|
||||
user_id => Review#review.user_id,
|
||||
target_type => Review#review.target_type,
|
||||
target_id => Review#review.target_id,
|
||||
rating => Review#review.rating,
|
||||
comment => Review#review.comment,
|
||||
status => Review#review.status,
|
||||
created_at => datetime_to_iso8601(Review#review.created_at),
|
||||
updated_at => datetime_to_iso8601(Review#review.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
@@ -1,84 +0,0 @@
|
||||
-module(handler_admin_subscriptions).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_subscriptions(Req);
|
||||
<<"DELETE">> -> cancel_subscription(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% GET /v1/admin/subscriptions - список всех подписок
|
||||
list_subscriptions(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
{ok, Subscriptions} = core_subscription:list_all(),
|
||||
Response = [subscription_to_json(S) || S <- Subscriptions],
|
||||
send_json(Req1, 200, Response);
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% DELETE /v1/admin/subscriptions/:id - отменить подписку
|
||||
cancel_subscription(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
SubscriptionId = cowboy_req:binding(id, Req1),
|
||||
case logic_subscription:cancel_subscription(AdminId, SubscriptionId) of
|
||||
{ok, Subscription} ->
|
||||
Response = subscription_to_json(Subscription),
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Subscription not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} -> User#user.role =:= admin;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
subscription_to_json(Subscription) ->
|
||||
#{
|
||||
id => Subscription#subscription.id,
|
||||
user_id => Subscription#subscription.user_id,
|
||||
plan => Subscription#subscription.plan,
|
||||
status => Subscription#subscription.status,
|
||||
trial_used => Subscription#subscription.trial_used,
|
||||
started_at => datetime_to_iso8601(Subscription#subscription.started_at),
|
||||
expires_at => datetime_to_iso8601(Subscription#subscription.expires_at),
|
||||
created_at => datetime_to_iso8601(Subscription#subscription.created_at),
|
||||
updated_at => datetime_to_iso8601(Subscription#subscription.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
@@ -1,10 +1,11 @@
|
||||
-module(handler_login).
|
||||
-include("records.hrl").
|
||||
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
-include("records.hrl"). %% ← необходим для #session{}
|
||||
|
||||
init(Req0, State) ->
|
||||
handle(Req0, State).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
@@ -18,41 +19,31 @@ handle(Req, _Opts) ->
|
||||
_ ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case core_user:get_by_email(Email) of
|
||||
{ok, User} ->
|
||||
case logic_auth:verify_password(Password, User#user.password_hash) of
|
||||
{ok, true} ->
|
||||
case User#user.status of
|
||||
active ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
{RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||
save_refresh_token(User#user.id, RefreshToken, ExpiresAt),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role
|
||||
},
|
||||
token => Token,
|
||||
refresh_token => RefreshToken
|
||||
},
|
||||
send_json(Req1, 200, Response);
|
||||
frozen ->
|
||||
send_error(Req1, 403, <<"Account frozen">>);
|
||||
deleted ->
|
||||
send_error(Req1, 403, <<"Account deleted">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
end;
|
||||
{error, not_found} ->
|
||||
case auth:authenticate_user_request(Req1, Email, Password) of
|
||||
{ok, Token, User} ->
|
||||
{RefreshToken, ExpiresAt} = auth:generate_refresh_token(maps:get(id, User)),
|
||||
save_refresh_token(maps:get(id, User), RefreshToken, ExpiresAt),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => maps:get(id, User),
|
||||
email => maps:get(email, User),
|
||||
role => maps:get(role, User)
|
||||
},
|
||||
token => Token,
|
||||
refresh_token => RefreshToken
|
||||
},
|
||||
send_json(Req1, 200, Response);
|
||||
{error, frozen} ->
|
||||
send_error(Req1, 403, <<"Account frozen">>);
|
||||
{error, deleted} ->
|
||||
send_error(Req1, 403, <<"Account deleted">>);
|
||||
{error, _Reason} ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Missing email or password">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req1, 400, <<"Invalid JSON">>)
|
||||
_:_ -> send_error(Req1, 400, <<"Invalid JSON">>)
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
@@ -63,7 +54,7 @@ handle(Req, _Opts) ->
|
||||
end.
|
||||
|
||||
save_refresh_token(UserId, Token, ExpiresAt) ->
|
||||
Session = #session{
|
||||
Session = #session{ %% record определён в records.hrl
|
||||
token = Token,
|
||||
user_id = UserId,
|
||||
expires_at = ExpiresAt,
|
||||
@@ -73,10 +64,14 @@ save_refresh_token(UserId, Token, ExpiresAt) ->
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
cowboy_req:reply(Status, #{
|
||||
<<"content-type">> => <<"application/json">>
|
||||
}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
cowboy_req:reply(Status, #{
|
||||
<<"content-type">> => <<"application/json">>
|
||||
}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
157
src/infra/auth.erl
Normal file
157
src/infra/auth.erl
Normal file
@@ -0,0 +1,157 @@
|
||||
-module(auth).
|
||||
-export([
|
||||
generate_user_token/2,
|
||||
generate_admin_token/2,
|
||||
verify_user_token/1,
|
||||
verify_admin_token/1,
|
||||
authenticate_user_request/3,
|
||||
authenticate_admin_request/3,
|
||||
generate_refresh_token/1
|
||||
]).
|
||||
|
||||
%% ========== КОНФИГУРАЦИЯ СЕКРЕТОВ ==========
|
||||
|
||||
-spec get_user_secret() -> binary().
|
||||
get_user_secret() ->
|
||||
case application:get_env(eventhub, jwt_secret) of
|
||||
{ok, Secret} when is_binary(Secret) -> Secret;
|
||||
undefined -> get_user_secret_from_env()
|
||||
end.
|
||||
|
||||
get_user_secret_from_env() ->
|
||||
case os:getenv("JWT_SECRET") of
|
||||
false -> <<"user-secret-key-32-bytes-minimum!">>;
|
||||
S -> list_to_binary(S)
|
||||
end.
|
||||
|
||||
-spec get_admin_secret() -> binary().
|
||||
get_admin_secret() ->
|
||||
case application:get_env(eventhub, admin_jwt_secret) of
|
||||
{ok, Secret} when is_binary(Secret) -> Secret;
|
||||
undefined -> get_admin_secret_from_env()
|
||||
end.
|
||||
|
||||
get_admin_secret_from_env() ->
|
||||
case os:getenv("ADMIN_JWT_SECRET") of
|
||||
false -> <<"admin-secret-key-32-bytes-minimum!">>;
|
||||
S -> list_to_binary(S)
|
||||
end.
|
||||
|
||||
-spec get_user_jwk() -> jose_jwk:key().
|
||||
get_user_jwk() -> jose_jwk:from_oct(get_user_secret()).
|
||||
|
||||
-spec get_admin_jwk() -> jose_jwk:key().
|
||||
get_admin_jwk() -> jose_jwk:from_oct(get_admin_secret()).
|
||||
|
||||
%% ========== ГЕНЕРАЦИЯ ТОКЕНОВ ==========
|
||||
|
||||
-spec generate_user_token(UserId :: binary(), Role :: binary()) -> binary().
|
||||
generate_user_token(UserId, Role) ->
|
||||
generate_token(get_user_jwk(), UserId, Role, <<"user">>).
|
||||
|
||||
-spec generate_admin_token(UserId :: binary(), Role :: binary()) -> binary().
|
||||
generate_admin_token(UserId, Role) ->
|
||||
generate_token(get_admin_jwk(), UserId, Role, <<"admin">>).
|
||||
|
||||
generate_token(JWK, UserId, Role, Audience) ->
|
||||
ExpTime = erlang:system_time(second) + 86400,
|
||||
Claims = #{
|
||||
<<"user_id">> => UserId,
|
||||
<<"role">> => Role,
|
||||
<<"aud">> => Audience,
|
||||
<<"exp">> => ExpTime,
|
||||
<<"iat">> => erlang:system_time(second)
|
||||
},
|
||||
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"HS256">>}, Claims),
|
||||
{_, Token} = jose_jws:compact(JWT),
|
||||
Token.
|
||||
|
||||
%% ========== ПРОВЕРКА ТОКЕНОВ ==========
|
||||
|
||||
-spec verify_user_token(Token :: binary()) ->
|
||||
{ok, UserId :: binary(), Role :: binary()} | {error, atom()}.
|
||||
verify_user_token(Token) ->
|
||||
verify_token(get_user_jwk(), Token, <<"user">>).
|
||||
|
||||
-spec verify_admin_token(Token :: binary()) ->
|
||||
{ok, UserId :: binary(), Role :: binary()} | {error, atom()}.
|
||||
verify_admin_token(Token) ->
|
||||
verify_token(get_admin_jwk(), Token, <<"admin">>).
|
||||
|
||||
verify_token(JWK, Token, ExpectedAud) ->
|
||||
try
|
||||
case jose_jwt:verify(JWK, Token) of
|
||||
{true, {jose_jwt, Claims}, _} ->
|
||||
validate_claims(Claims, ExpectedAud);
|
||||
{true, Claims, _} when is_map(Claims) ->
|
||||
validate_claims(Claims, ExpectedAud);
|
||||
_ ->
|
||||
{error, invalid_signature}
|
||||
end
|
||||
catch
|
||||
_:_ -> {error, invalid_token}
|
||||
end.
|
||||
|
||||
validate_claims(Claims, ExpectedAud) ->
|
||||
case maps:find(<<"aud">>, Claims) of
|
||||
{ok, ExpectedAud} ->
|
||||
case maps:find(<<"exp">>, Claims) of
|
||||
{ok, Exp} when is_integer(Exp) ->
|
||||
Now = erlang:system_time(second),
|
||||
if
|
||||
Exp > Now ->
|
||||
UserId = maps:get(<<"user_id">>, Claims, undefined),
|
||||
Role = maps:get(<<"role">>, Claims, <<"user">>),
|
||||
{ok, UserId, Role};
|
||||
true ->
|
||||
{error, expired}
|
||||
end;
|
||||
{ok, _Exp} -> {error, expired};
|
||||
_ -> {error, no_expiration}
|
||||
end;
|
||||
{ok, _} -> {error, invalid_audience};
|
||||
error -> {error, missing_audience}
|
||||
end.
|
||||
|
||||
%% ========== АУТЕНТИФИКАЦИЯ ЗАПРОСА ==========
|
||||
|
||||
-spec authenticate_user_request(Req :: cowboy_req:req(), Email :: binary(), Password :: binary()) ->
|
||||
{ok, Token :: binary(), User :: map()} | {error, atom()}.
|
||||
authenticate_user_request(_Req, Email, Password) ->
|
||||
case logic_auth:authenticate_user(Email, Password) of
|
||||
{ok, User} ->
|
||||
UserId = maps:get(id, User),
|
||||
Role = maps:get(role, User, <<"user">>),
|
||||
Token = generate_user_token(UserId, Role),
|
||||
{ok, Token, User};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
-spec authenticate_admin_request(Req :: cowboy_req:req(), Email :: binary(), Password :: binary()) ->
|
||||
{ok, Token :: binary(), User :: map()} | {error, atom()}.
|
||||
authenticate_admin_request(_Req, Email, Password) ->
|
||||
case logic_auth:authenticate_user(Email, Password) of
|
||||
{ok, User} ->
|
||||
Role = maps:get(role, User, <<"admin">>),
|
||||
case is_admin_role(Role) of
|
||||
true ->
|
||||
UserId = maps:get(id, User),
|
||||
Token = generate_admin_token(UserId, Role),
|
||||
{ok, Token, User};
|
||||
false -> {error, insufficient_permissions}
|
||||
end;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%% ========== REFRESH TOKEN ==========
|
||||
|
||||
-spec generate_refresh_token(UserId :: binary()) -> {binary(), integer()}.
|
||||
generate_refresh_token(_UserId) ->
|
||||
RefreshToken = base64:encode(crypto:strong_rand_bytes(32)),
|
||||
ExpiresAt = erlang:system_time(second) + 2592000, % 30 дней
|
||||
{RefreshToken, ExpiresAt}.
|
||||
|
||||
%% ========== ВНУТРЕННИЕ ==========
|
||||
|
||||
is_admin_role(Role) ->
|
||||
lists:member(Role, [<<"admin">>, <<"superadmin">>, <<"moderator">>, <<"support">>]).
|
||||
158
test/unit/admin_handler_banned_words_tests.erl
Normal file
158
test/unit/admin_handler_banned_words_tests.erl
Normal 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]).
|
||||
49
test/unit/admin_handler_health_tests.erl
Normal file
49
test/unit/admin_handler_health_tests.erl
Normal 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])).
|
||||
104
test/unit/admin_handler_login_tests.erl
Normal file
104
test/unit/admin_handler_login_tests.erl
Normal 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},
|
||||
{"Non‑admin 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]).
|
||||
189
test/unit/admin_handler_moderation_tests.erl
Normal file
189
test/unit/admin_handler_moderation_tests.erl
Normal 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]).
|
||||
145
test/unit/admin_handler_report_by_id_tests.erl
Normal file
145
test/unit/admin_handler_report_by_id_tests.erl
Normal 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]).
|
||||
121
test/unit/admin_handler_reports_tests.erl
Normal file
121
test/unit/admin_handler_reports_tests.erl
Normal 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]).
|
||||
146
test/unit/admin_handler_reviews_tests.erl
Normal file
146
test/unit/admin_handler_reviews_tests.erl
Normal 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]).
|
||||
@@ -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()).
|
||||
219
test/unit/admin_handler_subscriptions_tests.erl
Normal file
219
test/unit/admin_handler_subscriptions_tests.erl
Normal 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]).
|
||||
187
test/unit/admin_handler_ticket_by_id_tests.erl
Normal file
187
test/unit/admin_handler_ticket_by_id_tests.erl
Normal 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]).
|
||||
61
test/unit/admin_handler_ticket_stats_tests.erl
Normal file
61
test/unit/admin_handler_ticket_stats_tests.erl
Normal 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]).
|
||||
203
test/unit/admin_handler_tickets_tests.erl
Normal file
203
test/unit/admin_handler_tickets_tests.erl
Normal 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]).
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
139
test/unit/auth_test.erl
Normal 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">>).
|
||||
@@ -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)).
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user