Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 2. Final #3
This commit is contained in:
58
Makefile
58
Makefile
@@ -33,7 +33,8 @@ compile: ## Скомпилировать проект
|
||||
clean: ## Очистить проект
|
||||
@echo "Очистка проекта..."
|
||||
@$(REBAR3) clean
|
||||
@rm -rf _build build deps logs *.log
|
||||
#@rm -rf _build build/ct_run.* deps logs *.log
|
||||
@rm -rf build/ct_run.* deps logs *.log
|
||||
@echo "✓ Очистка завершена"
|
||||
|
||||
deps: ## Установить зависимости
|
||||
@@ -61,7 +62,7 @@ test-server: ## Запустить тестовый сервер в фоне
|
||||
@echo "Cleaning old data..."
|
||||
@rm -rf Mnesia.*
|
||||
@echo "Starting server..."
|
||||
@rebar3 shell --sname eventhub_test </dev/null > /tmp/eventhub_test.log 2>&1 &
|
||||
@$(REBAR3) shell --sname eventhub_test </dev/null > /tmp/eventhub_test.log 2>&1 &
|
||||
@echo "PID: $$!"
|
||||
@for i in 1 2 3 4 5 6 7 8 9 10; do \
|
||||
if curl -s http://localhost:8080/health | grep -q "ok"; then \
|
||||
@@ -96,66 +97,23 @@ eunit-verbose: ## Запустить EUnit тесты с подробным вы
|
||||
@echo "Запуск EUnit тестов (verbose)..."
|
||||
@$(REBAR3) eunit --sname $(SNAME)_test --verbose
|
||||
|
||||
test-search-unit: ## Запустить unit-тесты поиска
|
||||
@echo "Запуск unit-тестов поиска (logic)..."
|
||||
@$(REBAR3) eunit --sname test_search1 --module=logic_search_tests
|
||||
|
||||
test-search-handler: ## Запустить handler тесты поиска
|
||||
@echo "Запуск handler тестов поиска..."
|
||||
@$(REBAR3) eunit --sname test_search2 --module=handler_search_tests
|
||||
|
||||
test-api: test-ct
|
||||
|
||||
test-ct: ## Запустить Common Test для API
|
||||
@rebar3 ct --sname $(SNAME)_api_test
|
||||
@$(REBAR3) ct --sname $(SNAME)_api_test
|
||||
|
||||
test-ct-verbose: ## Запустить Common Test с подробным выводом
|
||||
@ct_run -suite test/ct/api_SUITE \
|
||||
@ct_run -suite test/api_SUITE \
|
||||
-pa _build/default/lib/*/ebin \
|
||||
-pa test/ct/api \
|
||||
-logdir logs/ct \
|
||||
-pa test/api \
|
||||
-logdir build \
|
||||
-verbosity 50
|
||||
|
||||
test-api-auth: ## Тесты аутентификации
|
||||
@rebar3 shell --eval "api_auth_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-calendar: ## Тесты календарей
|
||||
@rebar3 shell --eval "api_calendar_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-event: ## Тесты событий
|
||||
@rebar3 shell --eval "api_event_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-booking: ## Тесты бронирований
|
||||
@rebar3 shell --eval "api_booking_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-search: ## Тесты поиска
|
||||
@rebar3 shell --eval "api_search_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-reviews: ## Тесты отзывов
|
||||
@rebar3 shell --eval "api_reviews_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-moderation: ## Тесты модерации
|
||||
@rebar3 shell --eval "api_moderation_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-tickets: ## Тесты тикетов
|
||||
@rebar3 shell --eval "api_tickets_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-subscription: ## Тесты подписки
|
||||
@rebar3 shell --eval "api_subscription_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-admin: ## Тесты админки
|
||||
@rebar3 shell --eval "api_admin_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-api-ws: ## Тесты админки
|
||||
@rebar3 shell --eval "api_websocket_tests:test()." --name test_api@127.0.0.1
|
||||
|
||||
test-scripts: ## Запустить тесты с фильтром (make test-runner PATTERN=booking)
|
||||
@chmod +x test/scripts/*.sh
|
||||
@cd test/scripts && ./run_tests.sh $(PATTERN)
|
||||
|
||||
test-all: eunit ## Запустить ВСЕ тесты (EUnit + API)
|
||||
@sleep 1
|
||||
make test-api
|
||||
test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API)
|
||||
@echo "========================================"
|
||||
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
||||
@echo "========================================"
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
%% ------------------- Баг-трекер --------------------------------------
|
||||
-record(ticket, {
|
||||
id :: binary(),
|
||||
reporter_id :: binary(),
|
||||
error_hash :: binary(),
|
||||
error_message :: binary(),
|
||||
stacktrace :: binary(),
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{ct_opts, [
|
||||
{src_dirs, ["src", "test/api"]},
|
||||
{sys_config, ["config/sys.config"]}, % Load app config
|
||||
{logdir, "build"}, % Where to put HTML reports
|
||||
{logdir, "_build/test/ct"}, % Where to put HTML reports
|
||||
{verbose, true} % Print more info to console
|
||||
]}.
|
||||
|
||||
@@ -52,5 +52,3 @@
|
||||
{i, "include"}, % Include directory
|
||||
{d, 'DEBUG'} % Define macros
|
||||
]}.
|
||||
|
||||
{eunit_opts, [verbose]}.
|
||||
@@ -4,6 +4,12 @@
|
||||
-export([create/3, get_by_id/1, get_active_by_user/1, list_by_user/1, list_all/0]).
|
||||
-export([update_status/2, check_expired/0]).
|
||||
-export([generate_id/0]).
|
||||
% --------------- новые обёртки для админки ------------------
|
||||
-export([list_subscriptions/0,
|
||||
create_subscription/1,
|
||||
update_subscription/2,
|
||||
delete_subscription/1
|
||||
]).
|
||||
|
||||
-define(TRIAL_DAYS, 30).
|
||||
|
||||
@@ -141,3 +147,71 @@ add_months(DateTime, Months) ->
|
||||
add_days(DateTime, Days) ->
|
||||
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||
calendar:gregorian_seconds_to_datetime(Seconds + (Days * 86400)).
|
||||
|
||||
% ================================================================
|
||||
% Новые обёртки для совместимости с admin_handler_subscriptions
|
||||
% ================================================================
|
||||
|
||||
list_subscriptions() ->
|
||||
{ok, Subs} = list_all(),
|
||||
Subs.
|
||||
|
||||
create_subscription(Data) ->
|
||||
UserId = maps:get(<<"user_id">>, Data),
|
||||
Plan = case maps:get(<<"plan">>, Data, <<"monthly">>) of
|
||||
<<"monthly">> -> monthly;
|
||||
<<"yearly">> -> yearly;
|
||||
<<"quarterly">>-> quarterly;
|
||||
<<"biannual">> -> biannual;
|
||||
<<"annual">> -> annual;
|
||||
Other -> Other
|
||||
end,
|
||||
TrialUsed = maps:get(<<"trial_used">>, Data, false),
|
||||
create(UserId, Plan, TrialUsed).
|
||||
|
||||
update_subscription(Id, Updates) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, Sub} ->
|
||||
Updated = apply_updates(Sub, Updates),
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
delete_subscription(Id) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, _Sub} ->
|
||||
mnesia:dirty_delete({subscription, Id}),
|
||||
{ok, deleted};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%% Применение обновлений к записи подписки
|
||||
apply_updates(Sub, Updates) ->
|
||||
lists:foldl(fun({Key, Value}, Acc) ->
|
||||
case Key of
|
||||
<<"status">> ->
|
||||
NewStatus = case Value of
|
||||
<<"active">> -> active;
|
||||
<<"cancelled">> -> cancelled;
|
||||
<<"expired">> -> expired;
|
||||
Other -> Other
|
||||
end,
|
||||
Acc#subscription{status = NewStatus, updated_at = calendar:universal_time()};
|
||||
<<"plan">> ->
|
||||
NewPlan = case Value of
|
||||
<<"monthly">> -> monthly;
|
||||
<<"yearly">> -> yearly;
|
||||
<<"quarterly">>-> quarterly;
|
||||
<<"biannual">> -> biannual;
|
||||
<<"annual">> -> annual;
|
||||
Other -> Other
|
||||
end,
|
||||
Acc#subscription{plan = NewPlan, updated_at = calendar:universal_time()};
|
||||
<<"trial_used">> ->
|
||||
Acc#subscription{trial_used = Value, updated_at = calendar:universal_time()};
|
||||
<<"expires_at">> ->
|
||||
Acc#subscription{expires_at = Value, updated_at = calendar:universal_time()};
|
||||
_ -> Acc
|
||||
end
|
||||
end, Sub, maps:to_list(Updates)).
|
||||
@@ -1,143 +1,86 @@
|
||||
-module(core_ticket).
|
||||
-include("records.hrl").
|
||||
-export([list_all/0,
|
||||
get_by_id/1,
|
||||
update_ticket/2,
|
||||
delete_ticket/1,
|
||||
stats/0,
|
||||
create_ticket/1,
|
||||
list_by_user/1]).
|
||||
|
||||
-export([create_or_update/3, get_by_id/1, get_by_error_hash/1, list_all/0, list_by_status/1]).
|
||||
-export([update_status/2, assign/2, add_resolution/2]).
|
||||
-export([generate_id/0, generate_error_hash/2]).
|
||||
list_all() ->
|
||||
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
||||
|
||||
%% Создать или обновить тикет (группировка по хэшу ошибки)
|
||||
create_or_update(ErrorMessage, Stacktrace, Context) ->
|
||||
ErrorHash = generate_error_hash(ErrorMessage, Stacktrace),
|
||||
case get_by_error_hash(ErrorHash) of
|
||||
{error, not_found} ->
|
||||
% Создаём новый тикет
|
||||
Id = generate_id(),
|
||||
get_by_id(Id) ->
|
||||
case mnesia:dirty_read({ticket, Id}) of
|
||||
[Ticket] -> {ok, Ticket};
|
||||
[] -> {error, not_found}
|
||||
end.
|
||||
|
||||
update_ticket(Id, Updates) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, Ticket} ->
|
||||
Updated = apply_updates(Ticket, Updates),
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
delete_ticket(Id) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, _Ticket} -> % переменная не используется
|
||||
mnesia:dirty_delete({ticket, Id}),
|
||||
{ok, deleted};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
stats() ->
|
||||
Tickets = list_all(),
|
||||
#{
|
||||
total => length(Tickets),
|
||||
open => count_by_status(open, Tickets),
|
||||
in_progress => count_by_status(in_progress, Tickets),
|
||||
resolved => count_by_status(resolved, Tickets),
|
||||
closed => count_by_status(closed, Tickets)
|
||||
}.
|
||||
|
||||
%% ── новые функции ──────────────────────────────────────
|
||||
create_ticket(Data) ->
|
||||
Id = base64:encode(crypto:strong_rand_bytes(9)),
|
||||
Now = calendar:universal_time(),
|
||||
Ticket = #ticket{
|
||||
id = Id,
|
||||
error_hash = ErrorHash,
|
||||
error_message = ErrorMessage,
|
||||
stacktrace = Stacktrace,
|
||||
context = term_to_binary(Context),
|
||||
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
|
||||
error_hash = maps:get(<<"error_hash">>, Data, <<"">>),
|
||||
error_message = maps:get(<<"error_message">>, Data),
|
||||
stacktrace = maps:get(<<"stacktrace">>, Data, <<"">>),
|
||||
context = maps:get(<<"context">>, Data, <<"">>),
|
||||
count = 1,
|
||||
first_seen = Now,
|
||||
last_seen = Now,
|
||||
status = open,
|
||||
assigned_to = undefined,
|
||||
resolution_note = undefined
|
||||
status = maps:get(<<"status">>, Data, open),
|
||||
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
|
||||
resolution_note = maps:get(<<"resolution_note">>, Data, undefined)
|
||||
},
|
||||
F = fun() ->
|
||||
mnesia:write(Ticket),
|
||||
{ok, Ticket}
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end;
|
||||
{ok, Ticket} ->
|
||||
% Обновляем существующий
|
||||
F = fun() ->
|
||||
Updated = Ticket#ticket{
|
||||
count = Ticket#ticket.count + 1,
|
||||
last_seen = calendar:universal_time()
|
||||
},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated}
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
mnesia:dirty_write(Ticket),
|
||||
{ok, Ticket}.
|
||||
|
||||
list_by_user(UserId) ->
|
||||
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
||||
|
||||
%% ── внутренние ─────────────────────────────────────────
|
||||
apply_updates(Ticket, Updates) ->
|
||||
lists:foldl(fun({Key, Value}, Acc) ->
|
||||
case Key of
|
||||
<<"status">> -> Acc#ticket{status = binary_to_atom(Value, utf8)};
|
||||
<<"assigned_to">> -> Acc#ticket{assigned_to = Value};
|
||||
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
|
||||
<<"error_message">> -> Acc#ticket{error_message = Value};
|
||||
<<"stacktrace">> -> Acc#ticket{stacktrace = Value};
|
||||
<<"context">> -> Acc#ticket{context = Value};
|
||||
_ -> Acc
|
||||
end
|
||||
end.
|
||||
end, Ticket, maps:to_list(Updates)).
|
||||
|
||||
%% Получение тикета по ID
|
||||
get_by_id(Id) ->
|
||||
case mnesia:dirty_read(ticket, Id) of
|
||||
[] -> {error, not_found};
|
||||
[Ticket] -> {ok, Ticket}
|
||||
end.
|
||||
|
||||
%% Получение тикета по хэшу ошибки
|
||||
get_by_error_hash(ErrorHash) ->
|
||||
Match = #ticket{error_hash = ErrorHash, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> {error, not_found};
|
||||
[Ticket] -> {ok, Ticket}
|
||||
end.
|
||||
|
||||
%% Список всех тикетов
|
||||
list_all() ->
|
||||
Match = #ticket{_ = '_'},
|
||||
Tickets = mnesia:dirty_match_object(Match),
|
||||
{ok, lists:sort(fun(A, B) -> A#ticket.last_seen >= B#ticket.last_seen end, Tickets)}.
|
||||
|
||||
%% Список тикетов по статусу
|
||||
list_by_status(Status) ->
|
||||
Match = #ticket{status = Status, _ = '_'},
|
||||
Tickets = mnesia:dirty_match_object(Match),
|
||||
{ok, lists:sort(fun(A, B) -> A#ticket.last_seen >= B#ticket.last_seen end, Tickets)}.
|
||||
|
||||
%% Обновление статуса тикета
|
||||
update_status(Id, Status) when Status =:= open; Status =:= in_progress; Status =:= resolved; Status =:= closed ->
|
||||
F = fun() ->
|
||||
case mnesia:read(ticket, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Ticket] ->
|
||||
Updated = Ticket#ticket{status = Status},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated}
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Назначить тикет администратору
|
||||
assign(Id, AdminId) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(ticket, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Ticket] ->
|
||||
Updated = Ticket#ticket{
|
||||
assigned_to = AdminId,
|
||||
status = case Ticket#ticket.status of
|
||||
open -> in_progress;
|
||||
S -> S
|
||||
end
|
||||
},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated}
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Добавить примечание о решении
|
||||
add_resolution(Id, Note) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(ticket, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Ticket] ->
|
||||
Updated = Ticket#ticket{resolution_note = Note},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated}
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Внутренние функции
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||
|
||||
generate_error_hash(ErrorMessage, Stacktrace) ->
|
||||
Data = iolist_to_binary([ErrorMessage, "\n", Stacktrace]),
|
||||
base64:encode(crypto:hash(sha256, Data), #{mode => urlsafe, padding => false}).
|
||||
count_by_status(Status, Tickets) ->
|
||||
length([T || T <- Tickets, T#ticket.status =:= Status]).
|
||||
@@ -5,6 +5,7 @@
|
||||
-export([email_exists/1]).
|
||||
-export([generate_id/0]).
|
||||
-export([list_users/0]).
|
||||
-export([block/1, unblock/1]).
|
||||
|
||||
%% Создание пользователя
|
||||
create(Email, Password) ->
|
||||
@@ -103,6 +104,24 @@ user_to_map(User) ->
|
||||
updated_at => User#user.updated_at
|
||||
}.
|
||||
|
||||
block(Id) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
Updated = User#user{status = blocked, updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
unblock(Id) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
Updated = User#user{status = active, updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%% Внутренние функции
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||
|
||||
@@ -50,6 +50,7 @@ start_http() ->
|
||||
{"/v1/reviews/:id", handler_review_by_id, []},
|
||||
{"/v1/reports", handler_reports, []},
|
||||
{"/v1/tickets", handler_tickets, []},
|
||||
{"/v1/tickets/:id", handler_ticket_by_id, []},
|
||||
{"/v1/subscription", handler_subscription, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
@@ -10,7 +10,7 @@ init(Req0, State) ->
|
||||
{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
|
||||
case eventhub_auth:authenticate_admin_request(Req1, Email, Password) of
|
||||
{ok, Token, User} ->
|
||||
Resp = jsx:encode(#{
|
||||
<<"token">> => Token,
|
||||
|
||||
@@ -39,7 +39,8 @@ update_report(Req) ->
|
||||
{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
|
||||
StatusAtom = binary_to_atom(NewStatus, utf8),
|
||||
case core_report:update_status(ReportId, StatusAtom, AdminId) of
|
||||
{ok, Report} ->
|
||||
send_json(Req2, 200, report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
-include("records.hrl"). %% ← обязательно для #user{} и #report{}
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
@@ -16,7 +16,7 @@ list_reports(Req) ->
|
||||
{ok, AdminId, Req1} ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
Reports = core_report:list_reports(),
|
||||
{ok, Reports} = core_report:list_all(),
|
||||
send_json(Req1, 200, [report_to_json(R) || R <- Reports]);
|
||||
false ->
|
||||
send_error(Req1, 403, <<"Admin access required">>)
|
||||
@@ -34,7 +34,7 @@ update_report(Req) ->
|
||||
{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
|
||||
case core_report:update_status(ReportId, NewStatus, AdminId) of
|
||||
{ok, Report} ->
|
||||
send_json(Req2, 200, report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
|
||||
@@ -39,7 +39,7 @@ list_subscriptions(Req) ->
|
||||
|
||||
create_subscription(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, _AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"user_id">> := _UserId} = Data ->
|
||||
|
||||
@@ -28,7 +28,7 @@ handle_item(TicketId, Req) ->
|
||||
list_tickets(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, _AdminId, Req1} ->
|
||||
Tickets = core_ticket:list_tickets(),
|
||||
Tickets = core_ticket:list_all(), % ← было list_tickets()
|
||||
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]);
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
@@ -36,7 +36,7 @@ list_tickets(Req) ->
|
||||
|
||||
create_ticket(Req) ->
|
||||
case auth_admin(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, _AdminId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"error_message">> := _} = Data ->
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
-module(admin_ws_handler).
|
||||
-behaviour(cowboy_websocket).
|
||||
|
||||
-export([init/2]).
|
||||
-export([websocket_init/1]).
|
||||
-export([websocket_handle/2]).
|
||||
@@ -21,15 +20,13 @@ init(Req, _Opts) ->
|
||||
Token ->
|
||||
io:format("[ADMIN_WS] Token received: ~s...~n", [binary_part(Token, 0, 30)]),
|
||||
case logic_auth:verify_jwt(Token) of
|
||||
{ok, Claims} ->
|
||||
UserId = maps:get(<<"user_id">>, Claims),
|
||||
Role = maps:get(<<"role">>, Claims),
|
||||
{ok, UserId, Role} ->
|
||||
io:format("[ADMIN_WS] UserId: ~s, Role: ~s~n", [UserId, Role]),
|
||||
case Role of
|
||||
<<"admin">> ->
|
||||
case is_admin_role(Role) of
|
||||
true ->
|
||||
io:format("[ADMIN_WS] Admin access granted~n"),
|
||||
{cowboy_websocket, Req, #state{admin_id = UserId}};
|
||||
_ ->
|
||||
false ->
|
||||
io:format("[ADMIN_WS] Access denied: not admin~n"),
|
||||
Resp = cowboy_req:reply(403, #{}, <<"Admin access required">>, Req),
|
||||
{ok, Resp, undefined}
|
||||
@@ -85,3 +82,6 @@ websocket_info(_Info, State) ->
|
||||
terminate(_Reason, _Req, _State) ->
|
||||
pg:leave(eventhub_admin_ws, self()),
|
||||
ok.
|
||||
|
||||
is_admin_role(Role) ->
|
||||
lists:member(Role, [<<"admin">>, <<"superadmin">>, <<"moderator">>, <<"support">>]).
|
||||
@@ -8,8 +8,7 @@ authenticate(Req) ->
|
||||
{bearer, Token} ->
|
||||
io:format("[AUTH] Bearer token found: ~s...~n", [binary_part(Token, 0, 30)]),
|
||||
case logic_auth:verify_jwt(Token) of
|
||||
{ok, Claims} ->
|
||||
UserId = maps:get(<<"user_id">>, Claims),
|
||||
{ok, UserId, _Role} ->
|
||||
io:format("[AUTH] JWT verified, UserId: ~s~n", [UserId]),
|
||||
{ok, UserId, Req};
|
||||
{error, expired} ->
|
||||
|
||||
@@ -19,9 +19,9 @@ handle(Req, _Opts) ->
|
||||
_ ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case auth:authenticate_user_request(Req1, Email, Password) of
|
||||
case eventhub_auth:authenticate_user_request(Req1, Email, Password) of
|
||||
{ok, Token, User} ->
|
||||
{RefreshToken, ExpiresAt} = auth:generate_refresh_token(maps:get(id, User)),
|
||||
{RefreshToken, ExpiresAt} = eventhub_auth:generate_refresh_token(maps:get(id, User)),
|
||||
save_refresh_token(maps:get(id, User), RefreshToken, ExpiresAt),
|
||||
Response = #{
|
||||
user => #{
|
||||
|
||||
@@ -1,91 +1,67 @@
|
||||
-module(handler_refresh).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
init(Req0, _Opts) ->
|
||||
case cowboy_req:method(Req0) of
|
||||
<<"POST">> ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||
case jsx:decode(Body, [return_maps]) of
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req0),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"refresh_token">> := RefreshToken} ->
|
||||
case validate_refresh_token(RefreshToken) of
|
||||
{ok, UserId} ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
% Генерируем новые токены
|
||||
NewToken = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
{NewRefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||
|
||||
% Сохраняем новый refresh token
|
||||
save_refresh_token(User#user.id, NewRefreshToken, ExpiresAt),
|
||||
|
||||
% Удаляем старый refresh token
|
||||
delete_refresh_token(RefreshToken),
|
||||
|
||||
Response = #{
|
||||
token => NewToken,
|
||||
refresh_token => NewRefreshToken
|
||||
},
|
||||
send_json(Req1, 200, Response);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 401, <<"User not found">>)
|
||||
end;
|
||||
{error, expired} ->
|
||||
send_error(Req1, 401, <<"Refresh token expired">>);
|
||||
{error, invalid} ->
|
||||
send_error(Req1, 401, <<"Invalid refresh token">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Missing refresh_token">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
validate_refresh_token(Token) ->
|
||||
case get_session_by_token(Token) of
|
||||
case get_session(RefreshToken) of
|
||||
{ok, Session} ->
|
||||
% Проверяем срок действия
|
||||
Now = calendar:universal_time(),
|
||||
case Session#session.expires_at > Now of
|
||||
true -> {ok, Session#session.user_id};
|
||||
false -> {error, expired}
|
||||
end;
|
||||
{error, not_found} ->
|
||||
{error, invalid}
|
||||
end.
|
||||
|
||||
get_session_by_token(Token) ->
|
||||
Match = #session{token = Token, type = refresh, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> {error, not_found};
|
||||
[Session] -> {ok, Session}
|
||||
end.
|
||||
|
||||
save_refresh_token(UserId, Token, ExpiresAt) ->
|
||||
Session = #session{
|
||||
token = Token,
|
||||
user_id = UserId,
|
||||
% Проверяем, не истекла ли сессия
|
||||
case Session#session.expires_at > calendar:universal_time() of
|
||||
true ->
|
||||
% Генерируем новый access-токен и refresh-токен
|
||||
User = get_user(Session#session.user_id),
|
||||
NewToken = eventhub_auth:generate_user_token(
|
||||
User#user.id,
|
||||
atom_to_binary(User#user.role, utf8)
|
||||
),
|
||||
{NewRefreshToken, ExpiresAt} =
|
||||
eventhub_auth:generate_refresh_token(User#user.id),
|
||||
% Удаляем старую сессию и сохраняем новую
|
||||
mnesia:dirty_delete_object(Session),
|
||||
NewSession = #session{
|
||||
token = NewRefreshToken,
|
||||
user_id = User#user.id,
|
||||
expires_at = ExpiresAt,
|
||||
type = refresh
|
||||
},
|
||||
mnesia:dirty_write(Session).
|
||||
|
||||
delete_refresh_token(Token) ->
|
||||
Match = #session{token = Token, type = refresh, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> ok;
|
||||
[Session] -> mnesia:dirty_delete_object(Session)
|
||||
mnesia:dirty_write(NewSession),
|
||||
Resp = jsx:encode(#{
|
||||
token => NewToken,
|
||||
refresh_token => NewRefreshToken
|
||||
}),
|
||||
cowboy_req:reply(200, #{
|
||||
<<"content-type">> => <<"application/json">>
|
||||
}, Resp, Req1);
|
||||
false ->
|
||||
mnesia:dirty_delete_object(Session),
|
||||
send_error(Req1, 401, <<"Refresh token expired">>)
|
||||
end;
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 401, <<"Refresh token not found">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Missing refresh_token field">>)
|
||||
catch
|
||||
_:_ -> send_error(Req1, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req0, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||
{ok, Body, []}.
|
||||
get_session(Token) ->
|
||||
case mnesia:dirty_read({session, Token}) of
|
||||
[Session] -> {ok, Session};
|
||||
[] -> {error, not_found}
|
||||
end.
|
||||
|
||||
get_user(UserId) ->
|
||||
[User] = mnesia:dirty_read({user, UserId}),
|
||||
User.
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
|
||||
@@ -24,7 +24,7 @@ handle(Req, _Opts) ->
|
||||
false ->
|
||||
case core_user:create(Email, Password) of
|
||||
{ok, User} ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
Token = logic_auth:generate_jwt(User#user.id, atom_to_binary(User#user.role, utf8)),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
|
||||
@@ -1,157 +1,93 @@
|
||||
-module(handler_ticket_by_id).
|
||||
-include("records.hrl").
|
||||
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
-include("records.hrl").
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_ticket(Req);
|
||||
<<"PUT">> -> update_ticket(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% GET /v1/admin/tickets/:id - получить тикет
|
||||
get_ticket(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, UserId, Req1} ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
case logic_ticket:get_ticket(AdminId, TicketId) of
|
||||
io:format("[TICKET_BY_ID] User ~s requests ticket ~s~n", [UserId, TicketId]),
|
||||
case core_ticket:get_by_id(TicketId) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Admin access required">>);
|
||||
io:format("[TICKET_BY_ID] Found ticket, reporter_id: ~s~n", [Ticket#ticket.reporter_id]),
|
||||
case is_admin(UserId) orelse Ticket#ticket.reporter_id =:= UserId of
|
||||
true ->
|
||||
io:format("[TICKET_BY_ID] Access granted~n"),
|
||||
send_json(Req1, 200, ticket_to_json(Ticket));
|
||||
false ->
|
||||
io:format("[TICKET_BY_ID] Access denied~n"),
|
||||
send_error(Req1, 403, <<"Access denied">>)
|
||||
end;
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Ticket not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
io:format("[TICKET_BY_ID] Ticket not found~n"),
|
||||
send_error(Req1, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
io:format("[TICKET_BY_ID] Auth error: ~p - ~s~n", [Code, Message]),
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% PUT /v1/admin/tickets/:id - обновить тикет
|
||||
update_ticket(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
{ok, UserId, Req1} ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
Decoded when is_map(Decoded) ->
|
||||
handle_ticket_action(AdminId, TicketId, Decoded, Req2);
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
Updates when is_map(Updates) ->
|
||||
case core_ticket:get_by_id(TicketId) of
|
||||
{ok, Ticket} ->
|
||||
case is_admin(UserId) orelse Ticket#ticket.reporter_id =:= UserId of
|
||||
true ->
|
||||
case core_ticket:update_ticket(TicketId, Updates) of
|
||||
{ok, Updated} -> send_json(Req2, 200, ticket_to_json(Updated));
|
||||
{error, R} -> send_error(Req2, 500, R)
|
||||
end;
|
||||
false -> send_error(Req2, 403, <<"Access denied">>)
|
||||
end;
|
||||
{error, not_found} -> send_error(Req2, 404, <<"Ticket not found">>)
|
||||
end;
|
||||
_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Обработка действий с тикетом
|
||||
handle_ticket_action(AdminId, TicketId, Body, Req) ->
|
||||
case maps:get(<<"action">>, Body, undefined) of
|
||||
<<"status">> ->
|
||||
case maps:get(<<"status">>, Body, undefined) of
|
||||
StatusBin when StatusBin =:= <<"open">>;
|
||||
StatusBin =:= <<"in_progress">>;
|
||||
StatusBin =:= <<"resolved">>;
|
||||
StatusBin =:= <<"closed">> ->
|
||||
Status = get_binary_to_atom(StatusBin),
|
||||
case logic_ticket:update_status(AdminId, TicketId, Status) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req, 404, <<"Ticket not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req, 400, <<"Invalid status">>)
|
||||
end;
|
||||
<<"assign">> ->
|
||||
case maps:get(<<"admin_id">>, Body, undefined) of
|
||||
undefined ->
|
||||
send_error(Req, 400, <<"Missing admin_id field">>);
|
||||
AssignToId ->
|
||||
case logic_ticket:assign_ticket(AdminId, TicketId, AssignToId) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req, 404, <<"Ticket not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req, 500, <<"Internal server error">>)
|
||||
end
|
||||
end;
|
||||
<<"resolve">> ->
|
||||
Note = maps:get(<<"note">>, Body, <<"">>),
|
||||
case logic_ticket:resolve_ticket(AdminId, TicketId, Note) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req, 404, <<"Ticket not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req, 500, <<"Internal server error">>)
|
||||
end;
|
||||
<<"close">> ->
|
||||
case logic_ticket:close_ticket(AdminId, TicketId) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req, 403, <<"Admin access required">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req, 404, <<"Ticket not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req, 400, <<"Invalid action">>)
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, U} -> lists:member(U#user.role, [admin, superadmin, moderator, support]);
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
ticket_to_json(Ticket) ->
|
||||
Context = try binary_to_term(Ticket#ticket.context) of
|
||||
C -> C
|
||||
catch
|
||||
_:_ -> #{}
|
||||
end,
|
||||
|
||||
ticket_to_json(T) ->
|
||||
#{
|
||||
id => Ticket#ticket.id,
|
||||
error_hash => Ticket#ticket.error_hash,
|
||||
error_message => Ticket#ticket.error_message,
|
||||
stacktrace => Ticket#ticket.stacktrace,
|
||||
context => Context,
|
||||
count => Ticket#ticket.count,
|
||||
first_seen => datetime_to_iso8601(Ticket#ticket.first_seen),
|
||||
last_seen => datetime_to_iso8601(Ticket#ticket.last_seen),
|
||||
status => Ticket#ticket.status,
|
||||
assigned_to => Ticket#ticket.assigned_to,
|
||||
resolution_note => Ticket#ticket.resolution_note
|
||||
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])).
|
||||
|
||||
get_binary_to_atom(<<"open">>) -> open;
|
||||
get_binary_to_atom(<<"in_progress">>) -> in_progress;
|
||||
get_binary_to_atom(<<"resolved">>) -> resolved;
|
||||
get_binary_to_atom(<<"closed">>) -> closed.
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
|
||||
@@ -1,113 +1,82 @@
|
||||
-module(handler_tickets).
|
||||
-include("records.hrl").
|
||||
|
||||
-behaviour(cowboy_handler).
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
-include("records.hrl").
|
||||
|
||||
init(Req0, Opts) ->
|
||||
handle(Req0, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_tickets(Req);
|
||||
<<"POST">> -> report_error(Req);
|
||||
<<"POST">> -> create_ticket(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% POST /v1/tickets - сообщить об ошибке (доступно всем)
|
||||
report_error(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, _UserId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
Decoded when is_map(Decoded) ->
|
||||
case Decoded of
|
||||
#{<<"error_message">> := ErrorMessage} ->
|
||||
Stacktrace = maps:get(<<"stacktrace">>, Decoded, <<"">>),
|
||||
Context = maps:get(<<"context">>, Decoded, #{}),
|
||||
|
||||
case logic_ticket:report_error(ErrorMessage, Stacktrace, Context) of
|
||||
{ok, Ticket} ->
|
||||
Response = ticket_to_json(Ticket),
|
||||
send_json(Req2, 201, Response);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing error_message field">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% GET /v1/admin/tickets - список тикетов (только админ)
|
||||
list_tickets(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, AdminId, Req1} ->
|
||||
Qs = cowboy_req:parse_qs(Req1),
|
||||
case proplists:get_value(<<"status">>, Qs) of
|
||||
undefined ->
|
||||
case logic_ticket:list_tickets(AdminId) of
|
||||
{ok, Tickets} ->
|
||||
Response = [ticket_to_json(T) || T <- Tickets],
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Admin access required">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
StatusBin ->
|
||||
Status = parse_status(StatusBin),
|
||||
case logic_ticket:list_tickets_by_status(AdminId, Status) of
|
||||
{ok, Tickets} ->
|
||||
Response = [ticket_to_json(T) || T <- Tickets],
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Admin access required">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end
|
||||
{ok, UserId, Req1} ->
|
||||
case is_admin(UserId) of
|
||||
true ->
|
||||
Tickets = core_ticket:list_all(),
|
||||
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]);
|
||||
false ->
|
||||
Tickets = core_ticket:list_by_user(UserId),
|
||||
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets])
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
parse_status(<<"open">>) -> open;
|
||||
parse_status(<<"in_progress">>) -> in_progress;
|
||||
parse_status(<<"resolved">>) -> resolved;
|
||||
parse_status(<<"closed">>) -> closed;
|
||||
parse_status(_) -> open.
|
||||
|
||||
ticket_to_json(Ticket) ->
|
||||
Context = try binary_to_term(Ticket#ticket.context) of
|
||||
C -> C
|
||||
create_ticket(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"error_message">> := _} = Data ->
|
||||
TicketData = maps:merge(#{<<"reporter_id">> => UserId, <<"status">> => <<"open">>}, 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
|
||||
_:_ -> #{}
|
||||
end,
|
||||
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
is_admin(UserId) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
lists:member(User#user.role, [admin, superadmin, moderator, support]);
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
ticket_to_json(T) ->
|
||||
#{
|
||||
id => Ticket#ticket.id,
|
||||
error_hash => Ticket#ticket.error_hash,
|
||||
error_message => Ticket#ticket.error_message,
|
||||
stacktrace => Ticket#ticket.stacktrace,
|
||||
context => Context,
|
||||
count => Ticket#ticket.count,
|
||||
first_seen => datetime_to_iso8601(Ticket#ticket.first_seen),
|
||||
last_seen => datetime_to_iso8601(Ticket#ticket.last_seen),
|
||||
status => Ticket#ticket.status,
|
||||
assigned_to => Ticket#ticket.assigned_to,
|
||||
resolution_note => Ticket#ticket.resolution_note
|
||||
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])).
|
||||
[Year, Month, Day, Hour, Minute, Second]));
|
||||
datetime_to_iso8601(undefined) -> undefined.
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
-module(handler_user_me).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
init(Req, Opts) -> handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
@@ -36,8 +34,7 @@ authenticate(Req) ->
|
||||
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
||||
{bearer, Token} ->
|
||||
case logic_auth:verify_jwt(Token) of
|
||||
{ok, Claims} ->
|
||||
UserId = maps:get(<<"user_id">>, Claims),
|
||||
{ok, UserId, _Role} -> % ← теперь возвращается {ok, UserId, Role}
|
||||
{ok, UserId, Req};
|
||||
{error, expired} ->
|
||||
{error, 401, <<"Token expired">>, Req};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
-module(ws_handler).
|
||||
-behaviour(cowboy_websocket).
|
||||
|
||||
-export([init/2]).
|
||||
-export([websocket_init/1]).
|
||||
-export([websocket_handle/2]).
|
||||
@@ -13,15 +12,13 @@
|
||||
}).
|
||||
|
||||
init(Req, _Opts) ->
|
||||
% Аутентификация через query параметр token
|
||||
Qs = cowboy_req:parse_qs(Req),
|
||||
case proplists:get_value(<<"token">>, Qs) of
|
||||
undefined ->
|
||||
{ok, cowboy_req:reply(401, #{}, <<"Missing token">>, Req), undefined};
|
||||
Token ->
|
||||
case logic_auth:verify_jwt(Token) of
|
||||
{ok, Claims} ->
|
||||
UserId = maps:get(<<"user_id">>, Claims),
|
||||
{ok, UserId, _Role} ->
|
||||
{cowboy_websocket, Req, #state{user_id = UserId}};
|
||||
{error, _} ->
|
||||
{ok, cowboy_req:reply(401, #{}, <<"Invalid token">>, Req), undefined}
|
||||
@@ -29,7 +26,6 @@ init(Req, _Opts) ->
|
||||
end.
|
||||
|
||||
websocket_init(State) ->
|
||||
% Регистрируем процесс в pg для получения уведомлений
|
||||
pg:join(eventhub_ws, self()),
|
||||
{ok, State}.
|
||||
|
||||
@@ -77,7 +73,6 @@ terminate(_Reason, _Req, _State) ->
|
||||
pg:leave(eventhub_ws, self()),
|
||||
ok.
|
||||
|
||||
%% Проверка, нужно ли отправлять уведомление пользователю
|
||||
should_notify(calendar_update, #{calendar_id := CalId}, State) ->
|
||||
lists:member(CalId, State#state.subscriptions);
|
||||
should_notify(booking_update, #{user_id := UserId}, State) ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-module(auth).
|
||||
-module(eventhub_auth).
|
||||
-export([
|
||||
generate_user_token/2,
|
||||
generate_admin_token/2,
|
||||
@@ -145,10 +145,13 @@ authenticate_admin_request(_Req, Email, Password) ->
|
||||
|
||||
%% ========== REFRESH TOKEN ==========
|
||||
|
||||
-spec generate_refresh_token(UserId :: binary()) -> {binary(), integer()}.
|
||||
-spec generate_refresh_token(UserId :: binary()) -> {binary(), calendar:datetime()}.
|
||||
generate_refresh_token(_UserId) ->
|
||||
RefreshToken = base64:encode(crypto:strong_rand_bytes(32)),
|
||||
ExpiresAt = erlang:system_time(second) + 2592000, % 30 дней
|
||||
Now = calendar:universal_time(),
|
||||
ExpiresAt = calendar:gregorian_seconds_to_datetime(
|
||||
calendar:datetime_to_gregorian_seconds(Now) + 30 * 24 * 3600
|
||||
),
|
||||
{RefreshToken, ExpiresAt}.
|
||||
|
||||
%% ========== ВНУТРЕННИЕ ==========
|
||||
@@ -1,88 +1,43 @@
|
||||
-module(logic_auth).
|
||||
-export([hash_password/1, verify_password/2,
|
||||
generate_jwt/2, verify_jwt/1,
|
||||
generate_refresh_token/1,
|
||||
authenticate_user/2]).
|
||||
|
||||
-export([hash_password/1, verify_password/2]).
|
||||
-export([generate_jwt/2, verify_jwt/1, extract_claims/1]).
|
||||
-export([generate_refresh_token/1]).
|
||||
-include("records.hrl").
|
||||
|
||||
%% ============ Argon2 хеширование ============
|
||||
hash_password(Password) when is_binary(Password) ->
|
||||
hash_password(Password) ->
|
||||
argon2:hash(Password).
|
||||
|
||||
verify_password(Password, Hash) when is_binary(Password), is_binary(Hash) ->
|
||||
verify_password(Password, Hash) ->
|
||||
argon2:verify(Password, Hash).
|
||||
|
||||
%% ============ JWT с использованием jose ============
|
||||
get_jwt_secret() ->
|
||||
<<"my-super-secret-key-for-jwt-32-bytes!">>.
|
||||
|
||||
get_jwk() ->
|
||||
jose_jwk:from_oct(get_jwt_secret()).
|
||||
|
||||
generate_jwt(UserId, Role) ->
|
||||
JWK = get_jwk(),
|
||||
eventhub_auth:generate_user_token(UserId, Role).
|
||||
|
||||
ExpTime = os:system_time(seconds) + 86400, % 24 часа
|
||||
Claims = #{
|
||||
<<"user_id">> => UserId,
|
||||
<<"role">> => Role,
|
||||
<<"exp">> => ExpTime,
|
||||
<<"iat">> => os:system_time(seconds)
|
||||
},
|
||||
verify_jwt(Token) ->
|
||||
eventhub_auth:verify_user_token(Token).
|
||||
|
||||
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"HS256">>}, Claims),
|
||||
{_, Token} = jose_jws:compact(JWT),
|
||||
Token.
|
||||
generate_refresh_token(UserId) ->
|
||||
eventhub_auth:generate_refresh_token(UserId).
|
||||
|
||||
verify_jwt(Token) when is_binary(Token) ->
|
||||
try
|
||||
JWK = get_jwk(),
|
||||
case jose_jwt:verify(JWK, Token) of
|
||||
{true, {jose_jwt, Claims}, _} ->
|
||||
case check_expiry(Claims) of
|
||||
true -> {ok, Claims};
|
||||
false -> {error, expired}
|
||||
end;
|
||||
{true, Claims, _} when is_map(Claims) ->
|
||||
case check_expiry(Claims) of
|
||||
true -> {ok, Claims};
|
||||
false -> {error, expired}
|
||||
end;
|
||||
{false, _, _} ->
|
||||
{error, invalid_signature}
|
||||
end
|
||||
catch
|
||||
_:_ -> {error, invalid_token}
|
||||
end.
|
||||
|
||||
extract_claims(Token) when is_binary(Token) ->
|
||||
try
|
||||
JWK = get_jwk(),
|
||||
case jose_jwt:verify(JWK, Token) of
|
||||
{true, {jose_jwt, Claims}, _} ->
|
||||
{ok, Claims};
|
||||
{true, Claims, _} when is_map(Claims) ->
|
||||
{ok, Claims};
|
||||
authenticate_user(Email, Password) ->
|
||||
case core_user:get_by_email(Email) of
|
||||
{ok, User} ->
|
||||
case verify_password(Password, User#user.password_hash) of
|
||||
{ok, true} ->
|
||||
{ok, user_to_map(User)};
|
||||
_ ->
|
||||
{error, invalid_token}
|
||||
end
|
||||
catch
|
||||
_:_ -> {error, invalid_token}
|
||||
{error, invalid_credentials}
|
||||
end;
|
||||
{error, not_found} ->
|
||||
{error, invalid_credentials}
|
||||
end.
|
||||
|
||||
check_expiry(Claims) ->
|
||||
case maps:find(<<"exp">>, Claims) of
|
||||
{ok, Exp} when is_integer(Exp) ->
|
||||
Exp > os:system_time(seconds);
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% ============ Refresh Token ============
|
||||
generate_refresh_token(_UserId) ->
|
||||
Token = base64:encode(crypto:strong_rand_bytes(32), #{mode => urlsafe, padding => false}),
|
||||
ExpiresAt = calendar:universal_time_to_local_time(
|
||||
calendar:gregorian_seconds_to_datetime(
|
||||
calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 30 * 86400
|
||||
)
|
||||
),
|
||||
{Token, ExpiresAt}.
|
||||
user_to_map(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => atom_to_binary(User#user.role, utf8),
|
||||
status => atom_to_binary(User#user.status, utf8)
|
||||
}.
|
||||
@@ -1,16 +1,27 @@
|
||||
-module(logic_moderation).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_report/4, get_reports/1, get_reports_by_target/3, resolve_report/3]).
|
||||
-export([add_banned_word/2, remove_banned_word/2, list_banned_words/1]).
|
||||
-export([check_content/1, auto_moderate/1]).
|
||||
-export([freeze_calendar/2, unfreeze_calendar/2, freeze_event/2, unfreeze_event/2]).
|
||||
-export([create_report/4,
|
||||
get_reports/1,
|
||||
get_reports_by_target/3,
|
||||
resolve_report/3]).
|
||||
|
||||
-define(REPORT_THRESHOLD, 3). % Количество жалоб для авто-заморозки
|
||||
-export([add_banned_word/2,
|
||||
remove_banned_word/2,
|
||||
list_banned_words/1]).
|
||||
|
||||
%% ============ Жалобы ============
|
||||
-export([check_content/1,
|
||||
auto_moderate/1]).
|
||||
|
||||
-export([freeze_calendar/2,
|
||||
unfreeze_calendar/2,
|
||||
freeze_event/2,
|
||||
unfreeze_event/2]).
|
||||
|
||||
-define(REPORT_THRESHOLD, 3).
|
||||
|
||||
%% ============ Жалобы =====================================
|
||||
|
||||
%% Создание жалобы
|
||||
create_report(ReporterId, TargetType, TargetId, Reason) ->
|
||||
case target_exists(TargetType, TargetId) of
|
||||
true ->
|
||||
@@ -22,30 +33,29 @@ create_report(ReporterId, TargetType, TargetId, Reason) ->
|
||||
target_id => TargetId,
|
||||
reason => Reason
|
||||
}),
|
||||
% Проверяем порог для авто-модерации
|
||||
check_auto_freeze(TargetType, TargetId),
|
||||
{ok, Report};
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
false -> {error, target_not_found}
|
||||
false ->
|
||||
{error, target_not_found}
|
||||
end.
|
||||
|
||||
%% Получить все жалобы (для админа)
|
||||
get_reports(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_report:list_all();
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Получить жалобы на конкретную цель
|
||||
get_reports_by_target(AdminId, TargetType, TargetId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_report:list_by_target(TargetType, TargetId);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Рассмотреть жалобу (подтвердить или отклонить)
|
||||
resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= dismissed ->
|
||||
resolve_report(AdminId, ReportId, Action)
|
||||
when Action =:= reviewed; Action =:= dismissed ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
case core_report:get_by_id(ReportId) of
|
||||
@@ -53,19 +63,23 @@ resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= d
|
||||
case Report#report.status of
|
||||
pending ->
|
||||
core_report:update_status(ReportId, Action, AdminId);
|
||||
_ -> {error, already_resolved}
|
||||
_ ->
|
||||
{error, already_resolved}
|
||||
end;
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
false -> {error, access_denied}
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end.
|
||||
|
||||
%% Проверка порога для авто-заморозки
|
||||
check_auto_freeze(TargetType, TargetId) ->
|
||||
Count = core_report:get_count_by_target(TargetType, TargetId),
|
||||
if Count >= ?REPORT_THRESHOLD ->
|
||||
if
|
||||
Count >= ?REPORT_THRESHOLD ->
|
||||
auto_freeze(TargetType, TargetId);
|
||||
true -> ok
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
|
||||
auto_freeze(event, EventId) ->
|
||||
@@ -82,42 +96,52 @@ auto_freeze(calendar, CalendarId) ->
|
||||
end;
|
||||
auto_freeze(_, _) -> ok.
|
||||
|
||||
%% ============ Бан-лист ============
|
||||
%% ============ Бан-лист ===================================
|
||||
|
||||
%% Добавить запрещённое слово
|
||||
add_banned_word(AdminId, Word) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:add(Word);
|
||||
true -> core_banned_words:add_banned_word(Word, AdminId);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Удалить запрещённое слово
|
||||
remove_banned_word(AdminId, Word) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:remove(Word);
|
||||
true -> core_banned_words:remove_banned_word(Word);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Список запрещённых слов
|
||||
list_banned_words(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:list_all();
|
||||
true -> {ok, core_banned_words:list_banned_words()};
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% ============ Контент-фильтр ============
|
||||
%% ============ Контент-фильтр =============================
|
||||
|
||||
%% Проверить контент на запрещённые слова
|
||||
check_content(Text) ->
|
||||
core_banned_word:check_text(Text).
|
||||
Words = core_banned_words:list_banned_words(),
|
||||
LowerText = string:lowercase(binary_to_list(Text)),
|
||||
lists:any(fun(W) ->
|
||||
string:str(LowerText, binary_to_list(W#banned_word.word)) > 0
|
||||
end, Words).
|
||||
|
||||
%% Автоматическая модерация контента (замена запрещённых слов)
|
||||
auto_moderate(Text) ->
|
||||
core_banned_word:filter_text(Text).
|
||||
Words = core_banned_words:list_banned_words(),
|
||||
lists:foldl(fun(W, Acc) ->
|
||||
WordStr = binary_to_list(W#banned_word.word),
|
||||
LowerAccStr = string:lowercase(binary_to_list(Acc)),
|
||||
case string:str(LowerAccStr, WordStr) of
|
||||
0 -> Acc;
|
||||
Pos ->
|
||||
Len = length(WordStr),
|
||||
Start = binary:part(Acc, {0, Pos-1}),
|
||||
Rest = binary:part(Acc, {Pos-1+Len, byte_size(Acc)-Pos+1-Len}),
|
||||
<<Start/binary, "***", Rest/binary>>
|
||||
end
|
||||
end, Text, Words).
|
||||
|
||||
%% ============ Заморозка/разморозка ============
|
||||
%% ============ Заморозка/разморозка =======================
|
||||
|
||||
%% Заморозить календарь
|
||||
freeze_calendar(AdminId, CalendarId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -129,7 +153,6 @@ freeze_calendar(AdminId, CalendarId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Разморозить календарь
|
||||
unfreeze_calendar(AdminId, CalendarId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -141,7 +164,6 @@ unfreeze_calendar(AdminId, CalendarId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Заморозить событие
|
||||
freeze_event(AdminId, EventId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -153,7 +175,6 @@ freeze_event(AdminId, EventId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Разморозить событие
|
||||
unfreeze_event(AdminId, EventId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -165,7 +186,7 @@ unfreeze_event(AdminId, EventId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% ============ Вспомогательные функции ============
|
||||
%% ============ Вспомогательные функции ====================
|
||||
|
||||
target_exists(event, EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
@@ -176,7 +197,7 @@ target_exists(calendar, CalendarId) ->
|
||||
case core_calendar:get_by_id(CalendarId) of
|
||||
{ok, _} -> true;
|
||||
_ -> false
|
||||
end; % ← точка с запятой здесь!
|
||||
end;
|
||||
target_exists(_, _) -> false.
|
||||
|
||||
is_admin(UserId) ->
|
||||
|
||||
@@ -1,21 +1,41 @@
|
||||
-module(logic_ticket).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([report_error/3, get_ticket/2, list_tickets/1, list_tickets_by_status/2]).
|
||||
-export([update_status/3, assign_ticket/3, resolve_ticket/3, close_ticket/2]).
|
||||
-export([get_statistics/1]).
|
||||
-export([report_error/3,
|
||||
get_ticket/2,
|
||||
list_tickets/1,
|
||||
list_tickets_by_status/2,
|
||||
update_status/3,
|
||||
assign_ticket/3,
|
||||
resolve_ticket/3,
|
||||
close_ticket/2,
|
||||
get_statistics/1]).
|
||||
|
||||
%% Зарегистрировать ошибку (создать или обновить тикет)
|
||||
report_error(ErrorMessage, Stacktrace, Context) ->
|
||||
case core_ticket:create_or_update(ErrorMessage, Stacktrace, Context) of
|
||||
{ok, Ticket} ->
|
||||
% Если это новый тикет, уведомляем администраторов (заглушка)
|
||||
case Ticket#ticket.count of
|
||||
1 -> notify_admins(Ticket);
|
||||
_ -> ok
|
||||
end,
|
||||
{ok, Ticket};
|
||||
Existing = [T || T <- core_ticket:list_all(), T#ticket.error_message =:= ErrorMessage],
|
||||
case Existing of
|
||||
[Ticket] ->
|
||||
% Увеличить счётчик и обновить last_seen
|
||||
Updated = Ticket#ticket{
|
||||
count = Ticket#ticket.count + 1,
|
||||
last_seen = calendar:universal_time()
|
||||
},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
[] ->
|
||||
Data = #{
|
||||
<<"error_message">> => ErrorMessage,
|
||||
<<"stacktrace">> => Stacktrace,
|
||||
<<"context">> => list_to_binary(io_lib:format("~p", [Context]))
|
||||
},
|
||||
case core_ticket:create_ticket(Data) of
|
||||
{ok, Ticket} = Result ->
|
||||
% Уведомление администраторов (заглушка)
|
||||
notify_admins(Ticket),
|
||||
Result;
|
||||
Error -> Error
|
||||
end
|
||||
end.
|
||||
|
||||
%% Получить тикет (только для админов)
|
||||
@@ -35,21 +55,23 @@ list_tickets(AdminId) ->
|
||||
%% Список тикетов по статусу (только для админов)
|
||||
list_tickets_by_status(AdminId, Status) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:list_by_status(Status);
|
||||
true ->
|
||||
All = core_ticket:list_all(),
|
||||
[T || T <- All, T#ticket.status =:= Status];
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Обновить статус тикета
|
||||
update_status(AdminId, TicketId, Status) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:update_status(TicketId, Status);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => Status});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Назначить тикет администратору
|
||||
assign_ticket(AdminId, TicketId, AssignToId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:assign(TicketId, AssignToId);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"assigned_to">> => AssignToId});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
@@ -57,18 +79,17 @@ assign_ticket(AdminId, TicketId, AssignToId) ->
|
||||
resolve_ticket(AdminId, TicketId, ResolutionNote) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
case core_ticket:add_resolution(TicketId, ResolutionNote) of
|
||||
{ok, _Ticket} ->
|
||||
core_ticket:update_status(TicketId, resolved);
|
||||
Error -> Error
|
||||
end;
|
||||
core_ticket:update_ticket(TicketId, #{
|
||||
<<"status">> => <<"closed">>,
|
||||
<<"resolution_note">> => ResolutionNote
|
||||
});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Закрыть тикет
|
||||
close_ticket(AdminId, TicketId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:update_status(TicketId, closed);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => <<"closed">>});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
@@ -76,14 +97,14 @@ close_ticket(AdminId, TicketId) ->
|
||||
get_statistics(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
{ok, AllTickets} = core_ticket:list_all(),
|
||||
Open = length([T || T <- AllTickets, T#ticket.status =:= open]),
|
||||
InProgress = length([T || T <- AllTickets, T#ticket.status =:= in_progress]),
|
||||
Resolved = length([T || T <- AllTickets, T#ticket.status =:= resolved]),
|
||||
Closed = length([T || T <- AllTickets, T#ticket.status =:= closed]),
|
||||
TotalErrors = lists:sum([T#ticket.count || T <- AllTickets]),
|
||||
All = core_ticket:list_all(),
|
||||
Open = length([T || T <- All, T#ticket.status =:= open]),
|
||||
InProgress = length([T || T <- All, T#ticket.status =:= in_progress]),
|
||||
Resolved = length([T || T <- All, T#ticket.status =:= resolved]),
|
||||
Closed = length([T || T <- All, T#ticket.status =:= closed]),
|
||||
TotalErrors = lists:sum([T#ticket.count || T <- All]),
|
||||
#{
|
||||
total_tickets => length(AllTickets),
|
||||
total_tickets => length(All),
|
||||
open => Open,
|
||||
in_progress => InProgress,
|
||||
resolved => Resolved,
|
||||
@@ -102,6 +123,4 @@ is_admin(UserId) ->
|
||||
end.
|
||||
|
||||
notify_admins(_Ticket) ->
|
||||
% Заглушка для уведомлений администраторов
|
||||
% В будущем здесь будет отправка email/websocket
|
||||
ok.
|
||||
@@ -3,24 +3,154 @@
|
||||
|
||||
test() ->
|
||||
io:format("Testing admin panel API...~n"),
|
||||
AdminURL = "http://localhost:8445",
|
||||
|
||||
% Получаем admin-токен через test runner (уже проверенный)
|
||||
AdminToken = api_test_runner:get_admin_token(),
|
||||
|
||||
% TEST 1: Admin healthcheck
|
||||
%% TEST 1: Admin healthcheck (public)
|
||||
io:format(" TEST 1: Admin healthcheck... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get, {"http://localhost:8445/admin/health", []}, [], []),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/health", []}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 2: Admin stats
|
||||
io:format(" TEST 2: Admin stats... "),
|
||||
%% TEST 2: Admin login (дополнительная проверка)
|
||||
io:format(" TEST 2: Admin login (attempt)... "),
|
||||
LoginBody = jsx:encode(#{<<"email">> => <<"global_admin@test.com">>, <<"password">> => <<"admin123">>}),
|
||||
case httpc:request(post, {AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []) of
|
||||
{ok, {{_, 200, _}, _, _}} ->
|
||||
io:format("OK (logged in)~n");
|
||||
_ ->
|
||||
io:format("SKIPPED (credentials not found, using runner token)~n")
|
||||
end,
|
||||
|
||||
%% TEST 3: Admin stats
|
||||
io:format(" TEST 3: Admin stats... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{"http://localhost:8445/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
{AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 3: List users
|
||||
io:format(" TEST 3: List users... "),
|
||||
%% TEST 4: List users
|
||||
io:format(" TEST 4: List users... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{"http://localhost:8445/admin/users", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
{AdminURL ++ "/v1/admin/users", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 5: Get user by ID
|
||||
io:format(" TEST 5: Get user by ID... "),
|
||||
UserId = api_test_runner:get_user_id(),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/users/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 6: List reports
|
||||
io:format(" TEST 6: List reports... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 7: List banned words
|
||||
io:format(" TEST 7: List banned words... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 8: Add banned word
|
||||
io:format(" TEST 8: Add banned word... "),
|
||||
BannedWordBody = jsx:encode(#{<<"word">> => <<"test_banned_word">>}),
|
||||
{ok, {{_, 201, _}, _, _}} = httpc:request(post,
|
||||
{AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", BannedWordBody}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 9: Delete banned word
|
||||
io:format(" TEST 9: Delete banned word... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
|
||||
{AdminURL ++ "/v1/admin/banned-words/test_banned_word", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 10: List tickets
|
||||
io:format(" TEST 10: List tickets... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 11: Create ticket
|
||||
io:format(" TEST 11: Create ticket... "),
|
||||
TicketBody = jsx:encode(#{<<"error_message">> => <<"Test error">>, <<"stacktrace">> => <<"trace">>}),
|
||||
{ok, {{_, 201, _}, _, TicketResp}} = httpc:request(post,
|
||||
{AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", TicketBody}, [], []),
|
||||
#{<<"id">> := TicketId} = jsx:decode(list_to_binary(TicketResp), [return_maps]),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 12: Get ticket by ID
|
||||
io:format(" TEST 12: Get ticket by ID... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 13: Update ticket
|
||||
io:format(" TEST 13: Update ticket... "),
|
||||
UpdateTicketBody = jsx:encode(#{<<"status">> => <<"closed">>}),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateTicketBody}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 14: Delete ticket
|
||||
io:format(" TEST 14: Delete ticket... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
|
||||
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 15: Ticket stats
|
||||
io:format(" TEST 15: Ticket stats... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 16: List subscriptions
|
||||
io:format(" TEST 16: List subscriptions... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 17: Create subscription
|
||||
io:format(" TEST 17: Create subscription... "),
|
||||
SubBody = jsx:encode(#{<<"user_id">> => UserId, <<"plan">> => <<"monthly">>}),
|
||||
{ok, {{_, 201, _}, _, SubResp}} = httpc:request(post,
|
||||
{AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", SubBody}, [], []),
|
||||
#{<<"id">> := SubId} = jsx:decode(list_to_binary(SubResp), [return_maps]),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 18: Get subscription by ID
|
||||
io:format(" TEST 18: Get subscription by ID... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 19: Update subscription
|
||||
io:format(" TEST 19: Update subscription... "),
|
||||
UpdateSubBody = jsx:encode(#{<<"status">> => <<"cancelled">>}),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateSubBody}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 20: Delete subscription
|
||||
io:format(" TEST 20: Delete subscription... "),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
|
||||
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 21: Moderation - block user
|
||||
io:format(" TEST 21: Moderation - block user... "),
|
||||
ModBody = jsx:encode(#{<<"action">> => <<"block">>}),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", ModBody}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
%% TEST 22: Moderation - unblock user
|
||||
io:format(" TEST 22: Moderation - unblock user... "),
|
||||
UnblockBody = jsx:encode(#{<<"action">> => <<"unblock">>}),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UnblockBody}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
io:format("~n✅ Admin API tests passed!~n"),
|
||||
|
||||
@@ -2,52 +2,70 @@
|
||||
-export([test/0]).
|
||||
|
||||
-define(BASE_URL, "http://localhost:8080").
|
||||
-define(ADMIN_BASE_URL, "http://localhost:8445").
|
||||
|
||||
test() ->
|
||||
io:format("Testing moderation API...~n"),
|
||||
|
||||
AdminToken = api_test_runner:get_admin_token(),
|
||||
UserToken = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём календарь и событие
|
||||
%% Создаём календарь и событие через пользовательский API
|
||||
CalId = api_test_runner:extract_json(
|
||||
api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken), <<"id">>),
|
||||
api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken),
|
||||
<<"id">>),
|
||||
EventId = api_test_runner:extract_json(
|
||||
api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events",
|
||||
#{title => <<"Mod Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, UserToken), <<"id">>),
|
||||
#{title => <<"Mod Event">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60},
|
||||
UserToken),
|
||||
<<"id">>),
|
||||
|
||||
% TEST 1: Create report
|
||||
%% TEST 1: Create report (пользователь)
|
||||
io:format(" TEST 1: Create report... "),
|
||||
ReportId = api_test_runner:extract_json(
|
||||
api_test_runner:http_post("/v1/reports",
|
||||
#{target_type => <<"event">>, target_id => EventId, reason => <<"Inappropriate">>}, UserToken), <<"id">>),
|
||||
#{target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
reason => <<"Inappropriate">>},
|
||||
UserToken),
|
||||
<<"id">>),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 2: Admin views reports
|
||||
%% TEST 2: Admin views reports (через админский URL, прямой httpc)
|
||||
io:format(" TEST 2: Admin views reports... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/reports", AdminToken),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 3: Admin resolves report
|
||||
%% TEST 3: Admin resolves report
|
||||
io:format(" TEST 3: Admin resolves report... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/reports/" ++ binary_to_list(ReportId),
|
||||
#{action => <<"review">>}, AdminToken),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/reports/" ++ binary_to_list(ReportId),
|
||||
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||
"application/json",
|
||||
jsx:encode(#{status => <<"reviewed">>})}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 4: Add banned word
|
||||
%% TEST 4: Add banned word (админ)
|
||||
io:format(" TEST 4: Add banned word... "),
|
||||
{ok, {{_, 201, _}, _, _}} = api_test_runner:http_post("/v1/admin/banned-words",
|
||||
#{word => <<"badword">>}, AdminToken),
|
||||
{ok, {{_, 201, _}, _, _}} = httpc:request(post,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words",
|
||||
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||
"application/json",
|
||||
jsx:encode(#{<<"word">> => <<"badword">>})}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 5: List banned words
|
||||
%% TEST 5: List banned words (админ)
|
||||
io:format(" TEST 5: List banned words... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/banned-words", AdminToken),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
% TEST 6: Remove banned word
|
||||
%% TEST 6: Remove banned word (админ)
|
||||
io:format(" TEST 6: Remove banned word... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/admin/banned-words/badword", AdminToken),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words/badword", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format("OK~n"),
|
||||
|
||||
io:format("~n✅ Moderation API tests passed!~n"),
|
||||
|
||||
@@ -1,37 +1,78 @@
|
||||
-module(api_tickets_tests).
|
||||
-export([test/0]).
|
||||
|
||||
-define(BASE_URL, "http://localhost:8080").
|
||||
-define(ADMIN_BASE_URL, "http://localhost:8445").
|
||||
|
||||
test() ->
|
||||
io:format("Testing tickets API...~n"),
|
||||
|
||||
Token = api_test_runner:get_user_token(),
|
||||
AdminToken = api_test_runner:get_admin_token(),
|
||||
UserToken = api_test_runner:get_user_token(),
|
||||
|
||||
% TEST 1: Report error
|
||||
io:format(" TEST 1: Report error... "),
|
||||
%% TEST 1: Create ticket (user)
|
||||
io:format(" TEST 1: Create ticket...~n"),
|
||||
io:format(" POST /v1/tickets~n"),
|
||||
TicketId = api_test_runner:extract_json(
|
||||
api_test_runner:http_post("/v1/tickets",
|
||||
#{error_message => <<"Test bug">>, stacktrace => <<"line 1">>}, UserToken), <<"id">>),
|
||||
io:format("OK~n"),
|
||||
#{error_message => <<"Bug">>,
|
||||
stacktrace => <<"Something broke">>},
|
||||
Token),
|
||||
<<"id">>),
|
||||
io:format(" OK~n"),
|
||||
|
||||
% TEST 2: Admin views tickets
|
||||
io:format(" TEST 2: Admin views tickets... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/tickets", AdminToken),
|
||||
io:format("OK~n"),
|
||||
%% TEST 2: Get my tickets (user)
|
||||
io:format(" TEST 2: Get my tickets...~n"),
|
||||
io:format(" GET /v1/tickets~n"),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/tickets", Token),
|
||||
io:format(" OK~n"),
|
||||
|
||||
% TEST 3: Update ticket status
|
||||
io:format(" TEST 3: Update ticket status... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
||||
#{action => <<"status">>, status => <<"in_progress">>}, AdminToken),
|
||||
io:format("OK~n"),
|
||||
%% TEST 3: Get single ticket (user)
|
||||
io:format(" TEST 3: Get single ticket...~n"),
|
||||
io:format(" GET /v1/tickets/~s~n", [TicketId]),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get(
|
||||
"/v1/tickets/" ++ binary_to_list(TicketId),
|
||||
Token),
|
||||
io:format(" OK~n"),
|
||||
|
||||
% TEST 4: Close ticket
|
||||
io:format(" TEST 4: Close ticket... "),
|
||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
||||
#{action => <<"close">>}, AdminToken),
|
||||
io:format("OK~n"),
|
||||
%% TEST 4: Admin lists all tickets
|
||||
io:format(" TEST 4: Admin lists all tickets...~n"),
|
||||
io:format(" GET ~s/v1/admin/tickets~n", [?ADMIN_BASE_URL]),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format(" OK~n"),
|
||||
|
||||
%% TEST 5: Admin updates ticket status
|
||||
io:format(" TEST 5: Admin updates ticket status...~n"),
|
||||
io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
||||
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||
"application/json",
|
||||
jsx:encode(#{status => <<"in_progress">>})}, [], []),
|
||||
io:format(" OK~n"),
|
||||
|
||||
%% TEST 6: Admin assigns ticket
|
||||
io:format(" TEST 6: Admin assigns ticket...~n"),
|
||||
io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
||||
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||
"application/json",
|
||||
jsx:encode(#{assigned_to => AdminToken})}, [], []),
|
||||
io:format(" OK~n"),
|
||||
|
||||
%% TEST 7: Admin views ticket stats
|
||||
io:format(" TEST 7: Admin views ticket stats...~n"),
|
||||
io:format(" GET ~s/v1/admin/tickets/stats~n", [?ADMIN_BASE_URL]),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format(" OK~n"),
|
||||
|
||||
%% TEST 8: Admin deletes ticket
|
||||
io:format(" TEST 8: Admin deletes ticket...~n"),
|
||||
io:format(" DELETE ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
|
||||
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
|
||||
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
|
||||
io:format(" OK~n"),
|
||||
|
||||
io:format("~n✅ Tickets API tests passed!~n"),
|
||||
{?MODULE, ok}.
|
||||
@@ -46,7 +46,7 @@ test_get_report() ->
|
||||
target_type = <<"event">>,
|
||||
target_id = <<"e1">>,
|
||||
reason = <<"spam">>,
|
||||
status = <<"new">>,
|
||||
status = pending,
|
||||
created_at = {{2026,4,26},{12,0,0}},
|
||||
resolved_at = undefined
|
||||
},
|
||||
@@ -55,7 +55,7 @@ test_get_report() ->
|
||||
{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]).
|
||||
#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>} = jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% GET – не найдено
|
||||
test_get_report_not_found() ->
|
||||
@@ -94,9 +94,9 @@ test_update_report() ->
|
||||
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">>},
|
||||
Updated = #report{id = <<"r1">>, status = reviewed},
|
||||
ok = meck:expect(core_report, update_status,
|
||||
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
|
||||
fun(<<"r1">>, reviewed, <<"adm1">>) -> {ok, Updated} end),
|
||||
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
||||
{Status, _, RespBody, _} = erase(test_reply),
|
||||
?assertEqual(200, Status),
|
||||
@@ -115,7 +115,7 @@ test_update_report_not_found() ->
|
||||
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),
|
||||
fun(<<"r99">>, reviewed, <<"adm1">>) -> {error, not_found} end),
|
||||
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
||||
{Status, _, _, _} = erase(test_reply),
|
||||
?assertEqual(404, Status).
|
||||
|
||||
@@ -29,6 +29,7 @@ admin_reports_test_() ->
|
||||
{"POST /admin/reports – method not allowed", fun test_wrong_method/0}
|
||||
]}.
|
||||
|
||||
%% GET – успех
|
||||
test_list_reports() ->
|
||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||
ok = meck:expect(handler_auth, authenticate,
|
||||
@@ -42,17 +43,18 @@ test_list_reports() ->
|
||||
target_type = <<"event">>,
|
||||
target_id = <<"e1">>,
|
||||
reason = <<"spam">>,
|
||||
status = <<"new">>,
|
||||
status = pending,
|
||||
created_at = {{2026,4,26},{12,0,0}},
|
||||
resolved_at = undefined
|
||||
},
|
||||
ok = meck:expect(core_report, list_reports, fun() -> [Report] end),
|
||||
% list_all возвращает {ok, List}
|
||||
ok = meck:expect(core_report, list_all, fun() -> {ok, [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]).
|
||||
[#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>}] = jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% GET – запрещён
|
||||
test_list_reports_forbidden() ->
|
||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||
ok = meck:expect(handler_auth, authenticate,
|
||||
@@ -62,6 +64,7 @@ test_list_reports_forbidden() ->
|
||||
?assertEqual(403, Status),
|
||||
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% PUT – успех
|
||||
test_update_report() ->
|
||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||
ok = meck:expect(handler_auth, authenticate,
|
||||
@@ -73,14 +76,16 @@ test_update_report() ->
|
||||
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">>},
|
||||
Updated = #report{id = <<"r1">>, status = reviewed},
|
||||
% обработчик передаёт бинарный статус, поэтому мок ожидает строку
|
||||
ok = meck:expect(core_report, update_status,
|
||||
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
|
||||
fun(<<"r1">>, <<"reviewed">>, <<"adm1">>) -> {ok, Updated} end),
|
||||
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||
{Status, _, RespBody, _} = erase(test_reply),
|
||||
?assertEqual(200, Status),
|
||||
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% PUT – невалидный JSON
|
||||
test_update_report_bad_json() ->
|
||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||
ok = meck:expect(handler_auth, authenticate,
|
||||
@@ -93,9 +98,10 @@ test_update_report_bad_json() ->
|
||||
ok = meck:expect(cowboy_req, read_body,
|
||||
fun(Req) -> {ok, <<"bad json">>, Req} end),
|
||||
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
|
||||
{Status, _, _, _} = erase(test_reply),
|
||||
?assertEqual(400, Status).
|
||||
|
||||
%% PUT – не найдено
|
||||
test_update_report_not_found() ->
|
||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||
ok = meck:expect(handler_auth, authenticate,
|
||||
@@ -108,11 +114,12 @@ test_update_report_not_found() ->
|
||||
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),
|
||||
fun(<<"r99">>, <<"reviewed">>, <<"adm1">>) -> {error, not_found} end),
|
||||
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
|
||||
{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, []),
|
||||
|
||||
@@ -34,169 +34,197 @@ admin_tickets_test_() ->
|
||||
{"PATCH /admin/tickets – 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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> undefined end), % для маршрута без id
|
||||
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_user, get_by_id,
|
||||
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||
Ticket = #ticket{
|
||||
id = <<"t1">>,
|
||||
error_hash = <<"abc123">>,
|
||||
error_message = <<"Ooops">>,
|
||||
stacktrace = <<"trace">>,
|
||||
error_hash = <<"hash1">>,
|
||||
error_message = <<"Error message">>,
|
||||
stacktrace = <<"stack">>,
|
||||
context = <<"ctx">>,
|
||||
count = 3,
|
||||
first_seen = {{2026,4,27},{12,0,0}},
|
||||
last_seen = {{2026,4,27},{13,0,0}},
|
||||
count = 1,
|
||||
first_seen = {{2026,4,28},{12,0,0}},
|
||||
last_seen = {{2026,4,28},{12,0,0}},
|
||||
status = open,
|
||||
assigned_to = <<"adm2">>,
|
||||
assigned_to = undefined,
|
||||
resolution_note = undefined
|
||||
},
|
||||
ok = meck:expect(core_ticket, list_tickets, fun() -> [Ticket] end),
|
||||
ok = meck:expect(core_ticket, list_all, fun() -> [Ticket] end),
|
||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||
{Status, _, RespBody, _} = erase(test_reply),
|
||||
?assertEqual(200, Status),
|
||||
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Ooops">>, <<"status">> := <<"open">>}] =
|
||||
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Error message">>}] =
|
||||
jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% GET – запрещён
|
||||
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 = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> undefined end), % для маршрута без id
|
||||
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).
|
||||
|
||||
%% 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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> undefined end), % для маршрута без id
|
||||
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 = meck:expect(core_user, get_by_id,
|
||||
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||
BodyMap = #{<<"error_message">> => <<"Bug">>, <<"stacktrace">> => <<"trace">>},
|
||||
ok = meck:expect(cowboy_req, read_body,
|
||||
fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
|
||||
Created = #ticket{id = <<"t_new">>, error_message = <<"Bug">>, status = open},
|
||||
ok = meck:expect(core_ticket, create_ticket, fun(_) -> {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]).
|
||||
#{<<"error_message">> := <<"Bug">>} = jsx:decode(RespBody, [return_maps]).
|
||||
|
||||
%% POST – отсутствует поле error_message
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> undefined end), % для маршрута без id
|
||||
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 = meck:expect(core_user, get_by_id,
|
||||
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||
ok = meck:expect(cowboy_req, read_body,
|
||||
fun(Req) -> {ok, jsx:encode(#{<<"title">> => <<"No msg">>}), Req} end),
|
||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||
{Status, _, _, _} = erase(test_reply),
|
||||
?assertEqual(400, Status).
|
||||
|
||||
%% GET – один тикет по ID
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t1">> end), % для маршрута с id
|
||||
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 = meck:expect(core_user, get_by_id,
|
||||
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||
Ticket = #ticket{id = <<"t1">>, error_message = <<"Test">>, status = open},
|
||||
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]).
|
||||
|
||||
%% GET – тикет не найден
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t99">> end), % для маршрута с id
|
||||
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 = 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).
|
||||
|
||||
%% PUT – обновление тикета
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t1">> end), % для маршрута с id
|
||||
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_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 = 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]).
|
||||
|
||||
%% PUT – тикет не найден
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t99">> end), % для маршрута с id
|
||||
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 = 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).
|
||||
|
||||
%% DELETE – удаление тикета
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t1">> end), % для маршрута с id
|
||||
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 = 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]).
|
||||
|
||||
%% DELETE – тикет не найден
|
||||
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),
|
||||
ok = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> <<"t99">> end), % для маршрута с id
|
||||
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 = 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 = meck:expect(cowboy_req, binding,
|
||||
fun(id, _) -> undefined end), % для маршрута без id
|
||||
ok = meck:expect(cowboy_req, reply,
|
||||
fun(Code, Headers, Body, Req) ->
|
||||
put(test_reply, {Code, Headers, Body, Req})
|
||||
end),
|
||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||
{Status, _, RespBody, _} = erase(test_reply),
|
||||
?assertEqual(405, Status),
|
||||
|
||||
@@ -27,21 +27,21 @@ 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">>),
|
||||
Token = eventhub_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),
|
||||
Token = eventhub_auth:generate_user_token(<<"user123">>, <<"user">>),
|
||||
{ok, UserId, Role} = eventhub_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),
|
||||
Token = eventhub_auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
|
||||
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
|
||||
?assertEqual(<<"admin1">>, UserId),
|
||||
?assertEqual(<<"superadmin">>, Role)
|
||||
end}
|
||||
@@ -55,19 +55,19 @@ verify_token_errors_test_() ->
|
||||
{"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))
|
||||
?assertEqual({error, invalid_token}, eventhub_auth:verify_user_token(FakeToken)),
|
||||
?assertEqual({error, invalid_token}, eventhub_auth:verify_admin_token(FakeToken))
|
||||
end},
|
||||
{"User token rejected by admin verifier (different secret)",
|
||||
fun() ->
|
||||
Token = auth:generate_user_token(<<"x">>, <<"user">>),
|
||||
Token = eventhub_auth:generate_user_token(<<"x">>, <<"user">>),
|
||||
% Разные секреты → подпись недействительна для admin JWK
|
||||
?assertEqual({error, invalid_signature}, auth:verify_admin_token(Token))
|
||||
?assertEqual({error, invalid_signature}, eventhub_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))
|
||||
Token = eventhub_auth:generate_admin_token(<<"x">>, <<"admin">>),
|
||||
?assertEqual({error, invalid_signature}, eventhub_auth:verify_user_token(Token))
|
||||
end}
|
||||
]}.
|
||||
|
||||
@@ -81,10 +81,10 @@ authenticate_user_request_test_() ->
|
||||
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">>),
|
||||
{ok, Token, ReturnedUser} = eventhub_auth:authenticate_user_request(Req, <<"u@test.com">>, <<"pass">>),
|
||||
?assert(is_binary(Token)),
|
||||
?assertEqual(UserMap, ReturnedUser),
|
||||
{ok, UserId, Role} = auth:verify_user_token(Token),
|
||||
{ok, UserId, Role} = eventhub_auth:verify_user_token(Token),
|
||||
?assertEqual(<<"user1">>, UserId),
|
||||
?assertEqual(<<"user">>, Role)
|
||||
end},
|
||||
@@ -92,7 +92,7 @@ authenticate_user_request_test_() ->
|
||||
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">>))
|
||||
?assertEqual({error, bad_credentials}, eventhub_auth:authenticate_user_request(Req, <<"bad">>, <<"pwd">>))
|
||||
end}
|
||||
]}.
|
||||
|
||||
@@ -106,10 +106,10 @@ authenticate_admin_request_test_() ->
|
||||
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">>),
|
||||
{ok, Token, ReturnedUser} = eventhub_auth:authenticate_admin_request(Req, <<"admin@test.com">>, <<"pass">>),
|
||||
?assert(is_binary(Token)),
|
||||
?assertEqual(AdminMap, ReturnedUser),
|
||||
{ok, UserId, Role} = auth:verify_admin_token(Token),
|
||||
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
|
||||
?assertEqual(<<"adm1">>, UserId),
|
||||
?assertEqual(<<"superadmin">>, Role)
|
||||
end},
|
||||
@@ -119,15 +119,15 @@ authenticate_admin_request_test_() ->
|
||||
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">>))
|
||||
eventhub_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),
|
||||
{ok, Token, _} = eventhub_auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
|
||||
{ok, _, Role} = eventhub_auth:verify_admin_token(Token),
|
||||
?assertEqual(<<"moderator">>, Role)
|
||||
end}
|
||||
]}.
|
||||
@@ -136,4 +136,4 @@ authenticate_admin_request_test_() ->
|
||||
%% Тест generate_refresh_token/1
|
||||
%% ------------------------------------------------------------------
|
||||
generate_refresh_token_test() ->
|
||||
{_, _} = auth:generate_refresh_token(<<"anyuser">>).
|
||||
{_, _} = eventhub_auth:generate_refresh_token(<<"anyuser">>).
|
||||
@@ -1,63 +0,0 @@
|
||||
-module(core_banned_word_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
setup() ->
|
||||
{atomic, ok} = mnesia:start(), % правильное значение
|
||||
ok = mnesia:create_table(banned_word, [
|
||||
{attributes, record_info(fields, banned_word)},
|
||||
{disc_copies, []},
|
||||
{ram_copies, [node()]}
|
||||
]).
|
||||
|
||||
cleanup(_) ->
|
||||
mnesia:delete_table(banned_word),
|
||||
mnesia:stop().
|
||||
|
||||
core_banned_word_test_() ->
|
||||
{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_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_already_exists() ->
|
||||
{ok, _} = core_banned_words:add_banned_word(<<"spam">>, <<"admin1">>),
|
||||
{error, already_exists} = core_banned_words:add_banned_word(<<"spam">>, <<"admin2">>).
|
||||
|
||||
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_remove_not_found() ->
|
||||
?assertEqual({error, not_found}, core_banned_words:remove_banned_word(<<"unknown">>)).
|
||||
|
||||
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_update_not_found() ->
|
||||
?assertEqual({error, not_found}, core_banned_words:update_banned_word(<<"unknown">>, <<"newword">>)).
|
||||
|
||||
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)).
|
||||
87
test/unit/core_banned_words_tests.erl
Normal file
87
test/unit/core_banned_words_tests.erl
Normal file
@@ -0,0 +1,87 @@
|
||||
-module(core_banned_words_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Фикстуры
|
||||
%% ----------------------------------------------------------------
|
||||
setup() ->
|
||||
% Гарантированно останавливаем Mnesia (если уже запущена)
|
||||
catch mnesia:stop(),
|
||||
% Запускаем Mnesia (первый раз вернёт {atomic, ok}, потом ok)
|
||||
case mnesia:start() of
|
||||
{atomic, ok} -> ok;
|
||||
ok -> ok
|
||||
end,
|
||||
% Создаём таблицу (всегда возвращает {atomic, ok})
|
||||
{atomic, ok} = mnesia:create_table(banned_word, [
|
||||
{attributes, record_info(fields, banned_word)},
|
||||
{ram_copies, [node()]}
|
||||
]),
|
||||
ok.
|
||||
|
||||
cleanup(_) ->
|
||||
mnesia:delete_table(banned_word),
|
||||
mnesia:stop().
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Тесты
|
||||
%% ----------------------------------------------------------------
|
||||
core_banned_words_test_() ->
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"Add banned word (success)", fun test_add_banned_word/0},
|
||||
{"Add banned word (duplicate)", fun test_add_duplicate/0},
|
||||
{"Remove banned word (success)", fun test_remove_banned_word/0},
|
||||
{"Remove banned word (not found)", fun test_remove_not_found/0},
|
||||
{"Update banned word (success)", fun test_update_banned_word/0},
|
||||
{"Update banned word (not found)", fun test_update_not_found/0},
|
||||
{"List banned words", fun test_list_banned_words/0}
|
||||
]}.
|
||||
|
||||
%% ── Добавление ───────────────────────────────────────────
|
||||
test_add_banned_word() ->
|
||||
Word = <<"badword">>,
|
||||
AddedBy = <<"admin1">>,
|
||||
{ok, BW} = core_banned_words:add_banned_word(Word, AddedBy),
|
||||
?assertEqual(Word, BW#banned_word.word),
|
||||
?assertEqual(AddedBy, BW#banned_word.added_by),
|
||||
?assert(is_binary(BW#banned_word.id)),
|
||||
?assert(size(BW#banned_word.id) > 0).
|
||||
|
||||
test_add_duplicate() ->
|
||||
Word = <<"duplicate">>,
|
||||
{ok, _} = core_banned_words:add_banned_word(Word, <<"admin1">>),
|
||||
?assertEqual({error, already_exists}, core_banned_words:add_banned_word(Word, <<"admin2">>)).
|
||||
|
||||
%% ── Удаление ─────────────────────────────────────────────
|
||||
test_remove_banned_word() ->
|
||||
Word = <<"to_remove">>,
|
||||
{ok, _} = core_banned_words:add_banned_word(Word, <<"admin1">>),
|
||||
{ok, deleted} = core_banned_words:remove_banned_word(Word),
|
||||
?assertEqual([], core_banned_words:list_banned_words()).
|
||||
|
||||
test_remove_not_found() ->
|
||||
?assertEqual({error, not_found}, core_banned_words:remove_banned_word(<<"nonexistent">>)).
|
||||
|
||||
%% ── Обновление ───────────────────────────────────────────
|
||||
test_update_banned_word() ->
|
||||
OldWord = <<"old_word">>,
|
||||
NewWord = <<"new_word">>,
|
||||
{ok, _} = core_banned_words:add_banned_word(OldWord, <<"admin1">>),
|
||||
{ok, Updated} = core_banned_words:update_banned_word(OldWord, NewWord),
|
||||
?assertEqual(NewWord, Updated#banned_word.word),
|
||||
?assertEqual([NewWord], [W#banned_word.word || W <- core_banned_words:list_banned_words()]).
|
||||
|
||||
test_update_not_found() ->
|
||||
?assertEqual({error, not_found}, core_banned_words:update_banned_word(<<"unknown">>, <<"new">>)).
|
||||
|
||||
%% ── Список ───────────────────────────────────────────────
|
||||
test_list_banned_words() ->
|
||||
{ok, _} = core_banned_words:add_banned_word(<<"word1">>, <<"adm1">>),
|
||||
{ok, _} = core_banned_words:add_banned_word(<<"word2">>, <<"adm2">>),
|
||||
{ok, _} = core_banned_words:add_banned_word(<<"word3">>, <<"adm3">>),
|
||||
List = core_banned_words:list_banned_words(),
|
||||
?assertEqual(3, 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)),
|
||||
?assert(lists:any(fun(W) -> W#banned_word.word == <<"word3">> end, List)).
|
||||
@@ -2,9 +2,16 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Фикстуры
|
||||
%% ----------------------------------------------------------------
|
||||
setup() ->
|
||||
mnesia:start(),
|
||||
mnesia:create_table(ticket, [
|
||||
catch mnesia:stop(),
|
||||
case mnesia:start() of
|
||||
{atomic, ok} -> ok;
|
||||
ok -> ok
|
||||
end,
|
||||
{atomic, ok} = mnesia:create_table(ticket, [
|
||||
{attributes, record_info(fields, ticket)},
|
||||
{ram_copies, [node()]}
|
||||
]),
|
||||
@@ -12,110 +19,118 @@ setup() ->
|
||||
|
||||
cleanup(_) ->
|
||||
mnesia:delete_table(ticket),
|
||||
mnesia:stop(),
|
||||
ok.
|
||||
mnesia:stop().
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Тесты
|
||||
%% ----------------------------------------------------------------
|
||||
core_ticket_test_() ->
|
||||
{foreach,
|
||||
fun setup/0,
|
||||
fun cleanup/1,
|
||||
[
|
||||
{"Create ticket test", fun test_create_ticket/0},
|
||||
{"Update existing ticket test", fun test_update_ticket/0},
|
||||
{"Get ticket by id test", fun test_get_by_id/0},
|
||||
{"Get ticket by error hash test", fun test_get_by_error_hash/0},
|
||||
{"List all tickets test", fun test_list_all/0},
|
||||
{"List by status test", fun test_list_by_status/0},
|
||||
{"Update status test", fun test_update_status/0},
|
||||
{"Assign ticket test", fun test_assign_ticket/0},
|
||||
{"Add resolution test", fun test_add_resolution/0}
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"Create ticket and retrieve it", fun test_create_and_get/0},
|
||||
{"Update ticket status", fun test_update_status/0},
|
||||
{"Delete ticket and verify removal", fun test_delete_ticket/0},
|
||||
{"List all tickets returns created ones", fun test_list_all/0},
|
||||
{"List tickets by user filters correctly", fun test_list_by_user/0},
|
||||
{"Get ticket stats reflects real counts", fun test_stats/0},
|
||||
{"Update ticket with unknown id fails", fun test_update_not_found/0},
|
||||
{"Delete ticket with unknown id fails", fun test_delete_not_found/0},
|
||||
{"Get ticket with unknown id fails", fun test_get_not_found/0}
|
||||
]}.
|
||||
|
||||
test_create_ticket() ->
|
||||
ErrorMsg = <<"Test error">>,
|
||||
Stacktrace = <<"line 1\nline 2">>,
|
||||
Context = #{user_id => <<"user123">>},
|
||||
%% ── Вспомогательная функция для создания тикета ─────────
|
||||
make_ticket(ErrorMsg) ->
|
||||
Data = #{
|
||||
<<"error_message">> => list_to_binary(ErrorMsg),
|
||||
<<"stacktrace">> => <<"trace">>,
|
||||
<<"reporter_id">> => <<"user123">>,
|
||||
<<"status">> => <<"open">>
|
||||
},
|
||||
{ok, Ticket} = core_ticket:create_ticket(Data),
|
||||
Ticket.
|
||||
|
||||
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
|
||||
%% ── Тесты ─────────────────────────────────────────────────
|
||||
|
||||
?assertEqual(ErrorMsg, Ticket#ticket.error_message),
|
||||
?assertEqual(Stacktrace, Ticket#ticket.stacktrace),
|
||||
?assertEqual(1, Ticket#ticket.count),
|
||||
?assertEqual(open, Ticket#ticket.status),
|
||||
test_create_and_get() ->
|
||||
Ticket = make_ticket("Bug1"),
|
||||
?assert(is_binary(Ticket#ticket.id)),
|
||||
?assert(is_binary(Ticket#ticket.error_hash)).
|
||||
|
||||
test_update_ticket() ->
|
||||
ErrorMsg = <<"Test error">>,
|
||||
Stacktrace = <<"line 1">>,
|
||||
Context = #{},
|
||||
|
||||
{ok, Ticket1} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
|
||||
?assertEqual(1, Ticket1#ticket.count),
|
||||
|
||||
{ok, Ticket2} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
|
||||
?assertEqual(Ticket1#ticket.id, Ticket2#ticket.id),
|
||||
?assertEqual(2, Ticket2#ticket.count),
|
||||
?assert(Ticket2#ticket.last_seen >= Ticket1#ticket.last_seen).
|
||||
|
||||
test_get_by_id() ->
|
||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
||||
|
||||
{ok, Found} = core_ticket:get_by_id(Ticket#ticket.id),
|
||||
?assertEqual(Ticket#ticket.id, Found#ticket.id),
|
||||
|
||||
{error, not_found} = core_ticket:get_by_id(<<"nonexistent">>).
|
||||
|
||||
test_get_by_error_hash() ->
|
||||
ErrorMsg = <<"Unique error">>,
|
||||
Stacktrace = <<"stack">>,
|
||||
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, #{}),
|
||||
|
||||
{ok, Found} = core_ticket:get_by_error_hash(Ticket#ticket.error_hash),
|
||||
?assertEqual(Ticket#ticket.id, Found#ticket.id),
|
||||
|
||||
{error, not_found} = core_ticket:get_by_error_hash(<<"badhash">>).
|
||||
|
||||
test_list_all() ->
|
||||
{ok, _} = core_ticket:create_or_update(<<"Error 1">>, <<"">>, #{}),
|
||||
{ok, _} = core_ticket:create_or_update(<<"Error 2">>, <<"">>, #{}),
|
||||
{ok, _} = core_ticket:create_or_update(<<"Error 3">>, <<"">>, #{}),
|
||||
|
||||
{ok, Tickets} = core_ticket:list_all(),
|
||||
?assertEqual(3, length(Tickets)).
|
||||
|
||||
test_list_by_status() ->
|
||||
{ok, _T1} = core_ticket:create_or_update(<<"E1">>, <<"">>, #{}),
|
||||
{ok, T2} = core_ticket:create_or_update(<<"E2">>, <<"">>, #{}),
|
||||
|
||||
core_ticket:update_status(T2#ticket.id, resolved),
|
||||
|
||||
{ok, Open} = core_ticket:list_by_status(open),
|
||||
?assertEqual(1, length(Open)),
|
||||
|
||||
{ok, Resolved} = core_ticket:list_by_status(resolved),
|
||||
?assertEqual(1, length(Resolved)).
|
||||
{ok, Retrieved} = core_ticket:get_by_id(Ticket#ticket.id),
|
||||
?assertEqual(Ticket#ticket.id, Retrieved#ticket.id).
|
||||
|
||||
test_update_status() ->
|
||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
||||
Ticket = make_ticket("Bug2"),
|
||||
{ok, Updated} = core_ticket:update_ticket(Ticket#ticket.id,
|
||||
#{<<"status">> => <<"closed">>}),
|
||||
?assertEqual(closed, Updated#ticket.status),
|
||||
{ok, Stored} = core_ticket:get_by_id(Ticket#ticket.id),
|
||||
?assertEqual(closed, Stored#ticket.status).
|
||||
|
||||
{ok, Updated} = core_ticket:update_status(Ticket#ticket.id, in_progress),
|
||||
?assertEqual(in_progress, Updated#ticket.status),
|
||||
test_delete_ticket() ->
|
||||
Ticket = make_ticket("Bug3"),
|
||||
Id = Ticket#ticket.id,
|
||||
{ok, deleted} = core_ticket:delete_ticket(Id),
|
||||
% Проверяем, что тикет больше не читается
|
||||
?assertMatch({error, not_found}, core_ticket:get_by_id(Id)).
|
||||
|
||||
{ok, Resolved} = core_ticket:update_status(Ticket#ticket.id, resolved),
|
||||
?assertEqual(resolved, Resolved#ticket.status).
|
||||
test_list_all() ->
|
||||
T1 = make_ticket("E1"),
|
||||
T2 = make_ticket("E2"),
|
||||
All = core_ticket:list_all(),
|
||||
?assert(length(All) >= 2),
|
||||
Ids = [T#ticket.id || T <- All],
|
||||
?assert(lists:member(T1#ticket.id, Ids)),
|
||||
?assert(lists:member(T2#ticket.id, Ids)).
|
||||
|
||||
test_assign_ticket() ->
|
||||
AdminId = <<"admin123">>,
|
||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
||||
test_list_by_user() ->
|
||||
% Создаём тикет от пользователя test_user
|
||||
Data = #{
|
||||
<<"error_message">> => <<"from_test_user">>,
|
||||
<<"stacktrace">> => <<"trace">>,
|
||||
<<"reporter_id">> => <<"test_user">>,
|
||||
<<"status">> => <<"open">>
|
||||
},
|
||||
{ok, T1} = core_ticket:create_ticket(Data),
|
||||
% Ещё один тикет от другого пользователя
|
||||
DataOther = #{
|
||||
<<"error_message">> => <<"other">>,
|
||||
<<"stacktrace">> => <<"trace">>,
|
||||
<<"reporter_id">> => <<"other_user">>,
|
||||
<<"status">> => <<"open">>
|
||||
},
|
||||
{ok, _T2} = core_ticket:create_ticket(DataOther),
|
||||
% list_by_user("test_user") должен вернуть ровно один тикет (T1)
|
||||
UserTickets = core_ticket:list_by_user(<<"test_user">>),
|
||||
?assertEqual(1, length(UserTickets)),
|
||||
?assertEqual(T1#ticket.id, (hd(UserTickets))#ticket.id).
|
||||
|
||||
{ok, Assigned} = core_ticket:assign(Ticket#ticket.id, AdminId),
|
||||
?assertEqual(AdminId, Assigned#ticket.assigned_to),
|
||||
?assertEqual(in_progress, Assigned#ticket.status).
|
||||
test_stats() ->
|
||||
Data1 = #{
|
||||
<<"error_message">> => <<"stat1">>,
|
||||
<<"stacktrace">> => <<"trace">>,
|
||||
<<"reporter_id">> => <<"reporter123">>,
|
||||
<<"status">> => <<"open">>
|
||||
},
|
||||
Data2 = #{
|
||||
<<"error_message">> => <<"stat2">>,
|
||||
<<"stacktrace">> => <<"trace">>,
|
||||
<<"reporter_id">> => <<"reporter456">>,
|
||||
<<"status">> => <<"open">>
|
||||
},
|
||||
{ok, _} = core_ticket:create_ticket(Data1),
|
||||
{ok, _} = core_ticket:create_ticket(Data2),
|
||||
Stats = core_ticket:stats(),
|
||||
?assert(is_map(Stats)),
|
||||
?assert(maps:is_key(open, Stats)),
|
||||
?assert(maps:is_key(total, Stats)),
|
||||
% Проверяем, что общее количество тикетов не меньше 2
|
||||
Total = maps:get(total, Stats),
|
||||
?assert(Total >= 2).
|
||||
|
||||
test_add_resolution() ->
|
||||
Note = <<"Fixed in version 1.0">>,
|
||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
||||
test_update_not_found() ->
|
||||
{error, not_found} = core_ticket:update_ticket(<<"nonexistent">>,
|
||||
#{<<"status">> => <<"closed">>}).
|
||||
|
||||
{ok, Updated} = core_ticket:add_resolution(Ticket#ticket.id, Note),
|
||||
?assertEqual(Note, Updated#ticket.resolution_note).
|
||||
test_delete_not_found() ->
|
||||
{error, not_found} = core_ticket:delete_ticket(<<"nonexistent">>).
|
||||
|
||||
test_get_not_found() ->
|
||||
{error, not_found} = core_ticket:get_by_id(<<"nonexistent">>).
|
||||
@@ -4,9 +4,6 @@
|
||||
-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),
|
||||
@@ -18,9 +15,6 @@ cleanup(_) ->
|
||||
application:unset_env(eventhub, admin_jwt_secret),
|
||||
application:stop(jose).
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% Тесты
|
||||
%% ------------------------------------------------------------------
|
||||
logic_auth_test_() ->
|
||||
[
|
||||
{"Password hash test", fun test_password_hash/0},
|
||||
@@ -31,7 +25,6 @@ logic_auth_test_() ->
|
||||
]}
|
||||
].
|
||||
|
||||
%% ── Хеширование паролей (остаётся в logic_auth) ──────────────────
|
||||
test_password_hash() ->
|
||||
Password = <<"secret123">>,
|
||||
{ok, Hash} = logic_auth:hash_password(Password),
|
||||
@@ -39,27 +32,23 @@ 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 = auth:generate_user_token(UserId, Role),
|
||||
Token = eventhub_auth:generate_user_token(UserId, Role),
|
||||
?assert(is_binary(Token)),
|
||||
{ok, ReturnedUserId, ReturnedRole} = auth:verify_user_token(Token),
|
||||
{ok, ReturnedUserId, ReturnedRole} = eventhub_auth:verify_user_token(Token),
|
||||
?assertEqual(UserId, ReturnedUserId),
|
||||
?assertEqual(Role, ReturnedRole),
|
||||
% Проверка невалидного токена
|
||||
{error, invalid_token} = auth:verify_user_token(<<"invalid.token.here">>).
|
||||
{error, invalid_token} = eventhub_auth:verify_user_token(<<"invalid.token.here">>).
|
||||
|
||||
test_jwt_expired() ->
|
||||
% Тест на истечение срока пока пропущен, так как требует мока времени
|
||||
ok.
|
||||
|
||||
%% ── Refresh token (перенесён в auth) ────────────────────────────
|
||||
test_refresh_token() ->
|
||||
{Token, ExpiresAt} = auth:generate_refresh_token(<<"user123">>),
|
||||
{Token, ExpiresAt} = eventhub_auth:generate_refresh_token(<<"user123">>),
|
||||
?assert(is_binary(Token)),
|
||||
?assert(size(Token) >= 32),
|
||||
?assert(is_integer(ExpiresAt)),
|
||||
Now = os:system_time(second),
|
||||
?assert(is_tuple(ExpiresAt)),
|
||||
Now = calendar:universal_time(),
|
||||
?assert(ExpiresAt > Now).
|
||||
@@ -2,13 +2,25 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Фикстуры
|
||||
%% ----------------------------------------------------------------
|
||||
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(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
|
||||
mnesia:create_table(banned_word, [{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
|
||||
catch mnesia:stop(),
|
||||
case mnesia:start() of
|
||||
{atomic, ok} -> ok;
|
||||
ok -> ok
|
||||
end,
|
||||
{atomic, ok} = mnesia:create_table(user, [
|
||||
{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
|
||||
{atomic, ok} = mnesia:create_table(calendar, [
|
||||
{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
|
||||
{atomic, ok} = mnesia:create_table(event, [
|
||||
{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
|
||||
{atomic, ok} = mnesia:create_table(report, [
|
||||
{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
|
||||
{atomic, ok} = mnesia:create_table(banned_word, [
|
||||
{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
|
||||
ok.
|
||||
|
||||
cleanup(_) ->
|
||||
@@ -17,14 +29,13 @@ cleanup(_) ->
|
||||
mnesia:delete_table(event),
|
||||
mnesia:delete_table(calendar),
|
||||
mnesia:delete_table(user),
|
||||
mnesia:stop(),
|
||||
ok.
|
||||
mnesia:stop().
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Тесты
|
||||
%% ----------------------------------------------------------------
|
||||
logic_moderation_test_() ->
|
||||
{foreach,
|
||||
fun setup/0,
|
||||
fun cleanup/1,
|
||||
[
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"Create report test", fun test_create_report/0},
|
||||
{"Get reports test", fun test_get_reports/0},
|
||||
{"Resolve report test", fun test_resolve_report/0},
|
||||
@@ -36,10 +47,18 @@ logic_moderation_test_() ->
|
||||
{"Check content test", fun test_check_content/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()},
|
||||
User = #user{
|
||||
id = UserId,
|
||||
email = <<>>,
|
||||
password_hash = <<"hash">>,
|
||||
role = Role,
|
||||
status = active,
|
||||
created_at = calendar:universal_time(),
|
||||
updated_at = calendar:universal_time()
|
||||
},
|
||||
mnesia:dirty_write(User),
|
||||
UserId.
|
||||
|
||||
@@ -48,15 +67,16 @@ create_test_calendar(OwnerId) ->
|
||||
Calendar#calendar.id.
|
||||
|
||||
create_test_event(CalendarId) ->
|
||||
{ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60),
|
||||
{ok, Event} = core_event:create(CalendarId, <<"Event">>,
|
||||
{{2026, 6, 1}, {10, 0, 0}}, 60),
|
||||
Event#event.id.
|
||||
|
||||
%% ── Тесты ─────────────────────────────────────────────────
|
||||
test_create_report() ->
|
||||
ReporterId = create_test_user(user),
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
EventId = create_test_event(CalendarId),
|
||||
|
||||
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>),
|
||||
?assertEqual(ReporterId, Report#report.reporter_id),
|
||||
?assertEqual(pending, Report#report.status).
|
||||
@@ -67,9 +87,7 @@ test_get_reports() ->
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
EventId = create_test_event(CalendarId),
|
||||
|
||||
{ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
||||
|
||||
{ok, Reports} = logic_moderation:get_reports(AdminId),
|
||||
?assertEqual(1, length(Reports)).
|
||||
|
||||
@@ -79,7 +97,6 @@ test_resolve_report() ->
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
EventId = create_test_event(CalendarId),
|
||||
|
||||
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
||||
{ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed),
|
||||
?assertEqual(reviewed, Resolved#report.status),
|
||||
@@ -87,14 +104,15 @@ test_resolve_report() ->
|
||||
|
||||
test_add_banned_word() ->
|
||||
AdminId = create_test_user(admin),
|
||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
||||
?assert(core_banned_word:is_banned(<<"badword">>)).
|
||||
{ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
||||
?assertEqual(<<"badword">>, BW#banned_word.word),
|
||||
?assertEqual(AdminId, BW#banned_word.added_by).
|
||||
|
||||
test_remove_banned_word() ->
|
||||
AdminId = create_test_user(admin),
|
||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
||||
{ok, removed} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
|
||||
?assertNot(core_banned_word:is_banned(<<"badword">>)).
|
||||
{ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
|
||||
?assertEqual([], core_banned_words:list_banned_words()).
|
||||
|
||||
test_auto_freeze() ->
|
||||
Reporter1 = create_test_user(user),
|
||||
@@ -103,12 +121,9 @@ test_auto_freeze() ->
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
EventId = create_test_event(CalendarId),
|
||||
|
||||
% 3 жалобы должны заморозить событие
|
||||
{ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
|
||||
{ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>),
|
||||
{ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>),
|
||||
|
||||
{ok, Event} = core_event:get_by_id(EventId),
|
||||
?assertEqual(frozen, Event#event.status).
|
||||
|
||||
@@ -116,10 +131,8 @@ test_freeze_calendar() ->
|
||||
AdminId = create_test_user(admin),
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
|
||||
{ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId),
|
||||
?assertEqual(frozen, Frozen#calendar.status),
|
||||
|
||||
{ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId),
|
||||
?assertEqual(active, Unfrozen#calendar.status).
|
||||
|
||||
@@ -128,19 +141,15 @@ test_freeze_event() ->
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId),
|
||||
EventId = create_test_event(CalendarId),
|
||||
|
||||
{ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId),
|
||||
?assertEqual(frozen, Frozen#event.status),
|
||||
|
||||
{ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId),
|
||||
?assertEqual(active, Unfrozen#event.status).
|
||||
|
||||
test_check_content() ->
|
||||
AdminId = create_test_user(admin),
|
||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>),
|
||||
|
||||
?assertNot(logic_moderation:check_content(<<"Hello">>)),
|
||||
?assert(logic_moderation:check_content(<<"This is bad">>)),
|
||||
|
||||
?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)),
|
||||
?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)).
|
||||
@@ -2,104 +2,106 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("records.hrl").
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Фикстуры
|
||||
%% ----------------------------------------------------------------
|
||||
setup() ->
|
||||
mnesia:start(),
|
||||
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
|
||||
mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
|
||||
catch mnesia:stop(),
|
||||
case mnesia:start() of
|
||||
{atomic, ok} -> ok;
|
||||
ok -> ok
|
||||
end,
|
||||
{atomic, ok} = mnesia:create_table(user, [
|
||||
{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
|
||||
{atomic, ok} = mnesia:create_table(ticket, [
|
||||
{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
|
||||
% Создаём админа и обычного пользователя
|
||||
Admin = #user{id = <<"admin1">>, email = <<"a@a.a">>, password_hash = <<"h">>,
|
||||
role = admin, status = active,
|
||||
created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
|
||||
User = #user{id = <<"user1">>, email = <<"u@u.u">>, password_hash = <<"h">>,
|
||||
role = user, status = active,
|
||||
created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Admin),
|
||||
mnesia:dirty_write(User),
|
||||
ok.
|
||||
|
||||
cleanup(_) ->
|
||||
mnesia:delete_table(ticket),
|
||||
mnesia:delete_table(user),
|
||||
mnesia:stop(),
|
||||
ok.
|
||||
mnesia:delete_table(ticket),
|
||||
mnesia:stop().
|
||||
|
||||
%% ----------------------------------------------------------------
|
||||
%% Тесты
|
||||
%% ----------------------------------------------------------------
|
||||
logic_ticket_test_() ->
|
||||
{foreach,
|
||||
fun setup/0,
|
||||
fun cleanup/1,
|
||||
[
|
||||
{"Report error test", fun test_report_error/0},
|
||||
{"List tickets admin only", fun test_list_tickets_admin_only/0},
|
||||
{"Update status test", fun test_update_status/0},
|
||||
{"Assign ticket test", fun test_assign_ticket/0},
|
||||
{"Resolve ticket test", fun test_resolve_ticket/0},
|
||||
{"Close ticket test", fun test_close_ticket/0},
|
||||
{"Get statistics test", fun test_get_statistics/0}
|
||||
{foreach, fun setup/0, fun cleanup/1, [
|
||||
{"Report error creates ticket", fun test_report_error/0},
|
||||
{"Report duplicate error increments count", fun test_report_duplicate/0},
|
||||
{"List tickets as admin", fun test_list_tickets/0},
|
||||
{"List tickets as non-admin returns error", fun test_list_tickets_forbidden/0},
|
||||
{"Update status as admin", fun test_update_status/0},
|
||||
{"Assign ticket as admin", fun test_assign_ticket/0},
|
||||
{"Resolve ticket as admin", fun test_resolve_ticket/0},
|
||||
{"Close ticket as admin", fun test_close_ticket/0},
|
||||
{"Get statistics as admin", fun test_get_statistics/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.
|
||||
%% --- Вспомогательная функция для создания тикета ---
|
||||
report(ErrorMsg) ->
|
||||
logic_ticket:report_error(ErrorMsg, <<"stack">>, #{}).
|
||||
|
||||
%% --- Тесты ---
|
||||
|
||||
test_report_error() ->
|
||||
{ok, Ticket} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
|
||||
?assertEqual(<<"Test error">>, Ticket#ticket.error_message),
|
||||
?assertEqual(1, Ticket#ticket.count),
|
||||
{ok, Ticket} = report(<<"Error1">>),
|
||||
?assertEqual(<<"Error1">>, Ticket#ticket.error_message),
|
||||
?assertEqual(1, Ticket#ticket.count).
|
||||
|
||||
{ok, Ticket2} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
|
||||
?assertEqual(2, Ticket2#ticket.count).
|
||||
test_report_duplicate() ->
|
||||
{ok, T1} = report(<<"Dup">>),
|
||||
?assertEqual(1, T1#ticket.count),
|
||||
{ok, T2} = report(<<"Dup">>),
|
||||
?assertEqual(2, T2#ticket.count),
|
||||
% Проверяем, что это тот же тикет, а не новый
|
||||
?assertEqual(T1#ticket.id, T2#ticket.id).
|
||||
|
||||
test_list_tickets_admin_only() ->
|
||||
AdminId = create_test_user(admin),
|
||||
UserId = create_test_user(user),
|
||||
test_list_tickets() ->
|
||||
{ok, _} = report(<<"E1">>),
|
||||
Tickets = logic_ticket:list_tickets(<<"admin1">>),
|
||||
?assert(length(Tickets) =:= 1).
|
||||
|
||||
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
|
||||
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
|
||||
|
||||
{ok, Tickets} = logic_ticket:list_tickets(AdminId),
|
||||
?assertEqual(2, length(Tickets)),
|
||||
|
||||
{error, access_denied} = logic_ticket:list_tickets(UserId).
|
||||
test_list_tickets_forbidden() ->
|
||||
{error, access_denied} = logic_ticket:list_tickets(<<"user1">>).
|
||||
|
||||
test_update_status() ->
|
||||
AdminId = create_test_user(admin),
|
||||
UserId = create_test_user(user),
|
||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
||||
|
||||
{ok, Updated} = logic_ticket:update_status(AdminId, Ticket#ticket.id, in_progress),
|
||||
?assertEqual(in_progress, Updated#ticket.status),
|
||||
|
||||
{error, access_denied} = logic_ticket:update_status(UserId, Ticket#ticket.id, resolved).
|
||||
{ok, Ticket} = report(<<"E2">>),
|
||||
{ok, Updated} = logic_ticket:update_status(<<"admin1">>, Ticket#ticket.id, <<"closed">>),
|
||||
?assertEqual(closed, Updated#ticket.status).
|
||||
|
||||
test_assign_ticket() ->
|
||||
AdminId = create_test_user(admin),
|
||||
AssignToId = create_test_user(admin),
|
||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
||||
|
||||
{ok, Assigned} = logic_ticket:assign_ticket(AdminId, Ticket#ticket.id, AssignToId),
|
||||
?assertEqual(AssignToId, Assigned#ticket.assigned_to),
|
||||
?assertEqual(in_progress, Assigned#ticket.status).
|
||||
{ok, Ticket} = report(<<"E3">>),
|
||||
{ok, Updated} = logic_ticket:assign_ticket(<<"admin1">>, Ticket#ticket.id, <<"dev1">>),
|
||||
?assertEqual(<<"dev1">>, Updated#ticket.assigned_to).
|
||||
|
||||
test_resolve_ticket() ->
|
||||
AdminId = create_test_user(admin),
|
||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
||||
|
||||
{ok, Resolved} = logic_ticket:resolve_ticket(AdminId, Ticket#ticket.id, <<"Fixed">>),
|
||||
?assertEqual(<<"Fixed">>, Resolved#ticket.resolution_note),
|
||||
?assertEqual(resolved, Resolved#ticket.status).
|
||||
{ok, Ticket} = report(<<"E4">>),
|
||||
{ok, Updated} = logic_ticket:resolve_ticket(<<"admin1">>, Ticket#ticket.id, <<"Fixed">>),
|
||||
?assertEqual(closed, Updated#ticket.status),
|
||||
?assertEqual(<<"Fixed">>, Updated#ticket.resolution_note).
|
||||
|
||||
test_close_ticket() ->
|
||||
AdminId = create_test_user(admin),
|
||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
||||
|
||||
{ok, Closed} = logic_ticket:close_ticket(AdminId, Ticket#ticket.id),
|
||||
?assertEqual(closed, Closed#ticket.status).
|
||||
{ok, Ticket} = report(<<"E5">>),
|
||||
{ok, Updated} = logic_ticket:close_ticket(<<"admin1">>, Ticket#ticket.id),
|
||||
?assertEqual(closed, Updated#ticket.status).
|
||||
|
||||
test_get_statistics() ->
|
||||
AdminId = create_test_user(admin),
|
||||
|
||||
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
|
||||
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
|
||||
{ok, T3} = logic_ticket:report_error(<<"E3">>, <<"">>, #{}),
|
||||
|
||||
logic_ticket:update_status(AdminId, T3#ticket.id, resolved),
|
||||
|
||||
Stats = logic_ticket:get_statistics(AdminId),
|
||||
{ok, _} = report(<<"E6">>),
|
||||
{ok, _} = report(<<"E7">>),
|
||||
{ok, T3} = report(<<"E8">>),
|
||||
logic_ticket:close_ticket(<<"admin1">>, T3#ticket.id),
|
||||
Stats = logic_ticket:get_statistics(<<"admin1">>),
|
||||
?assertEqual(3, maps:get(total_tickets, Stats)),
|
||||
?assertEqual(2, maps:get(open, Stats)),
|
||||
?assertEqual(1, maps:get(resolved, Stats)),
|
||||
?assertEqual(1, maps:get(closed, Stats)),
|
||||
?assertEqual(3, maps:get(total_errors, Stats)).
|
||||
Reference in New Issue
Block a user