Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 2. Final #3
This commit is contained in:
58
Makefile
58
Makefile
@@ -33,7 +33,8 @@ compile: ## Скомпилировать проект
|
|||||||
clean: ## Очистить проект
|
clean: ## Очистить проект
|
||||||
@echo "Очистка проекта..."
|
@echo "Очистка проекта..."
|
||||||
@$(REBAR3) clean
|
@$(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 "✓ Очистка завершена"
|
@echo "✓ Очистка завершена"
|
||||||
|
|
||||||
deps: ## Установить зависимости
|
deps: ## Установить зависимости
|
||||||
@@ -61,7 +62,7 @@ test-server: ## Запустить тестовый сервер в фоне
|
|||||||
@echo "Cleaning old data..."
|
@echo "Cleaning old data..."
|
||||||
@rm -rf Mnesia.*
|
@rm -rf Mnesia.*
|
||||||
@echo "Starting server..."
|
@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: $$!"
|
@echo "PID: $$!"
|
||||||
@for i in 1 2 3 4 5 6 7 8 9 10; do \
|
@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 \
|
if curl -s http://localhost:8080/health | grep -q "ok"; then \
|
||||||
@@ -96,66 +97,23 @@ eunit-verbose: ## Запустить EUnit тесты с подробным вы
|
|||||||
@echo "Запуск EUnit тестов (verbose)..."
|
@echo "Запуск EUnit тестов (verbose)..."
|
||||||
@$(REBAR3) eunit --sname $(SNAME)_test --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-api: test-ct
|
||||||
|
|
||||||
test-ct: ## Запустить Common Test для API
|
test-ct: ## Запустить Common Test для API
|
||||||
@rebar3 ct --sname $(SNAME)_api_test
|
@$(REBAR3) ct --sname $(SNAME)_api_test
|
||||||
|
|
||||||
test-ct-verbose: ## Запустить Common 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 _build/default/lib/*/ebin \
|
||||||
-pa test/ct/api \
|
-pa test/api \
|
||||||
-logdir logs/ct \
|
-logdir build \
|
||||||
-verbosity 50
|
-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)
|
test-scripts: ## Запустить тесты с фильтром (make test-runner PATTERN=booking)
|
||||||
@chmod +x test/scripts/*.sh
|
@chmod +x test/scripts/*.sh
|
||||||
@cd test/scripts && ./run_tests.sh $(PATTERN)
|
@cd test/scripts && ./run_tests.sh $(PATTERN)
|
||||||
|
|
||||||
test-all: eunit ## Запустить ВСЕ тесты (EUnit + API)
|
test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API)
|
||||||
@sleep 1
|
|
||||||
make test-api
|
|
||||||
@echo "========================================"
|
@echo "========================================"
|
||||||
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
||||||
@echo "========================================"
|
@echo "========================================"
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
%% ------------------- Баг-трекер --------------------------------------
|
%% ------------------- Баг-трекер --------------------------------------
|
||||||
-record(ticket, {
|
-record(ticket, {
|
||||||
id :: binary(),
|
id :: binary(),
|
||||||
|
reporter_id :: binary(),
|
||||||
error_hash :: binary(),
|
error_hash :: binary(),
|
||||||
error_message :: binary(),
|
error_message :: binary(),
|
||||||
stacktrace :: binary(),
|
stacktrace :: binary(),
|
||||||
|
|||||||
@@ -44,13 +44,11 @@
|
|||||||
{ct_opts, [
|
{ct_opts, [
|
||||||
{src_dirs, ["src", "test/api"]},
|
{src_dirs, ["src", "test/api"]},
|
||||||
{sys_config, ["config/sys.config"]}, % Load app config
|
{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
|
{verbose, true} % Print more info to console
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{ct_compile_opts, [
|
{ct_compile_opts, [
|
||||||
{i, "include"}, % Include directory
|
{i, "include"}, % Include directory
|
||||||
{d, 'DEBUG'} % Define macros
|
{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([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([update_status/2, check_expired/0]).
|
||||||
-export([generate_id/0]).
|
-export([generate_id/0]).
|
||||||
|
% --------------- новые обёртки для админки ------------------
|
||||||
|
-export([list_subscriptions/0,
|
||||||
|
create_subscription/1,
|
||||||
|
update_subscription/2,
|
||||||
|
delete_subscription/1
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TRIAL_DAYS, 30).
|
-define(TRIAL_DAYS, 30).
|
||||||
|
|
||||||
@@ -140,4 +146,72 @@ add_months(DateTime, Months) ->
|
|||||||
|
|
||||||
add_days(DateTime, Days) ->
|
add_days(DateTime, Days) ->
|
||||||
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||||
calendar:gregorian_seconds_to_datetime(Seconds + (Days * 86400)).
|
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).
|
-module(core_ticket).
|
||||||
-include("records.hrl").
|
-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]).
|
|
||||||
|
|
||||||
%% Создать или обновить тикет (группировка по хэшу ошибки)
|
|
||||||
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(),
|
|
||||||
Now = calendar:universal_time(),
|
|
||||||
Ticket = #ticket{
|
|
||||||
id = Id,
|
|
||||||
error_hash = ErrorHash,
|
|
||||||
error_message = ErrorMessage,
|
|
||||||
stacktrace = Stacktrace,
|
|
||||||
context = term_to_binary(Context),
|
|
||||||
count = 1,
|
|
||||||
first_seen = Now,
|
|
||||||
last_seen = Now,
|
|
||||||
status = open,
|
|
||||||
assigned_to = undefined,
|
|
||||||
resolution_note = 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}
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Получение тикета по 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() ->
|
list_all() ->
|
||||||
Match = #ticket{_ = '_'},
|
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
||||||
Tickets = mnesia:dirty_match_object(Match),
|
|
||||||
{ok, lists:sort(fun(A, B) -> A#ticket.last_seen >= B#ticket.last_seen end, Tickets)}.
|
|
||||||
|
|
||||||
%% Список тикетов по статусу
|
get_by_id(Id) ->
|
||||||
list_by_status(Status) ->
|
case mnesia:dirty_read({ticket, Id}) of
|
||||||
Match = #ticket{status = Status, _ = '_'},
|
[Ticket] -> {ok, Ticket};
|
||||||
Tickets = mnesia:dirty_match_object(Match),
|
[] -> {error, not_found}
|
||||||
{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.
|
end.
|
||||||
|
|
||||||
%% Назначить тикет администратору
|
update_ticket(Id, Updates) ->
|
||||||
assign(Id, AdminId) ->
|
case get_by_id(Id) of
|
||||||
F = fun() ->
|
{ok, Ticket} ->
|
||||||
case mnesia:read(ticket, Id) of
|
Updated = apply_updates(Ticket, Updates),
|
||||||
[] ->
|
mnesia:dirty_write(Updated),
|
||||||
{error, not_found};
|
{ok, Updated};
|
||||||
[Ticket] ->
|
Error -> Error
|
||||||
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.
|
end.
|
||||||
|
|
||||||
%% Добавить примечание о решении
|
delete_ticket(Id) ->
|
||||||
add_resolution(Id, Note) ->
|
case get_by_id(Id) of
|
||||||
F = fun() ->
|
{ok, _Ticket} -> % переменная не используется
|
||||||
case mnesia:read(ticket, Id) of
|
mnesia:dirty_delete({ticket, Id}),
|
||||||
[] ->
|
{ok, deleted};
|
||||||
{error, not_found};
|
Error -> Error
|
||||||
[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.
|
end.
|
||||||
|
|
||||||
%% Внутренние функции
|
stats() ->
|
||||||
generate_id() ->
|
Tickets = list_all(),
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
#{
|
||||||
|
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)
|
||||||
|
}.
|
||||||
|
|
||||||
generate_error_hash(ErrorMessage, Stacktrace) ->
|
%% ── новые функции ──────────────────────────────────────
|
||||||
Data = iolist_to_binary([ErrorMessage, "\n", Stacktrace]),
|
create_ticket(Data) ->
|
||||||
base64:encode(crypto:hash(sha256, Data), #{mode => urlsafe, padding => false}).
|
Id = base64:encode(crypto:strong_rand_bytes(9)),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
|
Ticket = #ticket{
|
||||||
|
id = Id,
|
||||||
|
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 = maps:get(<<"status">>, Data, open),
|
||||||
|
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
|
||||||
|
resolution_note = maps:get(<<"resolution_note">>, Data, undefined)
|
||||||
|
},
|
||||||
|
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, Ticket, maps:to_list(Updates)).
|
||||||
|
|
||||||
|
count_by_status(Status, Tickets) ->
|
||||||
|
length([T || T <- Tickets, T#ticket.status =:= Status]).
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
-export([email_exists/1]).
|
-export([email_exists/1]).
|
||||||
-export([generate_id/0]).
|
-export([generate_id/0]).
|
||||||
-export([list_users/0]).
|
-export([list_users/0]).
|
||||||
|
-export([block/1, unblock/1]).
|
||||||
|
|
||||||
%% Создание пользователя
|
%% Создание пользователя
|
||||||
create(Email, Password) ->
|
create(Email, Password) ->
|
||||||
@@ -103,6 +104,24 @@ user_to_map(User) ->
|
|||||||
updated_at => User#user.updated_at
|
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() ->
|
generate_id() ->
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
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/reviews/:id", handler_review_by_id, []},
|
||||||
{"/v1/reports", handler_reports, []},
|
{"/v1/reports", handler_reports, []},
|
||||||
{"/v1/tickets", handler_tickets, []},
|
{"/v1/tickets", handler_tickets, []},
|
||||||
|
{"/v1/tickets/:id", handler_ticket_by_id, []},
|
||||||
{"/v1/subscription", handler_subscription, []}
|
{"/v1/subscription", handler_subscription, []}
|
||||||
]}
|
]}
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ init(Req0, State) ->
|
|||||||
{ok, Body, Req1} = cowboy_req:read_body(Req0),
|
{ok, Body, Req1} = cowboy_req:read_body(Req0),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
#{<<"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} ->
|
{ok, Token, User} ->
|
||||||
Resp = jsx:encode(#{
|
Resp = jsx:encode(#{
|
||||||
<<"token">> => Token,
|
<<"token">> => Token,
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"GET">> -> get_report(Req);
|
<<"GET">> -> get_report(Req);
|
||||||
<<"PUT">> -> update_report(Req);
|
<<"PUT">> -> update_report(Req);
|
||||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_report(Req) ->
|
get_report(Req) ->
|
||||||
@@ -39,7 +39,8 @@ update_report(Req) ->
|
|||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"status">> := NewStatus} ->
|
#{<<"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} ->
|
{ok, Report} ->
|
||||||
send_json(Req2, 200, report_to_json(Report));
|
send_json(Req2, 200, report_to_json(Report));
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-behaviour(cowboy_handler).
|
-behaviour(cowboy_handler).
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
-include("records.hrl"). %% ← обязательно для #user{} и #report{}
|
-include("records.hrl").
|
||||||
|
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
@@ -16,7 +16,7 @@ list_reports(Req) ->
|
|||||||
{ok, AdminId, Req1} ->
|
{ok, AdminId, Req1} ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
Reports = core_report:list_reports(),
|
{ok, Reports} = core_report:list_all(),
|
||||||
send_json(Req1, 200, [report_to_json(R) || R <- Reports]);
|
send_json(Req1, 200, [report_to_json(R) || R <- Reports]);
|
||||||
false ->
|
false ->
|
||||||
send_error(Req1, 403, <<"Admin access required">>)
|
send_error(Req1, 403, <<"Admin access required">>)
|
||||||
@@ -34,7 +34,7 @@ update_report(Req) ->
|
|||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"status">> := NewStatus} ->
|
#{<<"status">> := NewStatus} ->
|
||||||
case core_report:update_status(ReportId, NewStatus) of
|
case core_report:update_status(ReportId, NewStatus, AdminId) of
|
||||||
{ok, Report} ->
|
{ok, Report} ->
|
||||||
send_json(Req2, 200, report_to_json(Report));
|
send_json(Req2, 200, report_to_json(Report));
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ list_subscriptions(Req) ->
|
|||||||
|
|
||||||
create_subscription(Req) ->
|
create_subscription(Req) ->
|
||||||
case auth_admin(Req) of
|
case auth_admin(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, _AdminId, Req1} ->
|
||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"user_id">> := _UserId} = Data ->
|
#{<<"user_id">> := _UserId} = Data ->
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ handle_item(TicketId, Req) ->
|
|||||||
list_tickets(Req) ->
|
list_tickets(Req) ->
|
||||||
case auth_admin(Req) of
|
case auth_admin(Req) of
|
||||||
{ok, _AdminId, Req1} ->
|
{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]);
|
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]);
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Message, Req1} ->
|
||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
@@ -36,7 +36,7 @@ list_tickets(Req) ->
|
|||||||
|
|
||||||
create_ticket(Req) ->
|
create_ticket(Req) ->
|
||||||
case auth_admin(Req) of
|
case auth_admin(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, _AdminId, Req1} ->
|
||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"error_message">> := _} = Data ->
|
#{<<"error_message">> := _} = Data ->
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
-module(admin_ws_handler).
|
-module(admin_ws_handler).
|
||||||
-behaviour(cowboy_websocket).
|
-behaviour(cowboy_websocket).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
-export([websocket_init/1]).
|
-export([websocket_init/1]).
|
||||||
-export([websocket_handle/2]).
|
-export([websocket_handle/2]).
|
||||||
@@ -21,15 +20,13 @@ init(Req, _Opts) ->
|
|||||||
Token ->
|
Token ->
|
||||||
io:format("[ADMIN_WS] Token received: ~s...~n", [binary_part(Token, 0, 30)]),
|
io:format("[ADMIN_WS] Token received: ~s...~n", [binary_part(Token, 0, 30)]),
|
||||||
case logic_auth:verify_jwt(Token) of
|
case logic_auth:verify_jwt(Token) of
|
||||||
{ok, Claims} ->
|
{ok, UserId, Role} ->
|
||||||
UserId = maps:get(<<"user_id">>, Claims),
|
|
||||||
Role = maps:get(<<"role">>, Claims),
|
|
||||||
io:format("[ADMIN_WS] UserId: ~s, Role: ~s~n", [UserId, Role]),
|
io:format("[ADMIN_WS] UserId: ~s, Role: ~s~n", [UserId, Role]),
|
||||||
case Role of
|
case is_admin_role(Role) of
|
||||||
<<"admin">> ->
|
true ->
|
||||||
io:format("[ADMIN_WS] Admin access granted~n"),
|
io:format("[ADMIN_WS] Admin access granted~n"),
|
||||||
{cowboy_websocket, Req, #state{admin_id = UserId}};
|
{cowboy_websocket, Req, #state{admin_id = UserId}};
|
||||||
_ ->
|
false ->
|
||||||
io:format("[ADMIN_WS] Access denied: not admin~n"),
|
io:format("[ADMIN_WS] Access denied: not admin~n"),
|
||||||
Resp = cowboy_req:reply(403, #{}, <<"Admin access required">>, Req),
|
Resp = cowboy_req:reply(403, #{}, <<"Admin access required">>, Req),
|
||||||
{ok, Resp, undefined}
|
{ok, Resp, undefined}
|
||||||
@@ -84,4 +81,7 @@ websocket_info(_Info, State) ->
|
|||||||
|
|
||||||
terminate(_Reason, _Req, _State) ->
|
terminate(_Reason, _Req, _State) ->
|
||||||
pg:leave(eventhub_admin_ws, self()),
|
pg:leave(eventhub_admin_ws, self()),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
is_admin_role(Role) ->
|
||||||
|
lists:member(Role, [<<"admin">>, <<"superadmin">>, <<"moderator">>, <<"support">>]).
|
||||||
@@ -8,8 +8,7 @@ authenticate(Req) ->
|
|||||||
{bearer, Token} ->
|
{bearer, Token} ->
|
||||||
io:format("[AUTH] Bearer token found: ~s...~n", [binary_part(Token, 0, 30)]),
|
io:format("[AUTH] Bearer token found: ~s...~n", [binary_part(Token, 0, 30)]),
|
||||||
case logic_auth:verify_jwt(Token) of
|
case logic_auth:verify_jwt(Token) of
|
||||||
{ok, Claims} ->
|
{ok, UserId, _Role} ->
|
||||||
UserId = maps:get(<<"user_id">>, Claims),
|
|
||||||
io:format("[AUTH] JWT verified, UserId: ~s~n", [UserId]),
|
io:format("[AUTH] JWT verified, UserId: ~s~n", [UserId]),
|
||||||
{ok, UserId, Req};
|
{ok, UserId, Req};
|
||||||
{error, expired} ->
|
{error, expired} ->
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ handle(Req, _Opts) ->
|
|||||||
_ ->
|
_ ->
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
#{<<"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} ->
|
{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),
|
save_refresh_token(maps:get(id, User), RefreshToken, ExpiresAt),
|
||||||
Response = #{
|
Response = #{
|
||||||
user => #{
|
user => #{
|
||||||
|
|||||||
@@ -1,91 +1,67 @@
|
|||||||
-module(handler_refresh).
|
-module(handler_refresh).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
init(Req, Opts) ->
|
init(Req0, _Opts) ->
|
||||||
handle(Req, Opts).
|
case cowboy_req:method(Req0) of
|
||||||
|
|
||||||
handle(Req, _Opts) ->
|
|
||||||
case cowboy_req:method(Req) of
|
|
||||||
<<"POST">> ->
|
<<"POST">> ->
|
||||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
{ok, Body, Req1} = cowboy_req:read_body(Req0),
|
||||||
case jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"refresh_token">> := RefreshToken} ->
|
#{<<"refresh_token">> := RefreshToken} ->
|
||||||
case validate_refresh_token(RefreshToken) of
|
case get_session(RefreshToken) of
|
||||||
{ok, UserId} ->
|
{ok, Session} ->
|
||||||
case core_user:get_by_id(UserId) of
|
% Проверяем, не истекла ли сессия
|
||||||
{ok, User} ->
|
case Session#session.expires_at > calendar:universal_time() of
|
||||||
% Генерируем новые токены
|
true ->
|
||||||
NewToken = logic_auth:generate_jwt(User#user.id, User#user.role),
|
% Генерируем новый access-токен и refresh-токен
|
||||||
{NewRefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
User = get_user(Session#session.user_id),
|
||||||
|
NewToken = eventhub_auth:generate_user_token(
|
||||||
% Сохраняем новый refresh token
|
User#user.id,
|
||||||
save_refresh_token(User#user.id, NewRefreshToken, ExpiresAt),
|
atom_to_binary(User#user.role, utf8)
|
||||||
|
),
|
||||||
% Удаляем старый refresh token
|
{NewRefreshToken, ExpiresAt} =
|
||||||
delete_refresh_token(RefreshToken),
|
eventhub_auth:generate_refresh_token(User#user.id),
|
||||||
|
% Удаляем старую сессию и сохраняем новую
|
||||||
Response = #{
|
mnesia:dirty_delete_object(Session),
|
||||||
|
NewSession = #session{
|
||||||
|
token = NewRefreshToken,
|
||||||
|
user_id = User#user.id,
|
||||||
|
expires_at = ExpiresAt,
|
||||||
|
type = refresh
|
||||||
|
},
|
||||||
|
mnesia:dirty_write(NewSession),
|
||||||
|
Resp = jsx:encode(#{
|
||||||
token => NewToken,
|
token => NewToken,
|
||||||
refresh_token => NewRefreshToken
|
refresh_token => NewRefreshToken
|
||||||
},
|
}),
|
||||||
send_json(Req1, 200, Response);
|
cowboy_req:reply(200, #{
|
||||||
{error, not_found} ->
|
<<"content-type">> => <<"application/json">>
|
||||||
send_error(Req1, 401, <<"User not found">>)
|
}, Resp, Req1);
|
||||||
|
false ->
|
||||||
|
mnesia:dirty_delete_object(Session),
|
||||||
|
send_error(Req1, 401, <<"Refresh token expired">>)
|
||||||
end;
|
end;
|
||||||
{error, expired} ->
|
{error, not_found} ->
|
||||||
send_error(Req1, 401, <<"Refresh token expired">>);
|
send_error(Req1, 401, <<"Refresh token not found">>)
|
||||||
{error, invalid} ->
|
|
||||||
send_error(Req1, 401, <<"Invalid refresh token">>)
|
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
send_error(Req1, 400, <<"Missing refresh_token">>)
|
send_error(Req1, 400, <<"Missing refresh_token field">>)
|
||||||
|
catch
|
||||||
|
_:_ -> send_error(Req1, 400, <<"Invalid JSON">>)
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
send_error(Req, 405, <<"Method not allowed">>)
|
send_error(Req0, 405, <<"Method not allowed">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
validate_refresh_token(Token) ->
|
get_session(Token) ->
|
||||||
case get_session_by_token(Token) of
|
case mnesia:dirty_read({session, Token}) of
|
||||||
{ok, Session} ->
|
[Session] -> {ok, Session};
|
||||||
% Проверяем срок действия
|
[] -> {error, not_found}
|
||||||
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.
|
end.
|
||||||
|
|
||||||
get_session_by_token(Token) ->
|
get_user(UserId) ->
|
||||||
Match = #session{token = Token, type = refresh, _ = '_'},
|
[User] = mnesia:dirty_read({user, UserId}),
|
||||||
case mnesia:dirty_match_object(Match) of
|
User.
|
||||||
[] -> {error, not_found};
|
|
||||||
[Session] -> {ok, Session}
|
|
||||||
end.
|
|
||||||
|
|
||||||
save_refresh_token(UserId, Token, ExpiresAt) ->
|
|
||||||
Session = #session{
|
|
||||||
token = Token,
|
|
||||||
user_id = UserId,
|
|
||||||
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)
|
|
||||||
end.
|
|
||||||
|
|
||||||
send_json(Req, Status, Data) ->
|
|
||||||
Body = jsx:encode(Data),
|
|
||||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
|
||||||
{ok, Body, []}.
|
|
||||||
|
|
||||||
send_error(Req, Status, Message) ->
|
send_error(Req, Status, Message) ->
|
||||||
Body = jsx:encode(#{error => Message}),
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ handle(Req, _Opts) ->
|
|||||||
false ->
|
false ->
|
||||||
case core_user:create(Email, Password) of
|
case core_user:create(Email, Password) of
|
||||||
{ok, User} ->
|
{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 = #{
|
Response = #{
|
||||||
user => #{
|
user => #{
|
||||||
id => User#user.id,
|
id => User#user.id,
|
||||||
|
|||||||
@@ -1,157 +1,93 @@
|
|||||||
-module(handler_ticket_by_id).
|
-module(handler_ticket_by_id).
|
||||||
-include("records.hrl").
|
-behaviour(cowboy_handler).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
init(Req, Opts) ->
|
-include("records.hrl").
|
||||||
handle(Req, Opts).
|
|
||||||
|
|
||||||
handle(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"GET">> -> get_ticket(Req);
|
<<"GET">> -> get_ticket(Req);
|
||||||
<<"PUT">> -> update_ticket(Req);
|
<<"PUT">> -> update_ticket(Req);
|
||||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% GET /v1/admin/tickets/:id - получить тикет
|
|
||||||
get_ticket(Req) ->
|
get_ticket(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
TicketId = cowboy_req:binding(id, 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} ->
|
{ok, Ticket} ->
|
||||||
Response = ticket_to_json(Ticket),
|
io:format("[TICKET_BY_ID] Found ticket, reporter_id: ~s~n", [Ticket#ticket.reporter_id]),
|
||||||
send_json(Req1, 200, Response);
|
case is_admin(UserId) orelse Ticket#ticket.reporter_id =:= UserId of
|
||||||
{error, access_denied} ->
|
true ->
|
||||||
send_error(Req1, 403, <<"Admin access required">>);
|
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} ->
|
{error, not_found} ->
|
||||||
send_error(Req1, 404, <<"Ticket not found">>);
|
io:format("[TICKET_BY_ID] Ticket not found~n"),
|
||||||
{error, _} ->
|
send_error(Req1, 404, <<"Ticket not found">>)
|
||||||
send_error(Req1, 500, <<"Internal server error">>)
|
|
||||||
end;
|
end;
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Message, Req1} ->
|
||||||
|
io:format("[TICKET_BY_ID] Auth error: ~p - ~s~n", [Code, Message]),
|
||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% PUT /v1/admin/tickets/:id - обновить тикет
|
|
||||||
update_ticket(Req) ->
|
update_ticket(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
TicketId = cowboy_req:binding(id, Req1),
|
TicketId = cowboy_req:binding(id, Req1),
|
||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
Decoded when is_map(Decoded) ->
|
Updates when is_map(Updates) ->
|
||||||
handle_ticket_action(AdminId, TicketId, Decoded, Req2);
|
case core_ticket:get_by_id(TicketId) of
|
||||||
_ ->
|
{ok, Ticket} ->
|
||||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
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
|
catch
|
||||||
_:_ ->
|
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
|
||||||
end;
|
end;
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Message, Req1} ->
|
||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Обработка действий с тикетом
|
is_admin(UserId) ->
|
||||||
handle_ticket_action(AdminId, TicketId, Body, Req) ->
|
case core_user:get_by_id(UserId) of
|
||||||
case maps:get(<<"action">>, Body, undefined) of
|
{ok, U} -> lists:member(U#user.role, [admin, superadmin, moderator, support]);
|
||||||
<<"status">> ->
|
_ -> false
|
||||||
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">>)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Вспомогательные функции
|
ticket_to_json(T) ->
|
||||||
ticket_to_json(Ticket) ->
|
|
||||||
Context = try binary_to_term(Ticket#ticket.context) of
|
|
||||||
C -> C
|
|
||||||
catch
|
|
||||||
_:_ -> #{}
|
|
||||||
end,
|
|
||||||
|
|
||||||
#{
|
#{
|
||||||
id => Ticket#ticket.id,
|
id => T#ticket.id,
|
||||||
error_hash => Ticket#ticket.error_hash,
|
error_hash => T#ticket.error_hash,
|
||||||
error_message => Ticket#ticket.error_message,
|
error_message => T#ticket.error_message,
|
||||||
stacktrace => Ticket#ticket.stacktrace,
|
stacktrace => T#ticket.stacktrace,
|
||||||
context => Context,
|
context => T#ticket.context,
|
||||||
count => Ticket#ticket.count,
|
count => T#ticket.count,
|
||||||
first_seen => datetime_to_iso8601(Ticket#ticket.first_seen),
|
first_seen => datetime_to_iso8601(T#ticket.first_seen),
|
||||||
last_seen => datetime_to_iso8601(Ticket#ticket.last_seen),
|
last_seen => datetime_to_iso8601(T#ticket.last_seen),
|
||||||
status => Ticket#ticket.status,
|
status => T#ticket.status,
|
||||||
assigned_to => Ticket#ticket.assigned_to,
|
assigned_to => T#ticket.assigned_to,
|
||||||
resolution_note => Ticket#ticket.resolution_note
|
resolution_note => T#ticket.resolution_note
|
||||||
}.
|
}.
|
||||||
|
|
||||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
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",
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
send_json(Req, Status, Data) ->
|
send_json(Req, Status, Data) ->
|
||||||
Body = jsx:encode(Data),
|
Body = jsx:encode(Data),
|
||||||
|
|||||||
@@ -1,113 +1,82 @@
|
|||||||
-module(handler_tickets).
|
-module(handler_tickets).
|
||||||
-include("records.hrl").
|
-behaviour(cowboy_handler).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
init(Req, Opts) ->
|
-include("records.hrl").
|
||||||
handle(Req, Opts).
|
|
||||||
|
init(Req0, Opts) ->
|
||||||
|
handle(Req0, Opts).
|
||||||
|
|
||||||
handle(Req, _Opts) ->
|
handle(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"GET">> -> list_tickets(Req);
|
<<"GET">> -> list_tickets(Req);
|
||||||
<<"POST">> -> report_error(Req);
|
<<"POST">> -> create_ticket(Req);
|
||||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||||
end.
|
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) ->
|
list_tickets(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
Qs = cowboy_req:parse_qs(Req1),
|
case is_admin(UserId) of
|
||||||
case proplists:get_value(<<"status">>, Qs) of
|
true ->
|
||||||
undefined ->
|
Tickets = core_ticket:list_all(),
|
||||||
case logic_ticket:list_tickets(AdminId) of
|
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]);
|
||||||
{ok, Tickets} ->
|
false ->
|
||||||
Response = [ticket_to_json(T) || T <- Tickets],
|
Tickets = core_ticket:list_by_user(UserId),
|
||||||
send_json(Req1, 200, Response);
|
send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets])
|
||||||
{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
|
|
||||||
end;
|
end;
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Message, Req1} ->
|
||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Вспомогательные функции
|
create_ticket(Req) ->
|
||||||
parse_status(<<"open">>) -> open;
|
case handler_auth:authenticate(Req) of
|
||||||
parse_status(<<"in_progress">>) -> in_progress;
|
{ok, UserId, Req1} ->
|
||||||
parse_status(<<"resolved">>) -> resolved;
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
parse_status(<<"closed">>) -> closed;
|
try jsx:decode(Body, [return_maps]) of
|
||||||
parse_status(_) -> open.
|
#{<<"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
|
||||||
|
_:_ -> send_error(Req2, 400, <<"Invalid JSON">>)
|
||||||
|
end;
|
||||||
|
{error, Code, Message, Req1} ->
|
||||||
|
send_error(Req1, Code, Message)
|
||||||
|
end.
|
||||||
|
|
||||||
ticket_to_json(Ticket) ->
|
is_admin(UserId) ->
|
||||||
Context = try binary_to_term(Ticket#ticket.context) of
|
case core_user:get_by_id(UserId) of
|
||||||
C -> C
|
{ok, User} ->
|
||||||
catch
|
lists:member(User#user.role, [admin, superadmin, moderator, support]);
|
||||||
_:_ -> #{}
|
_ -> false
|
||||||
end,
|
end.
|
||||||
|
|
||||||
|
ticket_to_json(T) ->
|
||||||
#{
|
#{
|
||||||
id => Ticket#ticket.id,
|
id => T#ticket.id,
|
||||||
error_hash => Ticket#ticket.error_hash,
|
error_hash => T#ticket.error_hash,
|
||||||
error_message => Ticket#ticket.error_message,
|
error_message => T#ticket.error_message,
|
||||||
stacktrace => Ticket#ticket.stacktrace,
|
stacktrace => T#ticket.stacktrace,
|
||||||
context => Context,
|
context => T#ticket.context,
|
||||||
count => Ticket#ticket.count,
|
count => T#ticket.count,
|
||||||
first_seen => datetime_to_iso8601(Ticket#ticket.first_seen),
|
first_seen => datetime_to_iso8601(T#ticket.first_seen),
|
||||||
last_seen => datetime_to_iso8601(Ticket#ticket.last_seen),
|
last_seen => datetime_to_iso8601(T#ticket.last_seen),
|
||||||
status => Ticket#ticket.status,
|
status => T#ticket.status,
|
||||||
assigned_to => Ticket#ticket.assigned_to,
|
assigned_to => T#ticket.assigned_to,
|
||||||
resolution_note => Ticket#ticket.resolution_note
|
resolution_note => T#ticket.resolution_note
|
||||||
}.
|
}.
|
||||||
|
|
||||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
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",
|
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) ->
|
send_json(Req, Status, Data) ->
|
||||||
Body = jsx:encode(Data),
|
Body = jsx:encode(Data),
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
-module(handler_user_me).
|
-module(handler_user_me).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
init(Req, Opts) ->
|
init(Req, Opts) -> handle(Req, Opts).
|
||||||
handle(Req, Opts).
|
|
||||||
|
|
||||||
handle(Req, _Opts) ->
|
handle(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
@@ -14,10 +12,10 @@ handle(Req, _Opts) ->
|
|||||||
case core_user:get_by_id(UserId) of
|
case core_user:get_by_id(UserId) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
Response = #{
|
Response = #{
|
||||||
id => User#user.id,
|
id => User#user.id,
|
||||||
email => User#user.email,
|
email => User#user.email,
|
||||||
role => User#user.role,
|
role => User#user.role,
|
||||||
status => User#user.status,
|
status => User#user.status,
|
||||||
created_at => User#user.created_at,
|
created_at => User#user.created_at,
|
||||||
updated_at => User#user.updated_at
|
updated_at => User#user.updated_at
|
||||||
},
|
},
|
||||||
@@ -36,8 +34,7 @@ authenticate(Req) ->
|
|||||||
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
||||||
{bearer, Token} ->
|
{bearer, Token} ->
|
||||||
case logic_auth:verify_jwt(Token) of
|
case logic_auth:verify_jwt(Token) of
|
||||||
{ok, Claims} ->
|
{ok, UserId, _Role} -> % ← теперь возвращается {ok, UserId, Role}
|
||||||
UserId = maps:get(<<"user_id">>, Claims),
|
|
||||||
{ok, UserId, Req};
|
{ok, UserId, Req};
|
||||||
{error, expired} ->
|
{error, expired} ->
|
||||||
{error, 401, <<"Token expired">>, Req};
|
{error, 401, <<"Token expired">>, Req};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
-module(ws_handler).
|
-module(ws_handler).
|
||||||
-behaviour(cowboy_websocket).
|
-behaviour(cowboy_websocket).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
-export([websocket_init/1]).
|
-export([websocket_init/1]).
|
||||||
-export([websocket_handle/2]).
|
-export([websocket_handle/2]).
|
||||||
@@ -13,15 +12,13 @@
|
|||||||
}).
|
}).
|
||||||
|
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
% Аутентификация через query параметр token
|
|
||||||
Qs = cowboy_req:parse_qs(Req),
|
Qs = cowboy_req:parse_qs(Req),
|
||||||
case proplists:get_value(<<"token">>, Qs) of
|
case proplists:get_value(<<"token">>, Qs) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{ok, cowboy_req:reply(401, #{}, <<"Missing token">>, Req), undefined};
|
{ok, cowboy_req:reply(401, #{}, <<"Missing token">>, Req), undefined};
|
||||||
Token ->
|
Token ->
|
||||||
case logic_auth:verify_jwt(Token) of
|
case logic_auth:verify_jwt(Token) of
|
||||||
{ok, Claims} ->
|
{ok, UserId, _Role} ->
|
||||||
UserId = maps:get(<<"user_id">>, Claims),
|
|
||||||
{cowboy_websocket, Req, #state{user_id = UserId}};
|
{cowboy_websocket, Req, #state{user_id = UserId}};
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
{ok, cowboy_req:reply(401, #{}, <<"Invalid token">>, Req), undefined}
|
{ok, cowboy_req:reply(401, #{}, <<"Invalid token">>, Req), undefined}
|
||||||
@@ -29,7 +26,6 @@ init(Req, _Opts) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
websocket_init(State) ->
|
websocket_init(State) ->
|
||||||
% Регистрируем процесс в pg для получения уведомлений
|
|
||||||
pg:join(eventhub_ws, self()),
|
pg:join(eventhub_ws, self()),
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
@@ -39,9 +35,9 @@ websocket_handle({text, Msg}, State) ->
|
|||||||
#{<<"action">> := <<"subscribe">>, <<"calendar_id">> := CalendarId} ->
|
#{<<"action">> := <<"subscribe">>, <<"calendar_id">> := CalendarId} ->
|
||||||
io:format("Subscribe to calendar: ~s~n", [CalendarId]),
|
io:format("Subscribe to calendar: ~s~n", [CalendarId]),
|
||||||
NewSubs = case lists:member(CalendarId, State#state.subscriptions) of
|
NewSubs = case lists:member(CalendarId, State#state.subscriptions) of
|
||||||
true -> State#state.subscriptions;
|
true -> State#state.subscriptions;
|
||||||
false -> [CalendarId | State#state.subscriptions]
|
false -> [CalendarId | State#state.subscriptions]
|
||||||
end,
|
end,
|
||||||
Reply = jsx:encode(#{status => <<"subscribed">>, calendar_id => CalendarId}),
|
Reply = jsx:encode(#{status => <<"subscribed">>, calendar_id => CalendarId}),
|
||||||
io:format("Sending reply: ~s~n", [Reply]),
|
io:format("Sending reply: ~s~n", [Reply]),
|
||||||
{reply, {text, Reply}, State#state{subscriptions = NewSubs}};
|
{reply, {text, Reply}, State#state{subscriptions = NewSubs}};
|
||||||
@@ -77,7 +73,6 @@ terminate(_Reason, _Req, _State) ->
|
|||||||
pg:leave(eventhub_ws, self()),
|
pg:leave(eventhub_ws, self()),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% Проверка, нужно ли отправлять уведомление пользователю
|
|
||||||
should_notify(calendar_update, #{calendar_id := CalId}, State) ->
|
should_notify(calendar_update, #{calendar_id := CalId}, State) ->
|
||||||
lists:member(CalId, State#state.subscriptions);
|
lists:member(CalId, State#state.subscriptions);
|
||||||
should_notify(booking_update, #{user_id := UserId}, State) ->
|
should_notify(booking_update, #{user_id := UserId}, State) ->
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-module(auth).
|
-module(eventhub_auth).
|
||||||
-export([
|
-export([
|
||||||
generate_user_token/2,
|
generate_user_token/2,
|
||||||
generate_admin_token/2,
|
generate_admin_token/2,
|
||||||
@@ -145,10 +145,13 @@ authenticate_admin_request(_Req, Email, Password) ->
|
|||||||
|
|
||||||
%% ========== REFRESH TOKEN ==========
|
%% ========== REFRESH TOKEN ==========
|
||||||
|
|
||||||
-spec generate_refresh_token(UserId :: binary()) -> {binary(), integer()}.
|
-spec generate_refresh_token(UserId :: binary()) -> {binary(), calendar:datetime()}.
|
||||||
generate_refresh_token(_UserId) ->
|
generate_refresh_token(_UserId) ->
|
||||||
RefreshToken = base64:encode(crypto:strong_rand_bytes(32)),
|
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}.
|
{RefreshToken, ExpiresAt}.
|
||||||
|
|
||||||
%% ========== ВНУТРЕННИЕ ==========
|
%% ========== ВНУТРЕННИЕ ==========
|
||||||
@@ -1,88 +1,43 @@
|
|||||||
-module(logic_auth).
|
-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]).
|
-include("records.hrl").
|
||||||
-export([generate_jwt/2, verify_jwt/1, extract_claims/1]).
|
|
||||||
-export([generate_refresh_token/1]).
|
|
||||||
|
|
||||||
%% ============ Argon2 хеширование ============
|
hash_password(Password) ->
|
||||||
hash_password(Password) when is_binary(Password) ->
|
|
||||||
argon2:hash(Password).
|
argon2:hash(Password).
|
||||||
|
|
||||||
verify_password(Password, Hash) when is_binary(Password), is_binary(Hash) ->
|
verify_password(Password, Hash) ->
|
||||||
argon2:verify(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) ->
|
generate_jwt(UserId, Role) ->
|
||||||
JWK = get_jwk(),
|
eventhub_auth:generate_user_token(UserId, Role).
|
||||||
|
|
||||||
ExpTime = os:system_time(seconds) + 86400, % 24 часа
|
verify_jwt(Token) ->
|
||||||
Claims = #{
|
eventhub_auth:verify_user_token(Token).
|
||||||
<<"user_id">> => UserId,
|
|
||||||
<<"role">> => Role,
|
|
||||||
<<"exp">> => ExpTime,
|
|
||||||
<<"iat">> => os:system_time(seconds)
|
|
||||||
},
|
|
||||||
|
|
||||||
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"HS256">>}, Claims),
|
generate_refresh_token(UserId) ->
|
||||||
{_, Token} = jose_jws:compact(JWT),
|
eventhub_auth:generate_refresh_token(UserId).
|
||||||
Token.
|
|
||||||
|
|
||||||
verify_jwt(Token) when is_binary(Token) ->
|
authenticate_user(Email, Password) ->
|
||||||
try
|
case core_user:get_by_email(Email) of
|
||||||
JWK = get_jwk(),
|
{ok, User} ->
|
||||||
case jose_jwt:verify(JWK, Token) of
|
case verify_password(Password, User#user.password_hash) of
|
||||||
{true, {jose_jwt, Claims}, _} ->
|
{ok, true} ->
|
||||||
case check_expiry(Claims) of
|
{ok, user_to_map(User)};
|
||||||
true -> {ok, Claims};
|
_ ->
|
||||||
false -> {error, expired}
|
{error, invalid_credentials}
|
||||||
end;
|
end;
|
||||||
{true, Claims, _} when is_map(Claims) ->
|
{error, not_found} ->
|
||||||
case check_expiry(Claims) of
|
{error, invalid_credentials}
|
||||||
true -> {ok, Claims};
|
|
||||||
false -> {error, expired}
|
|
||||||
end;
|
|
||||||
{false, _, _} ->
|
|
||||||
{error, invalid_signature}
|
|
||||||
end
|
|
||||||
catch
|
|
||||||
_:_ -> {error, invalid_token}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
extract_claims(Token) when is_binary(Token) ->
|
user_to_map(User) ->
|
||||||
try
|
#{
|
||||||
JWK = get_jwk(),
|
id => User#user.id,
|
||||||
case jose_jwt:verify(JWK, Token) of
|
email => User#user.email,
|
||||||
{true, {jose_jwt, Claims}, _} ->
|
role => atom_to_binary(User#user.role, utf8),
|
||||||
{ok, Claims};
|
status => atom_to_binary(User#user.status, utf8)
|
||||||
{true, Claims, _} when is_map(Claims) ->
|
}.
|
||||||
{ok, Claims};
|
|
||||||
_ ->
|
|
||||||
{error, invalid_token}
|
|
||||||
end
|
|
||||||
catch
|
|
||||||
_:_ -> {error, invalid_token}
|
|
||||||
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}.
|
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
-module(logic_moderation).
|
-module(logic_moderation).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([create_report/4, get_reports/1, get_reports_by_target/3, resolve_report/3]).
|
-export([create_report/4,
|
||||||
-export([add_banned_word/2, remove_banned_word/2, list_banned_words/1]).
|
get_reports/1,
|
||||||
-export([check_content/1, auto_moderate/1]).
|
get_reports_by_target/3,
|
||||||
-export([freeze_calendar/2, unfreeze_calendar/2, freeze_event/2, unfreeze_event/2]).
|
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) ->
|
create_report(ReporterId, TargetType, TargetId, Reason) ->
|
||||||
case target_exists(TargetType, TargetId) of
|
case target_exists(TargetType, TargetId) of
|
||||||
true ->
|
true ->
|
||||||
@@ -22,30 +33,29 @@ create_report(ReporterId, TargetType, TargetId, Reason) ->
|
|||||||
target_id => TargetId,
|
target_id => TargetId,
|
||||||
reason => Reason
|
reason => Reason
|
||||||
}),
|
}),
|
||||||
% Проверяем порог для авто-модерации
|
|
||||||
check_auto_freeze(TargetType, TargetId),
|
check_auto_freeze(TargetType, TargetId),
|
||||||
{ok, Report};
|
{ok, Report};
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end;
|
end;
|
||||||
false -> {error, target_not_found}
|
false ->
|
||||||
|
{error, target_not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получить все жалобы (для админа)
|
|
||||||
get_reports(AdminId) ->
|
get_reports(AdminId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_report:list_all();
|
true -> core_report:list_all();
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получить жалобы на конкретную цель
|
|
||||||
get_reports_by_target(AdminId, TargetType, TargetId) ->
|
get_reports_by_target(AdminId, TargetType, TargetId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_report:list_by_target(TargetType, TargetId);
|
true -> core_report:list_by_target(TargetType, TargetId);
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Рассмотреть жалобу (подтвердить или отклонить)
|
resolve_report(AdminId, ReportId, Action)
|
||||||
resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= dismissed ->
|
when Action =:= reviewed; Action =:= dismissed ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
case core_report:get_by_id(ReportId) of
|
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
|
case Report#report.status of
|
||||||
pending ->
|
pending ->
|
||||||
core_report:update_status(ReportId, Action, AdminId);
|
core_report:update_status(ReportId, Action, AdminId);
|
||||||
_ -> {error, already_resolved}
|
_ ->
|
||||||
|
{error, already_resolved}
|
||||||
end;
|
end;
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end;
|
end;
|
||||||
false -> {error, access_denied}
|
false ->
|
||||||
|
{error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Проверка порога для авто-заморозки
|
|
||||||
check_auto_freeze(TargetType, TargetId) ->
|
check_auto_freeze(TargetType, TargetId) ->
|
||||||
Count = core_report:get_count_by_target(TargetType, TargetId),
|
Count = core_report:get_count_by_target(TargetType, TargetId),
|
||||||
if Count >= ?REPORT_THRESHOLD ->
|
if
|
||||||
auto_freeze(TargetType, TargetId);
|
Count >= ?REPORT_THRESHOLD ->
|
||||||
true -> ok
|
auto_freeze(TargetType, TargetId);
|
||||||
|
true ->
|
||||||
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
auto_freeze(event, EventId) ->
|
auto_freeze(event, EventId) ->
|
||||||
@@ -82,42 +96,52 @@ auto_freeze(calendar, CalendarId) ->
|
|||||||
end;
|
end;
|
||||||
auto_freeze(_, _) -> ok.
|
auto_freeze(_, _) -> ok.
|
||||||
|
|
||||||
%% ============ Бан-лист ============
|
%% ============ Бан-лист ===================================
|
||||||
|
|
||||||
%% Добавить запрещённое слово
|
|
||||||
add_banned_word(AdminId, Word) ->
|
add_banned_word(AdminId, Word) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_banned_word:add(Word);
|
true -> core_banned_words:add_banned_word(Word, AdminId);
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Удалить запрещённое слово
|
|
||||||
remove_banned_word(AdminId, Word) ->
|
remove_banned_word(AdminId, Word) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_banned_word:remove(Word);
|
true -> core_banned_words:remove_banned_word(Word);
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список запрещённых слов
|
|
||||||
list_banned_words(AdminId) ->
|
list_banned_words(AdminId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_banned_word:list_all();
|
true -> {ok, core_banned_words:list_banned_words()};
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ============ Контент-фильтр ============
|
%% ============ Контент-фильтр =============================
|
||||||
|
|
||||||
%% Проверить контент на запрещённые слова
|
|
||||||
check_content(Text) ->
|
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) ->
|
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) ->
|
freeze_calendar(AdminId, CalendarId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
@@ -129,7 +153,6 @@ freeze_calendar(AdminId, CalendarId) ->
|
|||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Разморозить календарь
|
|
||||||
unfreeze_calendar(AdminId, CalendarId) ->
|
unfreeze_calendar(AdminId, CalendarId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
@@ -141,7 +164,6 @@ unfreeze_calendar(AdminId, CalendarId) ->
|
|||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Заморозить событие
|
|
||||||
freeze_event(AdminId, EventId) ->
|
freeze_event(AdminId, EventId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
@@ -153,7 +175,6 @@ freeze_event(AdminId, EventId) ->
|
|||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Разморозить событие
|
|
||||||
unfreeze_event(AdminId, EventId) ->
|
unfreeze_event(AdminId, EventId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
@@ -165,7 +186,7 @@ unfreeze_event(AdminId, EventId) ->
|
|||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ============ Вспомогательные функции ============
|
%% ============ Вспомогательные функции ====================
|
||||||
|
|
||||||
target_exists(event, EventId) ->
|
target_exists(event, EventId) ->
|
||||||
case core_event:get_by_id(EventId) of
|
case core_event:get_by_id(EventId) of
|
||||||
@@ -176,7 +197,7 @@ target_exists(calendar, CalendarId) ->
|
|||||||
case core_calendar:get_by_id(CalendarId) of
|
case core_calendar:get_by_id(CalendarId) of
|
||||||
{ok, _} -> true;
|
{ok, _} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end; % ← точка с запятой здесь!
|
end;
|
||||||
target_exists(_, _) -> false.
|
target_exists(_, _) -> false.
|
||||||
|
|
||||||
is_admin(UserId) ->
|
is_admin(UserId) ->
|
||||||
|
|||||||
@@ -1,89 +1,110 @@
|
|||||||
-module(logic_ticket).
|
-module(logic_ticket).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([report_error/3, get_ticket/2, list_tickets/1, list_tickets_by_status/2]).
|
-export([report_error/3,
|
||||||
-export([update_status/3, assign_ticket/3, resolve_ticket/3, close_ticket/2]).
|
get_ticket/2,
|
||||||
-export([get_statistics/1]).
|
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) ->
|
report_error(ErrorMessage, Stacktrace, Context) ->
|
||||||
case core_ticket:create_or_update(ErrorMessage, Stacktrace, Context) of
|
Existing = [T || T <- core_ticket:list_all(), T#ticket.error_message =:= ErrorMessage],
|
||||||
{ok, Ticket} ->
|
case Existing of
|
||||||
% Если это новый тикет, уведомляем администраторов (заглушка)
|
[Ticket] ->
|
||||||
case Ticket#ticket.count of
|
% Увеличить счётчик и обновить last_seen
|
||||||
1 -> notify_admins(Ticket);
|
Updated = Ticket#ticket{
|
||||||
_ -> ok
|
count = Ticket#ticket.count + 1,
|
||||||
end,
|
last_seen = calendar:universal_time()
|
||||||
{ok, Ticket};
|
},
|
||||||
Error -> Error
|
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.
|
end.
|
||||||
|
|
||||||
%% Получить тикет (только для админов)
|
%% Получить тикет (только для админов)
|
||||||
get_ticket(AdminId, TicketId) ->
|
get_ticket(AdminId, TicketId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_ticket:get_by_id(TicketId);
|
true -> core_ticket:get_by_id(TicketId);
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список всех тикетов (только для админов)
|
%% Список всех тикетов (только для админов)
|
||||||
list_tickets(AdminId) ->
|
list_tickets(AdminId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_ticket:list_all();
|
true -> core_ticket:list_all();
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список тикетов по статусу (только для админов)
|
%% Список тикетов по статусу (только для админов)
|
||||||
list_tickets_by_status(AdminId, Status) ->
|
list_tickets_by_status(AdminId, Status) ->
|
||||||
case is_admin(AdminId) of
|
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}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Обновить статус тикета
|
%% Обновить статус тикета
|
||||||
update_status(AdminId, TicketId, Status) ->
|
update_status(AdminId, TicketId, Status) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_ticket:update_status(TicketId, Status);
|
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => Status});
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Назначить тикет администратору
|
%% Назначить тикет администратору
|
||||||
assign_ticket(AdminId, TicketId, AssignToId) ->
|
assign_ticket(AdminId, TicketId, AssignToId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_ticket:assign(TicketId, AssignToId);
|
true -> core_ticket:update_ticket(TicketId, #{<<"assigned_to">> => AssignToId});
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Отметить тикет как решённый с примечанием
|
%% Отметить тикет как решённый с примечанием
|
||||||
resolve_ticket(AdminId, TicketId, ResolutionNote) ->
|
resolve_ticket(AdminId, TicketId, ResolutionNote) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
case core_ticket:add_resolution(TicketId, ResolutionNote) of
|
core_ticket:update_ticket(TicketId, #{
|
||||||
{ok, _Ticket} ->
|
<<"status">> => <<"closed">>,
|
||||||
core_ticket:update_status(TicketId, resolved);
|
<<"resolution_note">> => ResolutionNote
|
||||||
Error -> Error
|
});
|
||||||
end;
|
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Закрыть тикет
|
%% Закрыть тикет
|
||||||
close_ticket(AdminId, TicketId) ->
|
close_ticket(AdminId, TicketId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true -> core_ticket:update_status(TicketId, closed);
|
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => <<"closed">>});
|
||||||
false -> {error, access_denied}
|
false -> {error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получить статистику по тикетам
|
%% Получить статистику по тикетам
|
||||||
get_statistics(AdminId) ->
|
get_statistics(AdminId) ->
|
||||||
case is_admin(AdminId) of
|
case is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
{ok, AllTickets} = core_ticket:list_all(),
|
All = core_ticket:list_all(),
|
||||||
Open = length([T || T <- AllTickets, T#ticket.status =:= open]),
|
Open = length([T || T <- All, T#ticket.status =:= open]),
|
||||||
InProgress = length([T || T <- AllTickets, T#ticket.status =:= in_progress]),
|
InProgress = length([T || T <- All, T#ticket.status =:= in_progress]),
|
||||||
Resolved = length([T || T <- AllTickets, T#ticket.status =:= resolved]),
|
Resolved = length([T || T <- All, T#ticket.status =:= resolved]),
|
||||||
Closed = length([T || T <- AllTickets, T#ticket.status =:= closed]),
|
Closed = length([T || T <- All, T#ticket.status =:= closed]),
|
||||||
TotalErrors = lists:sum([T#ticket.count || T <- AllTickets]),
|
TotalErrors = lists:sum([T#ticket.count || T <- All]),
|
||||||
#{
|
#{
|
||||||
total_tickets => length(AllTickets),
|
total_tickets => length(All),
|
||||||
open => Open,
|
open => Open,
|
||||||
in_progress => InProgress,
|
in_progress => InProgress,
|
||||||
resolved => Resolved,
|
resolved => Resolved,
|
||||||
@@ -102,6 +123,4 @@ is_admin(UserId) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
notify_admins(_Ticket) ->
|
notify_admins(_Ticket) ->
|
||||||
% Заглушка для уведомлений администраторов
|
|
||||||
% В будущем здесь будет отправка email/websocket
|
|
||||||
ok.
|
ok.
|
||||||
@@ -3,24 +3,154 @@
|
|||||||
|
|
||||||
test() ->
|
test() ->
|
||||||
io:format("Testing admin panel API...~n"),
|
io:format("Testing admin panel API...~n"),
|
||||||
|
AdminURL = "http://localhost:8445",
|
||||||
|
|
||||||
|
% Получаем admin-токен через test runner (уже проверенный)
|
||||||
AdminToken = api_test_runner:get_admin_token(),
|
AdminToken = api_test_runner:get_admin_token(),
|
||||||
|
|
||||||
% TEST 1: Admin healthcheck
|
%% TEST 1: Admin healthcheck (public)
|
||||||
io:format(" TEST 1: Admin healthcheck... "),
|
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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 2: Admin stats
|
%% TEST 2: Admin login (дополнительная проверка)
|
||||||
io:format(" TEST 2: Admin stats... "),
|
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,
|
{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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 3: List users
|
%% TEST 4: List users
|
||||||
io:format(" TEST 3: List users... "),
|
io:format(" TEST 4: List users... "),
|
||||||
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
{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("OK~n"),
|
||||||
|
|
||||||
io:format("~n✅ Admin API tests passed!~n"),
|
io:format("~n✅ Admin API tests passed!~n"),
|
||||||
|
|||||||
@@ -2,52 +2,70 @@
|
|||||||
-export([test/0]).
|
-export([test/0]).
|
||||||
|
|
||||||
-define(BASE_URL, "http://localhost:8080").
|
-define(BASE_URL, "http://localhost:8080").
|
||||||
|
-define(ADMIN_BASE_URL, "http://localhost:8445").
|
||||||
|
|
||||||
test() ->
|
test() ->
|
||||||
io:format("Testing moderation API...~n"),
|
io:format("Testing moderation API...~n"),
|
||||||
|
|
||||||
AdminToken = api_test_runner:get_admin_token(),
|
AdminToken = api_test_runner:get_admin_token(),
|
||||||
UserToken = api_test_runner:get_user_token(),
|
UserToken = api_test_runner:get_user_token(),
|
||||||
|
|
||||||
% Создаём календарь и событие
|
%% Создаём календарь и событие через пользовательский API
|
||||||
CalId = api_test_runner:extract_json(
|
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(
|
EventId = api_test_runner:extract_json(
|
||||||
api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events",
|
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... "),
|
io:format(" TEST 1: Create report... "),
|
||||||
ReportId = api_test_runner:extract_json(
|
ReportId = api_test_runner:extract_json(
|
||||||
api_test_runner:http_post("/v1/reports",
|
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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 2: Admin views reports
|
%% TEST 2: Admin views reports (через админский URL, прямой httpc)
|
||||||
io:format(" TEST 2: Admin views reports... "),
|
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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 3: Admin resolves report
|
%% TEST 3: Admin resolves report
|
||||||
io:format(" 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),
|
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
|
||||||
#{action => <<"review">>}, AdminToken),
|
{?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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 4: Add banned word
|
%% TEST 4: Add banned word (админ)
|
||||||
io:format(" TEST 4: Add banned word... "),
|
io:format(" TEST 4: Add banned word... "),
|
||||||
{ok, {{_, 201, _}, _, _}} = api_test_runner:http_post("/v1/admin/banned-words",
|
{ok, {{_, 201, _}, _, _}} = httpc:request(post,
|
||||||
#{word => <<"badword">>}, AdminToken),
|
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words",
|
||||||
|
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
|
||||||
|
"application/json",
|
||||||
|
jsx:encode(#{<<"word">> => <<"badword">>})}, [], []),
|
||||||
io:format("OK~n"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 5: List banned words
|
%% TEST 5: List banned words (админ)
|
||||||
io:format(" 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"),
|
io:format("OK~n"),
|
||||||
|
|
||||||
% TEST 6: Remove banned word
|
%% TEST 6: Remove banned word (админ)
|
||||||
io:format(" 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("OK~n"),
|
||||||
|
|
||||||
io:format("~n✅ Moderation API tests passed!~n"),
|
io:format("~n✅ Moderation API tests passed!~n"),
|
||||||
|
|||||||
@@ -1,37 +1,78 @@
|
|||||||
-module(api_tickets_tests).
|
-module(api_tickets_tests).
|
||||||
-export([test/0]).
|
-export([test/0]).
|
||||||
|
|
||||||
-define(BASE_URL, "http://localhost:8080").
|
-define(ADMIN_BASE_URL, "http://localhost:8445").
|
||||||
|
|
||||||
test() ->
|
test() ->
|
||||||
io:format("Testing tickets API...~n"),
|
io:format("Testing tickets API...~n"),
|
||||||
|
Token = api_test_runner:get_user_token(),
|
||||||
AdminToken = api_test_runner:get_admin_token(),
|
AdminToken = api_test_runner:get_admin_token(),
|
||||||
UserToken = api_test_runner:get_user_token(),
|
|
||||||
|
|
||||||
% TEST 1: Report error
|
%% TEST 1: Create ticket (user)
|
||||||
io:format(" TEST 1: Report error... "),
|
io:format(" TEST 1: Create ticket...~n"),
|
||||||
|
io:format(" POST /v1/tickets~n"),
|
||||||
TicketId = api_test_runner:extract_json(
|
TicketId = api_test_runner:extract_json(
|
||||||
api_test_runner:http_post("/v1/tickets",
|
api_test_runner:http_post("/v1/tickets",
|
||||||
#{error_message => <<"Test bug">>, stacktrace => <<"line 1">>}, UserToken), <<"id">>),
|
#{error_message => <<"Bug">>,
|
||||||
io:format("OK~n"),
|
stacktrace => <<"Something broke">>},
|
||||||
|
Token),
|
||||||
|
<<"id">>),
|
||||||
|
io:format(" OK~n"),
|
||||||
|
|
||||||
% TEST 2: Admin views tickets
|
%% TEST 2: Get my tickets (user)
|
||||||
io:format(" TEST 2: Admin views tickets... "),
|
io:format(" TEST 2: Get my tickets...~n"),
|
||||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/tickets", AdminToken),
|
io:format(" GET /v1/tickets~n"),
|
||||||
io:format("OK~n"),
|
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/tickets", Token),
|
||||||
|
io:format(" OK~n"),
|
||||||
|
|
||||||
% TEST 3: Update ticket status
|
%% TEST 3: Get single ticket (user)
|
||||||
io:format(" TEST 3: Update ticket status... "),
|
io:format(" TEST 3: Get single ticket...~n"),
|
||||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
io:format(" GET /v1/tickets/~s~n", [TicketId]),
|
||||||
#{action => <<"status">>, status => <<"in_progress">>}, AdminToken),
|
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get(
|
||||||
io:format("OK~n"),
|
"/v1/tickets/" ++ binary_to_list(TicketId),
|
||||||
|
Token),
|
||||||
|
io:format(" OK~n"),
|
||||||
|
|
||||||
% TEST 4: Close ticket
|
%% TEST 4: Admin lists all tickets
|
||||||
io:format(" TEST 4: Close ticket... "),
|
io:format(" TEST 4: Admin lists all tickets...~n"),
|
||||||
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
|
io:format(" GET ~s/v1/admin/tickets~n", [?ADMIN_BASE_URL]),
|
||||||
#{action => <<"close">>}, AdminToken),
|
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
|
||||||
io:format("OK~n"),
|
{?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"),
|
io:format("~n✅ Tickets API tests passed!~n"),
|
||||||
{?MODULE, ok}.
|
{?MODULE, ok}.
|
||||||
@@ -46,7 +46,7 @@ test_get_report() ->
|
|||||||
target_type = <<"event">>,
|
target_type = <<"event">>,
|
||||||
target_id = <<"e1">>,
|
target_id = <<"e1">>,
|
||||||
reason = <<"spam">>,
|
reason = <<"spam">>,
|
||||||
status = <<"new">>,
|
status = pending,
|
||||||
created_at = {{2026,4,26},{12,0,0}},
|
created_at = {{2026,4,26},{12,0,0}},
|
||||||
resolved_at = undefined
|
resolved_at = undefined
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@ test_get_report() ->
|
|||||||
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
#{<<"id">> := <<"r1">>, <<"status">> := <<"new">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
%% GET – не найдено
|
%% GET – не найдено
|
||||||
test_get_report_not_found() ->
|
test_get_report_not_found() ->
|
||||||
@@ -94,9 +94,9 @@ test_update_report() ->
|
|||||||
fun(id, _) -> <<"r1">> end),
|
fun(id, _) -> <<"r1">> end),
|
||||||
ok = meck:expect(cowboy_req, read_body,
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
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,
|
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, []),
|
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
@@ -115,7 +115,7 @@ test_update_report_not_found() ->
|
|||||||
ok = meck:expect(cowboy_req, read_body,
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
||||||
ok = meck:expect(core_report, update_status,
|
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, []),
|
{ok, _, _} = admin_handler_report_by_id:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(404, Status).
|
?assertEqual(404, Status).
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ admin_reports_test_() ->
|
|||||||
{"POST /admin/reports – method not allowed", fun test_wrong_method/0}
|
{"POST /admin/reports – method not allowed", fun test_wrong_method/0}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
%% GET – успех
|
||||||
test_list_reports() ->
|
test_list_reports() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
@@ -42,17 +43,18 @@ test_list_reports() ->
|
|||||||
target_type = <<"event">>,
|
target_type = <<"event">>,
|
||||||
target_id = <<"e1">>,
|
target_id = <<"e1">>,
|
||||||
reason = <<"spam">>,
|
reason = <<"spam">>,
|
||||||
status = <<"new">>,
|
status = pending,
|
||||||
created_at = {{2026,4,26},{12,0,0}},
|
created_at = {{2026,4,26},{12,0,0}},
|
||||||
resolved_at = undefined
|
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, []),
|
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
[#{<<"id">> := <<"r1">>, <<"target_type">> := <<"event">>, <<"status">> := <<"new">>}]
|
[#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>}] = jsx:decode(RespBody, [return_maps]).
|
||||||
= jsx:decode(RespBody, [return_maps]).
|
|
||||||
|
|
||||||
|
%% GET – запрещён
|
||||||
test_list_reports_forbidden() ->
|
test_list_reports_forbidden() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
@@ -62,6 +64,7 @@ test_list_reports_forbidden() ->
|
|||||||
?assertEqual(403, Status),
|
?assertEqual(403, Status),
|
||||||
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% PUT – успех
|
||||||
test_update_report() ->
|
test_update_report() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
@@ -73,14 +76,16 @@ test_update_report() ->
|
|||||||
fun(id, _) -> <<"r1">> end),
|
fun(id, _) -> <<"r1">> end),
|
||||||
ok = meck:expect(cowboy_req, read_body,
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
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,
|
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, []),
|
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% PUT – невалидный JSON
|
||||||
test_update_report_bad_json() ->
|
test_update_report_bad_json() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
@@ -93,9 +98,10 @@ test_update_report_bad_json() ->
|
|||||||
ok = meck:expect(cowboy_req, read_body,
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
fun(Req) -> {ok, <<"bad json">>, Req} end),
|
fun(Req) -> {ok, <<"bad json">>, Req} end),
|
||||||
{ok, _, _} = admin_handler_reports:init(req, []),
|
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(400, Status).
|
?assertEqual(400, Status).
|
||||||
|
|
||||||
|
%% PUT – не найдено
|
||||||
test_update_report_not_found() ->
|
test_update_report_not_found() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
|
||||||
ok = meck:expect(handler_auth, authenticate,
|
ok = meck:expect(handler_auth, authenticate,
|
||||||
@@ -108,11 +114,12 @@ test_update_report_not_found() ->
|
|||||||
ok = meck:expect(cowboy_req, read_body,
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
|
||||||
ok = meck:expect(core_report, update_status,
|
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, []),
|
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(404, Status).
|
?assertEqual(404, Status).
|
||||||
|
|
||||||
|
%% Неправильный метод
|
||||||
test_wrong_method() ->
|
test_wrong_method() ->
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
|
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
|
||||||
{ok, _, _} = admin_handler_reports:init(req, []),
|
{ok, _, _} = admin_handler_reports:init(req, []),
|
||||||
|
|||||||
@@ -21,182 +21,210 @@ cleanup(_) ->
|
|||||||
|
|
||||||
admin_tickets_test_() ->
|
admin_tickets_test_() ->
|
||||||
{setup, fun setup/0, fun cleanup/1, [
|
{setup, fun setup/0, fun cleanup/1, [
|
||||||
{"GET /admin/tickets – success", fun test_list/0},
|
{"GET /admin/tickets – success", fun test_list/0},
|
||||||
{"GET /admin/tickets – forbidden", fun test_list_forbidden/0},
|
{"GET /admin/tickets – forbidden", fun test_list_forbidden/0},
|
||||||
{"POST /admin/tickets – success", fun test_create/0},
|
{"POST /admin/tickets – success", fun test_create/0},
|
||||||
{"POST /admin/tickets – missing error_message", fun test_create_missing/0},
|
{"POST /admin/tickets – missing error_message", fun test_create_missing/0},
|
||||||
{"GET /admin/tickets/:id – success", fun test_get/0},
|
{"GET /admin/tickets/:id – success", fun test_get/0},
|
||||||
{"GET /admin/tickets/:id – not found", fun test_get_not_found/0},
|
{"GET /admin/tickets/:id – not found", fun test_get_not_found/0},
|
||||||
{"PUT /admin/tickets/:id – success", fun test_update/0},
|
{"PUT /admin/tickets/:id – success", fun test_update/0},
|
||||||
{"PUT /admin/tickets/:id – not found", fun test_update_not_found/0},
|
{"PUT /admin/tickets/:id – not found", fun test_update_not_found/0},
|
||||||
{"DELETE /admin/tickets/:id – success", fun test_delete/0},
|
{"DELETE /admin/tickets/:id – success", fun test_delete/0},
|
||||||
{"DELETE /admin/tickets/:id – not found", fun test_delete_not_found/0},
|
{"DELETE /admin/tickets/:id – not found", fun test_delete_not_found/0},
|
||||||
{"PATCH /admin/tickets – method not allowed", fun test_wrong_method/0}
|
{"PATCH /admin/tickets – method not allowed", fun test_wrong_method/0}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
%% GET – список тикетов (успех)
|
||||||
test_list() ->
|
test_list() ->
|
||||||
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
|
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> 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},
|
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{
|
Ticket = #ticket{
|
||||||
id = <<"t1">>,
|
id = <<"t1">>,
|
||||||
error_hash = <<"abc123">>,
|
error_hash = <<"hash1">>,
|
||||||
error_message = <<"Ooops">>,
|
error_message = <<"Error message">>,
|
||||||
stacktrace = <<"trace">>,
|
stacktrace = <<"stack">>,
|
||||||
context = <<"ctx">>,
|
context = <<"ctx">>,
|
||||||
count = 3,
|
count = 1,
|
||||||
first_seen = {{2026,4,27},{12,0,0}},
|
first_seen = {{2026,4,28},{12,0,0}},
|
||||||
last_seen = {{2026,4,27},{13,0,0}},
|
last_seen = {{2026,4,28},{12,0,0}},
|
||||||
status = open,
|
status = open,
|
||||||
assigned_to = <<"adm2">>,
|
assigned_to = undefined,
|
||||||
resolution_note = 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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Ooops">>, <<"status">> := <<"open">>}] =
|
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Error message">>}] =
|
||||||
jsx:decode(RespBody, [return_maps]).
|
jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% GET – запрещён
|
||||||
test_list_forbidden() ->
|
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(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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(403, Status).
|
?assertEqual(403, Status).
|
||||||
|
|
||||||
|
%% POST – создание тикета
|
||||||
test_create() ->
|
test_create() ->
|
||||||
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
|
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> 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},
|
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,
|
||||||
BodyMap = #{<<"error_message">> => <<"New bug">>, <<"stacktrace">> => <<"trace">>},
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
|
BodyMap = #{<<"error_message">> => <<"Bug">>, <<"stacktrace">> => <<"trace">>},
|
||||||
Created = #ticket{
|
ok = meck:expect(cowboy_req, read_body,
|
||||||
id = <<"t_new">>,
|
fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
|
||||||
error_hash = <<"hash">>,
|
Created = #ticket{id = <<"t_new">>, error_message = <<"Bug">>, status = open},
|
||||||
error_message = <<"New bug">>,
|
ok = meck:expect(core_ticket, create_ticket, fun(_) -> {ok, Created} end),
|
||||||
stacktrace = <<"trace">>,
|
|
||||||
context = <<>>,
|
|
||||||
count = 1,
|
|
||||||
first_seen = {{2026,4,27},{14,0,0}},
|
|
||||||
last_seen = {{2026,4,27},{14,0,0}},
|
|
||||||
status = open,
|
|
||||||
assigned_to = undefined,
|
|
||||||
resolution_note = undefined
|
|
||||||
},
|
|
||||||
ok = meck:expect(core_ticket, create_ticket, fun(Data) ->
|
|
||||||
true = maps:is_key(<<"error_message">>, Data),
|
|
||||||
{ok, Created}
|
|
||||||
end),
|
|
||||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(201, Status),
|
?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() ->
|
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(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},
|
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,
|
||||||
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"desc">> => <<"no msg">>}), Req} end),
|
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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(400, Status).
|
?assertEqual(400, Status).
|
||||||
|
|
||||||
|
%% GET – один тикет по ID
|
||||||
test_get() ->
|
test_get() ->
|
||||||
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
|
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> 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},
|
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,
|
||||||
Ticket = #ticket{
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
id = <<"t1">>,
|
Ticket = #ticket{id = <<"t1">>, error_message = <<"Test">>, status = open},
|
||||||
error_hash = <<"abc">>,
|
ok = meck:expect(core_ticket, get_by_id,
|
||||||
error_message = <<"msg">>,
|
fun(<<"t1">>) -> {ok, Ticket} end),
|
||||||
stacktrace = <<>>,
|
|
||||||
context = <<>>,
|
|
||||||
count = 1,
|
|
||||||
first_seen = {{2026,4,27},{12,0,0}},
|
|
||||||
last_seen = {{2026,4,27},{12,0,0}},
|
|
||||||
status = open,
|
|
||||||
assigned_to = undefined,
|
|
||||||
resolution_note = undefined
|
|
||||||
},
|
|
||||||
ok = meck:expect(core_ticket, get_by_id, fun(<<"t1">>) -> {ok, Ticket} end),
|
|
||||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
#{<<"id">> := <<"t1">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"id">> := <<"t1">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% GET – тикет не найден
|
||||||
test_get_not_found() ->
|
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(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},
|
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,
|
||||||
ok = meck:expect(core_ticket, get_by_id, fun(_) -> {error, not_found} end),
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
|
ok = meck:expect(core_ticket, get_by_id,
|
||||||
|
fun(_) -> {error, not_found} end),
|
||||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(404, Status).
|
?assertEqual(404, Status).
|
||||||
|
|
||||||
|
%% PUT – обновление тикета
|
||||||
test_update() ->
|
test_update() ->
|
||||||
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
|
|
||||||
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> 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},
|
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,
|
||||||
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
|
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},
|
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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
#{<<"status">> := <<"closed">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"status">> := <<"closed">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% PUT – тикет не найден
|
||||||
test_update_not_found() ->
|
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(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},
|
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,
|
||||||
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
ok = meck:expect(core_ticket, update_ticket, fun(_, _) -> {error, not_found} 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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(404, Status).
|
?assertEqual(404, Status).
|
||||||
|
|
||||||
|
%% DELETE – удаление тикета
|
||||||
test_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(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},
|
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,
|
||||||
ok = meck:expect(core_ticket, delete_ticket, fun(<<"t1">>) -> {ok, deleted} end),
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
|
ok = meck:expect(core_ticket, delete_ticket,
|
||||||
|
fun(<<"t1">>) -> {ok, deleted} end),
|
||||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(200, Status),
|
?assertEqual(200, Status),
|
||||||
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
|
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
|
||||||
|
|
||||||
|
%% DELETE – тикет не найден
|
||||||
test_delete_not_found() ->
|
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(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},
|
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,
|
||||||
ok = meck:expect(core_ticket, delete_ticket, fun(_) -> {error, not_found} end),
|
fun(<<"adm1">>) -> {ok, AdminUser} end),
|
||||||
|
ok = meck:expect(core_ticket, delete_ticket,
|
||||||
|
fun(_) -> {error, not_found} end),
|
||||||
{ok, _, _} = admin_handler_tickets:init(req, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, _, _} = erase(test_reply),
|
{Status, _, _, _} = erase(test_reply),
|
||||||
?assertEqual(404, Status).
|
?assertEqual(404, Status).
|
||||||
|
|
||||||
|
%% Неправильный метод
|
||||||
test_wrong_method() ->
|
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, 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, []),
|
{ok, _, _} = admin_handler_tickets:init(req, []),
|
||||||
{Status, _, RespBody, _} = erase(test_reply),
|
{Status, _, RespBody, _} = erase(test_reply),
|
||||||
?assertEqual(405, Status),
|
?assertEqual(405, Status),
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ generate_user_token_test_() ->
|
|||||||
{setup, fun setup/0, fun cleanup/1, [
|
{setup, fun setup/0, fun cleanup/1, [
|
||||||
{"Generate user token returns a binary",
|
{"Generate user token returns a binary",
|
||||||
fun() ->
|
fun() ->
|
||||||
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
|
Token = eventhub_auth:generate_user_token(<<"user123">>, <<"user">>),
|
||||||
?assert(is_binary(Token)),
|
?assert(is_binary(Token)),
|
||||||
?assert(size(Token) > 0)
|
?assert(size(Token) > 0)
|
||||||
end},
|
end},
|
||||||
{"Generated user token can be verified",
|
{"Generated user token can be verified",
|
||||||
fun() ->
|
fun() ->
|
||||||
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
|
Token = eventhub_auth:generate_user_token(<<"user123">>, <<"user">>),
|
||||||
{ok, UserId, Role} = auth:verify_user_token(Token),
|
{ok, UserId, Role} = eventhub_auth:verify_user_token(Token),
|
||||||
?assertEqual(<<"user123">>, UserId),
|
?assertEqual(<<"user123">>, UserId),
|
||||||
?assertEqual(<<"user">>, Role)
|
?assertEqual(<<"user">>, Role)
|
||||||
end},
|
end},
|
||||||
{"Generate admin token with superadmin role",
|
{"Generate admin token with superadmin role",
|
||||||
fun() ->
|
fun() ->
|
||||||
Token = auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
|
Token = eventhub_auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
|
||||||
{ok, UserId, Role} = auth:verify_admin_token(Token),
|
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
|
||||||
?assertEqual(<<"admin1">>, UserId),
|
?assertEqual(<<"admin1">>, UserId),
|
||||||
?assertEqual(<<"superadmin">>, Role)
|
?assertEqual(<<"superadmin">>, Role)
|
||||||
end}
|
end}
|
||||||
@@ -55,19 +55,19 @@ verify_token_errors_test_() ->
|
|||||||
{"Invalid token signature returns error",
|
{"Invalid token signature returns error",
|
||||||
fun() ->
|
fun() ->
|
||||||
FakeToken = <<"not.a.valid.token">>,
|
FakeToken = <<"not.a.valid.token">>,
|
||||||
?assertEqual({error, invalid_token}, auth:verify_user_token(FakeToken)),
|
?assertEqual({error, invalid_token}, eventhub_auth:verify_user_token(FakeToken)),
|
||||||
?assertEqual({error, invalid_token}, auth:verify_admin_token(FakeToken))
|
?assertEqual({error, invalid_token}, eventhub_auth:verify_admin_token(FakeToken))
|
||||||
end},
|
end},
|
||||||
{"User token rejected by admin verifier (different secret)",
|
{"User token rejected by admin verifier (different secret)",
|
||||||
fun() ->
|
fun() ->
|
||||||
Token = auth:generate_user_token(<<"x">>, <<"user">>),
|
Token = eventhub_auth:generate_user_token(<<"x">>, <<"user">>),
|
||||||
% Разные секреты → подпись недействительна для admin JWK
|
% Разные секреты → подпись недействительна для admin JWK
|
||||||
?assertEqual({error, invalid_signature}, auth:verify_admin_token(Token))
|
?assertEqual({error, invalid_signature}, eventhub_auth:verify_admin_token(Token))
|
||||||
end},
|
end},
|
||||||
{"Admin token rejected by user verifier (different secret)",
|
{"Admin token rejected by user verifier (different secret)",
|
||||||
fun() ->
|
fun() ->
|
||||||
Token = auth:generate_admin_token(<<"x">>, <<"admin">>),
|
Token = eventhub_auth:generate_admin_token(<<"x">>, <<"admin">>),
|
||||||
?assertEqual({error, invalid_signature}, auth:verify_user_token(Token))
|
?assertEqual({error, invalid_signature}, eventhub_auth:verify_user_token(Token))
|
||||||
end}
|
end}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
@@ -81,10 +81,10 @@ authenticate_user_request_test_() ->
|
|||||||
UserMap = #{id => <<"user1">>, email => <<"u@test.com">>, role => <<"user">>},
|
UserMap = #{id => <<"user1">>, email => <<"u@test.com">>, role => <<"user">>},
|
||||||
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
|
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
|
||||||
Req = undefined,
|
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)),
|
?assert(is_binary(Token)),
|
||||||
?assertEqual(UserMap, ReturnedUser),
|
?assertEqual(UserMap, ReturnedUser),
|
||||||
{ok, UserId, Role} = auth:verify_user_token(Token),
|
{ok, UserId, Role} = eventhub_auth:verify_user_token(Token),
|
||||||
?assertEqual(<<"user1">>, UserId),
|
?assertEqual(<<"user1">>, UserId),
|
||||||
?assertEqual(<<"user">>, Role)
|
?assertEqual(<<"user">>, Role)
|
||||||
end},
|
end},
|
||||||
@@ -92,7 +92,7 @@ authenticate_user_request_test_() ->
|
|||||||
fun() ->
|
fun() ->
|
||||||
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {error, bad_credentials} end),
|
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {error, bad_credentials} end),
|
||||||
Req = undefined,
|
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}
|
end}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
@@ -106,10 +106,10 @@ authenticate_admin_request_test_() ->
|
|||||||
AdminMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>},
|
AdminMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>},
|
||||||
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, AdminMap} end),
|
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, AdminMap} end),
|
||||||
Req = undefined,
|
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)),
|
?assert(is_binary(Token)),
|
||||||
?assertEqual(AdminMap, ReturnedUser),
|
?assertEqual(AdminMap, ReturnedUser),
|
||||||
{ok, UserId, Role} = auth:verify_admin_token(Token),
|
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
|
||||||
?assertEqual(<<"adm1">>, UserId),
|
?assertEqual(<<"adm1">>, UserId),
|
||||||
?assertEqual(<<"superadmin">>, Role)
|
?assertEqual(<<"superadmin">>, Role)
|
||||||
end},
|
end},
|
||||||
@@ -119,15 +119,15 @@ authenticate_admin_request_test_() ->
|
|||||||
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
|
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
|
||||||
Req = undefined,
|
Req = undefined,
|
||||||
?assertEqual({error, insufficient_permissions},
|
?assertEqual({error, insufficient_permissions},
|
||||||
auth:authenticate_admin_request(Req, <<"u@test.com">>, <<"pwd">>))
|
eventhub_auth:authenticate_admin_request(Req, <<"u@test.com">>, <<"pwd">>))
|
||||||
end},
|
end},
|
||||||
{"Moderator role is accepted as admin",
|
{"Moderator role is accepted as admin",
|
||||||
fun() ->
|
fun() ->
|
||||||
ModMap = #{id => <<"moder1">>, email => <<"mod@test.com">>, role => <<"moderator">>},
|
ModMap = #{id => <<"moder1">>, email => <<"mod@test.com">>, role => <<"moderator">>},
|
||||||
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, ModMap} end),
|
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, ModMap} end),
|
||||||
Req = undefined,
|
Req = undefined,
|
||||||
{ok, Token, _} = auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
|
{ok, Token, _} = eventhub_auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
|
||||||
{ok, _, Role} = auth:verify_admin_token(Token),
|
{ok, _, Role} = eventhub_auth:verify_admin_token(Token),
|
||||||
?assertEqual(<<"moderator">>, Role)
|
?assertEqual(<<"moderator">>, Role)
|
||||||
end}
|
end}
|
||||||
]}.
|
]}.
|
||||||
@@ -136,4 +136,4 @@ authenticate_admin_request_test_() ->
|
|||||||
%% Тест generate_refresh_token/1
|
%% Тест generate_refresh_token/1
|
||||||
%% ------------------------------------------------------------------
|
%% ------------------------------------------------------------------
|
||||||
generate_refresh_token_test() ->
|
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_lib("eunit/include/eunit.hrl").
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Фикстуры
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
setup() ->
|
setup() ->
|
||||||
mnesia:start(),
|
catch mnesia:stop(),
|
||||||
mnesia:create_table(ticket, [
|
case mnesia:start() of
|
||||||
|
{atomic, ok} -> ok;
|
||||||
|
ok -> ok
|
||||||
|
end,
|
||||||
|
{atomic, ok} = mnesia:create_table(ticket, [
|
||||||
{attributes, record_info(fields, ticket)},
|
{attributes, record_info(fields, ticket)},
|
||||||
{ram_copies, [node()]}
|
{ram_copies, [node()]}
|
||||||
]),
|
]),
|
||||||
@@ -12,110 +19,118 @@ setup() ->
|
|||||||
|
|
||||||
cleanup(_) ->
|
cleanup(_) ->
|
||||||
mnesia:delete_table(ticket),
|
mnesia:delete_table(ticket),
|
||||||
mnesia:stop(),
|
mnesia:stop().
|
||||||
ok.
|
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Тесты
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
core_ticket_test_() ->
|
core_ticket_test_() ->
|
||||||
{foreach,
|
{foreach, fun setup/0, fun cleanup/1, [
|
||||||
fun setup/0,
|
{"Create ticket and retrieve it", fun test_create_and_get/0},
|
||||||
fun cleanup/1,
|
{"Update ticket status", fun test_update_status/0},
|
||||||
[
|
{"Delete ticket and verify removal", fun test_delete_ticket/0},
|
||||||
{"Create ticket test", fun test_create_ticket/0},
|
{"List all tickets returns created ones", fun test_list_all/0},
|
||||||
{"Update existing ticket test", fun test_update_ticket/0},
|
{"List tickets by user filters correctly", fun test_list_by_user/0},
|
||||||
{"Get ticket by id test", fun test_get_by_id/0},
|
{"Get ticket stats reflects real counts", fun test_stats/0},
|
||||||
{"Get ticket by error hash test", fun test_get_by_error_hash/0},
|
{"Update ticket with unknown id fails", fun test_update_not_found/0},
|
||||||
{"List all tickets test", fun test_list_all/0},
|
{"Delete ticket with unknown id fails", fun test_delete_not_found/0},
|
||||||
{"List by status test", fun test_list_by_status/0},
|
{"Get ticket with unknown id fails", fun test_get_not_found/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}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
test_create_ticket() ->
|
%% ── Вспомогательная функция для создания тикета ─────────
|
||||||
ErrorMsg = <<"Test error">>,
|
make_ticket(ErrorMsg) ->
|
||||||
Stacktrace = <<"line 1\nline 2">>,
|
Data = #{
|
||||||
Context = #{user_id => <<"user123">>},
|
<<"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),
|
test_create_and_get() ->
|
||||||
?assertEqual(Stacktrace, Ticket#ticket.stacktrace),
|
Ticket = make_ticket("Bug1"),
|
||||||
?assertEqual(1, Ticket#ticket.count),
|
|
||||||
?assertEqual(open, Ticket#ticket.status),
|
|
||||||
?assert(is_binary(Ticket#ticket.id)),
|
?assert(is_binary(Ticket#ticket.id)),
|
||||||
?assert(is_binary(Ticket#ticket.error_hash)).
|
{ok, Retrieved} = core_ticket:get_by_id(Ticket#ticket.id),
|
||||||
|
?assertEqual(Ticket#ticket.id, Retrieved#ticket.id).
|
||||||
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)).
|
|
||||||
|
|
||||||
test_update_status() ->
|
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),
|
test_delete_ticket() ->
|
||||||
?assertEqual(in_progress, Updated#ticket.status),
|
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),
|
test_list_all() ->
|
||||||
?assertEqual(resolved, Resolved#ticket.status).
|
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() ->
|
test_list_by_user() ->
|
||||||
AdminId = <<"admin123">>,
|
% Создаём тикет от пользователя test_user
|
||||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
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),
|
test_stats() ->
|
||||||
?assertEqual(AdminId, Assigned#ticket.assigned_to),
|
Data1 = #{
|
||||||
?assertEqual(in_progress, Assigned#ticket.status).
|
<<"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() ->
|
test_update_not_found() ->
|
||||||
Note = <<"Fixed in version 1.0">>,
|
{error, not_found} = core_ticket:update_ticket(<<"nonexistent">>,
|
||||||
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
|
#{<<"status">> => <<"closed">>}).
|
||||||
|
|
||||||
{ok, Updated} = core_ticket:add_resolution(Ticket#ticket.id, Note),
|
test_delete_not_found() ->
|
||||||
?assertEqual(Note, Updated#ticket.resolution_note).
|
{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(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
|
||||||
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
|
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
|
||||||
|
|
||||||
%% ------------------------------------------------------------------
|
|
||||||
%% Фикстуры
|
|
||||||
%% ------------------------------------------------------------------
|
|
||||||
setup() ->
|
setup() ->
|
||||||
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
|
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
|
||||||
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_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:unset_env(eventhub, admin_jwt_secret),
|
||||||
application:stop(jose).
|
application:stop(jose).
|
||||||
|
|
||||||
%% ------------------------------------------------------------------
|
|
||||||
%% Тесты
|
|
||||||
%% ------------------------------------------------------------------
|
|
||||||
logic_auth_test_() ->
|
logic_auth_test_() ->
|
||||||
[
|
[
|
||||||
{"Password hash test", fun test_password_hash/0},
|
{"Password hash test", fun test_password_hash/0},
|
||||||
@@ -31,7 +25,6 @@ logic_auth_test_() ->
|
|||||||
]}
|
]}
|
||||||
].
|
].
|
||||||
|
|
||||||
%% ── Хеширование паролей (остаётся в logic_auth) ──────────────────
|
|
||||||
test_password_hash() ->
|
test_password_hash() ->
|
||||||
Password = <<"secret123">>,
|
Password = <<"secret123">>,
|
||||||
{ok, Hash} = logic_auth:hash_password(Password),
|
{ok, Hash} = logic_auth:hash_password(Password),
|
||||||
@@ -39,27 +32,23 @@ test_password_hash() ->
|
|||||||
{ok, true} = logic_auth:verify_password(Password, Hash),
|
{ok, true} = logic_auth:verify_password(Password, Hash),
|
||||||
{ok, false} = logic_auth:verify_password(<<"wrong">>, Hash).
|
{ok, false} = logic_auth:verify_password(<<"wrong">>, Hash).
|
||||||
|
|
||||||
%% ── JWT тесты (перенесены в auth) ─────────────────────────────────
|
|
||||||
test_jwt() ->
|
test_jwt() ->
|
||||||
UserId = <<"user123">>,
|
UserId = <<"user123">>,
|
||||||
Role = <<"user">>,
|
Role = <<"user">>,
|
||||||
Token = auth:generate_user_token(UserId, Role),
|
Token = eventhub_auth:generate_user_token(UserId, Role),
|
||||||
?assert(is_binary(Token)),
|
?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(UserId, ReturnedUserId),
|
||||||
?assertEqual(Role, ReturnedRole),
|
?assertEqual(Role, ReturnedRole),
|
||||||
% Проверка невалидного токена
|
{error, invalid_token} = eventhub_auth:verify_user_token(<<"invalid.token.here">>).
|
||||||
{error, invalid_token} = auth:verify_user_token(<<"invalid.token.here">>).
|
|
||||||
|
|
||||||
test_jwt_expired() ->
|
test_jwt_expired() ->
|
||||||
% Тест на истечение срока пока пропущен, так как требует мока времени
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% ── Refresh token (перенесён в auth) ────────────────────────────
|
|
||||||
test_refresh_token() ->
|
test_refresh_token() ->
|
||||||
{Token, ExpiresAt} = auth:generate_refresh_token(<<"user123">>),
|
{Token, ExpiresAt} = eventhub_auth:generate_refresh_token(<<"user123">>),
|
||||||
?assert(is_binary(Token)),
|
?assert(is_binary(Token)),
|
||||||
?assert(size(Token) >= 32),
|
?assert(size(Token) >= 32),
|
||||||
?assert(is_integer(ExpiresAt)),
|
?assert(is_tuple(ExpiresAt)),
|
||||||
Now = os:system_time(second),
|
Now = calendar:universal_time(),
|
||||||
?assert(ExpiresAt > Now).
|
?assert(ExpiresAt > Now).
|
||||||
@@ -2,13 +2,25 @@
|
|||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Фикстуры
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
setup() ->
|
setup() ->
|
||||||
mnesia:start(),
|
catch mnesia:stop(),
|
||||||
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
|
case mnesia:start() of
|
||||||
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
|
{atomic, ok} -> ok;
|
||||||
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
|
ok -> ok
|
||||||
mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
|
end,
|
||||||
mnesia:create_table(banned_word, [{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
|
{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.
|
ok.
|
||||||
|
|
||||||
cleanup(_) ->
|
cleanup(_) ->
|
||||||
@@ -17,29 +29,36 @@ cleanup(_) ->
|
|||||||
mnesia:delete_table(event),
|
mnesia:delete_table(event),
|
||||||
mnesia:delete_table(calendar),
|
mnesia:delete_table(calendar),
|
||||||
mnesia:delete_table(user),
|
mnesia:delete_table(user),
|
||||||
mnesia:stop(),
|
mnesia:stop().
|
||||||
ok.
|
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Тесты
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
logic_moderation_test_() ->
|
logic_moderation_test_() ->
|
||||||
{foreach,
|
{foreach, fun setup/0, fun cleanup/1, [
|
||||||
fun setup/0,
|
{"Create report test", fun test_create_report/0},
|
||||||
fun cleanup/1,
|
{"Get reports test", fun test_get_reports/0},
|
||||||
[
|
{"Resolve report test", fun test_resolve_report/0},
|
||||||
{"Create report test", fun test_create_report/0},
|
{"Add banned word test", fun test_add_banned_word/0},
|
||||||
{"Get reports test", fun test_get_reports/0},
|
{"Remove banned word test", fun test_remove_banned_word/0},
|
||||||
{"Resolve report test", fun test_resolve_report/0},
|
{"Auto freeze by reports test", fun test_auto_freeze/0},
|
||||||
{"Add banned word test", fun test_add_banned_word/0},
|
{"Freeze/unfreeze calendar test", fun test_freeze_calendar/0},
|
||||||
{"Remove banned word test", fun test_remove_banned_word/0},
|
{"Freeze/unfreeze event test", fun test_freeze_event/0},
|
||||||
{"Auto freeze by reports test", fun test_auto_freeze/0},
|
{"Check content test", fun test_check_content/0}
|
||||||
{"Freeze/unfreeze calendar test", fun test_freeze_calendar/0},
|
]}.
|
||||||
{"Freeze/unfreeze event test", fun test_freeze_event/0},
|
|
||||||
{"Check content test", fun test_check_content/0}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
|
%% ── Вспомогательные функции ──────────────────────────────
|
||||||
create_test_user(Role) ->
|
create_test_user(Role) ->
|
||||||
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
|
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
|
||||||
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
|
User = #user{
|
||||||
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
|
id = UserId,
|
||||||
|
email = <<>>,
|
||||||
|
password_hash = <<"hash">>,
|
||||||
|
role = Role,
|
||||||
|
status = active,
|
||||||
|
created_at = calendar:universal_time(),
|
||||||
|
updated_at = calendar:universal_time()
|
||||||
|
},
|
||||||
mnesia:dirty_write(User),
|
mnesia:dirty_write(User),
|
||||||
UserId.
|
UserId.
|
||||||
|
|
||||||
@@ -48,15 +67,16 @@ create_test_calendar(OwnerId) ->
|
|||||||
Calendar#calendar.id.
|
Calendar#calendar.id.
|
||||||
|
|
||||||
create_test_event(CalendarId) ->
|
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.
|
Event#event.id.
|
||||||
|
|
||||||
|
%% ── Тесты ─────────────────────────────────────────────────
|
||||||
test_create_report() ->
|
test_create_report() ->
|
||||||
ReporterId = create_test_user(user),
|
ReporterId = create_test_user(user),
|
||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
EventId = create_test_event(CalendarId),
|
EventId = create_test_event(CalendarId),
|
||||||
|
|
||||||
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>),
|
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>),
|
||||||
?assertEqual(ReporterId, Report#report.reporter_id),
|
?assertEqual(ReporterId, Report#report.reporter_id),
|
||||||
?assertEqual(pending, Report#report.status).
|
?assertEqual(pending, Report#report.status).
|
||||||
@@ -67,9 +87,7 @@ test_get_reports() ->
|
|||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
EventId = create_test_event(CalendarId),
|
EventId = create_test_event(CalendarId),
|
||||||
|
|
||||||
{ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
{ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
||||||
|
|
||||||
{ok, Reports} = logic_moderation:get_reports(AdminId),
|
{ok, Reports} = logic_moderation:get_reports(AdminId),
|
||||||
?assertEqual(1, length(Reports)).
|
?assertEqual(1, length(Reports)).
|
||||||
|
|
||||||
@@ -79,7 +97,6 @@ test_resolve_report() ->
|
|||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
EventId = create_test_event(CalendarId),
|
EventId = create_test_event(CalendarId),
|
||||||
|
|
||||||
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
|
||||||
{ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed),
|
{ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed),
|
||||||
?assertEqual(reviewed, Resolved#report.status),
|
?assertEqual(reviewed, Resolved#report.status),
|
||||||
@@ -87,14 +104,15 @@ test_resolve_report() ->
|
|||||||
|
|
||||||
test_add_banned_word() ->
|
test_add_banned_word() ->
|
||||||
AdminId = create_test_user(admin),
|
AdminId = create_test_user(admin),
|
||||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
{ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
||||||
?assert(core_banned_word:is_banned(<<"badword">>)).
|
?assertEqual(<<"badword">>, BW#banned_word.word),
|
||||||
|
?assertEqual(AdminId, BW#banned_word.added_by).
|
||||||
|
|
||||||
test_remove_banned_word() ->
|
test_remove_banned_word() ->
|
||||||
AdminId = create_test_user(admin),
|
AdminId = create_test_user(admin),
|
||||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
|
||||||
{ok, removed} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
|
{ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
|
||||||
?assertNot(core_banned_word:is_banned(<<"badword">>)).
|
?assertEqual([], core_banned_words:list_banned_words()).
|
||||||
|
|
||||||
test_auto_freeze() ->
|
test_auto_freeze() ->
|
||||||
Reporter1 = create_test_user(user),
|
Reporter1 = create_test_user(user),
|
||||||
@@ -103,12 +121,9 @@ test_auto_freeze() ->
|
|||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
EventId = create_test_event(CalendarId),
|
EventId = create_test_event(CalendarId),
|
||||||
|
|
||||||
% 3 жалобы должны заморозить событие
|
|
||||||
{ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
|
{ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
|
||||||
{ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>),
|
{ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>),
|
||||||
{ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>),
|
{ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>),
|
||||||
|
|
||||||
{ok, Event} = core_event:get_by_id(EventId),
|
{ok, Event} = core_event:get_by_id(EventId),
|
||||||
?assertEqual(frozen, Event#event.status).
|
?assertEqual(frozen, Event#event.status).
|
||||||
|
|
||||||
@@ -116,10 +131,8 @@ test_freeze_calendar() ->
|
|||||||
AdminId = create_test_user(admin),
|
AdminId = create_test_user(admin),
|
||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
|
|
||||||
{ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId),
|
{ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId),
|
||||||
?assertEqual(frozen, Frozen#calendar.status),
|
?assertEqual(frozen, Frozen#calendar.status),
|
||||||
|
|
||||||
{ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId),
|
{ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId),
|
||||||
?assertEqual(active, Unfrozen#calendar.status).
|
?assertEqual(active, Unfrozen#calendar.status).
|
||||||
|
|
||||||
@@ -128,19 +141,15 @@ test_freeze_event() ->
|
|||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId),
|
CalendarId = create_test_calendar(OwnerId),
|
||||||
EventId = create_test_event(CalendarId),
|
EventId = create_test_event(CalendarId),
|
||||||
|
|
||||||
{ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId),
|
{ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId),
|
||||||
?assertEqual(frozen, Frozen#event.status),
|
?assertEqual(frozen, Frozen#event.status),
|
||||||
|
|
||||||
{ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId),
|
{ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId),
|
||||||
?assertEqual(active, Unfrozen#event.status).
|
?assertEqual(active, Unfrozen#event.status).
|
||||||
|
|
||||||
test_check_content() ->
|
test_check_content() ->
|
||||||
AdminId = create_test_user(admin),
|
AdminId = create_test_user(admin),
|
||||||
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>),
|
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>),
|
||||||
|
|
||||||
?assertNot(logic_moderation:check_content(<<"Hello">>)),
|
?assertNot(logic_moderation:check_content(<<"Hello">>)),
|
||||||
?assert(logic_moderation:check_content(<<"This is bad">>)),
|
?assert(logic_moderation:check_content(<<"This is bad">>)),
|
||||||
|
|
||||||
?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)),
|
?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)),
|
||||||
?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)).
|
?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)).
|
||||||
@@ -2,104 +2,106 @@
|
|||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Фикстуры
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
setup() ->
|
setup() ->
|
||||||
mnesia:start(),
|
catch mnesia:stop(),
|
||||||
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
|
case mnesia:start() of
|
||||||
mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
|
{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.
|
ok.
|
||||||
|
|
||||||
cleanup(_) ->
|
cleanup(_) ->
|
||||||
mnesia:delete_table(ticket),
|
|
||||||
mnesia:delete_table(user),
|
mnesia:delete_table(user),
|
||||||
mnesia:stop(),
|
mnesia:delete_table(ticket),
|
||||||
ok.
|
mnesia:stop().
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
|
%% Тесты
|
||||||
|
%% ----------------------------------------------------------------
|
||||||
logic_ticket_test_() ->
|
logic_ticket_test_() ->
|
||||||
{foreach,
|
{foreach, fun setup/0, fun cleanup/1, [
|
||||||
fun setup/0,
|
{"Report error creates ticket", fun test_report_error/0},
|
||||||
fun cleanup/1,
|
{"Report duplicate error increments count", fun test_report_duplicate/0},
|
||||||
[
|
{"List tickets as admin", fun test_list_tickets/0},
|
||||||
{"Report error test", fun test_report_error/0},
|
{"List tickets as non-admin returns error", fun test_list_tickets_forbidden/0},
|
||||||
{"List tickets admin only", fun test_list_tickets_admin_only/0},
|
{"Update status as admin", fun test_update_status/0},
|
||||||
{"Update status test", fun test_update_status/0},
|
{"Assign ticket as admin", fun test_assign_ticket/0},
|
||||||
{"Assign ticket test", fun test_assign_ticket/0},
|
{"Resolve ticket as admin", fun test_resolve_ticket/0},
|
||||||
{"Resolve ticket test", fun test_resolve_ticket/0},
|
{"Close ticket as admin", fun test_close_ticket/0},
|
||||||
{"Close ticket test", fun test_close_ticket/0},
|
{"Get statistics as admin", fun test_get_statistics/0}
|
||||||
{"Get statistics test", fun test_get_statistics/0}
|
]}.
|
||||||
]}.
|
|
||||||
|
|
||||||
create_test_user(Role) ->
|
%% --- Вспомогательная функция для создания тикета ---
|
||||||
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
|
report(ErrorMsg) ->
|
||||||
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
|
logic_ticket:report_error(ErrorMsg, <<"stack">>, #{}).
|
||||||
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
|
|
||||||
mnesia:dirty_write(User),
|
%% --- Тесты ---
|
||||||
UserId.
|
|
||||||
|
|
||||||
test_report_error() ->
|
test_report_error() ->
|
||||||
{ok, Ticket} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
|
{ok, Ticket} = report(<<"Error1">>),
|
||||||
?assertEqual(<<"Test error">>, Ticket#ticket.error_message),
|
?assertEqual(<<"Error1">>, Ticket#ticket.error_message),
|
||||||
?assertEqual(1, Ticket#ticket.count),
|
?assertEqual(1, Ticket#ticket.count).
|
||||||
|
|
||||||
{ok, Ticket2} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
|
test_report_duplicate() ->
|
||||||
?assertEqual(2, Ticket2#ticket.count).
|
{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() ->
|
test_list_tickets() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, _} = report(<<"E1">>),
|
||||||
UserId = create_test_user(user),
|
Tickets = logic_ticket:list_tickets(<<"admin1">>),
|
||||||
|
?assert(length(Tickets) =:= 1).
|
||||||
|
|
||||||
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
|
test_list_tickets_forbidden() ->
|
||||||
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
|
{error, access_denied} = logic_ticket:list_tickets(<<"user1">>).
|
||||||
|
|
||||||
{ok, Tickets} = logic_ticket:list_tickets(AdminId),
|
|
||||||
?assertEqual(2, length(Tickets)),
|
|
||||||
|
|
||||||
{error, access_denied} = logic_ticket:list_tickets(UserId).
|
|
||||||
|
|
||||||
test_update_status() ->
|
test_update_status() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, Ticket} = report(<<"E2">>),
|
||||||
UserId = create_test_user(user),
|
{ok, Updated} = logic_ticket:update_status(<<"admin1">>, Ticket#ticket.id, <<"closed">>),
|
||||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
?assertEqual(closed, Updated#ticket.status).
|
||||||
|
|
||||||
{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).
|
|
||||||
|
|
||||||
test_assign_ticket() ->
|
test_assign_ticket() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, Ticket} = report(<<"E3">>),
|
||||||
AssignToId = create_test_user(admin),
|
{ok, Updated} = logic_ticket:assign_ticket(<<"admin1">>, Ticket#ticket.id, <<"dev1">>),
|
||||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
?assertEqual(<<"dev1">>, Updated#ticket.assigned_to).
|
||||||
|
|
||||||
{ok, Assigned} = logic_ticket:assign_ticket(AdminId, Ticket#ticket.id, AssignToId),
|
|
||||||
?assertEqual(AssignToId, Assigned#ticket.assigned_to),
|
|
||||||
?assertEqual(in_progress, Assigned#ticket.status).
|
|
||||||
|
|
||||||
test_resolve_ticket() ->
|
test_resolve_ticket() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, Ticket} = report(<<"E4">>),
|
||||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
{ok, Updated} = logic_ticket:resolve_ticket(<<"admin1">>, Ticket#ticket.id, <<"Fixed">>),
|
||||||
|
?assertEqual(closed, Updated#ticket.status),
|
||||||
{ok, Resolved} = logic_ticket:resolve_ticket(AdminId, Ticket#ticket.id, <<"Fixed">>),
|
?assertEqual(<<"Fixed">>, Updated#ticket.resolution_note).
|
||||||
?assertEqual(<<"Fixed">>, Resolved#ticket.resolution_note),
|
|
||||||
?assertEqual(resolved, Resolved#ticket.status).
|
|
||||||
|
|
||||||
test_close_ticket() ->
|
test_close_ticket() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, Ticket} = report(<<"E5">>),
|
||||||
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
|
{ok, Updated} = logic_ticket:close_ticket(<<"admin1">>, Ticket#ticket.id),
|
||||||
|
?assertEqual(closed, Updated#ticket.status).
|
||||||
{ok, Closed} = logic_ticket:close_ticket(AdminId, Ticket#ticket.id),
|
|
||||||
?assertEqual(closed, Closed#ticket.status).
|
|
||||||
|
|
||||||
test_get_statistics() ->
|
test_get_statistics() ->
|
||||||
AdminId = create_test_user(admin),
|
{ok, _} = report(<<"E6">>),
|
||||||
|
{ok, _} = report(<<"E7">>),
|
||||||
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
|
{ok, T3} = report(<<"E8">>),
|
||||||
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
|
logic_ticket:close_ticket(<<"admin1">>, T3#ticket.id),
|
||||||
{ok, T3} = logic_ticket:report_error(<<"E3">>, <<"">>, #{}),
|
Stats = logic_ticket:get_statistics(<<"admin1">>),
|
||||||
|
|
||||||
logic_ticket:update_status(AdminId, T3#ticket.id, resolved),
|
|
||||||
|
|
||||||
Stats = logic_ticket:get_statistics(AdminId),
|
|
||||||
?assertEqual(3, maps:get(total_tickets, Stats)),
|
?assertEqual(3, maps:get(total_tickets, Stats)),
|
||||||
?assertEqual(2, maps:get(open, 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)).
|
?assertEqual(3, maps:get(total_errors, Stats)).
|
||||||
Reference in New Issue
Block a user