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

This commit is contained in:
2026-04-28 12:42:10 +03:00
parent 4ed6a961ab
commit 7ea4efd7d9
38 changed files with 1252 additions and 1124 deletions

View File

@@ -3,24 +3,154 @@
test() ->
io:format("Testing admin panel API...~n"),
AdminURL = "http://localhost:8445",
% Получаем admin-токен через test runner (уже проверенный)
AdminToken = api_test_runner:get_admin_token(),
% TEST 1: Admin healthcheck
io:format(" TEST 1: Admin healthcheck... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get, {"http://localhost:8445/admin/health", []}, [], []),
%% TEST 1: Admin healthcheck (public)
io:format(" TEST 1: Admin healthcheck... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get, {AdminURL ++ "/v1/admin/health", []}, [], []),
io:format("OK~n"),
% TEST 2: Admin stats
io:format(" TEST 2: Admin stats... "),
%% TEST 2: Admin login (дополнительная проверка)
io:format(" TEST 2: Admin login (attempt)... "),
LoginBody = jsx:encode(#{<<"email">> => <<"global_admin@test.com">>, <<"password">> => <<"admin123">>}),
case httpc:request(post, {AdminURL ++ "/v1/admin/login", [], "application/json", LoginBody}, [], []) of
{ok, {{_, 200, _}, _, _}} ->
io:format("OK (logged in)~n");
_ ->
io:format("SKIPPED (credentials not found, using runner token)~n")
end,
%% TEST 3: Admin stats
io:format(" TEST 3: Admin stats... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{"http://localhost:8445/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
{AdminURL ++ "/v1/admin/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
% TEST 3: List users
io:format(" TEST 3: List users... "),
%% TEST 4: List users
io:format(" TEST 4: List users... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{"http://localhost:8445/admin/users", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
{AdminURL ++ "/v1/admin/users", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 5: Get user by ID
io:format(" TEST 5: Get user by ID... "),
UserId = api_test_runner:get_user_id(),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/users/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 6: List reports
io:format(" TEST 6: List reports... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 7: List banned words
io:format(" TEST 7: List banned words... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 8: Add banned word
io:format(" TEST 8: Add banned word... "),
BannedWordBody = jsx:encode(#{<<"word">> => <<"test_banned_word">>}),
{ok, {{_, 201, _}, _, _}} = httpc:request(post,
{AdminURL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", BannedWordBody}, [], []),
io:format("OK~n"),
%% TEST 9: Delete banned word
io:format(" TEST 9: Delete banned word... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
{AdminURL ++ "/v1/admin/banned-words/test_banned_word", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 10: List tickets
io:format(" TEST 10: List tickets... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 11: Create ticket
io:format(" TEST 11: Create ticket... "),
TicketBody = jsx:encode(#{<<"error_message">> => <<"Test error">>, <<"stacktrace">> => <<"trace">>}),
{ok, {{_, 201, _}, _, TicketResp}} = httpc:request(post,
{AdminURL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", TicketBody}, [], []),
#{<<"id">> := TicketId} = jsx:decode(list_to_binary(TicketResp), [return_maps]),
io:format("OK~n"),
%% TEST 12: Get ticket by ID
io:format(" TEST 12: Get ticket by ID... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 13: Update ticket
io:format(" TEST 13: Update ticket... "),
UpdateTicketBody = jsx:encode(#{<<"status">> => <<"closed">>}),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateTicketBody}, [], []),
io:format("OK~n"),
%% TEST 14: Delete ticket
io:format(" TEST 14: Delete ticket... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
{AdminURL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 15: Ticket stats
io:format(" TEST 15: Ticket stats... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 16: List subscriptions
io:format(" TEST 16: List subscriptions... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 17: Create subscription
io:format(" TEST 17: Create subscription... "),
SubBody = jsx:encode(#{<<"user_id">> => UserId, <<"plan">> => <<"monthly">>}),
{ok, {{_, 201, _}, _, SubResp}} = httpc:request(post,
{AdminURL ++ "/v1/admin/subscriptions", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", SubBody}, [], []),
#{<<"id">> := SubId} = jsx:decode(list_to_binary(SubResp), [return_maps]),
io:format("OK~n"),
%% TEST 18: Get subscription by ID
io:format(" TEST 18: Get subscription by ID... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 19: Update subscription
io:format(" TEST 19: Update subscription... "),
UpdateSubBody = jsx:encode(#{<<"status">> => <<"cancelled">>}),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UpdateSubBody}, [], []),
io:format("OK~n"),
%% TEST 20: Delete subscription
io:format(" TEST 20: Delete subscription... "),
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
{AdminURL ++ "/v1/admin/subscriptions/" ++ binary_to_list(SubId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
%% TEST 21: Moderation - block user
io:format(" TEST 21: Moderation - block user... "),
ModBody = jsx:encode(#{<<"action">> => <<"block">>}),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", ModBody}, [], []),
io:format("OK~n"),
%% TEST 22: Moderation - unblock user
io:format(" TEST 22: Moderation - unblock user... "),
UnblockBody = jsx:encode(#{<<"action">> => <<"unblock">>}),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{AdminURL ++ "/v1/admin/user/" ++ binary_to_list(UserId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", UnblockBody}, [], []),
io:format("OK~n"),
io:format("~n✅ Admin API tests passed!~n"),

View File

@@ -2,52 +2,70 @@
-export([test/0]).
-define(BASE_URL, "http://localhost:8080").
-define(ADMIN_BASE_URL, "http://localhost:8445").
test() ->
io:format("Testing moderation API...~n"),
AdminToken = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
UserToken = api_test_runner:get_user_token(),
% Создаём календарь и событие
CalId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken), <<"id">>),
%% Создаём календарь и событие через пользовательский API
CalId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken),
<<"id">>),
EventId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/calendars/" ++ binary_to_list(CalId) ++ "/events",
#{title => <<"Mod Event">>, start_time => <<"2026-06-01T10:00:00Z">>, duration => 60}, UserToken), <<"id">>),
#{title => <<"Mod Event">>,
start_time => <<"2026-06-01T10:00:00Z">>,
duration => 60},
UserToken),
<<"id">>),
% TEST 1: Create report
%% TEST 1: Create report (пользователь)
io:format(" TEST 1: Create report... "),
ReportId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/reports",
#{target_type => <<"event">>, target_id => EventId, reason => <<"Inappropriate">>}, UserToken), <<"id">>),
#{target_type => <<"event">>,
target_id => EventId,
reason => <<"Inappropriate">>},
UserToken),
<<"id">>),
io:format("OK~n"),
% TEST 2: Admin views reports
%% TEST 2: Admin views reports (через админский URL, прямой httpc)
io:format(" TEST 2: Admin views reports... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/reports", AdminToken),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{?ADMIN_BASE_URL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
% TEST 3: Admin resolves report
%% TEST 3: Admin resolves report
io:format(" TEST 3: Admin resolves report... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/reports/" ++ binary_to_list(ReportId),
#{action => <<"review">>}, AdminToken),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{?ADMIN_BASE_URL ++ "/v1/admin/reports/" ++ binary_to_list(ReportId),
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
"application/json",
jsx:encode(#{status => <<"reviewed">>})}, [], []),
io:format("OK~n"),
% TEST 4: Add banned word
%% TEST 4: Add banned word (админ)
io:format(" TEST 4: Add banned word... "),
{ok, {{_, 201, _}, _, _}} = api_test_runner:http_post("/v1/admin/banned-words",
#{word => <<"badword">>}, AdminToken),
{ok, {{_, 201, _}, _, _}} = httpc:request(post,
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words",
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
"application/json",
jsx:encode(#{<<"word">> => <<"badword">>})}, [], []),
io:format("OK~n"),
% TEST 5: List banned words
%% TEST 5: List banned words (админ)
io:format(" TEST 5: List banned words... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/banned-words", AdminToken),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
% TEST 6: Remove banned word
%% TEST 6: Remove banned word (админ)
io:format(" TEST 6: Remove banned word... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_delete("/v1/admin/banned-words/badword", AdminToken),
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
{?ADMIN_BASE_URL ++ "/v1/admin/banned-words/badword", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format("OK~n"),
io:format("~n✅ Moderation API tests passed!~n"),

View File

@@ -1,37 +1,78 @@
-module(api_tickets_tests).
-export([test/0]).
-define(BASE_URL, "http://localhost:8080").
-define(ADMIN_BASE_URL, "http://localhost:8445").
test() ->
io:format("Testing tickets API...~n"),
Token = api_test_runner:get_user_token(),
AdminToken = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
% TEST 1: Report error
io:format(" TEST 1: Report error... "),
%% TEST 1: Create ticket (user)
io:format(" TEST 1: Create ticket...~n"),
io:format(" POST /v1/tickets~n"),
TicketId = api_test_runner:extract_json(
api_test_runner:http_post("/v1/tickets",
#{error_message => <<"Test bug">>, stacktrace => <<"line 1">>}, UserToken), <<"id">>),
io:format("OK~n"),
#{error_message => <<"Bug">>,
stacktrace => <<"Something broke">>},
Token),
<<"id">>),
io:format(" OK~n"),
% TEST 2: Admin views tickets
io:format(" TEST 2: Admin views tickets... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/admin/tickets", AdminToken),
io:format("OK~n"),
%% TEST 2: Get my tickets (user)
io:format(" TEST 2: Get my tickets...~n"),
io:format(" GET /v1/tickets~n"),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get("/v1/tickets", Token),
io:format(" OK~n"),
% TEST 3: Update ticket status
io:format(" TEST 3: Update ticket status... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
#{action => <<"status">>, status => <<"in_progress">>}, AdminToken),
io:format("OK~n"),
%% TEST 3: Get single ticket (user)
io:format(" TEST 3: Get single ticket...~n"),
io:format(" GET /v1/tickets/~s~n", [TicketId]),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_get(
"/v1/tickets/" ++ binary_to_list(TicketId),
Token),
io:format(" OK~n"),
% TEST 4: Close ticket
io:format(" TEST 4: Close ticket... "),
{ok, {{_, 200, _}, _, _}} = api_test_runner:http_put("/v1/admin/tickets/" ++ binary_to_list(TicketId),
#{action => <<"close">>}, AdminToken),
io:format("OK~n"),
%% TEST 4: Admin lists all tickets
io:format(" TEST 4: Admin lists all tickets...~n"),
io:format(" GET ~s/v1/admin/tickets~n", [?ADMIN_BASE_URL]),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{?ADMIN_BASE_URL ++ "/v1/admin/tickets", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format(" OK~n"),
%% TEST 5: Admin updates ticket status
io:format(" TEST 5: Admin updates ticket status...~n"),
io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId),
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
"application/json",
jsx:encode(#{status => <<"in_progress">>})}, [], []),
io:format(" OK~n"),
%% TEST 6: Admin assigns ticket
io:format(" TEST 6: Admin assigns ticket...~n"),
io:format(" PUT ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
{ok, {{_, 200, _}, _, _}} = httpc:request(put,
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId),
[{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}],
"application/json",
jsx:encode(#{assigned_to => AdminToken})}, [], []),
io:format(" OK~n"),
%% TEST 7: Admin views ticket stats
io:format(" TEST 7: Admin views ticket stats...~n"),
io:format(" GET ~s/v1/admin/tickets/stats~n", [?ADMIN_BASE_URL]),
{ok, {{_, 200, _}, _, _}} = httpc:request(get,
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/stats", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format(" OK~n"),
%% TEST 8: Admin deletes ticket
io:format(" TEST 8: Admin deletes ticket...~n"),
io:format(" DELETE ~s/v1/admin/tickets/~s~n", [?ADMIN_BASE_URL, TicketId]),
{ok, {{_, 200, _}, _, _}} = httpc:request(delete,
{?ADMIN_BASE_URL ++ "/v1/admin/tickets/" ++ binary_to_list(TicketId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),
io:format(" OK~n"),
io:format("~n✅ Tickets API tests passed!~n"),
{?MODULE, ok}.

View File

@@ -46,7 +46,7 @@ test_get_report() ->
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = <<"new">>,
status = pending,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
@@ -55,7 +55,7 @@ test_get_report() ->
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"r1">>, <<"status">> := <<"new">>} = jsx:decode(RespBody, [return_maps]).
#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>} = jsx:decode(RespBody, [return_maps]).
%% GET не найдено
test_get_report_not_found() ->
@@ -94,9 +94,9 @@ test_update_report() ->
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = <<"reviewed">>},
Updated = #report{id = <<"r1">>, status = reviewed},
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
fun(<<"r1">>, reviewed, <<"adm1">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
@@ -115,7 +115,7 @@ test_update_report_not_found() ->
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(_, _) -> {error, not_found} end),
fun(<<"r99">>, reviewed, <<"adm1">>) -> {error, not_found} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).

View File

@@ -29,6 +29,7 @@ admin_reports_test_() ->
{"POST /admin/reports method not allowed", fun test_wrong_method/0}
]}.
%% GET успех
test_list_reports() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
@@ -42,17 +43,18 @@ test_list_reports() ->
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = <<"new">>,
status = pending,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
ok = meck:expect(core_report, list_reports, fun() -> [Report] end),
% list_all возвращает {ok, List}
ok = meck:expect(core_report, list_all, fun() -> {ok, [Report]} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
[#{<<"id">> := <<"r1">>, <<"target_type">> := <<"event">>, <<"status">> := <<"new">>}]
= jsx:decode(RespBody, [return_maps]).
[#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>}] = jsx:decode(RespBody, [return_maps]).
%% GET запрещён
test_list_reports_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
@@ -62,6 +64,7 @@ test_list_reports_forbidden() ->
?assertEqual(403, Status),
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
%% PUT успех
test_update_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
@@ -73,14 +76,16 @@ test_update_report() ->
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = <<"reviewed">>},
Updated = #report{id = <<"r1">>, status = reviewed},
% обработчик передаёт бинарный статус, поэтому мок ожидает строку
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, <<"reviewed">>) -> {ok, Updated} end),
fun(<<"r1">>, <<"reviewed">>, <<"adm1">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]).
%% PUT невалидный JSON
test_update_report_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
@@ -93,9 +98,10 @@ test_update_report_bad_json() ->
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% PUT не найдено
test_update_report_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
@@ -108,11 +114,12 @@ test_update_report_not_found() ->
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(_, _) -> {error, not_found} end),
fun(<<"r99">>, <<"reviewed">>, <<"adm1">>) -> {error, not_found} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply), %% исправлено: четыре элемента
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неправильный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
{ok, _, _} = admin_handler_reports:init(req, []),

View File

@@ -21,182 +21,210 @@ cleanup(_) ->
admin_tickets_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets success", fun test_list/0},
{"GET /admin/tickets forbidden", fun test_list_forbidden/0},
{"POST /admin/tickets success", fun test_create/0},
{"POST /admin/tickets missing error_message", fun test_create_missing/0},
{"GET /admin/tickets/:id success", fun test_get/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0},
{"PUT /admin/tickets/:id success", fun test_update/0},
{"PUT /admin/tickets/:id not found", fun test_update_not_found/0},
{"DELETE /admin/tickets/:id success", fun test_delete/0},
{"DELETE /admin/tickets/:id not found", fun test_delete_not_found/0},
{"PATCH /admin/tickets method not allowed", fun test_wrong_method/0}
{"GET /admin/tickets success", fun test_list/0},
{"GET /admin/tickets forbidden", fun test_list_forbidden/0},
{"POST /admin/tickets success", fun test_create/0},
{"POST /admin/tickets missing error_message", fun test_create_missing/0},
{"GET /admin/tickets/:id success", fun test_get/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0},
{"PUT /admin/tickets/:id success", fun test_update/0},
{"PUT /admin/tickets/:id not found", fun test_update_not_found/0},
{"DELETE /admin/tickets/:id success", fun test_delete/0},
{"DELETE /admin/tickets/:id not found", fun test_delete_not_found/0},
{"PATCH /admin/tickets method not allowed", fun test_wrong_method/0}
]}.
%% GET список тикетов (успех)
test_list() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> undefined end), % для маршрута без id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"abc123">>,
error_message = <<"Ooops">>,
stacktrace = <<"trace">>,
error_hash = <<"hash1">>,
error_message = <<"Error message">>,
stacktrace = <<"stack">>,
context = <<"ctx">>,
count = 3,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{13,0,0}},
count = 1,
first_seen = {{2026,4,28},{12,0,0}},
last_seen = {{2026,4,28},{12,0,0}},
status = open,
assigned_to = <<"adm2">>,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, list_tickets, fun() -> [Ticket] end),
ok = meck:expect(core_ticket, list_all, fun() -> [Ticket] end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Ooops">>, <<"status">> := <<"open">>}] =
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Error message">>}] =
jsx:decode(RespBody, [return_maps]).
%% GET запрещён
test_list_forbidden() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> undefined end), % для маршрута без id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% POST создание тикета
test_create() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> undefined end), % для маршрута без id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
BodyMap = #{<<"error_message">> => <<"New bug">>, <<"stacktrace">> => <<"trace">>},
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
Created = #ticket{
id = <<"t_new">>,
error_hash = <<"hash">>,
error_message = <<"New bug">>,
stacktrace = <<"trace">>,
context = <<>>,
count = 1,
first_seen = {{2026,4,27},{14,0,0}},
last_seen = {{2026,4,27},{14,0,0}},
status = open,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, create_ticket, fun(Data) ->
true = maps:is_key(<<"error_message">>, Data),
{ok, Created}
end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
BodyMap = #{<<"error_message">> => <<"Bug">>, <<"stacktrace">> => <<"trace">>},
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
Created = #ticket{id = <<"t_new">>, error_message = <<"Bug">>, status = open},
ok = meck:expect(core_ticket, create_ticket, fun(_) -> {ok, Created} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status),
#{<<"error_message">> := <<"New bug">>, <<"status">> := <<"open">>} = jsx:decode(RespBody, [return_maps]).
#{<<"error_message">> := <<"Bug">>} = jsx:decode(RespBody, [return_maps]).
%% POST отсутствует поле error_message
test_create_missing() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> undefined end), % для маршрута без id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"desc">> => <<"no msg">>}), Req} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"title">> => <<"No msg">>}), Req} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% GET один тикет по ID
test_get() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"abc">>,
error_message = <<"msg">>,
stacktrace = <<>>,
context = <<>>,
count = 1,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{12,0,0}},
status = open,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, get_by_id, fun(<<"t1">>) -> {ok, Ticket} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{id = <<"t1">>, error_message = <<"Test">>, status = open},
ok = meck:expect(core_ticket, get_by_id,
fun(<<"t1">>) -> {ok, Ticket} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"t1">>} = jsx:decode(RespBody, [return_maps]).
%% GET тикет не найден
test_get_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, get_by_id, fun(_) -> {error, not_found} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT обновление тикета
test_update() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
Updated = #ticket{id = <<"t1">>, status = closed},
ok = meck:expect(core_ticket, update_ticket, fun(<<"t1">>, _) -> {ok, Updated} end),
ok = meck:expect(core_ticket, update_ticket,
fun(<<"t1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"closed">>} = jsx:decode(RespBody, [return_maps]).
%% PUT тикет не найден
test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_ticket, update_ticket, fun(_, _) -> {error, not_found} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_ticket, update_ticket,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% DELETE удаление тикета
test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket, fun(<<"t1">>) -> {ok, deleted} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket,
fun(<<"t1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
%% DELETE тикет не найден
test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end), % для маршрута с id
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket, fun(_) -> {error, not_found} end),
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неправильный метод
test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> undefined end), % для маршрута без id
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),

View File

@@ -27,21 +27,21 @@ generate_user_token_test_() ->
{setup, fun setup/0, fun cleanup/1, [
{"Generate user token returns a binary",
fun() ->
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
Token = eventhub_auth:generate_user_token(<<"user123">>, <<"user">>),
?assert(is_binary(Token)),
?assert(size(Token) > 0)
end},
{"Generated user token can be verified",
fun() ->
Token = auth:generate_user_token(<<"user123">>, <<"user">>),
{ok, UserId, Role} = auth:verify_user_token(Token),
Token = eventhub_auth:generate_user_token(<<"user123">>, <<"user">>),
{ok, UserId, Role} = eventhub_auth:verify_user_token(Token),
?assertEqual(<<"user123">>, UserId),
?assertEqual(<<"user">>, Role)
end},
{"Generate admin token with superadmin role",
fun() ->
Token = auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
{ok, UserId, Role} = auth:verify_admin_token(Token),
Token = eventhub_auth:generate_admin_token(<<"admin1">>, <<"superadmin">>),
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
?assertEqual(<<"admin1">>, UserId),
?assertEqual(<<"superadmin">>, Role)
end}
@@ -55,19 +55,19 @@ verify_token_errors_test_() ->
{"Invalid token signature returns error",
fun() ->
FakeToken = <<"not.a.valid.token">>,
?assertEqual({error, invalid_token}, auth:verify_user_token(FakeToken)),
?assertEqual({error, invalid_token}, auth:verify_admin_token(FakeToken))
?assertEqual({error, invalid_token}, eventhub_auth:verify_user_token(FakeToken)),
?assertEqual({error, invalid_token}, eventhub_auth:verify_admin_token(FakeToken))
end},
{"User token rejected by admin verifier (different secret)",
fun() ->
Token = auth:generate_user_token(<<"x">>, <<"user">>),
Token = eventhub_auth:generate_user_token(<<"x">>, <<"user">>),
% Разные секреты → подпись недействительна для admin JWK
?assertEqual({error, invalid_signature}, auth:verify_admin_token(Token))
?assertEqual({error, invalid_signature}, eventhub_auth:verify_admin_token(Token))
end},
{"Admin token rejected by user verifier (different secret)",
fun() ->
Token = auth:generate_admin_token(<<"x">>, <<"admin">>),
?assertEqual({error, invalid_signature}, auth:verify_user_token(Token))
Token = eventhub_auth:generate_admin_token(<<"x">>, <<"admin">>),
?assertEqual({error, invalid_signature}, eventhub_auth:verify_user_token(Token))
end}
]}.
@@ -81,10 +81,10 @@ authenticate_user_request_test_() ->
UserMap = #{id => <<"user1">>, email => <<"u@test.com">>, role => <<"user">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
Req = undefined,
{ok, Token, ReturnedUser} = auth:authenticate_user_request(Req, <<"u@test.com">>, <<"pass">>),
{ok, Token, ReturnedUser} = eventhub_auth:authenticate_user_request(Req, <<"u@test.com">>, <<"pass">>),
?assert(is_binary(Token)),
?assertEqual(UserMap, ReturnedUser),
{ok, UserId, Role} = auth:verify_user_token(Token),
{ok, UserId, Role} = eventhub_auth:verify_user_token(Token),
?assertEqual(<<"user1">>, UserId),
?assertEqual(<<"user">>, Role)
end},
@@ -92,7 +92,7 @@ authenticate_user_request_test_() ->
fun() ->
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {error, bad_credentials} end),
Req = undefined,
?assertEqual({error, bad_credentials}, auth:authenticate_user_request(Req, <<"bad">>, <<"pwd">>))
?assertEqual({error, bad_credentials}, eventhub_auth:authenticate_user_request(Req, <<"bad">>, <<"pwd">>))
end}
]}.
@@ -106,10 +106,10 @@ authenticate_admin_request_test_() ->
AdminMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, AdminMap} end),
Req = undefined,
{ok, Token, ReturnedUser} = auth:authenticate_admin_request(Req, <<"admin@test.com">>, <<"pass">>),
{ok, Token, ReturnedUser} = eventhub_auth:authenticate_admin_request(Req, <<"admin@test.com">>, <<"pass">>),
?assert(is_binary(Token)),
?assertEqual(AdminMap, ReturnedUser),
{ok, UserId, Role} = auth:verify_admin_token(Token),
{ok, UserId, Role} = eventhub_auth:verify_admin_token(Token),
?assertEqual(<<"adm1">>, UserId),
?assertEqual(<<"superadmin">>, Role)
end},
@@ -119,15 +119,15 @@ authenticate_admin_request_test_() ->
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, UserMap} end),
Req = undefined,
?assertEqual({error, insufficient_permissions},
auth:authenticate_admin_request(Req, <<"u@test.com">>, <<"pwd">>))
eventhub_auth:authenticate_admin_request(Req, <<"u@test.com">>, <<"pwd">>))
end},
{"Moderator role is accepted as admin",
fun() ->
ModMap = #{id => <<"moder1">>, email => <<"mod@test.com">>, role => <<"moderator">>},
ok = meck:expect(logic_auth, authenticate_user, fun(_Email, _Password) -> {ok, ModMap} end),
Req = undefined,
{ok, Token, _} = auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
{ok, _, Role} = auth:verify_admin_token(Token),
{ok, Token, _} = eventhub_auth:authenticate_admin_request(Req, <<"mod@test.com">>, <<"pwd">>),
{ok, _, Role} = eventhub_auth:verify_admin_token(Token),
?assertEqual(<<"moderator">>, Role)
end}
]}.
@@ -136,4 +136,4 @@ authenticate_admin_request_test_() ->
%% Тест generate_refresh_token/1
%% ------------------------------------------------------------------
generate_refresh_token_test() ->
{_, _} = auth:generate_refresh_token(<<"anyuser">>).
{_, _} = eventhub_auth:generate_refresh_token(<<"anyuser">>).

View File

@@ -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)).

View 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)).

View File

@@ -2,9 +2,16 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
%% ----------------------------------------------------------------
%% Фикстуры
%% ----------------------------------------------------------------
setup() ->
mnesia:start(),
mnesia:create_table(ticket, [
catch mnesia:stop(),
case mnesia:start() of
{atomic, ok} -> ok;
ok -> ok
end,
{atomic, ok} = mnesia:create_table(ticket, [
{attributes, record_info(fields, ticket)},
{ram_copies, [node()]}
]),
@@ -12,110 +19,118 @@ setup() ->
cleanup(_) ->
mnesia:delete_table(ticket),
mnesia:stop(),
ok.
mnesia:stop().
%% ----------------------------------------------------------------
%% Тесты
%% ----------------------------------------------------------------
core_ticket_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create ticket test", fun test_create_ticket/0},
{"Update existing ticket test", fun test_update_ticket/0},
{"Get ticket by id test", fun test_get_by_id/0},
{"Get ticket by error hash test", fun test_get_by_error_hash/0},
{"List all tickets test", fun test_list_all/0},
{"List by status test", fun test_list_by_status/0},
{"Update status test", fun test_update_status/0},
{"Assign ticket test", fun test_assign_ticket/0},
{"Add resolution test", fun test_add_resolution/0}
]}.
{foreach, fun setup/0, fun cleanup/1, [
{"Create ticket and retrieve it", fun test_create_and_get/0},
{"Update ticket status", fun test_update_status/0},
{"Delete ticket and verify removal", fun test_delete_ticket/0},
{"List all tickets returns created ones", fun test_list_all/0},
{"List tickets by user filters correctly", fun test_list_by_user/0},
{"Get ticket stats reflects real counts", fun test_stats/0},
{"Update ticket with unknown id fails", fun test_update_not_found/0},
{"Delete ticket with unknown id fails", fun test_delete_not_found/0},
{"Get ticket with unknown id fails", fun test_get_not_found/0}
]}.
test_create_ticket() ->
ErrorMsg = <<"Test error">>,
Stacktrace = <<"line 1\nline 2">>,
Context = #{user_id => <<"user123">>},
%% ── Вспомогательная функция для создания тикета ─────────
make_ticket(ErrorMsg) ->
Data = #{
<<"error_message">> => list_to_binary(ErrorMsg),
<<"stacktrace">> => <<"trace">>,
<<"reporter_id">> => <<"user123">>,
<<"status">> => <<"open">>
},
{ok, Ticket} = core_ticket:create_ticket(Data),
Ticket.
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
%% ── Тесты ─────────────────────────────────────────────────
?assertEqual(ErrorMsg, Ticket#ticket.error_message),
?assertEqual(Stacktrace, Ticket#ticket.stacktrace),
?assertEqual(1, Ticket#ticket.count),
?assertEqual(open, Ticket#ticket.status),
test_create_and_get() ->
Ticket = make_ticket("Bug1"),
?assert(is_binary(Ticket#ticket.id)),
?assert(is_binary(Ticket#ticket.error_hash)).
test_update_ticket() ->
ErrorMsg = <<"Test error">>,
Stacktrace = <<"line 1">>,
Context = #{},
{ok, Ticket1} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
?assertEqual(1, Ticket1#ticket.count),
{ok, Ticket2} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
?assertEqual(Ticket1#ticket.id, Ticket2#ticket.id),
?assertEqual(2, Ticket2#ticket.count),
?assert(Ticket2#ticket.last_seen >= Ticket1#ticket.last_seen).
test_get_by_id() ->
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
{ok, Found} = core_ticket:get_by_id(Ticket#ticket.id),
?assertEqual(Ticket#ticket.id, Found#ticket.id),
{error, not_found} = core_ticket:get_by_id(<<"nonexistent">>).
test_get_by_error_hash() ->
ErrorMsg = <<"Unique error">>,
Stacktrace = <<"stack">>,
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, #{}),
{ok, Found} = core_ticket:get_by_error_hash(Ticket#ticket.error_hash),
?assertEqual(Ticket#ticket.id, Found#ticket.id),
{error, not_found} = core_ticket:get_by_error_hash(<<"badhash">>).
test_list_all() ->
{ok, _} = core_ticket:create_or_update(<<"Error 1">>, <<"">>, #{}),
{ok, _} = core_ticket:create_or_update(<<"Error 2">>, <<"">>, #{}),
{ok, _} = core_ticket:create_or_update(<<"Error 3">>, <<"">>, #{}),
{ok, Tickets} = core_ticket:list_all(),
?assertEqual(3, length(Tickets)).
test_list_by_status() ->
{ok, _T1} = core_ticket:create_or_update(<<"E1">>, <<"">>, #{}),
{ok, T2} = core_ticket:create_or_update(<<"E2">>, <<"">>, #{}),
core_ticket:update_status(T2#ticket.id, resolved),
{ok, Open} = core_ticket:list_by_status(open),
?assertEqual(1, length(Open)),
{ok, Resolved} = core_ticket:list_by_status(resolved),
?assertEqual(1, length(Resolved)).
{ok, Retrieved} = core_ticket:get_by_id(Ticket#ticket.id),
?assertEqual(Ticket#ticket.id, Retrieved#ticket.id).
test_update_status() ->
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
Ticket = make_ticket("Bug2"),
{ok, Updated} = core_ticket:update_ticket(Ticket#ticket.id,
#{<<"status">> => <<"closed">>}),
?assertEqual(closed, Updated#ticket.status),
{ok, Stored} = core_ticket:get_by_id(Ticket#ticket.id),
?assertEqual(closed, Stored#ticket.status).
{ok, Updated} = core_ticket:update_status(Ticket#ticket.id, in_progress),
?assertEqual(in_progress, Updated#ticket.status),
test_delete_ticket() ->
Ticket = make_ticket("Bug3"),
Id = Ticket#ticket.id,
{ok, deleted} = core_ticket:delete_ticket(Id),
% Проверяем, что тикет больше не читается
?assertMatch({error, not_found}, core_ticket:get_by_id(Id)).
{ok, Resolved} = core_ticket:update_status(Ticket#ticket.id, resolved),
?assertEqual(resolved, Resolved#ticket.status).
test_list_all() ->
T1 = make_ticket("E1"),
T2 = make_ticket("E2"),
All = core_ticket:list_all(),
?assert(length(All) >= 2),
Ids = [T#ticket.id || T <- All],
?assert(lists:member(T1#ticket.id, Ids)),
?assert(lists:member(T2#ticket.id, Ids)).
test_assign_ticket() ->
AdminId = <<"admin123">>,
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
test_list_by_user() ->
% Создаём тикет от пользователя test_user
Data = #{
<<"error_message">> => <<"from_test_user">>,
<<"stacktrace">> => <<"trace">>,
<<"reporter_id">> => <<"test_user">>,
<<"status">> => <<"open">>
},
{ok, T1} = core_ticket:create_ticket(Data),
% Ещё один тикет от другого пользователя
DataOther = #{
<<"error_message">> => <<"other">>,
<<"stacktrace">> => <<"trace">>,
<<"reporter_id">> => <<"other_user">>,
<<"status">> => <<"open">>
},
{ok, _T2} = core_ticket:create_ticket(DataOther),
% list_by_user("test_user") должен вернуть ровно один тикет (T1)
UserTickets = core_ticket:list_by_user(<<"test_user">>),
?assertEqual(1, length(UserTickets)),
?assertEqual(T1#ticket.id, (hd(UserTickets))#ticket.id).
{ok, Assigned} = core_ticket:assign(Ticket#ticket.id, AdminId),
?assertEqual(AdminId, Assigned#ticket.assigned_to),
?assertEqual(in_progress, Assigned#ticket.status).
test_stats() ->
Data1 = #{
<<"error_message">> => <<"stat1">>,
<<"stacktrace">> => <<"trace">>,
<<"reporter_id">> => <<"reporter123">>,
<<"status">> => <<"open">>
},
Data2 = #{
<<"error_message">> => <<"stat2">>,
<<"stacktrace">> => <<"trace">>,
<<"reporter_id">> => <<"reporter456">>,
<<"status">> => <<"open">>
},
{ok, _} = core_ticket:create_ticket(Data1),
{ok, _} = core_ticket:create_ticket(Data2),
Stats = core_ticket:stats(),
?assert(is_map(Stats)),
?assert(maps:is_key(open, Stats)),
?assert(maps:is_key(total, Stats)),
% Проверяем, что общее количество тикетов не меньше 2
Total = maps:get(total, Stats),
?assert(Total >= 2).
test_add_resolution() ->
Note = <<"Fixed in version 1.0">>,
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
test_update_not_found() ->
{error, not_found} = core_ticket:update_ticket(<<"nonexistent">>,
#{<<"status">> => <<"closed">>}).
{ok, Updated} = core_ticket:add_resolution(Ticket#ticket.id, Note),
?assertEqual(Note, Updated#ticket.resolution_note).
test_delete_not_found() ->
{error, not_found} = core_ticket:delete_ticket(<<"nonexistent">>).
test_get_not_found() ->
{error, not_found} = core_ticket:get_by_id(<<"nonexistent">>).

View File

@@ -4,9 +4,6 @@
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
%% ------------------------------------------------------------------
%% Фикстуры
%% ------------------------------------------------------------------
setup() ->
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
@@ -18,9 +15,6 @@ cleanup(_) ->
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose).
%% ------------------------------------------------------------------
%% Тесты
%% ------------------------------------------------------------------
logic_auth_test_() ->
[
{"Password hash test", fun test_password_hash/0},
@@ -31,7 +25,6 @@ logic_auth_test_() ->
]}
].
%% ── Хеширование паролей (остаётся в logic_auth) ──────────────────
test_password_hash() ->
Password = <<"secret123">>,
{ok, Hash} = logic_auth:hash_password(Password),
@@ -39,27 +32,23 @@ test_password_hash() ->
{ok, true} = logic_auth:verify_password(Password, Hash),
{ok, false} = logic_auth:verify_password(<<"wrong">>, Hash).
%% ── JWT тесты (перенесены в auth) ─────────────────────────────────
test_jwt() ->
UserId = <<"user123">>,
Role = <<"user">>,
Token = auth:generate_user_token(UserId, Role),
Token = eventhub_auth:generate_user_token(UserId, Role),
?assert(is_binary(Token)),
{ok, ReturnedUserId, ReturnedRole} = auth:verify_user_token(Token),
{ok, ReturnedUserId, ReturnedRole} = eventhub_auth:verify_user_token(Token),
?assertEqual(UserId, ReturnedUserId),
?assertEqual(Role, ReturnedRole),
% Проверка невалидного токена
{error, invalid_token} = auth:verify_user_token(<<"invalid.token.here">>).
{error, invalid_token} = eventhub_auth:verify_user_token(<<"invalid.token.here">>).
test_jwt_expired() ->
% Тест на истечение срока пока пропущен, так как требует мока времени
ok.
%% ── Refresh token (перенесён в auth) ────────────────────────────
test_refresh_token() ->
{Token, ExpiresAt} = auth:generate_refresh_token(<<"user123">>),
{Token, ExpiresAt} = eventhub_auth:generate_refresh_token(<<"user123">>),
?assert(is_binary(Token)),
?assert(size(Token) >= 32),
?assert(is_integer(ExpiresAt)),
Now = os:system_time(second),
?assert(is_tuple(ExpiresAt)),
Now = calendar:universal_time(),
?assert(ExpiresAt > Now).

View File

@@ -2,13 +2,25 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
%% ----------------------------------------------------------------
%% Фикстуры
%% ----------------------------------------------------------------
setup() ->
mnesia:start(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
mnesia:create_table(banned_word, [{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
catch mnesia:stop(),
case mnesia:start() of
{atomic, ok} -> ok;
ok -> ok
end,
{atomic, ok} = mnesia:create_table(user, [
{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
{atomic, ok} = mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
{atomic, ok} = mnesia:create_table(event, [
{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
{atomic, ok} = mnesia:create_table(report, [
{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
{atomic, ok} = mnesia:create_table(banned_word, [
{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
@@ -17,29 +29,36 @@ cleanup(_) ->
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
mnesia:stop().
%% ----------------------------------------------------------------
%% Тесты
%% ----------------------------------------------------------------
logic_moderation_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create report test", fun test_create_report/0},
{"Get reports test", fun test_get_reports/0},
{"Resolve report test", fun test_resolve_report/0},
{"Add banned word test", fun test_add_banned_word/0},
{"Remove banned word test", fun test_remove_banned_word/0},
{"Auto freeze by reports test", fun test_auto_freeze/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}
]}.
{foreach, fun setup/0, fun cleanup/1, [
{"Create report test", fun test_create_report/0},
{"Get reports test", fun test_get_reports/0},
{"Resolve report test", fun test_resolve_report/0},
{"Add banned word test", fun test_add_banned_word/0},
{"Remove banned word test", fun test_remove_banned_word/0},
{"Auto freeze by reports test", fun test_auto_freeze/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) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
User = #user{
id = UserId,
email = <<>>,
password_hash = <<"hash">>,
role = Role,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
@@ -48,15 +67,16 @@ create_test_calendar(OwnerId) ->
Calendar#calendar.id.
create_test_event(CalendarId) ->
{ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60),
{ok, Event} = core_event:create(CalendarId, <<"Event">>,
{{2026, 6, 1}, {10, 0, 0}}, 60),
Event#event.id.
%% ── Тесты ─────────────────────────────────────────────────
test_create_report() ->
ReporterId = create_test_user(user),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>),
?assertEqual(ReporterId, Report#report.reporter_id),
?assertEqual(pending, Report#report.status).
@@ -67,9 +87,7 @@ test_get_reports() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
{ok, Reports} = logic_moderation:get_reports(AdminId),
?assertEqual(1, length(Reports)).
@@ -79,7 +97,6 @@ test_resolve_report() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
{ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed),
?assertEqual(reviewed, Resolved#report.status),
@@ -87,14 +104,15 @@ test_resolve_report() ->
test_add_banned_word() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
?assert(core_banned_word:is_banned(<<"badword">>)).
{ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
?assertEqual(<<"badword">>, BW#banned_word.word),
?assertEqual(AdminId, BW#banned_word.added_by).
test_remove_banned_word() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
{ok, removed} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
?assertNot(core_banned_word:is_banned(<<"badword">>)).
{ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
?assertEqual([], core_banned_words:list_banned_words()).
test_auto_freeze() ->
Reporter1 = create_test_user(user),
@@ -103,12 +121,9 @@ test_auto_freeze() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
% 3 жалобы должны заморозить событие
{ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
{ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>),
{ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>),
{ok, Event} = core_event:get_by_id(EventId),
?assertEqual(frozen, Event#event.status).
@@ -116,10 +131,8 @@ test_freeze_calendar() ->
AdminId = create_test_user(admin),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
{ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId),
?assertEqual(frozen, Frozen#calendar.status),
{ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId),
?assertEqual(active, Unfrozen#calendar.status).
@@ -128,19 +141,15 @@ test_freeze_event() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId),
?assertEqual(frozen, Frozen#event.status),
{ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId),
?assertEqual(active, Unfrozen#event.status).
test_check_content() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>),
?assertNot(logic_moderation:check_content(<<"Hello">>)),
?assert(logic_moderation:check_content(<<"This is bad">>)),
?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)),
?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)).

View File

@@ -2,104 +2,106 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
%% ----------------------------------------------------------------
%% Фикстуры
%% ----------------------------------------------------------------
setup() ->
mnesia:start(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
catch mnesia:stop(),
case mnesia:start() of
{atomic, ok} -> ok;
ok -> ok
end,
{atomic, ok} = mnesia:create_table(user, [
{attributes, record_info(fields, user)}, {ram_copies, [node()]}]),
{atomic, ok} = mnesia:create_table(ticket, [
{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
% Создаём админа и обычного пользователя
Admin = #user{id = <<"admin1">>, email = <<"a@a.a">>, password_hash = <<"h">>,
role = admin, status = active,
created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
User = #user{id = <<"user1">>, email = <<"u@u.u">>, password_hash = <<"h">>,
role = user, status = active,
created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(Admin),
mnesia:dirty_write(User),
ok.
cleanup(_) ->
mnesia:delete_table(ticket),
mnesia:delete_table(user),
mnesia:stop(),
ok.
mnesia:delete_table(ticket),
mnesia:stop().
%% ----------------------------------------------------------------
%% Тесты
%% ----------------------------------------------------------------
logic_ticket_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Report error test", fun test_report_error/0},
{"List tickets admin only", fun test_list_tickets_admin_only/0},
{"Update status test", fun test_update_status/0},
{"Assign ticket test", fun test_assign_ticket/0},
{"Resolve ticket test", fun test_resolve_ticket/0},
{"Close ticket test", fun test_close_ticket/0},
{"Get statistics test", fun test_get_statistics/0}
]}.
{foreach, fun setup/0, fun cleanup/1, [
{"Report error creates ticket", fun test_report_error/0},
{"Report duplicate error increments count", fun test_report_duplicate/0},
{"List tickets as admin", fun test_list_tickets/0},
{"List tickets as non-admin returns error", fun test_list_tickets_forbidden/0},
{"Update status as admin", fun test_update_status/0},
{"Assign ticket as admin", fun test_assign_ticket/0},
{"Resolve ticket as admin", fun test_resolve_ticket/0},
{"Close ticket as admin", fun test_close_ticket/0},
{"Get statistics as admin", fun test_get_statistics/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
%% --- Вспомогательная функция для создания тикета ---
report(ErrorMsg) ->
logic_ticket:report_error(ErrorMsg, <<"stack">>, #{}).
%% --- Тесты ---
test_report_error() ->
{ok, Ticket} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
?assertEqual(<<"Test error">>, Ticket#ticket.error_message),
?assertEqual(1, Ticket#ticket.count),
{ok, Ticket} = report(<<"Error1">>),
?assertEqual(<<"Error1">>, Ticket#ticket.error_message),
?assertEqual(1, Ticket#ticket.count).
{ok, Ticket2} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
?assertEqual(2, Ticket2#ticket.count).
test_report_duplicate() ->
{ok, T1} = report(<<"Dup">>),
?assertEqual(1, T1#ticket.count),
{ok, T2} = report(<<"Dup">>),
?assertEqual(2, T2#ticket.count),
% Проверяем, что это тот же тикет, а не новый
?assertEqual(T1#ticket.id, T2#ticket.id).
test_list_tickets_admin_only() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
test_list_tickets() ->
{ok, _} = report(<<"E1">>),
Tickets = logic_ticket:list_tickets(<<"admin1">>),
?assert(length(Tickets) =:= 1).
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
{ok, Tickets} = logic_ticket:list_tickets(AdminId),
?assertEqual(2, length(Tickets)),
{error, access_denied} = logic_ticket:list_tickets(UserId).
test_list_tickets_forbidden() ->
{error, access_denied} = logic_ticket:list_tickets(<<"user1">>).
test_update_status() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Updated} = logic_ticket:update_status(AdminId, Ticket#ticket.id, in_progress),
?assertEqual(in_progress, Updated#ticket.status),
{error, access_denied} = logic_ticket:update_status(UserId, Ticket#ticket.id, resolved).
{ok, Ticket} = report(<<"E2">>),
{ok, Updated} = logic_ticket:update_status(<<"admin1">>, Ticket#ticket.id, <<"closed">>),
?assertEqual(closed, Updated#ticket.status).
test_assign_ticket() ->
AdminId = create_test_user(admin),
AssignToId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Assigned} = logic_ticket:assign_ticket(AdminId, Ticket#ticket.id, AssignToId),
?assertEqual(AssignToId, Assigned#ticket.assigned_to),
?assertEqual(in_progress, Assigned#ticket.status).
{ok, Ticket} = report(<<"E3">>),
{ok, Updated} = logic_ticket:assign_ticket(<<"admin1">>, Ticket#ticket.id, <<"dev1">>),
?assertEqual(<<"dev1">>, Updated#ticket.assigned_to).
test_resolve_ticket() ->
AdminId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Resolved} = logic_ticket:resolve_ticket(AdminId, Ticket#ticket.id, <<"Fixed">>),
?assertEqual(<<"Fixed">>, Resolved#ticket.resolution_note),
?assertEqual(resolved, Resolved#ticket.status).
{ok, Ticket} = report(<<"E4">>),
{ok, Updated} = logic_ticket:resolve_ticket(<<"admin1">>, Ticket#ticket.id, <<"Fixed">>),
?assertEqual(closed, Updated#ticket.status),
?assertEqual(<<"Fixed">>, Updated#ticket.resolution_note).
test_close_ticket() ->
AdminId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Closed} = logic_ticket:close_ticket(AdminId, Ticket#ticket.id),
?assertEqual(closed, Closed#ticket.status).
{ok, Ticket} = report(<<"E5">>),
{ok, Updated} = logic_ticket:close_ticket(<<"admin1">>, Ticket#ticket.id),
?assertEqual(closed, Updated#ticket.status).
test_get_statistics() ->
AdminId = create_test_user(admin),
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
{ok, T3} = logic_ticket:report_error(<<"E3">>, <<"">>, #{}),
logic_ticket:update_status(AdminId, T3#ticket.id, resolved),
Stats = logic_ticket:get_statistics(AdminId),
{ok, _} = report(<<"E6">>),
{ok, _} = report(<<"E7">>),
{ok, T3} = report(<<"E8">>),
logic_ticket:close_ticket(<<"admin1">>, T3#ticket.id),
Stats = logic_ticket:get_statistics(<<"admin1">>),
?assertEqual(3, maps:get(total_tickets, Stats)),
?assertEqual(2, maps:get(open, Stats)),
?assertEqual(1, maps:get(resolved, Stats)),
?assertEqual(1, maps:get(closed, Stats)),
?assertEqual(3, maps:get(total_errors, Stats)).