Rewrite and expand EUnit suite; run unit tests in CI from shared test image. Refs EventHub/EventHubBack#36 [skip ci]

This commit is contained in:
2026-07-17 15:16:46 +03:00
parent 9d2780fef4
commit 4fa802702f
54 changed files with 4240 additions and 2281 deletions
+8 -3
View File
@@ -93,7 +93,7 @@ jobs:
--sbom=false \ --sbom=false \
. .
- name: Build API test image - name: Build test image (EUnit + CT)
run: | run: |
set -euo pipefail set -euo pipefail
bash scripts/retry-docker-build.sh 5 60 -- \ bash scripts/retry-docker-build.sh 5 60 -- \
@@ -101,14 +101,19 @@ jobs:
--network=host \ --network=host \
--pull \ --pull \
--file docker/ApiTests.Dockerfile \ --file docker/ApiTests.Dockerfile \
--tag eventhub-api-tests:latest \ --tag eventhub-tests:latest \
--load \ --load \
--provenance=false \ --provenance=false \
--sbom=false \ --sbom=false \
. .
- name: Run unit tests (EUnit)
run: >
docker run --rm eventhub-tests:latest
rebar3 eunit --sname ci_eunit --verbose
- name: Run API tests (local CT) - name: Run API tests (local CT)
run: docker run --rm eventhub-api-tests:latest run: docker run --rm eventhub-tests:latest
- name: Push tested eventhub image - name: Push tested eventhub image
if: github.event_name == 'push' if: github.event_name == 'push'
+5 -2
View File
@@ -1,4 +1,4 @@
# API common_test — только builder-стадия (полный OTP + компиляторы). # EUnit + API common_test — один builder-образ, разные CMD в CI.
ARG ERLANG_BUILDER_IMAGE=git.sabilin.com/eventhub/erlang-builder:latest ARG ERLANG_BUILDER_IMAGE=git.sabilin.com/eventhub/erlang-builder:latest
FROM ${ERLANG_BUILDER_IMAGE} FROM ${ERLANG_BUILDER_IMAGE}
@@ -7,9 +7,12 @@ WORKDIR /app
COPY rebar.config ./ COPY rebar.config ./
COPY include/ include/ COPY include/ include/
COPY src/ src/ COPY src/ src/
COPY test/unit/ test/unit/
COPY test/api_*_SUITE.erl test/ COPY test/api_*_SUITE.erl test/
COPY test/api/ test/api/ COPY test/api/ test/api/
RUN rebar3 compile # default profile: CT (test/api); test profile: EUnit (test/unit)
RUN rebar3 compile && rebar3 as test compile
# По умолчанию CT; для eunit: docker run … rebar3 eunit --sname ci_eunit --verbose
CMD ["rebar3", "ct", "--sname", "ci_api_test", "-v"] CMD ["rebar3", "ct", "--sname", "ci_api_test", "-v"]
@@ -0,0 +1,153 @@
-module(admin_handler_admins_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(TARGET_ID, <<"adm_target_1">>).
-define(PLAIN_ID, <<"adm_plain_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => superadmin}),
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => ?TARGET_ID,
email => <<"target@test.local">>,
role => admin,
nickname => <<"target">>
})),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_admins_by_id_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET not found", {timeout, 60, fun test_get_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PUT update nickname", {timeout, 60, fun test_update/0}},
{"PUT not found", {timeout, 60, fun test_update_not_found/0}},
{"PUT forbidden for non-superadmin", {timeout, 60, fun test_update_forbidden/0}},
{"DELETE success", {timeout, 60, fun test_delete/0}},
{"DELETE not found", {timeout, 60, fun test_delete_not_found/0}},
{"DELETE forbidden for non-superadmin", {timeout, 60, fun test_delete_forbidden/0}},
{"PATCH method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_get() ->
{Status, _, Body} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(?TARGET_ID, maps:get(<<"id">>, Result)),
?assertEqual(<<"target@test.local">>, maps:get(<<"email">>, Result)).
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/admins/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => none
}),
?assertEqual(401, Status).
test_update() ->
{Status, _, Body} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"nickname">> => <<"updated">>})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"updated">>, maps:get(<<"nickname">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/admins/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"nickname">> => <<"x">>})
}),
?assertEqual(404, Status).
test_update_forbidden() ->
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => ?PLAIN_ID,
email => <<"plain@test.local">>,
role => admin
})),
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?PLAIN_ID,
body => jsx:encode(#{<<"nickname">> => <<"nope">>})
}),
?assertEqual(403, Status).
test_delete() ->
{Status, _, Body} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
?assertMatch({error, not_found}, core_admin:get_by_id(?TARGET_ID)).
test_delete_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/admins/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_delete_forbidden() ->
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => ?PLAIN_ID,
email => <<"plain@test.local">>,
role => admin
})),
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?PLAIN_ID
}),
?assertEqual(403, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins_by_id, #{
method => <<"PATCH">>,
path => <<"/v1/admin/admins/", ?TARGET_ID/binary>>,
bindings => #{id => ?TARGET_ID},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+147
View File
@@ -0,0 +1,147 @@
-module(admin_handler_admins_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(PLAIN_ID, <<"adm_plain_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
catch application:ensure_all_started(argon2),
eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => superadmin}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_admins_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET list success", {timeout, 60, fun test_list/0}},
{"GET list filter by role", {timeout, 60, fun test_list_filter/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST create success", {timeout, 60, fun test_create_ok/0}},
{"POST create forbidden for non-superadmin", {timeout, 60, fun test_create_forbidden/0}},
{"POST create missing fields", {timeout, 60, fun test_create_missing_fields/0}},
{"DELETE method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
argon2_ready() ->
case catch argon2:hash(<<"probe">>) of
{ok, _} -> true;
_ -> false
end.
require_argon2(Fun) ->
case argon2_ready() of
true -> Fun();
false -> {skip, "argon2 NIF not available"}
end.
seed_extra_admins() ->
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => <<"adm_mod">>,
email => <<"mod@test.local">>,
role => moderator,
nickname => <<"mod">>
})),
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => <<"adm_sup">>,
email => <<"support@test.local">>,
role => support,
nickname => <<"sup">>
})),
ok.
test_list() ->
seed_extra_admins(),
{Status, _, Body} = eh_test_support:call(admin_handler_admins, #{
method => <<"GET">>,
path => <<"/v1/admin/admins">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(3, length(Result)).
test_list_filter() ->
seed_extra_admins(),
{Status, _, Body} = eh_test_support:call(admin_handler_admins, #{
method => <<"GET">>,
path => <<"/v1/admin/admins">>,
auth => ?ADMIN_ID,
qs => [{<<"role">>, <<"moderator">>}]
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"mod@test.local">>, maps:get(<<"email">>, Only)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins, #{
method => <<"GET">>,
path => <<"/v1/admin/admins">>,
auth => none
}),
?assertEqual(401, Status).
test_create_ok() ->
require_argon2(fun() ->
{Status, _, Body} = eh_test_support:call(admin_handler_admins, #{
method => <<"POST">>,
path => <<"/v1/admin/admins">>,
auth => ?ADMIN_ID,
body => jsx:encode(#{
<<"email">> => <<"newadmin@test.local">>,
<<"password">> => <<"SecretPass1!">>,
<<"role">> => <<"admin">>
})
}),
?assertEqual(201, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"newadmin@test.local">>, maps:get(<<"email">>, Result)),
?assertEqual(<<"admin">>, maps:get(<<"role">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1)
end).
test_create_forbidden() ->
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => ?PLAIN_ID,
email => <<"plain@test.local">>,
role => admin
})),
{Status, _, _} = eh_test_support:call(admin_handler_admins, #{
method => <<"POST">>,
path => <<"/v1/admin/admins">>,
auth => ?PLAIN_ID,
body => jsx:encode(#{
<<"email">> => <<"x@test.local">>,
<<"password">> => <<"SecretPass1!">>,
<<"role">> => <<"admin">>
})
}),
?assertEqual(403, Status).
test_create_missing_fields() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins, #{
method => <<"POST">>,
path => <<"/v1/admin/admins">>,
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"email">> => <<"x@test.local">>})
}),
?assertEqual(400, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_admins, #{
method => <<"DELETE">>,
path => <<"/v1/admin/admins">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+97
View File
@@ -0,0 +1,97 @@
-module(admin_handler_audit_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(PLAIN_ID, <<"adm_plain_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => superadmin}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_audit_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET list success", {timeout, 60, fun test_list/0}},
{"GET list filter by action", {timeout, 60, fun test_list_filter/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"GET forbidden for non-superadmin", {timeout, 60, fun test_forbidden/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_audit() ->
{ok, _} = core_admin_audit:log(
?ADMIN_ID, <<"admin@test.local">>, superadmin,
<<"create_admin">>, <<"admin">>, <<"adm_x">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
?ADMIN_ID, <<"admin@test.local">>, superadmin,
<<"update_me">>, <<"admin">>, ?ADMIN_ID, <<"127.0.0.1">>
),
ok.
test_list() ->
seed_audit(),
{Status, _, Body} = eh_test_support:call(admin_handler_audit, #{
method => <<"GET">>,
path => <<"/v1/admin/audit">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)),
[First | _] = Result,
?assert(is_map_key(<<"action">>, First)),
?assert(is_map_key(<<"admin_id">>, First)).
test_list_filter() ->
seed_audit(),
{Status, _, Body} = eh_test_support:call(admin_handler_audit, #{
method => <<"GET">>,
path => <<"/v1/admin/audit">>,
auth => ?ADMIN_ID,
qs => [{<<"action">>, <<"update_me">>}]
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"update_me">>, maps:get(<<"action">>, Only)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_audit, #{
method => <<"GET">>,
path => <<"/v1/admin/audit">>,
auth => none
}),
?assertEqual(401, Status).
test_forbidden() ->
ok = mnesia:dirty_write(eh_test_support:make_admin(#{
id => ?PLAIN_ID,
email => <<"plain@test.local">>,
role => admin
})),
{Status, _, _} = eh_test_support:call(admin_handler_audit, #{
method => <<"GET">>,
path => <<"/v1/admin/audit">>,
auth => ?PLAIN_ID
}),
?assertEqual(403, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_audit, #{
method => <<"POST">>,
path => <<"/v1/admin/audit">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+115 -118
View File
@@ -2,157 +2,154 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, banned_word, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_banned_words, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_banned_words), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_banned_words_test_() -> admin_banned_words_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/banned-words success", fun test_list/0}, {"GET list success", fun test_list/0},
{"GET /admin/banned-words forbidden", fun test_list_forbidden/0}, {"GET list forbidden without admin", fun test_list_forbidden/0},
{"POST /admin/banned-words success", fun test_add/0}, {"POST add success", fun test_add/0},
{"POST /admin/banned-words missing field", fun test_add_missing/0}, {"POST add missing field", fun test_add_missing/0},
{"POST /admin/banned-words already exists", fun test_add_exists/0}, {"POST add already exists", fun test_add_exists/0},
{"DELETE /admin/banned-words/:word success", fun test_delete/0}, {"DELETE success", fun test_delete/0},
{"DELETE /admin/banned-words/:word not found", fun test_delete_not_found/0}, {"DELETE not found", fun test_delete_not_found/0},
{"PUT /admin/banned-words/:word success", fun test_update/0}, {"PUT success", fun test_update/0},
{"PUT /admin/banned-words/:word not found", fun test_update_not_found/0}, {"PUT not found", fun test_update_not_found/0},
{"PATCH /admin/banned-words method not allowed", fun test_wrong_method/0} {"POST batch success", fun test_batch/0},
{"PATCH method not allowed", fun test_wrong_method/0}
]}. ]}.
test_list() -> test_list() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {ok, _} = core_banned_words:add_banned_word(<<"badword">>, ?ADMIN_ID),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, _} = core_banned_words:add_banned_word(<<"spamword">>, ?ADMIN_ID),
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{
AdminUser = #user{id = <<"adm1">>, role = admin}, method => <<"GET">>,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), path => <<"/v1/admin/banned-words">>,
Words = [ auth => ?ADMIN_ID
#banned_word{id = <<"bw1">>, word = <<"badword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}}, }),
#banned_word{id = <<"bw2">>, word = <<"spamword">>, added_by = <<"adm2">>, added_at = {{2026,4,27},{12,30,0}}}
],
ok = meck:expect(core_banned_words, list_banned_words, fun() -> Words end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
Result = jsx:decode(RespBody, [return_maps]), Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)), ?assertEqual(2, length(Result)),
First = hd(Result), Words = [maps:get(<<"word">>, W) || W <- Result],
?assertEqual(<<"badword">>, maps:get(<<"word">>, First)), ?assert(lists:member(<<"badword">>, Words)),
?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, First)). ?assert(lists:member(<<"spamword">>, Words)).
test_list_forbidden() -> test_list_forbidden() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), path => <<"/v1/admin/banned-words">>,
{ok, _, _} = admin_handler_banned_words:init(req, []), auth => none
{Status, _, _, _} = erase(test_reply), }),
?assertEqual(403, Status). ?assertEqual(401, Status).
test_add() -> test_add() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), method => <<"POST">>,
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/banned-words">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, auth => ?ADMIN_ID,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), body => jsx:encode(#{<<"word">> => <<"banned">>})
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"banned">>}), Req} end), }),
NewBW = #banned_word{id = <<"bw_new">>, word = <<"banned">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{13,0,0}}},
ok = meck:expect(core_banned_words, add_banned_word, fun(<<"banned">>, <<"adm1">>) -> {ok, NewBW} end),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status), ?assertEqual(201, Status),
#{<<"word">> := <<"banned">>, <<"added_by">> := <<"adm1">>} = jsx:decode(RespBody, [return_maps]). #{<<"status">> := <<"added">>} = jsx:decode(Body, [return_maps]),
?assert(core_banned_words:is_word_banned(<<"banned">>)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_add_missing() -> test_add_missing() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), method => <<"POST">>,
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/banned-words">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, auth => ?ADMIN_ID,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), body => jsx:encode(#{<<"other">> => <<"data">>})
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end), }),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status). ?assertEqual(400, Status).
test_add_exists() -> test_add_exists() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {ok, _} = core_banned_words:add_banned_word(<<"already">>, ?ADMIN_ID),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"POST">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/banned-words">>,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), auth => ?ADMIN_ID,
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"already">>}), Req} end), body => jsx:encode(#{<<"word">> => <<"already">>})
ok = meck:expect(core_banned_words, add_banned_word, fun(_, _) -> {error, already_exists} end), }),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(409, Status). ?assertEqual(409, Status).
test_delete() -> test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"badword">> end), {ok, _} = core_banned_words:add_banned_word(<<"badword">>, ?ADMIN_ID),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"DELETE">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/banned-words/badword">>,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), bindings => #{word => <<"badword">>},
ok = meck:expect(core_banned_words, remove_banned_word, fun(<<"badword">>) -> {ok, deleted} end), auth => ?ADMIN_ID
{ok, _, _} = admin_handler_banned_words:init(req, []), }),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]). #{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
?assertEqual(false, core_banned_words:is_word_banned(<<"badword">>)).
test_delete_not_found() -> test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"unknown">> end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), method => <<"DELETE">>,
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/banned-words/unknown">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{word => <<"unknown">>},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), auth => ?ADMIN_ID
ok = meck:expect(core_banned_words, remove_banned_word, fun(_) -> {error, not_found} end), }),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status). ?assertEqual(404, Status).
test_update() -> test_update() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"oldword">> end), {ok, _} = core_banned_words:add_banned_word(<<"old">>, ?ADMIN_ID),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"PUT">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/banned-words/old">>,
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), bindings => #{word => <<"old">>},
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end), auth => ?ADMIN_ID,
UpdatedBW = #banned_word{id = <<"bw1">>, word = <<"newword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}}, body => jsx:encode(#{<<"word">> => <<"new">>})
ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {ok, UpdatedBW} end), }),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
Resp = jsx:decode(RespBody, [return_maps]), #{<<"status">> := <<"updated">>} = jsx:decode(Body, [return_maps]),
?assertEqual(<<"newword">>, maps:get(<<"word">>, Resp)), ?assert(core_banned_words:is_word_banned(<<"new">>)),
?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, Resp)). ?assertEqual(false, core_banned_words:is_word_banned(<<"old">>)).
test_update_not_found() -> test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"missing">> end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), method => <<"PUT">>,
ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/banned-words/missing">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{word => <<"missing">>},
ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), auth => ?ADMIN_ID,
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end), body => jsx:encode(#{<<"word">> => <<"x">>})
ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {error, not_found} end), }),
{ok, _, _} = admin_handler_banned_words:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status). ?assertEqual(404, Status).
test_batch() ->
{ok, _} = core_banned_words:add_banned_word(<<"exists">>, ?ADMIN_ID),
{Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{
method => <<"POST">>,
path => <<"/v1/admin/banned-words/batch">>,
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"words">> => [<<"new1">>, <<"exists">>, <<"new2">>]})
}),
?assertEqual(200, Status),
#{<<"added">> := 2, <<"skipped">> := 1} = jsx:decode(Body, [return_maps]),
?assert(core_banned_words:is_word_banned(<<"new1">>)),
?assert(core_banned_words:is_word_banned(<<"new2">>)).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), method => <<"PATCH">>,
{ok, _, _} = admin_handler_banned_words:init(req, []), path => <<"/v1/admin/banned-words">>,
{Status, _, RespBody, _} = erase(test_reply), auth => ?ADMIN_ID
?assertEqual(405, Status), }),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). ?assertEqual(405, Status).
@@ -0,0 +1,136 @@
-module(admin_handler_calendar_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_cal_by_id">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_calendar_by_id_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET not found", {timeout, 60, fun test_get_not_found/0}},
{"PUT update title", {timeout, 60, fun test_update/0}},
{"PUT not found", {timeout, 60, fun test_update_not_found/0}},
{"DELETE soft delete", {timeout, 60, fun test_delete/0}},
{"DELETE not found", {timeout, 60, fun test_delete_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PATCH method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_calendar() ->
{ok, Cal} = core_calendar:create(?OWNER_ID, <<"Target Cal">>, <<"desc">>, auto),
Cal.
test_get() ->
Cal = seed_calendar(),
CalId = Cal#calendar.id,
{Status, _, Body} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/", CalId/binary>>,
bindings => #{id => CalId},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(CalId, maps:get(<<"id">>, Result)),
?assertEqual(<<"Target Cal">>, maps:get(<<"title">>, Result)).
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_update() ->
Cal = seed_calendar(),
CalId = Cal#calendar.id,
{Status, _, Body} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/calendars/", CalId/binary>>,
bindings => #{id => CalId},
auth => ?ADMIN_ID,
body => jsx:encode(#{
<<"title">> => <<"Updated Cal">>,
<<"status">> => <<"frozen">>
})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"Updated Cal">>, maps:get(<<"title">>, Result)),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/calendars/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"title">> => <<"X">>})
}),
?assertEqual(404, Status).
test_delete() ->
Cal = seed_calendar(),
CalId = Cal#calendar.id,
{Status, _, Body} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/calendars/", CalId/binary>>,
bindings => #{id => CalId},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
?assertEqual(#{}, jsx:decode(Body, [return_maps])),
{ok, Deleted} = core_calendar:get_by_id(CalId),
?assertEqual(deleted, Deleted#calendar.status),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_delete_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/calendars/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_unauthorized() ->
Cal = seed_calendar(),
CalId = Cal#calendar.id,
{Status, _, _} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/", CalId/binary>>,
bindings => #{id => CalId},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
Cal = seed_calendar(),
CalId = Cal#calendar.id,
{Status, _, _} = eh_test_support:call(admin_handler_calendar_by_id, #{
method => <<"PATCH">>,
path => <<"/v1/admin/calendars/", CalId/binary>>,
bindings => #{id => CalId},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,83 @@
-module(admin_handler_calendar_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, review, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_cal_stats">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_calendar_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with calendars", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_calendar_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_calendars">>, Result)),
?assertEqual(#{}, maps:get(<<"calendars_by_type">>, Result)),
?assertEqual(#{}, maps:get(<<"calendars_by_status">>, Result)),
?assertEqual([], maps:get(<<"top_calendars_by_reviews">>, Result)),
?assertEqual([], maps:get(<<"top_calendars_by_rating">>, Result)).
test_stats_with_data() ->
{ok, C1} = core_calendar:create(?OWNER_ID, <<"Personal">>, <<"d">>, auto, personal),
{ok, _} = core_calendar:create(?OWNER_ID, <<"Shop">>, <<"d">>, manual, commercial),
{ok, _} = core_calendar:update(C1#calendar.id, [
{rating_avg, 4.5},
{rating_count, 3}
]),
{Status, _, Body} = eh_test_support:call(admin_handler_calendar_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_calendars">>, Result)),
ByType = maps:get(<<"calendars_by_type">>, Result),
?assertEqual(1, maps:get(<<"personal">>, ByType)),
?assertEqual(1, maps:get(<<"commercial">>, ByType)),
ByStatus = maps:get(<<"calendars_by_status">>, Result),
?assertEqual(2, maps:get(<<"active">>, ByStatus)),
TopRating = maps:get(<<"top_calendars_by_rating">>, Result),
?assert(length(TopRating) >= 1),
[First | _] = TopRating,
?assertEqual(C1#calendar.id, maps:get(<<"id">>, First)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendar_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendar_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/calendars/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,76 @@
-module(admin_handler_calendars_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_cal_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_calendars_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET list success", {timeout, 60, fun test_list/0}},
{"GET list filter by status", {timeout, 60, fun test_list_filter/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_calendars() ->
{ok, C1} = core_calendar:create(?OWNER_ID, <<"Alpha">>, <<"desc a">>, auto, personal),
{ok, C2} = core_calendar:create(?OWNER_ID, <<"Beta">>, <<"desc b">>, manual, commercial),
{ok, _} = core_calendar:update(C2#calendar.id, [{status, frozen}]),
{C1, C2}.
test_list() ->
seed_calendars(),
{Status, _, Body} = eh_test_support:call(admin_handler_calendars, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)).
test_list_filter() ->
seed_calendars(),
{Status, _, Body} = eh_test_support:call(admin_handler_calendars, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars">>,
auth => ?ADMIN_ID,
qs => [{<<"status">>, <<"active">>}]
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"Alpha">>, maps:get(<<"title">>, Only)),
?assertEqual(<<"active">>, maps:get(<<"status">>, Only)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendars, #{
method => <<"GET">>,
path => <<"/v1/admin/calendars">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_calendars, #{
method => <<"POST">>,
path => <<"/v1/admin/calendars">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,142 @@
-module(admin_handler_event_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, event, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_evt_by_id">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_event_by_id_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET not found", {timeout, 60, fun test_get_not_found/0}},
{"PUT update title/status", {timeout, 60, fun test_update/0}},
{"PUT not found", {timeout, 60, fun test_update_not_found/0}},
{"DELETE soft delete", {timeout, 60, fun test_delete/0}},
{"DELETE not found", {timeout, 60, fun test_delete_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PATCH method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_event() ->
{ok, Cal} = core_calendar:create(?OWNER_ID, <<"Cal">>, <<"">>, auto),
{ok, Event} = core_event:create(
Cal#calendar.id,
<<"Target Event">>,
eh_test_support:future_start(),
60
),
Event.
test_get() ->
Event = seed_event(),
EventId = Event#event.id,
{Status, _, Body} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/events/", EventId/binary>>,
bindings => #{id => EventId},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(EventId, maps:get(<<"id">>, Result)),
?assertEqual(<<"Target Event">>, maps:get(<<"title">>, Result)).
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/events/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_update() ->
Event = seed_event(),
EventId = Event#event.id,
{Status, _, Body} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/events/", EventId/binary>>,
bindings => #{id => EventId},
auth => ?ADMIN_ID,
body => jsx:encode(#{
<<"title">> => <<"Updated Event">>,
<<"status">> => <<"cancelled">>
})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"Updated Event">>, maps:get(<<"title">>, Result)),
?assertEqual(<<"cancelled">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/events/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"title">> => <<"X">>})
}),
?assertEqual(404, Status).
test_delete() ->
Event = seed_event(),
EventId = Event#event.id,
{Status, _, Body} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/events/", EventId/binary>>,
bindings => #{id => EventId},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
{ok, Deleted} = core_event:get_by_id(EventId),
?assertEqual(deleted, Deleted#event.status),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_delete_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/events/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_unauthorized() ->
Event = seed_event(),
EventId = Event#event.id,
{Status, _, _} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/events/", EventId/binary>>,
bindings => #{id => EventId},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
Event = seed_event(),
EventId = Event#event.id,
{Status, _, _} = eh_test_support:call(admin_handler_event_by_id, #{
method => <<"PATCH">>,
path => <<"/v1/admin/events/", EventId/binary>>,
bindings => #{id => EventId},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,84 @@
-module(admin_handler_event_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, event, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_evt_stats">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_event_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with events", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_event_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/events/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_events">>, Result)),
?assertEqual(#{}, maps:get(<<"events_by_type">>, Result)),
?assertEqual(#{}, maps:get(<<"events_by_status">>, Result)),
?assertEqual([], maps:get(<<"top_events_by_rating">>, Result)).
test_stats_with_data() ->
{ok, Cal} = core_calendar:create(?OWNER_ID, <<"Cal">>, <<"">>, auto),
CalId = Cal#calendar.id,
Start = eh_test_support:future_start(),
{ok, E1} = core_event:create(CalId, <<"Rated">>, Start, 60),
{ok, _} = core_event:create(CalId, <<"Other">>, Start, 30),
{ok, _} = core_event:update(E1#event.id, [
{rating_avg, 4.8},
{rating_count, 5}
]),
{Status, _, Body} = eh_test_support:call(admin_handler_event_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/events/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_events">>, Result)),
ByType = maps:get(<<"events_by_type">>, Result),
?assertEqual(2, maps:get(<<"single">>, ByType)),
ByStatus = maps:get(<<"events_by_status">>, Result),
?assertEqual(2, maps:get(<<"active">>, ByStatus)),
Top = maps:get(<<"top_events_by_rating">>, Result),
?assert(length(Top) >= 1),
[First | _] = Top,
?assertEqual(E1#event.id, maps:get(<<"id">>, First)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_event_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/events/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_event_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/events/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+81
View File
@@ -0,0 +1,81 @@
-module(admin_handler_events_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, calendar, event, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(OWNER_ID, <<"owner_evt_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_events_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET list success", {timeout, 60, fun test_list/0}},
{"GET list filter by title", {timeout, 60, fun test_list_filter/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
event_start() ->
{{Y, M, D}, _} = eh_test_support:days_from_now(7),
{{Y, M, D}, {12, 0, 0}}.
seed_events() ->
{ok, Cal} = core_calendar:create(?OWNER_ID, <<"Cal">>, <<"">>, auto),
CalId = Cal#calendar.id,
Start = event_start(),
{ok, E1} = core_event:create(CalId, <<"Meetup">>, Start, 60),
{ok, E2} = core_event:create(CalId, <<"Workshop">>, Start, 90),
{E1, E2}.
test_list() ->
seed_events(),
{Status, _, Body} = eh_test_support:call(admin_handler_events, #{
method => <<"GET">>,
path => <<"/v1/admin/events">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)).
test_list_filter() ->
seed_events(),
{Status, _, Body} = eh_test_support:call(admin_handler_events, #{
method => <<"GET">>,
path => <<"/v1/admin/events">>,
auth => ?ADMIN_ID,
qs => [{<<"title">>, <<"Meetup">>}]
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"Meetup">>, maps:get(<<"title">>, Only)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_events, #{
method => <<"GET">>,
path => <<"/v1/admin/events">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_events, #{
method => <<"POST">>,
path => <<"/v1/admin/events">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+23 -35
View File
@@ -1,49 +1,37 @@
-module(admin_handler_health_tests). -module(admin_handler_health_tests).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
%% ------------------------------------------------------------------
%% Фикстуры
%% ------------------------------------------------------------------
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(cowboy_req). eh_test_support:unload_cowboy(),
ok.
%% ------------------------------------------------------------------ admin_health_test_() ->
%% Тесты {foreach, fun setup/0, fun cleanup/1, [
%% ------------------------------------------------------------------ {"GET health success", fun test_health_get/0},
admin_handler_health_test_() -> {"POST health method not allowed", fun test_health_wrong_method/0}
{setup, fun setup/0, fun cleanup/1, [
{"GET /admin/health returns 200 with status ok",
fun test_health_get/0},
{"POST /admin/health returns 405 Method not allowed",
fun test_health_post/0}
]}. ]}.
%% ── Успешный GET ─────────────────────────────────────────────
test_health_get() -> test_health_get() ->
% Мокаем method → <<"GET">> {Status, _, Body} = eh_test_support:call(admin_handler_health, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
% reply/4 будем перехватывать path => <<"/admin/health">>,
ok = meck:expect(cowboy_req, reply, auth => none
fun(Code, Headers, Body, Req) -> }),
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_health:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
?assertEqual(#{<<"status">> => <<"ok">>}, jsx:decode(RespBody, [return_maps])). Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"ok">>, maps:get(<<"status">>, Result)),
?assertEqual(<<"eventhub">>, maps:get(<<"service">>, Result)),
?assert(is_map_key(<<"version">>, Result)),
?assert(is_map_key(<<"git_sha">>, Result)).
%% ── Метод не разрешён ─────────────────────────────────────── test_health_wrong_method() ->
test_health_post() -> {Status, _, Body} = eh_test_support:call(admin_handler_health, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), method => <<"POST">>,
ok = meck:expect(cowboy_req, reply, path => <<"/admin/health">>,
fun(Code, Headers, Body, Req) -> auth => none
put(test_reply, {Code, Headers, Body, Req}) }),
end),
{ok, _, _} = admin_handler_health:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status), ?assertEqual(405, Status),
?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])). #{<<"error">> := <<"Method not allowed">>} = jsx:decode(Body, [return_maps]).
+114 -102
View File
@@ -2,122 +2,134 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>). -define(TABLES, [admin, admin_audit, auth_session, ticket]).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>). -define(EMAIL, <<"admin@test.local">>).
-define(PASSWORD, <<"SecretPass1!">>).
setup() -> setup() ->
ok = meck:new(logic_auth, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_admin, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(admin_utils, [non_strict]), catch application:ensure_all_started(argon2),
mnesia:start(),
catch ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]), catch ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]),
mnesia:create_table(auth_session, [
{attributes, record_info(fields, auth_session)},
{ram_copies, [node()]}
]),
mnesia:add_table_index(auth_session, #auth_session.family_id),
mnesia:add_table_index(auth_session, #auth_session.subject_id),
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose).
cleanup(_) ->
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
application:stop(jose),
catch mnesia:delete_table(auth_session),
catch ets:delete(eventhub_counters),
mnesia:stop(),
meck:unload(admin_utils),
meck:unload(core_admin),
meck:unload(cowboy_req),
meck:unload(logic_auth),
ok. ok.
admin_handler_login_test_() -> cleanup(_) ->
{setup, fun setup/0, fun cleanup/1, [ eh_test_support:unload_cowboy(),
{"Valid admin login returns 200 and token", fun test_valid_admin_login/0}, eh_test_support:delete_tables(?TABLES),
{"Invalid credentials return 401", fun test_invalid_credentials/0}, eh_test_support:stop_mnesia(),
{"Nonadmin role returns 403", fun test_insufficient_permissions/0}, ok.
{"Malformed JSON returns 400", fun test_malformed_json/0},
{"Missing body returns 400", fun test_missing_body/0}, admin_login_test_() ->
{"Wrong HTTP method returns 405", fun test_wrong_method/0} {foreach, fun setup/0, fun cleanup/1, [
{"POST login success", fun test_login_ok/0},
{"POST login bad credentials", fun test_login_bad_credentials/0},
{"POST login insufficient permissions", fun test_login_forbidden_role/0},
{"POST login missing fields", fun test_login_missing_fields/0},
{"POST login missing body", fun test_login_missing_body/0},
{"POST login invalid JSON", fun test_login_invalid_json/0},
{"GET login method not allowed", fun test_login_wrong_method/0}
]}. ]}.
%% ── Вспомогательная функция для создания запроса и ожидания reply ── %% argon2 is a Rust NIF; rebar hooks only build it on unix. Skip hash-dependent
prepare_req(Method, HasBody, Body) -> %% cases when the NIF is unavailable (common on Windows without cargo).
ok = meck:expect(cowboy_req, method, fun(_) -> Method end), argon2_ready() ->
ok = meck:expect(cowboy_req, has_body, fun(_) -> HasBody end), case catch argon2:hash(<<"probe">>) of
case {HasBody, Body} of {ok, _} -> true;
{true, undefined} -> ok; _ -> false
{true, _} -> end.
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, Body, Req} end);
{false, _} -> ok
end,
% Устанавливаем мок на reply, который сохраняет ответ в словаре процесса
meck:expect(cowboy_req, reply,
fun(Code, Headers, RespBody, Req) ->
put(test_reply, {Code, Headers, RespBody}),
Req
end),
req.
%% ── Тесты ──────────────────────────────────────────────────── require_argon2(Fun) ->
case argon2_ready() of
true -> Fun();
false -> {skip, "argon2 NIF not available"}
end.
test_valid_admin_login() -> test_login_ok() ->
UserMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>}, require_argon2(fun() ->
ok = meck:expect(logic_auth, authenticate_admin, {ok, Admin} = core_admin:create(?EMAIL, ?PASSWORD, admin),
fun(<<"admin@test.com">>, <<"pass">>) -> {ok, UserMap} end), {Status, _, Body} = eh_test_support:call(admin_handler_login, #{
ok = meck:expect(core_admin, update_last_login, fun(_) -> ok end), method => <<"POST">>,
ok = meck:expect(admin_utils, log_admin_action, fun(_, _, _, _, _, _) -> ok end), path => <<"/v1/admin/login">>,
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"admin@test.com">>, password => <<"pass">>})), auth => none,
{ok, _, _} = admin_handler_login:init(Req0, []), body => jsx:encode(#{<<"email">> => ?EMAIL, <<"password">> => ?PASSWORD})
{Code, Headers, Body} = get(test_reply), }),
?assertEqual(200, Code), ?assertEqual(200, Status),
?assertEqual(<<"application/json">>, maps:get(<<"content-type">>, Headers)),
Resp = jsx:decode(Body, [return_maps]), Resp = jsx:decode(Body, [return_maps]),
?assert(is_map_key(<<"token">>, Resp)), ?assert(is_map_key(<<"token">>, Resp)),
?assert(is_map_key(<<"refresh_token">>, Resp)), ?assert(is_map_key(<<"refresh_token">>, Resp)),
?assertEqual(<<"superadmin">>, maps:get(<<"role">>, maps:get(<<"user">>, Resp))). User = maps:get(<<"user">>, Resp),
?assertEqual(Admin#admin.id, maps:get(<<"id">>, User)),
?assertEqual(?EMAIL, maps:get(<<"email">>, User)),
?assertEqual(<<"admin">>, maps:get(<<"role">>, User)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1)
end).
test_invalid_credentials() -> test_login_bad_credentials() ->
ok = meck:expect(logic_auth, authenticate_admin, %% Unknown email does not need password verification.
fun(_, _) -> {error, bad_credentials} end), {Status, _, Body} = eh_test_support:call(admin_handler_login, #{
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"bad@test.com">>, password => <<"wrong">>})), method => <<"POST">>,
{ok, _, _} = admin_handler_login:init(Req0, []), path => <<"/v1/admin/login">>,
{Code, _, Body} = get(test_reply), auth => none,
?assertEqual(401, Code), body => jsx:encode(#{<<"email">> => <<"nobody@test.local">>, <<"password">> => <<"x">>})
#{<<"error">> := <<"bad_credentials">>} = jsx:decode(Body, [return_maps]). }),
?assertEqual(401, Status),
#{<<"error">> := <<"invalid_credentials">>} = jsx:decode(Body, [return_maps]).
test_insufficient_permissions() -> test_login_forbidden_role() ->
UserMap = #{id => <<"user1">>, email => <<"user@test.com">>, role => <<"user">>}, require_argon2(fun() ->
ok = meck:expect(logic_auth, authenticate_admin, {ok, Hash} = logic_auth:hash_password(?PASSWORD),
fun(_, _) -> {ok, UserMap} end), eh_test_support:seed_admin(#{
Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"user@test.com">>, password => <<"pass">>})), id => <<"adm_guest">>,
{ok, _, _} = admin_handler_login:init(Req0, []), email => <<"guest@test.local">>,
{Code, _, Body} = get(test_reply), password_hash => Hash,
?assertEqual(403, Code), role => guest
#{<<"error">> := <<"insufficient_permissions">>} = jsx:decode(Body, [return_maps]). }),
{Status, _, Body} = eh_test_support:call(admin_handler_login, #{
method => <<"POST">>,
path => <<"/v1/admin/login">>,
auth => none,
body => jsx:encode(#{
<<"email">> => <<"guest@test.local">>,
<<"password">> => ?PASSWORD
})
}),
?assertEqual(403, Status),
#{<<"error">> := <<"insufficient_permissions">>} = jsx:decode(Body, [return_maps])
end).
test_malformed_json() -> test_login_missing_fields() ->
Req0 = prepare_req(<<"POST">>, true, <<"not a json">>), {Status, _, _} = eh_test_support:call(admin_handler_login, #{
{ok, _, _} = admin_handler_login:init(Req0, []), method => <<"POST">>,
{Code, _, Body} = get(test_reply), path => <<"/v1/admin/login">>,
?assertEqual(400, Code), auth => none,
#{<<"error">> := <<"Invalid JSON">>} = jsx:decode(Body, [return_maps]). body => jsx:encode(#{<<"email">> => ?EMAIL})
}),
?assertEqual(400, Status).
test_missing_body() -> test_login_missing_body() ->
Req0 = prepare_req(<<"POST">>, false, undefined), {Status, _, _} = eh_test_support:call(admin_handler_login, #{
{ok, _, _} = admin_handler_login:init(Req0, []), method => <<"POST">>,
{Code, _, Body} = get(test_reply), path => <<"/v1/admin/login">>,
?assertEqual(400, Code), auth => none,
#{<<"error">> := <<"Missing request body">>} = jsx:decode(Body, [return_maps]). has_body => false
}),
?assertEqual(400, Status).
test_wrong_method() -> test_login_invalid_json() ->
Req0 = prepare_req(<<"GET">>, false, undefined), {Status, _, _} = eh_test_support:call(admin_handler_login, #{
{ok, _, _} = admin_handler_login:init(Req0, []), method => <<"POST">>,
{Code, _, Body} = get(test_reply), path => <<"/v1/admin/login">>,
?assertEqual(405, Code), auth => none,
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(Body, [return_maps]). body => <<"not-json">>
}),
?assertEqual(400, Status).
test_login_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_login, #{
method => <<"GET">>,
path => <<"/v1/admin/login">>,
auth => none
}),
?assertEqual(405, Status).
+89
View File
@@ -0,0 +1,89 @@
-module(admin_handler_me_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{
id => ?ADMIN_ID,
role => admin,
nickname => <<"me-admin">>,
email => <<"me@test.local">>
}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_me_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PUT update profile", {timeout, 60, fun test_update/0}},
{"PUT unauthorized", {timeout, 60, fun test_update_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_get() ->
{Status, _, Body} = eh_test_support:call(admin_handler_me, #{
method => <<"GET">>,
path => <<"/v1/admin/me">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(?ADMIN_ID, maps:get(<<"id">>, Result)),
?assertEqual(<<"me@test.local">>, maps:get(<<"email">>, Result)),
?assertEqual(<<"me-admin">>, maps:get(<<"nickname">>, Result)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_me, #{
method => <<"GET">>,
path => <<"/v1/admin/me">>,
auth => none
}),
?assertEqual(401, Status).
test_update() ->
{Status, _, Body} = eh_test_support:call(admin_handler_me, #{
method => <<"PUT">>,
path => <<"/v1/admin/me">>,
auth => ?ADMIN_ID,
body => jsx:encode(#{
<<"nickname">> => <<"new-nick">>,
<<"timezone">> => <<"Europe/Moscow">>,
<<"language">> => <<"en">>
})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"new-nick">>, maps:get(<<"nickname">>, Result)),
?assertEqual(<<"Europe/Moscow">>, maps:get(<<"timezone">>, Result)),
?assertEqual(<<"en">>, maps:get(<<"language">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_me, #{
method => <<"PUT">>,
path => <<"/v1/admin/me">>,
auth => none,
body => jsx:encode(#{<<"nickname">> => <<"x">>})
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_me, #{
method => <<"POST">>,
path => <<"/v1/admin/me">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+92 -166
View File
@@ -2,188 +2,114 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, user, calendar, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(USER_ID, <<"user_mod_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_calendar, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:new(core_event, [non_strict]), ok = mnesia:dirty_write(eh_test_support:make_user(#{
ok = meck:new(core_review, [non_strict]), id => ?USER_ID,
ok = meck:expect(cowboy_req, reply, email => <<"user@test.local">>,
fun(Code, Headers, Body, Req) -> status => active
put(test_reply, {Code, Headers, Body, Req}) })),
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_review), eh_test_support:unload_cowboy(),
meck:unload(core_event), eh_test_support:delete_tables(?TABLES),
meck:unload(core_calendar), eh_test_support:stop_mnesia(),
meck:unload(core_user), ok.
meck:unload(handler_auth),
meck:unload(cowboy_req).
admin_moderation_test_() -> admin_moderation_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"Freeze calendar success", fun test_freeze_calendar/0}, {"PUT block user success", fun test_block_user/0},
{"Freeze calendar not found", fun test_freeze_calendar_not_found/0}, {"PUT block user not found", fun test_block_user_not_found/0},
{"Unfreeze calendar success", fun test_unfreeze_calendar/0}, {"PUT freeze calendar success", fun test_freeze_calendar/0},
{"Freeze event success", fun test_freeze_event/0}, {"PUT missing action", fun test_missing_action/0},
{"Unfreeze event success", fun test_unfreeze_event/0}, {"PUT invalid target", fun test_invalid_target/0},
{"Hide review success", fun test_hide_review/0}, {"PUT unauthorized", fun test_unauthorized/0},
{"Show review success", fun test_show_review/0}, {"GET method not allowed", fun test_wrong_method/0}
{"Block user success", fun test_block_user/0},
{"Unblock user success", fun test_unblock_user/0},
{"Invalid target type", fun test_invalid_target/0},
{"Invalid action", fun test_invalid_action/0},
{"Missing action field", fun test_missing_action/0},
{"Forbidden non admin", fun test_forbidden/0},
{"Wrong method POST", fun test_wrong_method/0}
]}. ]}.
%% ── Вспомогательные функции ────────────────────────────── test_block_user() ->
prepare_req(TargetType, TargetId, Action) -> {Status, _, Body} = eh_test_support:call(admin_handler_moderation, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), method => <<"PUT">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/user/", ?USER_ID/binary>>,
fun(Req) -> {ok, <<"adm1">>, Req} end), bindings => #{target_type => <<"user">>, id => ?USER_ID},
AdminUser = #user{id = <<"adm1">>, role = admin}, auth => ?ADMIN_ID,
ok = meck:expect(core_user, get_by_id, body => jsx:encode(#{<<"action">> => <<"block">>, <<"reason">> => <<"spam">>})
fun(<<"adm1">>) -> {ok, AdminUser} end), }),
ok = meck:expect(cowboy_req, binding,
fun(target_type, _) -> TargetType;
(id, _) -> TargetId
end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"action">> => Action}), Req} end).
%% ── Календари ───────────────────────────────────────────
test_freeze_calendar() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"freeze">>),
Frozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = frozen},
ok = meck:expect(core_calendar, freeze, fun(<<"c1">>) -> {ok, Frozen} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(?USER_ID, maps:get(<<"id">>, Result)),
?assertEqual(<<"blocked">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_freeze_calendar_not_found() -> test_block_user_not_found() ->
prepare_req(<<"calendar">>, <<"c99">>, <<"freeze">>), {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{
ok = meck:expect(core_calendar, freeze, fun(_) -> {error, not_found} end), method => <<"PUT">>,
{ok, _, _} = admin_handler_moderation:init(req, []), path => <<"/v1/admin/user/missing">>,
{Status, _, _, _} = erase(test_reply), bindings => #{target_type => <<"user">>, id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"action">> => <<"block">>, <<"reason">> => <<"x">>})
}),
?assertEqual(404, Status). ?assertEqual(404, Status).
test_unfreeze_calendar() -> test_freeze_calendar() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"unfreeze">>), {ok, Cal} = core_calendar:create(?USER_ID, <<"Cal">>, <<"desc">>, auto),
Unfrozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = active}, CalId = Cal#calendar.id,
ok = meck:expect(core_calendar, unfreeze, fun(<<"c1">>) -> {ok, Unfrozen} end), {Status, _, Body} = eh_test_support:call(admin_handler_moderation, #{
{ok, _, _} = admin_handler_moderation:init(req, []), method => <<"PUT">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/calendar/", CalId/binary>>,
bindings => #{target_type => <<"calendar">>, id => CalId},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"action">> => <<"freeze">>, <<"reason">> => <<"policy">>})
}),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(CalId, maps:get(<<"id">>, Result)),
%% ── События ───────────────────────────────────────────── ?assertEqual(<<"frozen">>, maps:get(<<"status">>, Result)).
test_freeze_event() ->
prepare_req(<<"event">>, <<"e1">>, <<"freeze">>),
FrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = frozen},
ok = meck:expect(core_event, freeze, fun(<<"e1">>) -> {ok, FrozenE} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
test_unfreeze_event() ->
prepare_req(<<"event">>, <<"e1">>, <<"unfreeze">>),
UnfrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = active},
ok = meck:expect(core_event, unfreeze, fun(<<"e1">>) -> {ok, UnfrozenE} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Отзывы ──────────────────────────────────────────────
test_hide_review() ->
prepare_req(<<"review">>, <<"r1">>, <<"hide">>),
Hidden = #review{id = <<"r1">>, status = hidden},
ok = meck:expect(core_review, hide, fun(<<"r1">>) -> {ok, Hidden} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"hidden">>} = jsx:decode(RespBody, [return_maps]).
test_show_review() ->
prepare_req(<<"review">>, <<"r1">>, <<"show">>),
Visible = #review{id = <<"r1">>, status = active},
ok = meck:expect(core_review, show, fun(<<"r1">>) -> {ok, Visible} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Пользователи ────────────────────────────────────────
test_block_user() ->
prepare_req(<<"user">>, <<"u1">>, <<"block">>),
Blocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = frozen},
ok = meck:expect(core_user, block, fun(<<"u1">>) -> {ok, Blocked} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]).
test_unblock_user() ->
prepare_req(<<"user">>, <<"u1">>, <<"unblock">>),
Unblocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = active},
ok = meck:expect(core_user, unblock, fun(<<"u1">>) -> {ok, Unblocked} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
%% ── Ошибки ──────────────────────────────────────────────
test_invalid_target() ->
prepare_req(<<"bad_type">>, <<"x">>, <<"freeze">>),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_invalid_action() ->
prepare_req(<<"calendar">>, <<"c1">>, <<"delete">>),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
test_missing_action() -> test_missing_action() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{
ok = meck:expect(handler_auth, authenticate, method => <<"PUT">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/user/", ?USER_ID/binary>>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{target_type => <<"user">>, id => ?USER_ID},
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID,
fun(<<"adm1">>) -> {ok, AdminUser} end), body => jsx:encode(#{<<"reason">> => <<"x">>})
ok = meck:expect(cowboy_req, binding, }),
fun(target_type, _) -> <<"calendar">>;
(id, _) -> <<"c1">>
end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end),
{ok, _, _} = admin_handler_moderation:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status). ?assertEqual(400, Status).
test_forbidden() -> test_invalid_target() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{
ok = meck:expect(handler_auth, authenticate, method => <<"PUT">>,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), path => <<"/v1/admin/unknown/x">>,
{ok, _, _} = admin_handler_moderation:init(req, []), bindings => #{target_type => <<"unknown">>, id => <<"x">>},
{Status, _, _, _} = erase(test_reply), auth => ?ADMIN_ID,
?assertEqual(403, Status). body => jsx:encode(#{<<"action">> => <<"block">>})
}),
?assertEqual(400, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_moderation, #{
method => <<"PUT">>,
path => <<"/v1/admin/user/", ?USER_ID/binary>>,
bindings => #{target_type => <<"user">>, id => ?USER_ID},
auth => none,
body => jsx:encode(#{<<"action">> => <<"block">>})
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{
ok = meck:expect(cowboy_req, reply, method => <<"GET">>,
fun(Code, Headers, Body, Req) -> path => <<"/v1/admin/user/", ?USER_ID/binary>>,
put(test_reply, {Code, Headers, Body, Req}) bindings => #{target_type => <<"user">>, id => ?USER_ID},
end), auth => ?ADMIN_ID
{ok, _, _} = admin_handler_moderation:init(req, []), }),
{Status, _, RespBody, _} = erase(test_reply), ?assertEqual(405, Status).
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).
@@ -0,0 +1,112 @@
-module(admin_handler_node_metrics_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, node_metric, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_node_metrics_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET empty range", {timeout, 60, fun test_empty/0}},
{"GET with seeded metric", {timeout, 60, fun test_with_data/0}},
{"GET missing from", {timeout, 60, fun test_missing_from/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
range_qs() ->
From = handler_utils:datetime_to_iso8601(eh_test_support:days_from_now(-1)),
To = handler_utils:datetime_to_iso8601(eh_test_support:days_from_now(1)),
[{<<"from">>, From}, {<<"to">>, To}].
seed_metric() ->
Now = calendar:universal_time(),
Metric = #node_metric{
timestamp = Now,
node = node(),
memory_total = 1000,
memory_processes = 100,
memory_atom = 10,
memory_binary = 20,
memory_ets = 30,
process_count = 5,
run_queue = 0,
mnesia_commits = 1,
mnesia_failures = 0,
table_sizes = #{},
active_sessions = 0,
ws_connections = 0,
io_in = 0,
io_out = 0,
memory_available = 500,
cpu_utilization = 1.5
},
ok = core_node_metric:save(Metric),
Metric.
test_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_node_metrics, #{
method => <<"GET">>,
path => <<"/v1/admin/nodes/metrics">>,
qs => range_qs(),
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual([], Result).
test_with_data() ->
_ = seed_metric(),
{Status, _, Body} = eh_test_support:call(admin_handler_node_metrics, #{
method => <<"GET">>,
path => <<"/v1/admin/nodes/metrics">>,
qs => range_qs(),
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[M] = Result,
?assertEqual(1000, maps:get(<<"memory_total">>, M)),
?assertEqual(5, maps:get(<<"process_count">>, M)).
test_missing_from() ->
To = handler_utils:datetime_to_iso8601(eh_test_support:days_from_now(1)),
{Status, _, _} = eh_test_support:call(admin_handler_node_metrics, #{
method => <<"GET">>,
path => <<"/v1/admin/nodes/metrics">>,
qs => [{<<"to">>, To}],
auth => ?ADMIN_ID
}),
?assertEqual(400, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_node_metrics, #{
method => <<"GET">>,
path => <<"/v1/admin/nodes/metrics">>,
qs => range_qs(),
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_node_metrics, #{
method => <<"POST">>,
path => <<"/v1/admin/nodes/metrics">>,
qs => range_qs(),
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+83
View File
@@ -0,0 +1,83 @@
-module(admin_handler_refresh_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, auth_session, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
_ = mnesia:add_table_index(auth_session, #auth_session.family_id),
_ = mnesia:add_table_index(auth_session, #auth_session.subject_id),
eh_test_support:ensure_jwt(),
catch ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]),
eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => admin}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_refresh_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"POST refresh success", {timeout, 60, fun test_refresh_ok/0}},
{"POST refresh invalid token", {timeout, 60, fun test_refresh_invalid/0}},
{"POST refresh missing field", {timeout, 60, fun test_refresh_missing_field/0}},
{"POST refresh invalid JSON", {timeout, 60, fun test_refresh_invalid_json/0}},
{"GET refresh method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_refresh_ok() ->
{ok, _Access, Refresh} = logic_auth_session:issue_admin_tokens(?ADMIN_ID, <<"admin">>),
{Status, _, Body} = eh_test_support:call(admin_handler_refresh, #{
method => <<"POST">>,
path => <<"/v1/admin/refresh">>,
auth => none,
body => jsx:encode(#{<<"refresh_token">> => Refresh})
}),
?assertEqual(200, Status),
Resp = jsx:decode(Body, [return_maps]),
?assert(is_map_key(<<"token">>, Resp)),
?assert(is_map_key(<<"refresh_token">>, Resp)),
NewRefresh = maps:get(<<"refresh_token">>, Resp),
?assertNotEqual(Refresh, NewRefresh),
{ok, _, _} = eventhub_auth:verify_admin_token(maps:get(<<"token">>, Resp)).
test_refresh_invalid() ->
{Status, _, Body} = eh_test_support:call(admin_handler_refresh, #{
method => <<"POST">>,
path => <<"/v1/admin/refresh">>,
auth => none,
body => jsx:encode(#{<<"refresh_token">> => <<"not-a-valid-token">>})
}),
?assertEqual(401, Status),
#{<<"error">> := _} = jsx:decode(Body, [return_maps]).
test_refresh_missing_field() ->
{Status, _, _} = eh_test_support:call(admin_handler_refresh, #{
method => <<"POST">>,
path => <<"/v1/admin/refresh">>,
auth => none,
body => jsx:encode(#{<<"token">> => <<"x">>})
}),
?assertEqual(400, Status).
test_refresh_invalid_json() ->
{Status, _, _} = eh_test_support:call(admin_handler_refresh, #{
method => <<"POST">>,
path => <<"/v1/admin/refresh">>,
auth => none,
body => <<"not-json">>
}),
?assertEqual(400, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_refresh, #{
method => <<"GET">>,
path => <<"/v1/admin/refresh">>,
auth => none
}),
?assertEqual(405, Status).
+93 -125
View File
@@ -2,144 +2,112 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, report, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_report, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_report), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_report_by_id_test_() -> admin_report_by_id_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/reports/:id success", fun test_get_report/0}, {"GET success", fun test_get/0},
{"GET /admin/reports/:id not found", fun test_get_report_not_found/0}, {"GET not found", fun test_get_not_found/0},
{"GET /admin/reports/:id forbidden", fun test_get_report_forbidden/0}, {"PUT update status", fun test_update/0},
{"PUT /admin/reports/:id success", fun test_update_report/0}, {"PUT missing status", fun test_update_missing/0},
{"PUT /admin/reports/:id not found", fun test_update_report_not_found/0}, {"PUT not found", fun test_update_not_found/0},
{"PUT /admin/reports/:id bad JSON", fun test_update_report_bad_json/0}, {"GET unauthorized", fun test_unauthorized/0},
{"DELETE /admin/reports/:id method not allowed", fun test_wrong_method/0} {"PATCH method not allowed", fun test_wrong_method/0}
]}. ]}.
%% GET успех seed_report() ->
test_get_report() -> {ok, Report} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Report.
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
Report = #report{
id = <<"r1">>,
reporter_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = pending,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
ok = meck:expect(core_report, get_by_id,
fun(<<"r1">>) -> {ok, Report} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>} = jsx:decode(RespBody, [return_maps]).
%% GET – не найдено test_get() ->
test_get_report_not_found() -> Report = seed_report(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Id = Report#report.id,
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_report_by_id, #{
fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"GET">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/reports/", Id/binary>>,
ok = meck:expect(core_user, get_by_id, bindings => #{id => Id},
fun(<<"adm1">>) -> {ok, AdminUser} end), auth => ?ADMIN_ID
ok = meck:expect(cowboy_req, binding, }),
fun(id, _) -> <<"r99">> end), ?assertEqual(200, Status),
ok = meck:expect(core_report, get_by_id, Result = jsx:decode(Body, [return_maps]),
fun(_) -> {error, not_found} end), ?assertEqual(Id, maps:get(<<"id">>, Result)),
{ok, _, _} = admin_handler_report_by_id:init(req, []), ?assertEqual(<<"spam">>, maps:get(<<"reason">>, Result)).
{Status, _, _, _} = erase(test_reply),
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/reports/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status). ?assertEqual(404, Status).
%% GET запрещён test_update() ->
test_get_report_forbidden() -> Report = seed_report(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Id = Report#report.id,
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_report_by_id, #{
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), method => <<"PUT">>,
{ok, _, _} = admin_handler_report_by_id:init(req, []), path => <<"/v1/admin/reports/", Id/binary>>,
{Status, _, _, _} = erase(test_reply), bindings => #{id => Id},
?assertEqual(403, Status). auth => ?ADMIN_ID,
body => jsx:encode(#{<<"status">> => <<"reviewed">>})
%% PUT успех }),
test_update_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = reviewed},
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, reviewed, <<"adm1">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"reviewed">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
%% PUT – не найдено test_update_missing() ->
test_update_report_not_found() -> Report = seed_report(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), Id = Report#report.id,
ok = meck:expect(handler_auth, authenticate, {Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{
fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"PUT">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/reports/", Id/binary>>,
ok = meck:expect(core_user, get_by_id, bindings => #{id => Id},
fun(<<"adm1">>) -> {ok, AdminUser} end), auth => ?ADMIN_ID,
ok = meck:expect(cowboy_req, binding, body => jsx:encode(#{<<"other">> => <<"x">>})
fun(id, _) -> <<"r99">> end), }),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(<<"r99">>, reviewed, <<"adm1">>) -> {error, not_found} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT – невалидный JSON
test_update_report_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_report_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status). ?assertEqual(400, Status).
%% Неверный метод test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/reports/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"status">> => <<"dismissed">>})
}),
?assertEqual(404, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/reports/x">>,
bindings => #{id => <<"x">>},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), {Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{
{ok, _, _} = admin_handler_report_by_id:init(req, []), method => <<"PATCH">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/reports/x">>,
?assertEqual(405, Status), bindings => #{id => <<"x">>},
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,74 @@
-module(admin_handler_report_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, report, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_report_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with reports", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_report_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reports/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_reports">>, Result)),
?assertEqual(#{}, maps:get(<<"reports_by_target_type">>, Result)),
?assertEqual(#{}, maps:get(<<"reports_by_status">>, Result)),
?assertEqual([], maps:get(<<"top_targets_by_reports">>, Result)).
test_stats_with_data() ->
{ok, _} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>),
{ok, _} = core_report:create(<<"u2">>, event, <<"ev1">>, <<"abuse">>),
{Status, _, Body} = eh_test_support:call(admin_handler_report_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reports/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_reports">>, Result)),
ByType = maps:get(<<"reports_by_target_type">>, Result),
?assertEqual(1, maps:get(<<"calendar">>, ByType)),
?assertEqual(1, maps:get(<<"event">>, ByType)),
ByStatus = maps:get(<<"reports_by_status">>, Result),
?assertEqual(2, maps:get(<<"pending">>, ByStatus)),
?assert(length(maps:get(<<"top_targets_by_reports">>, Result)) >= 1).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_report_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reports/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_report_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/reports/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+53 -110
View File
@@ -2,127 +2,70 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, report, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_report, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_report), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_reports_test_() -> admin_reports_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/reports success", fun test_list_reports/0}, {"GET list success", fun test_list/0},
{"GET /admin/reports forbidden", fun test_list_reports_forbidden/0}, {"GET list filter by status", fun test_list_filter/0},
{"PUT /admin/reports/:id success", fun test_update_report/0}, {"GET list unauthorized", fun test_list_unauthorized/0},
{"PUT /admin/reports/:id missing status", fun test_update_report_bad_json/0}, {"POST method not allowed", fun test_wrong_method/0}
{"PUT /admin/reports/:id not found", fun test_update_report_not_found/0},
{"POST /admin/reports method not allowed", fun test_wrong_method/0}
]}. ]}.
%% GET успех test_list() ->
test_list_reports() -> {ok, _} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, _} = core_report:create(<<"u2">>, event, <<"ev1">>, <<"abuse">>),
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_reports, #{
fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"GET">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/reports">>,
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID
fun(<<"adm1">>) -> {ok, AdminUser} end), }),
Report = #report{
id = <<"r1">>,
reporter_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
reason = <<"spam">>,
status = pending,
created_at = {{2026,4,26},{12,0,0}},
resolved_at = undefined
},
% 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), ?assertEqual(200, Status),
[#{<<"id">> := <<"r1">>, <<"status">> := <<"pending">>}] = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)).
%% GET запрещён test_list_filter() ->
test_list_reports_forbidden() -> {ok, _} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, R} = core_report:create(<<"u2">>, event, <<"ev1">>, <<"abuse">>),
ok = meck:expect(handler_auth, authenticate, {ok, _} = core_report:update_status(R#report.id, reviewed, ?ADMIN_ID),
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), {Status, _, Body} = eh_test_support:call(admin_handler_reports, #{
{ok, _, _} = admin_handler_reports:init(req, []), method => <<"GET">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/reports">>,
?assertEqual(403, Status), auth => ?ADMIN_ID,
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]). qs => [{<<"status">>, <<"pending">>}]
}),
%% PUT успех
test_update_report() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
Updated = #report{id = <<"r1">>, status = reviewed},
% обработчик передаёт бинарный статус, поэтому мок ожидает строку
ok = meck:expect(core_report, update_status,
fun(<<"r1">>, <<"reviewed">>, <<"adm1">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"reviewed">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"pending">>, maps:get(<<"status">>, Only)).
%% PUT невалидный JSON test_list_unauthorized() ->
test_update_report_bad_json() -> {Status, _, _} = eh_test_support:call(admin_handler_reports, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), method => <<"GET">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/reports">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), auth => none
AdminUser = #user{id = <<"adm1">>, role = admin}, }),
ok = meck:expect(core_user, get_by_id, ?assertEqual(401, Status).
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% PUT не найдено
test_update_report_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"r99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end),
ok = meck:expect(core_report, update_status,
fun(<<"r99">>, <<"reviewed">>, <<"adm1">>) -> {error, not_found} end),
{ok, _, _} = admin_handler_reports:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неправильный метод
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_reports, #{
{ok, _, _} = admin_handler_reports:init(req, []), method => <<"POST">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/reports">>,
?assertEqual(405, Status), auth => ?ADMIN_ID
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). }),
?assertEqual(405, Status).
@@ -0,0 +1,103 @@
-module(admin_handler_reviews_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, review, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_reviews_by_id_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET not found", {timeout, 60, fun test_get_not_found/0}},
{"PUT update status", {timeout, 60, fun test_update/0}},
{"PUT not found", {timeout, 60, fun test_update_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PATCH method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_review() ->
{ok, Review} = core_review:create(<<"u1">>, calendar, <<"cal1">>, 5, <<"great">>),
Review.
test_get() ->
Review = seed_review(),
Id = Review#review.id,
{Status, _, Body} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/", Id/binary>>,
bindings => #{id => Id},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(Id, maps:get(<<"id">>, Result)),
?assertEqual(<<"great">>, maps:get(<<"comment">>, Result)).
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_update() ->
Review = seed_review(),
Id = Review#review.id,
{Status, _, Body} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/reviews/", Id/binary>>,
bindings => #{id => Id},
auth => ?ADMIN_ID,
body => jsx:encode(#{
<<"status">> => <<"hidden">>,
<<"reason">> => <<"moderation">>
})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"hidden">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/reviews/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"status">> => <<"hidden">>})
}),
?assertEqual(404, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/x">>,
bindings => #{id => <<"x">>},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_by_id, #{
method => <<"PATCH">>,
path => <<"/v1/admin/reviews/x">>,
bindings => #{id => <<"x">>},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,76 @@
-module(admin_handler_reviews_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, review, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_reviews_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with reviews", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_reviews_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_reviews">>, Result)),
?assertEqual(#{}, maps:get(<<"reviews_by_target_type">>, Result)),
?assertEqual(#{}, maps:get(<<"reviews_by_status">>, Result)),
?assertEqual([], maps:get(<<"top_targets_by_reviews">>, Result)).
test_stats_with_data() ->
{ok, _} = core_review:create(<<"u1">>, calendar, <<"cal1">>, 5, <<"great">>),
{ok, _} = core_review:create(<<"u2">>, event, <<"ev1">>, 1, <<"bad">>),
{Status, _, Body} = eh_test_support:call(admin_handler_reviews_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_reviews">>, Result)),
ByType = maps:get(<<"reviews_by_target_type">>, Result),
?assertEqual(1, maps:get(<<"calendar">>, ByType)),
?assertEqual(1, maps:get(<<"event">>, ByType)),
ByStatus = maps:get(<<"reviews_by_status">>, Result),
?assertEqual(2, maps:get(<<"visible">>, ByStatus)),
?assert(length(maps:get(<<"top_targets_by_reviews">>, Result)) >= 1),
?assert(length(maps:get(<<"top_targets_by_positive_reviews">>, Result)) >= 1),
?assert(length(maps:get(<<"top_targets_by_negative_reviews">>, Result)) >= 1).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/reviews/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+67 -127
View File
@@ -2,145 +2,85 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, review, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_review, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_review), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_reviews_test_() -> admin_reviews_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/reviews/:id success", fun test_get_review/0}, {"GET list success", fun test_list/0},
{"GET /admin/reviews/:id not found", fun test_get_review_not_found/0}, {"PATCH bulk success", fun test_bulk/0},
{"GET /admin/reviews/:id forbidden", fun test_get_review_forbidden/0}, {"PATCH bulk missing reason", fun test_bulk_missing/0},
{"PUT /admin/reviews/:id success", fun test_update_review/0}, {"GET unauthorized", fun test_unauthorized/0},
{"PUT /admin/reviews/:id not found", fun test_update_review_not_found/0}, {"POST method not allowed", fun test_wrong_method/0}
{"PUT /admin/reviews/:id bad JSON", fun test_update_review_bad_json/0},
{"DELETE /admin/reviews/:id method not allowed", fun test_wrong_method/0}
]}. ]}.
%% GET успех seed_review() ->
test_get_review() -> {ok, Review} = core_review:create(<<"u1">>, calendar, <<"cal1">>, 5, <<"great">>),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Review.
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end), test_list() ->
AdminUser = #user{id = <<"adm1">>, role = admin}, _ = seed_review(),
ok = meck:expect(core_user, get_by_id, {ok, _} = core_review:create(<<"u2">>, event, <<"ev1">>, 3, <<"ok">>),
fun(<<"adm1">>) -> {ok, AdminUser} end), {Status, _, Body} = eh_test_support:call(admin_handler_reviews, #{
ok = meck:expect(cowboy_req, binding, method => <<"GET">>,
fun(id, _) -> <<"rv1">> end), path => <<"/v1/admin/reviews">>,
Review = #review{ auth => ?ADMIN_ID
id = <<"rv1">>, }),
user_id = <<"u1">>,
target_type = <<"event">>,
target_id = <<"e1">>,
rating = 5,
comment = <<"Great!">>,
status = <<"active">>,
created_at = {{2026,4,26},{12,0,0}},
updated_at = {{2026,4,26},{12,0,0}}
},
ok = meck:expect(core_review, get_by_id,
fun(<<"rv1">>) -> {ok, Review} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"id">> := <<"rv1">>, <<"comment">> := <<"Great!">>, <<"rating">> := 5} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, length(Result)).
%% GET не найдено test_bulk() ->
test_get_review_not_found() -> Review = seed_review(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Id = Review#review.id,
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_reviews, #{
fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"PATCH">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/reviews">>,
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID,
fun(<<"adm1">>) -> {ok, AdminUser} end), body => jsx:encode(#{
ok = meck:expect(cowboy_req, binding, <<"operations">> => [#{<<"id">> => Id, <<"status">> => <<"hidden">>}],
fun(id, _) -> <<"rv99">> end), <<"reason">> => <<"moderation">>
ok = meck:expect(core_review, get_by_id, })
fun(_) -> {error, not_found} end), }),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% GET запрещён
test_get_review_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(403, Status).
%% PUT успех
test_update_review() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end),
Updated = #review{id = <<"rv1">>, status = <<"hidden">>},
ok = meck:expect(core_review, update_status,
fun(<<"rv1">>, <<"hidden">>) -> {ok, Updated} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"hidden">>} = jsx:decode(RespBody, [return_maps]). #{<<"updated_count">> := 1} = jsx:decode(Body, [return_maps]),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
%% PUT не найдено test_bulk_missing() ->
test_update_review_not_found() -> {Status, _, _} = eh_test_support:call(admin_handler_reviews, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), method => <<"PATCH">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/reviews">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), auth => ?ADMIN_ID,
AdminUser = #user{id = <<"adm1">>, role = admin}, body => jsx:encode(#{<<"operations">> => []})
ok = meck:expect(core_user, get_by_id, }),
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end),
ok = meck:expect(core_review, update_status,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT невалидный JSON
test_update_review_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"rv1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"bad json">>, Req} end),
{ok, _, _} = admin_handler_reviews:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status). ?assertEqual(400, Status).
%% Неверный метод test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_reviews, #{
method => <<"GET">>,
path => <<"/v1/admin/reviews">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), {Status, _, _} = eh_test_support:call(admin_handler_reviews, #{
{ok, _, _} = admin_handler_reviews:init(req, []), method => <<"POST">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/reviews">>,
?assertEqual(405, Status), auth => ?ADMIN_ID
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). }),
?assertEqual(405, Status).
+60 -86
View File
@@ -2,101 +2,75 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, user, event, calendar, review, report,
ticket, subscription]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(admin_utils, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_admin, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => admin}),
ok = meck:new(logic_stats, [non_strict]),
ok = meck:expect(cowboy_req, reply,
fun(Code, _, _, _) -> put(test_reply, Code) end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(logic_stats), eh_test_support:unload_cowboy(),
meck:unload(core_admin), eh_test_support:delete_tables(?TABLES),
meck:unload(admin_utils), eh_test_support:stop_mnesia(),
meck:unload(handler_auth), ok.
meck:unload(cowboy_req).
admin_stats_test_() -> admin_stats_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/stats as superadmin returns 200 with system metrics", fun test_superadmin/0}, {"GET stats success empty", fun test_stats_empty/0},
{"GET /admin/stats as superadmin with date filter", fun test_superadmin_dates/0}, {"GET stats with seeded data", fun test_stats_with_data/0},
{"GET /admin/stats as moderator returns 200 with own metrics", fun test_moderator/0}, {"GET unauthorized", fun test_unauthorized/0},
{"GET /admin/stats as support returns 200 with assigned tickets", fun test_support/0}, {"POST method not allowed", fun test_wrong_method/0}
{"GET /admin/stats with nonadmin token returns 403", fun test_forbidden/0},
{"POST /admin/stats returns 405", fun test_wrong_method/0}
]}. ]}.
%% --- Суперадмин (без дат) --- test_stats_empty() ->
test_superadmin() -> {Status, _, Body} = eh_test_support:call(admin_handler_stats, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end), path => <<"/v1/admin/stats">>,
ok = meck:expect(handler_auth, authenticate, auth => ?ADMIN_ID
fun(Req) -> {ok, <<"adm1">>, Req} end), }),
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), ?assertEqual(200, Status),
ok = meck:expect(core_admin, get_by_id, Result = jsx:decode(Body, [return_maps]),
fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end), ?assertEqual(0, maps:get(<<"users_total">>, Result)),
ok = meck:expect(logic_stats, get_stats, ?assertEqual(0, maps:get(<<"events_total">>, Result)),
fun(superadmin, _) -> #{users => 10, events => 25} end), ?assertEqual(0, maps:get(<<"tickets_total">>, Result)),
{ok, _, _} = admin_handler_stats:init(req, []), ?assert(is_map_key(<<"registrations_by_day">>, Result)).
?assertEqual(200, erase(test_reply)).
%% --- Суперадмин (с датами) --- test_stats_with_data() ->
test_superadmin_dates() -> ok = mnesia:dirty_write(eh_test_support:make_user(#{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), id => <<"u1">>,
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> email => <<"u1@test.local">>,
[{<<"from">>, <<"2026-01-01T00:00:00">>}, {<<"to">>, <<"2026-06-01T00:00:00">>}] status => active
end), })),
ok = meck:expect(handler_auth, authenticate, {ok, _} = core_report:create(<<"u1">>, calendar, <<"c1">>, <<"spam">>),
fun(Req) -> {ok, <<"adm1">>, Req} end), {ok, _} = core_ticket:create(<<"u1">>, <<"hash1">>, <<"boom">>, <<"stack">>),
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), {Status, _, Body} = eh_test_support:call(admin_handler_stats, #{
ok = meck:expect(core_admin, get_by_id, method => <<"GET">>,
fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end), path => <<"/v1/admin/stats">>,
ok = meck:expect(logic_stats, get_stats, auth => ?ADMIN_ID
fun(superadmin, _, _, _) -> #{users => 10} end), }),
{ok, _, _} = admin_handler_stats:init(req, []), ?assertEqual(200, Status),
?assertEqual(200, erase(test_reply)). Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, maps:get(<<"users_total">>, Result)),
?assertEqual(1, maps:get(<<"reports_total">>, Result)),
?assertEqual(1, maps:get(<<"tickets_total">>, Result)).
%% --- Модератор --- test_unauthorized() ->
test_moderator() -> {Status, _, _} = eh_test_support:call(admin_handler_stats, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end), path => <<"/v1/admin/stats">>,
ok = meck:expect(handler_auth, authenticate, auth => none
fun(Req) -> {ok, <<"mod1">>, Req} end), }),
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), ?assertEqual(401, Status).
ok = meck:expect(core_admin, get_by_id,
fun(<<"mod1">>) -> {ok, #admin{id = <<"mod1">>, role = moderator}} end),
ok = meck:expect(logic_stats, get_stats,
fun(moderator, _) -> #{reports_reviewed => 5} end),
{ok, _, _} = admin_handler_stats:init(req, []),
?assertEqual(200, erase(test_reply)).
%% --- Поддержка ---
test_support() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(cowboy_req, parse_qs, fun(_) -> [] end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"sup1">>, Req} end),
ok = meck:expect(admin_utils, is_admin, fun(_) -> true end),
ok = meck:expect(core_admin, get_by_id,
fun(<<"sup1">>) -> {ok, #admin{id = <<"sup1">>, role = support}} end),
ok = meck:expect(logic_stats, get_stats,
fun(support, _) -> #{tickets_assigned => 3} end),
{ok, _, _} = admin_handler_stats:init(req, []),
?assertEqual(200, erase(test_reply)).
%% --- Не админ ---
test_forbidden() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end),
{ok, _, _} = admin_handler_stats:init(req, []),
?assertEqual(403, erase(test_reply)).
%% --- Неверный метод ---
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_stats, #{
{ok, _, _} = admin_handler_stats:init(req, []), method => <<"POST">>,
?assertEqual(405, erase(test_reply)). path => <<"/v1/admin/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,85 @@
-module(admin_handler_subscription_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, subscription, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_subscription_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with subscriptions", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_subscription_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_subscriptions">>, Result)),
?assertEqual(0, maps:get(<<"trial_subscriptions">>, Result)),
?assertEqual([], maps:get(<<"ending_paid_subscriptions">>, Result)).
test_stats_with_data() ->
{ok, _} = core_subscription:create(<<"u1">>, monthly, false),
Now = calendar:universal_time(),
Expires = eh_test_support:days_from_now(15),
ok = mnesia:dirty_write(#subscription{
id = <<"sub_paid">>,
user_id = <<"u2">>,
plan = annual,
status = active,
trial_used = true,
started_at = Now,
expires_at = Expires,
created_at = Now,
updated_at = Now
}),
{Status, _, Body} = eh_test_support:call(admin_handler_subscription_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_subscriptions">>, Result)),
?assertEqual(1, maps:get(<<"trial_subscriptions">>, Result)),
ByPlan = maps:get(<<"subscriptions_by_plan">>, Result),
?assertEqual(1, maps:get(<<"monthly">>, ByPlan)),
?assertEqual(1, maps:get(<<"annual">>, ByPlan)),
Ending = maps:get(<<"ending_paid_subscriptions">>, Result),
?assertEqual(1, length(Ending)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscription_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscription_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/subscriptions/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,124 @@
-module(admin_handler_subscriptions_by_id_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, subscription, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_subscriptions_by_id_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET success", {timeout, 60, fun test_get/0}},
{"GET not found", {timeout, 60, fun test_get_not_found/0}},
{"PUT update status", {timeout, 60, fun test_update/0}},
{"PUT not found", {timeout, 60, fun test_update_not_found/0}},
{"DELETE success", {timeout, 60, fun test_delete/0}},
{"DELETE not found", {timeout, 60, fun test_delete_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"PATCH method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_sub() ->
{ok, Sub} = core_subscription:create(<<"u1">>, monthly, true),
Sub.
test_get() ->
Sub = seed_sub(),
Id = Sub#subscription.id,
{Status, _, Body} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/", Id/binary>>,
bindings => #{id => Id},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(Id, maps:get(<<"id">>, Result)),
?assertEqual(<<"monthly">>, maps:get(<<"plan">>, Result)).
test_get_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_update() ->
Sub = seed_sub(),
Id = Sub#subscription.id,
{Status, _, Body} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/subscriptions/", Id/binary>>,
bindings => #{id => Id},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"status">> => <<"cancelled">>})
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"cancelled">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
test_update_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"PUT">>,
path => <<"/v1/admin/subscriptions/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID,
body => jsx:encode(#{<<"status">> => <<"cancelled">>})
}),
?assertEqual(404, Status).
test_delete() ->
Sub = seed_sub(),
Id = Sub#subscription.id,
{Status, _, Body} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/subscriptions/", Id/binary>>,
bindings => #{id => Id},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
?assertEqual({error, not_found}, core_subscription:get_by_id(Id)).
test_delete_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"DELETE">>,
path => <<"/v1/admin/subscriptions/missing">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/subscriptions/x">>,
bindings => #{id => <<"x">>},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_subscriptions_by_id, #{
method => <<"PATCH">>,
path => <<"/v1/admin/subscriptions/x">>,
bindings => #{id => <<"x">>},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+51 -200
View File
@@ -2,218 +2,69 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, subscription, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_subscription, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_subscription), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_subscriptions_test_() -> admin_subscriptions_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/subscriptions success", fun test_list/0}, {"GET list success", fun test_list/0},
{"GET /admin/subscriptions forbidden", fun test_list_forbidden/0}, {"GET list filter by plan", fun test_list_filter/0},
{"POST /admin/subscriptions success", fun test_create/0}, {"GET unauthorized", fun test_unauthorized/0},
{"POST /admin/subscriptions missing user_id", fun test_create_missing/0}, {"POST method not allowed", fun test_wrong_method/0}
{"GET /admin/subscriptions/:id success", fun test_get/0},
{"GET /admin/subscriptions/:id not found", fun test_get_not_found/0},
{"PUT /admin/subscriptions/:id success", fun test_update/0},
{"PUT /admin/subscriptions/:id not found", fun test_update_not_found/0},
{"DELETE /admin/subscriptions/:id success", fun test_delete/0},
{"DELETE /admin/subscriptions/:id not found", fun test_delete_not_found/0},
{"PATCH /admin/subscriptions method not allowed", fun test_wrong_method/0}
]}. ]}.
%% GET список
test_list() -> test_list() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), {ok, _} = core_subscription:create(<<"u1">>, monthly, true),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, _} = core_subscription:create(<<"u2">>, annual, false),
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_subscriptions, #{
fun(Req) -> {ok, <<"adm1">>, Req} end), method => <<"GET">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, path => <<"/v1/admin/subscriptions">>,
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID
fun(<<"adm1">>) -> {ok, AdminUser} end), }),
Sub1 = #subscription{
id = <<"s1">>,
user_id = <<"u1">>,
plan = monthly,
status = active,
trial_used = false,
started_at = {{2026,4,27},{12,0,0}},
expires_at = {{2026,5,27},{12,0,0}},
created_at = {{2026,4,27},{12,0,0}},
updated_at = {{2026,4,27},{12,0,0}}
},
ok = meck:expect(core_subscription, list_subscriptions, fun() -> [Sub1] end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
[#{<<"id">> := <<"s1">>, <<"plan">> := <<"monthly">>, <<"status">> := <<"active">>}] = Result = jsx:decode(Body, [return_maps]),
jsx:decode(RespBody, [return_maps]). ?assertEqual(2, length(Result)).
test_list_forbidden() -> test_list_filter() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), {ok, _} = core_subscription:create(<<"u1">>, monthly, true),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, _} = core_subscription:create(<<"u2">>, annual, false),
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_subscriptions, #{
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), method => <<"GET">>,
{ok, _, _} = admin_handler_subscriptions:init(req, []), path => <<"/v1/admin/subscriptions">>,
{Status, _, _, _} = erase(test_reply), auth => ?ADMIN_ID,
?assertEqual(403, Status). qs => [{<<"plan">>, <<"monthly">>}]
}),
%% POST создание
test_create() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
BodyMap = #{<<"user_id">> => <<"u1">>, <<"plan">> => <<"yearly">>},
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(BodyMap), Req} end),
Created = #subscription{
id = <<"s_new">>, user_id = <<"u1">>, plan = yearly, status = active,
trial_used = false,
started_at = {{2026,4,27},{14,0,0}}, expires_at = {{2027,4,27},{14,0,0}},
created_at = {{2026,4,27},{14,0,0}}, updated_at = {{2026,4,27},{14,0,0}}
},
ok = meck:expect(core_subscription, create_subscription, fun(_) -> {ok, Created} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(201, Status),
#{<<"plan">> := <<"yearly">>, <<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]).
test_create_missing() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"plan">> => <<"monthly">>}), Req} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% GET по ID
test_get() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
Sub = #subscription{
id = <<"s1">>, user_id = <<"u1">>, plan = monthly, status = active,
trial_used = false,
started_at = {{2026,4,27},{12,0,0}}, expires_at = {{2026,5,27},{12,0,0}},
created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{12,0,0}}
},
ok = meck:expect(core_subscription, get_by_id,
fun(<<"s1">>) -> {ok, Sub} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"id">> := <<"s1">>, <<"plan">> := <<"monthly">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"monthly">>, maps:get(<<"plan">>, Only)).
test_get_not_found() -> test_unauthorized() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end), {Status, _, _} = eh_test_support:call(admin_handler_subscriptions, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/subscriptions">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), auth => none
AdminUser = #user{id = <<"adm1">>, role = admin}, }),
ok = meck:expect(core_user, get_by_id, ?assertEqual(401, Status).
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT обновление
test_update() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end),
Updated = #subscription{id = <<"s1">>, status = cancelled},
ok = meck:expect(core_subscription, update_subscription,
fun(<<"s1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"cancelled">>} = jsx:decode(RespBody, [return_maps]).
test_update_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end),
ok = meck:expect(core_subscription, update_subscription,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% DELETE
test_delete() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, delete_subscription,
fun(<<"s1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
test_delete_not_found() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_subscription, delete_subscription,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_subscriptions:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неверный метод
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), {Status, _, _} = eh_test_support:call(admin_handler_subscriptions, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), method => <<"POST">>,
{ok, _, _} = admin_handler_subscriptions:init(req, []), path => <<"/v1/admin/subscriptions">>,
{Status, _, RespBody, _} = erase(test_reply), auth => ?ADMIN_ID
?assertEqual(405, Status), }),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). ?assertEqual(405, Status).
+88 -160
View File
@@ -2,186 +2,114 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_ticket, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_ticket), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_ticket_by_id_test_() -> admin_ticket_by_id_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets/:id success", fun test_get/0}, {"GET success", fun test_get/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0}, {"GET not found", fun test_get_not_found/0},
{"GET /admin/tickets/:id forbidden", fun test_get_forbidden/0}, {"PUT resolve", fun test_resolve/0},
{"PUT /admin/tickets/:id success", fun test_update/0}, {"DELETE success", fun test_delete/0},
{"PUT /admin/tickets/:id not found", fun test_update_not_found/0}, {"DELETE not found", fun test_delete_not_found/0},
{"PUT /admin/tickets/:id bad JSON", fun test_update_bad_json/0}, {"GET unauthorized", fun test_unauthorized/0},
{"DELETE /admin/tickets/:id success", fun test_delete/0}, {"PATCH method not allowed", fun test_wrong_method/0}
{"DELETE /admin/tickets/:id not found", fun test_delete_not_found/0},
{"POST /admin/tickets/:id method not allowed", fun test_wrong_method/0}
]}. ]}.
%% GET успех seed_ticket() ->
{ok, Ticket} = core_ticket:create(<<"u1">>, <<"hash_x">>, <<"boom">>, <<"stack">>),
Ticket.
test_get() -> test_get() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Ticket = seed_ticket(),
ok = meck:expect(handler_auth, authenticate, Id = Ticket#ticket.id,
fun(Req) -> {ok, <<"adm1">>, Req} end), {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{
AdminUser = #user{id = <<"adm1">>, role = admin}, method => <<"GET">>,
ok = meck:expect(core_user, get_by_id, path => <<"/v1/admin/tickets/", Id/binary>>,
fun(<<"adm1">>) -> {ok, AdminUser} end), bindings => #{id => Id},
ok = meck:expect(cowboy_req, binding, auth => ?ADMIN_ID
fun(id, _) -> <<"t1">> end), }),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"hash">>,
error_message = <<"msg">>,
stacktrace = <<"trace">>,
context = <<"ctx">>,
count = 5,
first_seen = {{2026,4,27},{12,0,0}},
last_seen = {{2026,4,27},{13,0,0}},
status = open,
assigned_to = <<"dev1">>,
resolution_note = undefined
},
ok = meck:expect(core_ticket, get_by_id,
fun(<<"t1">>) -> {ok, Ticket} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"id">> := <<"t1">>, <<"error_message">> := <<"msg">>, <<"count">> := 5} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(Id, maps:get(<<"id">>, Result)),
?assertEqual(<<"boom">>, maps:get(<<"error_message">>, Result)).
%% GET не найдено
test_get_not_found() -> test_get_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{
ok = meck:expect(handler_auth, authenticate, method => <<"GET">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/tickets/missing">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{id => <<"missing">>},
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID
fun(<<"adm1">>) -> {ok, AdminUser} end), }),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(core_ticket, get_by_id,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status). ?assertEqual(404, Status).
%% GET запрещён test_resolve() ->
test_get_forbidden() -> Ticket = seed_ticket(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), Id = Ticket#ticket.id,
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), method => <<"PUT">>,
{ok, _, _} = admin_handler_ticket_by_id:init(req, []), path => <<"/v1/admin/tickets/", Id/binary>>,
{Status, _, _, _} = erase(test_reply), bindings => #{id => Id},
?assertEqual(403, Status). auth => ?ADMIN_ID,
body => jsx:encode(#{
%% PUT успех <<"status">> => <<"resolved">>,
test_update() -> <<"resolution_note">> => <<"fixed">>
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), })
ok = meck:expect(handler_auth, authenticate, }),
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>, <<"assigned_to">> => <<"adm2">>}), Req} end),
Updated = #ticket{id = <<"t1">>, status = closed, assigned_to = <<"adm2">>},
ok = meck:expect(core_ticket, update_ticket,
fun(<<"t1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"closed">>, <<"assigned_to">> := <<"adm2">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"resolved">>, maps:get(<<"status">>, Result)),
?assertEqual(<<"fixed">>, maps:get(<<"resolution_note">>, Result)).
%% PUT не найдено
test_update_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
ok = meck:expect(core_ticket, update_ticket,
fun(_, _) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT невалидный JSON
test_update_bad_json() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"adm1">>, Req} end),
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t1">> end),
ok = meck:expect(cowboy_req, read_body,
fun(Req) -> {ok, <<"not json">>, Req} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(400, Status).
%% DELETE успех
test_delete() -> test_delete() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), Ticket = seed_ticket(),
ok = meck:expect(handler_auth, authenticate, Id = Ticket#ticket.id,
fun(Req) -> {ok, <<"adm1">>, Req} end), {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{
AdminUser = #user{id = <<"adm1">>, role = admin}, method => <<"DELETE">>,
ok = meck:expect(core_user, get_by_id, path => <<"/v1/admin/tickets/", Id/binary>>,
fun(<<"adm1">>) -> {ok, AdminUser} end), bindings => #{id => Id},
ok = meck:expect(cowboy_req, binding, auth => ?ADMIN_ID
fun(id, _) -> <<"t1">> end), }),
ok = meck:expect(core_ticket, delete_ticket,
fun(<<"t1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]). #{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
?assertEqual({error, not_found}, core_ticket:get_by_id(Id)).
%% DELETE не найдено
test_delete_not_found() -> test_delete_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{
ok = meck:expect(handler_auth, authenticate, method => <<"DELETE">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/tickets/missing">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{id => <<"missing">>},
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID
fun(<<"adm1">>) -> {ok, AdminUser} end), }),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"t99">> end),
ok = meck:expect(core_ticket, delete_ticket,
fun(_) -> {error, not_found} end),
{ok, _, _} = admin_handler_ticket_by_id:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status). ?assertEqual(404, Status).
%% Неверный метод test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{
method => <<"GET">>,
path => <<"/v1/admin/tickets/x">>,
bindings => #{id => <<"x">>},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{
ok = meck:expect(cowboy_req, reply, method => <<"PATCH">>,
fun(Code, Headers, Body, Req) -> path => <<"/v1/admin/tickets/x">>,
put(test_reply, {Code, Headers, Body, Req}) bindings => #{id => <<"x">>},
end), auth => ?ADMIN_ID
{ok, _, _} = admin_handler_ticket_by_id:init(req, []), }),
{Status, _, RespBody, _} = erase(test_reply), ?assertEqual(405, Status).
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).
+55 -45
View File
@@ -2,60 +2,70 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_ticket, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_ticket), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_ticket_stats_test_() -> admin_ticket_stats_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets/stats success", fun test_stats/0}, {"GET stats empty", fun test_stats_empty/0},
{"GET /admin/tickets/stats forbidden", fun test_stats_forbidden/0}, {"GET stats with tickets", fun test_stats_with_data/0},
{"POST /admin/tickets/stats method not allowed", fun test_wrong_method/0} {"GET unauthorized", fun test_unauthorized/0},
{"POST method not allowed", fun test_wrong_method/0}
]}. ]}.
test_stats() -> test_stats_empty() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {Status, _, Body} = eh_test_support:call(admin_handler_ticket_stats, #{
ok = meck:expect(handler_auth, authenticate, method => <<"GET">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/tickets/stats">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"adm1">>) -> {ok, AdminUser} end),
StatsData = #{
open => 5,
in_progress => 3,
resolved => 12,
closed => 20
},
ok = meck:expect(core_ticket, stats, fun() -> StatsData end),
{ok, _, _} = admin_handler_ticket_stats:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"open">> := 5, <<"resolved">> := 12} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_tickets">>, Result)),
?assertEqual(0, maps:get(<<"open">>, Result)),
?assertEqual(0, maps:get(<<"total_errors">>, Result)).
test_stats_forbidden() -> test_stats_with_data() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, T1} = core_ticket:create(<<"u1">>, <<"h1">>, <<"err1">>, <<"s">>),
ok = meck:expect(handler_auth, authenticate, {ok, _} = core_ticket:create(<<"u2">>, <<"h2">>, <<"err2">>, <<"s">>),
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), {ok, _} = logic_ticket:update_status(?ADMIN_ID, T1#ticket.id, in_progress),
{ok, _, _} = admin_handler_ticket_stats:init(req, []), {Status, _, Body} = eh_test_support:call(admin_handler_ticket_stats, #{
{Status, _, _, _} = erase(test_reply), method => <<"GET">>,
?assertEqual(403, Status). path => <<"/v1/admin/tickets/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_tickets">>, Result)),
?assertEqual(1, maps:get(<<"open">>, Result)),
?assertEqual(1, maps:get(<<"in_progress">>, Result)),
?assertEqual(2, maps:get(<<"total_errors">>, Result)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_ticket_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/tickets/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_ticket_stats, #{
{ok, _, _} = admin_handler_ticket_stats:init(req, []), method => <<"POST">>,
{Status, _, RespBody, _} = erase(test_reply), path => <<"/v1/admin/tickets/stats">>,
?assertEqual(405, Status), auth => ?ADMIN_ID
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). }),
?assertEqual(405, Status).
+78 -206
View File
@@ -2,230 +2,102 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
ok = meck:new(core_ticket, [non_strict]), eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_ticket), eh_test_support:unload_cowboy(),
meck:unload(core_user), eh_test_support:delete_tables(?TABLES),
meck:unload(handler_auth), eh_test_support:stop_mnesia(),
meck:unload(cowboy_req). ok.
admin_tickets_test_() -> admin_tickets_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/tickets success", fun test_list/0}, {"GET list success", fun test_list/0},
{"GET /admin/tickets forbidden", fun test_list_forbidden/0}, {"GET list filter by status", fun test_list_filter/0},
{"POST /admin/tickets success", fun test_create/0}, {"PUT update success", fun test_update/0},
{"POST /admin/tickets missing error_message", fun test_create_missing/0}, {"PUT update not found", fun test_update_not_found/0},
{"GET /admin/tickets/:id success", fun test_get/0}, {"GET unauthorized", fun test_unauthorized/0},
{"GET /admin/tickets/:id not found", fun test_get_not_found/0}, {"POST method not allowed", fun test_wrong_method/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 список тикетов (успех) seed_ticket() ->
{ok, Ticket} = core_ticket:create(<<"u1">>, <<"hash_a">>, <<"error A">>, <<"stack">>),
Ticket.
test_list() -> test_list() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), _ = seed_ticket(),
ok = meck:expect(cowboy_req, binding, {ok, _} = core_ticket:create(<<"u2">>, <<"hash_b">>, <<"error B">>, <<"stack">>),
fun(id, _) -> undefined end), % для маршрута без id {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(handler_auth, authenticate, method => <<"GET">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/tickets">>,
AdminUser = #user{id = <<"adm1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"adm1">>) -> {ok, AdminUser} end),
Ticket = #ticket{
id = <<"t1">>,
error_hash = <<"hash1">>,
error_message = <<"Error message">>,
stacktrace = <<"stack">>,
context = <<"ctx">>,
count = 1,
first_seen = {{2026,4,28},{12,0,0}},
last_seen = {{2026,4,28},{12,0,0}},
status = open,
assigned_to = undefined,
resolution_note = undefined
},
ok = meck:expect(core_ticket, list_all, fun() -> [Ticket] end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
[#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Error message">>}] = Result = jsx:decode(Body, [return_maps]),
jsx:decode(RespBody, [return_maps]). ?assertEqual(2, length(Result)).
%% GET запрещён test_list_filter() ->
test_list_forbidden() -> Ticket = seed_ticket(),
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), {ok, _} = logic_ticket:update_status(?ADMIN_ID, Ticket#ticket.id, in_progress),
ok = meck:expect(cowboy_req, binding, {ok, _} = core_ticket:create(<<"u2">>, <<"hash_b">>, <<"error B">>, <<"stack">>),
fun(id, _) -> undefined end), % для маршрута без id {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(handler_auth, authenticate, method => <<"GET">>,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), path => <<"/v1/admin/tickets">>,
{ok, _, _} = admin_handler_tickets:init(req, []), auth => ?ADMIN_ID,
{Status, _, _, _} = erase(test_reply), qs => [{<<"status">>, <<"open">>}]
?assertEqual(403, Status). }),
%% POST создание тикета
test_create() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> 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">> => <<"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">> := <<"Bug">>} = jsx:decode(RespBody, [return_maps]).
%% POST отсутствует поле error_message
test_create_missing() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> 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(#{<<"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, method, fun(_) -> <<"GET">> 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_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), ?assertEqual(200, Status),
#{<<"id">> := <<"t1">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"open">>, maps:get(<<"status">>, Only)).
%% GET тикет не найден
test_get_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> 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, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% PUT обновление тикета
test_update() -> test_update() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), Ticket = seed_ticket(),
ok = meck:expect(cowboy_req, binding, Id = Ticket#ticket.id,
fun(id, _) -> <<"t1">> end), % для маршрута с id {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(handler_auth, authenticate, method => <<"PUT">>,
fun(Req) -> {ok, <<"adm1">>, Req} end), path => <<"/v1/admin/tickets/", Id/binary>>,
AdminUser = #user{id = <<"adm1">>, role = admin}, bindings => #{id => Id},
ok = meck:expect(core_user, get_by_id, auth => ?ADMIN_ID,
fun(<<"adm1">>) -> {ok, AdminUser} end), body => jsx:encode(#{<<"status">> => <<"in_progress">>})
ok = meck:expect(cowboy_req, read_body, }),
fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end),
Updated = #ticket{id = <<"t1">>, status = closed},
ok = meck:expect(core_ticket, update_ticket,
fun(<<"t1">>, _) -> {ok, Updated} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"closed">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"in_progress">>, maps:get(<<"status">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
%% PUT тикет не найден
test_update_not_found() -> test_update_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(cowboy_req, binding, method => <<"PUT">>,
fun(id, _) -> <<"t99">> end), % для маршрута с id path => <<"/v1/admin/tickets/missing">>,
ok = meck:expect(handler_auth, authenticate, bindings => #{id => <<"missing">>},
fun(Req) -> {ok, <<"adm1">>, Req} end), auth => ?ADMIN_ID,
AdminUser = #user{id = <<"adm1">>, role = admin}, body => jsx:encode(#{<<"status">> => <<"closed">>})
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). ?assertEqual(404, Status).
%% DELETE удаление тикета test_unauthorized() ->
test_delete() -> {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), method => <<"GET">>,
ok = meck:expect(cowboy_req, binding, path => <<"/v1/admin/tickets">>,
fun(id, _) -> <<"t1">> end), % для маршрута с id auth => none
ok = meck:expect(handler_auth, authenticate, }),
fun(Req) -> {ok, <<"adm1">>, Req} end), ?assertEqual(401, Status).
AdminUser = #user{id = <<"adm1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"adm1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_ticket, delete_ticket,
fun(<<"t1">>) -> {ok, deleted} end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]).
%% DELETE тикет не найден
test_delete_not_found() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> 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, _, _} = admin_handler_tickets:init(req, []),
{Status, _, _, _} = erase(test_reply),
?assertEqual(404, Status).
%% Неправильный метод
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{
ok = meck:expect(cowboy_req, binding, method => <<"POST">>,
fun(id, _) -> undefined end), % для маршрута без id path => <<"/v1/admin/tickets">>,
ok = meck:expect(cowboy_req, reply, auth => ?ADMIN_ID
fun(Code, Headers, Body, Req) -> }),
put(test_reply, {Code, Headers, Body, Req}) ?assertEqual(405, Status).
end),
{ok, _, _} = admin_handler_tickets:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).
+104 -168
View File
@@ -2,189 +2,125 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [admin, admin_audit, user, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
-define(USER_ID, <<"user_by_id_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok = mnesia:dirty_write(eh_test_support:make_user(#{
id => ?USER_ID,
email => <<"target@test.local">>,
nickname => <<"target">>,
status => active
})),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_user), eh_test_support:unload_cowboy(),
meck:unload(handler_auth), eh_test_support:delete_tables(?TABLES),
meck:unload(cowboy_req). eh_test_support:stop_mnesia(),
ok.
admin_user_by_id_test_() -> admin_user_by_id_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/users/:id success", fun test_get_user/0}, {"GET success", fun test_get/0},
{"GET /admin/users/:id not found", fun test_get_user_not_found/0}, {"GET not found", fun test_get_not_found/0},
{"GET /admin/users/:id forbidden", fun test_get_user_forbidden/0}, {"PUT update status", fun test_update/0},
{"PUT /admin/users/:id success", fun test_update_user/0}, {"PUT not found", fun test_update_not_found/0},
{"PUT /admin/users/:id not found", fun test_update_user_not_found/0}, {"DELETE soft delete", fun test_delete/0},
{"DELETE /admin/users/:id success", fun test_delete_user/0}, {"DELETE not found", fun test_delete_not_found/0},
{"DELETE /admin/users/:id not found", fun test_delete_user_not_found/0}, {"GET unauthorized", fun test_unauthorized/0},
{"POST /admin/users/:id method not allowed", fun test_wrong_method/0}, {"PATCH method not allowed", fun test_wrong_method/0}
{"convert_updates/1", fun test_convert_updates/0}
]}. ]}.
%% GET success test_get() ->
test_get_user() -> {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/", ?USER_ID/binary>>,
fun(Req) -> {ok, <<"admin1">>, Req} end), bindings => #{id => ?USER_ID},
AdminUser = #user{id = <<"admin1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"admin1">>) -> {ok, AdminUser};
(<<"user1">>) -> {ok, #user{id = <<"user1">>, email = <<"u@t.com">>,
role = user, status = active,
created_at = {{2026,4,27},{12,0,0}},
updated_at = {{2026,4,27},{12,0,0}}}}
end),
ok = meck:expect(cowboy_req, binding,
fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"id">> := <<"user1">>, <<"email">> := <<"u@t.com">>, <<"role">> := <<"user">>} Result = jsx:decode(Body, [return_maps]),
= jsx:decode(RespBody, [return_maps]). ?assertEqual(?USER_ID, maps:get(<<"id">>, Result)),
?assertEqual(<<"target@test.local">>, maps:get(<<"email">>, Result)).
%% GET not found test_get_not_found() ->
test_get_user_not_found() -> {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"GET">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/missing">>,
fun(Req) -> {ok, <<"admin1">>, Req} end), bindings => #{id => <<"missing">>},
AdminUser = #user{id = <<"admin1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"admin1">>) -> {ok, AdminUser}; ?assertEqual(404, Status).
(_) -> {error, not_found}
end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(404, Status),
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% GET forbidden test_update() ->
test_get_user_forbidden() -> {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), method => <<"PUT">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/", ?USER_ID/binary>>,
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), bindings => #{id => ?USER_ID},
ok = meck:expect(cowboy_req, reply, auth => ?ADMIN_ID,
fun(Code, Headers, Body, Req) -> body => jsx:encode(#{
put(test_reply, {Code, Headers, Body, Req}) <<"status">> => <<"frozen">>,
end), <<"reason">> => <<"abuse">>
{ok, _, _} = admin_handler_user_by_id:init(req, []), })
{Status, _, RespBody, _} = erase(test_reply), }),
?assertEqual(403, Status),
#{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]).
%% PUT success
test_update_user() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end),
ok = meck:expect(handler_auth, authenticate,
fun(Req) -> {ok, <<"admin1">>, Req} end),
AdminUser = #user{id = <<"admin1">>, role = admin},
ok = meck:expect(core_user, get_by_id,
fun(<<"admin1">>) -> {ok, AdminUser} end),
User = #user{id = <<"user1">>, email = <<"u@t.com">>, role = user, status = frozen,
created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{13,0,0}}},
ok = meck:expect(core_user, update, fun(<<"user1">>, _) -> {ok, User} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{status => <<"frozen">>}), Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]). Result = jsx:decode(Body, [return_maps]),
?assertEqual(<<"frozen">>, maps:get(<<"status">>, Result)),
?assertEqual(<<"abuse">>, maps:get(<<"reason">>, Result)),
Audits = core_admin_audit:list(),
?assert(length(Audits) >= 1).
%% PUT not found test_update_not_found() ->
test_update_user_not_found() -> {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), method => <<"PUT">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/missing">>,
fun(Req) -> {ok, <<"admin1">>, Req} end), bindings => #{id => <<"missing">>},
AdminUser = #user{id = <<"admin1">>, role = admin}, auth => ?ADMIN_ID,
ok = meck:expect(core_user, get_by_id, body => jsx:encode(#{<<"status">> => <<"frozen">>})
fun(<<"admin1">>) -> {ok, AdminUser} end), }),
ok = meck:expect(core_user, update, fun(_, _) -> {error, not_found} end), ?assertEqual(404, Status).
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end),
ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, <<"{}">>, Req} end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(404, Status),
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% DELETE success test_delete() ->
test_delete_user() -> {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), method => <<"DELETE">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/", ?USER_ID/binary>>,
fun(Req) -> {ok, <<"admin1">>, Req} end), bindings => #{id => ?USER_ID},
AdminUser = #user{id = <<"admin1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"admin1">>) -> {ok, AdminUser} end),
ok = meck:expect(core_user, delete, fun(<<"user1">>) -> {ok, deleted} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end),
ok = meck:expect(cowboy_req, reply,
fun(Code, Headers, Body, Req) ->
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_user_by_id:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status), ?assertEqual(200, Status),
#{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]). #{<<"status">> := <<"deleted">>} = jsx:decode(Body, [return_maps]),
{ok, User} = core_user:get_by_id(?USER_ID),
?assertEqual(deleted, User#user.status).
%% DELETE not found test_delete_not_found() ->
test_delete_user_not_found() -> {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), method => <<"DELETE">>,
ok = meck:expect(handler_auth, authenticate, path => <<"/v1/admin/users/missing">>,
fun(Req) -> {ok, <<"admin1">>, Req} end), bindings => #{id => <<"missing">>},
AdminUser = #user{id = <<"admin1">>, role = admin}, auth => ?ADMIN_ID
ok = meck:expect(core_user, get_by_id, }),
fun(<<"admin1">>) -> {ok, AdminUser} end), ?assertEqual(404, Status).
ok = meck:expect(core_user, delete, fun(_) -> {error, not_found} end),
ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end), test_unauthorized() ->
ok = meck:expect(cowboy_req, reply, {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{
fun(Code, Headers, Body, Req) -> method => <<"GET">>,
put(test_reply, {Code, Headers, Body, Req}) path => <<"/v1/admin/users/", ?USER_ID/binary>>,
end), bindings => #{id => ?USER_ID},
{ok, _, _} = admin_handler_user_by_id:init(req, []), auth => none
{Status, _, RespBody, _} = erase(test_reply), }),
?assertEqual(404, Status), ?assertEqual(401, Status).
#{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]).
%% Wrong method
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{
ok = meck:expect(cowboy_req, reply, method => <<"PATCH">>,
fun(Code, Headers, Body, Req) -> path => <<"/v1/admin/users/", ?USER_ID/binary>>,
put(test_reply, {Code, Headers, Body, Req}) bindings => #{id => ?USER_ID},
end), auth => ?ADMIN_ID
{ok, _, _} = admin_handler_user_by_id:init(req, []), }),
{Status, _, RespBody, _} = erase(test_reply), ?assertEqual(405, Status).
?assertEqual(405, Status),
#{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]).
%% convert_updates/1
test_convert_updates() ->
Updates = [
{<<"status">>, <<"frozen">>},
{<<"role">>, <<"admin">>},
{<<"email">>, <<"test@test.com">>}
],
Converted = admin_handler_user_by_id:convert_updates(Updates),
?assertEqual({status, frozen}, lists:keyfind(status, 1, Converted)),
?assertEqual({role, admin}, lists:keyfind(role, 1, Converted)),
?assertEqual({<<"email">>, <<"test@test.com">>}, lists:keyfind(<<"email">>, 1, Converted)).
@@ -0,0 +1,84 @@
-module(admin_handler_user_stats_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, user, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_user_stats_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET stats empty", {timeout, 60, fun test_stats_empty/0}},
{"GET stats with users", {timeout, 60, fun test_stats_with_data/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
test_stats_empty() ->
{Status, _, Body} = eh_test_support:call(admin_handler_user_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/users/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(0, maps:get(<<"total_users">>, Result)),
?assertEqual(0, maps:get(<<"pending_users">>, Result)).
test_stats_with_data() ->
ok = mnesia:dirty_write(eh_test_support:make_user(#{
id => <<"u1">>,
email => <<"alice@test.local">>,
nickname => <<"alice">>,
status => active,
role => user
})),
ok = mnesia:dirty_write(eh_test_support:make_user(#{
id => <<"u2">>,
email => <<"bob@test.local">>,
nickname => <<"bob">>,
status => pending,
role => user
})),
{Status, _, Body} = eh_test_support:call(admin_handler_user_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/users/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(2, maps:get(<<"total_users">>, Result)),
?assertEqual(1, maps:get(<<"pending_users">>, Result)),
ByRole = maps:get(<<"users_by_role">>, Result),
?assertEqual(2, maps:get(<<"user">>, ByRole)),
ByStatus = maps:get(<<"users_by_status">>, Result),
?assertEqual(1, maps:get(<<"active">>, ByStatus)),
?assertEqual(1, maps:get(<<"pending">>, ByStatus)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_user_stats, #{
method => <<"GET">>,
path => <<"/v1/admin/users/stats">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_user_stats, #{
method => <<"POST">>,
path => <<"/v1/admin/users/stats">>,
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
@@ -0,0 +1,93 @@
-module(admin_handler_user_verification_token_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, user, verification, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok.
cleanup(_) ->
eh_test_support:unload_cowboy(),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
admin_user_verification_token_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"GET create token", {timeout, 60, fun test_create/0}},
{"GET reuse existing token", {timeout, 60, fun test_reuse/0}},
{"GET user not found", {timeout, 60, fun test_not_found/0}},
{"GET unauthorized", {timeout, 60, fun test_unauthorized/0}},
{"POST method not allowed", {timeout, 60, fun test_wrong_method/0}}
]}.
seed_user() ->
User = eh_test_support:make_user(#{
id => <<"u_verify">>,
email => <<"verify@test.local">>,
nickname => <<"verify">>,
status => pending
}),
ok = mnesia:dirty_write(User),
User.
test_create() ->
User = seed_user(),
Id = User#user.id,
{Status, _, Body} = eh_test_support:call(admin_handler_user_verification_token, #{
method => <<"GET">>,
path => <<"/v1/admin/users/", Id/binary, "/verification-token">>,
bindings => #{id => Id},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assert(is_binary(maps:get(<<"token">>, Result))),
?assert(is_binary(maps:get(<<"expires_at">>, Result))).
test_reuse() ->
User = seed_user(),
Id = User#user.id,
{ok, Token1, _} = core_verification:create_token(Id),
{Status, _, Body} = eh_test_support:call(admin_handler_user_verification_token, #{
method => <<"GET">>,
path => <<"/v1/admin/users/", Id/binary, "/verification-token">>,
bindings => #{id => Id},
auth => ?ADMIN_ID
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(Token1, maps:get(<<"token">>, Result)).
test_not_found() ->
{Status, _, _} = eh_test_support:call(admin_handler_user_verification_token, #{
method => <<"GET">>,
path => <<"/v1/admin/users/missing/verification-token">>,
bindings => #{id => <<"missing">>},
auth => ?ADMIN_ID
}),
?assertEqual(404, Status).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_user_verification_token, #{
method => <<"GET">>,
path => <<"/v1/admin/users/x/verification-token">>,
bindings => #{id => <<"x">>},
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() ->
{Status, _, _} = eh_test_support:call(admin_handler_user_verification_token, #{
method => <<"POST">>,
path => <<"/v1/admin/users/x/verification-token">>,
bindings => #{id => <<"x">>},
auth => ?ADMIN_ID
}),
?assertEqual(405, Status).
+69 -51
View File
@@ -1,65 +1,83 @@
-module(admin_handler_users_tests). -module(admin_handler_users_tests).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin, admin_audit, user, ticket]).
-define(ADMIN_ID, <<"adm_test_1">>).
setup() -> setup() ->
ok = meck:new(cowboy_req, [non_strict]), eh_test_support:start_mnesia(),
ok = meck:new(handler_auth, [non_strict]), % вместо auth eh_test_support:ensure_tables(?TABLES),
ok = meck:new(core_user, [non_strict]), eh_test_support:ensure_jwt(),
eh_test_support:seed_admin(#{id => ?ADMIN_ID}),
ok. ok.
cleanup(_) -> cleanup(_) ->
meck:unload(core_user), eh_test_support:unload_cowboy(),
meck:unload(handler_auth), eh_test_support:delete_tables(?TABLES),
meck:unload(cowboy_req). eh_test_support:stop_mnesia(),
ok.
admin_users_test_() -> admin_users_test_() ->
{setup, fun setup/0, fun cleanup/1, [ {foreach, fun setup/0, fun cleanup/1, [
{"GET /admin/users with valid admin token returns 200 and list of users", fun test_list_users/0}, {"GET list success", fun test_list/0},
{"GET /admin/users with non-admin token returns 403", fun test_list_users_forbidden/0}, {"GET list filter by status", fun test_list_filter/0},
{"POST /admin/users returns 405", fun test_wrong_method/0} {"GET unauthorized", fun test_unauthorized/0},
{"POST method not allowed", fun test_wrong_method/0}
]}. ]}.
test_list_users() -> seed_users() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), ok = mnesia:dirty_write(eh_test_support:make_user(#{
ok = meck:expect(handler_auth, authenticate, id => <<"u1">>,
fun(Req) -> {ok, <<"admin1">>, Req} end), email => <<"alice@test.local">>,
User = #{ nickname => <<"alice">>,
id => <<"user1">>, status => active
email => <<"user@test.com">>, })),
role => <<"user">>, ok = mnesia:dirty_write(eh_test_support:make_user(#{
status => <<"active">>, id => <<"u2">>,
created_at => {{2025,4,27},{12,0,0}}, email => <<"bob@test.local">>,
updated_at => {{2025,4,27},{12,30,0}} nickname => <<"bob">>,
}, status => frozen
ok = meck:expect(core_user, list_users, fun() -> {ok, [User]} end), })),
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> ok.
put(test_reply, {Code, Headers, Body, Req})
end),
{ok, _, _} = admin_handler_users:init(req, []),
{Status, _, RespBody, _} = erase(test_reply),
?assertEqual(200, Status),
Users = jsx:decode(RespBody, [return_maps]),
?assertEqual(1, length(Users)),
?assertEqual(<<"user1">>, maps:get(<<"id">>, hd(Users))).
test_list_users_forbidden() -> test_list() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), seed_users(),
ok = meck:expect(handler_auth, authenticate, {Status, _, Body} = eh_test_support:call(admin_handler_users, #{
fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), method => <<"GET">>,
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> path => <<"/v1/admin/users">>,
put(test_reply, {Code, Headers, Body, Req}) auth => ?ADMIN_ID
end), }),
{ok, _, _} = admin_handler_users:init(req, []), ?assertEqual(200, Status),
{Status, _, RespBody, _} = erase(test_reply), Result = jsx:decode(Body, [return_maps]),
?assertEqual(403, Status), ?assertEqual(2, length(Result)).
?assertEqual(#{<<"error">> => <<"Admin access required">>}, jsx:decode(RespBody, [return_maps])).
test_list_filter() ->
seed_users(),
{Status, _, Body} = eh_test_support:call(admin_handler_users, #{
method => <<"GET">>,
path => <<"/v1/admin/users">>,
auth => ?ADMIN_ID,
qs => [{<<"status">>, <<"active">>}]
}),
?assertEqual(200, Status),
Result = jsx:decode(Body, [return_maps]),
?assertEqual(1, length(Result)),
[Only] = Result,
?assertEqual(<<"alice@test.local">>, maps:get(<<"email">>, Only)).
test_unauthorized() ->
{Status, _, _} = eh_test_support:call(admin_handler_users, #{
method => <<"GET">>,
path => <<"/v1/admin/users">>,
auth => none
}),
?assertEqual(401, Status).
test_wrong_method() -> test_wrong_method() ->
ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), {Status, _, _} = eh_test_support:call(admin_handler_users, #{
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> method => <<"POST">>,
put(test_reply, {Code, Headers, Body, Req}) path => <<"/v1/admin/users">>,
end), auth => ?ADMIN_ID
{ok, _, _} = admin_handler_users:init(req, []), }),
{Status, _, RespBody, _} = erase(test_reply), ?assertEqual(405, Status).
?assertEqual(405, Status),
?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])).
+92 -19
View File
@@ -1,11 +1,24 @@
-module(admin_utils_tests). -module(admin_utils_tests).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin]).
-define(ADMIN_ID, <<"adm_utils_1">>).
-define(SUPER_ID, <<"adm_super_1">>).
%%%===================================================================
%%% client_ip / ip_to_binary (cowboy via eh_test_support)
%%%===================================================================
client_ip_setup() ->
ok.
client_ip_cleanup(_) ->
eh_test_support:unload_cowboy(),
ok.
client_ip_test_() -> client_ip_test_() ->
{setup, {foreach, fun client_ip_setup/0, fun client_ip_cleanup/1, [
fun() -> ok = meck:new(cowboy_req, [non_strict]) end,
fun(_) -> meck:unload(cowboy_req) end,
[
{"Uses X-Forwarded-For when present", fun test_forwarded_for/0}, {"Uses X-Forwarded-For when present", fun test_forwarded_for/0},
{"Falls back to X-Real-IP", fun test_real_ip/0}, {"Falls back to X-Real-IP", fun test_real_ip/0},
{"Falls back to peer address", fun test_peer_fallback/0}, {"Falls back to peer address", fun test_peer_fallback/0},
@@ -13,25 +26,85 @@ client_ip_test_() ->
]}. ]}.
test_forwarded_for() -> test_forwarded_for() ->
ok = meck:expect(cowboy_req, header, fun Req = eh_test_support:with_cowboy(#{
(<<"x-forwarded-for">>, _) -> <<"203.0.113.10, 10.0.0.1">>; headers => #{<<"x-forwarded-for">> => <<"203.0.113.10, 10.0.0.1">>},
(_, _) -> undefined peer => {10, 0, 0, 2}
end), }),
ok = meck:expect(cowboy_req, peer, fun(_) -> {10, 0, 0, 2} end), ?assertEqual(<<"203.0.113.10">>, admin_utils:client_ip(Req)).
?assertEqual(<<"203.0.113.10">>, admin_utils:client_ip(req)).
test_real_ip() -> test_real_ip() ->
ok = meck:expect(cowboy_req, header, fun Req = eh_test_support:with_cowboy(#{
(<<"x-real-ip">>, _) -> <<"198.51.100.5">>; headers => #{<<"x-real-ip">> => <<"198.51.100.5">>},
(_, _) -> undefined peer => {10, 0, 0, 2}
end), }),
ok = meck:expect(cowboy_req, peer, fun(_) -> {10, 0, 0, 2} end), ?assertEqual(<<"198.51.100.5">>, admin_utils:client_ip(Req)).
?assertEqual(<<"198.51.100.5">>, admin_utils:client_ip(req)).
test_peer_fallback() -> test_peer_fallback() ->
ok = meck:expect(cowboy_req, header, fun(_, _) -> undefined end), Req = eh_test_support:with_cowboy(#{
ok = meck:expect(cowboy_req, peer, fun(_) -> {127, 0, 0, 1} end), headers => #{},
?assertEqual(<<"127.0.0.1">>, admin_utils:client_ip(req)). peer => {127, 0, 0, 1}
}),
?assertEqual(<<"127.0.0.1">>, admin_utils:client_ip(Req)).
test_ipv4_tuple() -> test_ipv4_tuple() ->
?assertEqual(<<"192.168.1.1">>, admin_utils:ip_to_binary({192, 168, 1, 1})). ?assertEqual(<<"192.168.1.1">>, admin_utils:ip_to_binary({192, 168, 1, 1})).
%%%===================================================================
%%% Role / permission helpers (real admin table, no core_admin mock)
%%%===================================================================
role_setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
eh_test_support:seed_admin(#{id => ?ADMIN_ID, role => admin}),
eh_test_support:seed_admin(#{
id => ?SUPER_ID,
email => <<"super@test.local">>,
role => superadmin,
nickname => <<"super">>
}),
ok.
role_cleanup(_) ->
eh_test_support:clear_tables(?TABLES),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
role_utils_test_() ->
{foreach, fun role_setup/0, fun role_cleanup/1, [
{"is_admin / is_superadmin", fun test_is_admin_roles/0},
{"check_role atom and list", fun test_check_role/0},
{"get_permissions by role", fun test_get_permissions/0}
]}.
test_is_admin_roles() ->
?assertEqual(true, admin_utils:is_admin(?ADMIN_ID)),
?assertEqual(true, admin_utils:is_admin(?SUPER_ID)),
?assertEqual(false, admin_utils:is_admin(<<"missing">>)),
?assertEqual(true, admin_utils:is_superadmin(?SUPER_ID)),
?assertEqual(false, admin_utils:is_superadmin(?ADMIN_ID)),
?assertEqual(false, admin_utils:is_superadmin(<<"missing">>)).
test_check_role() ->
?assertEqual(true, admin_utils:check_role(?ADMIN_ID, admin)),
?assertEqual(false, admin_utils:check_role(?ADMIN_ID, superadmin)),
?assertEqual(true, admin_utils:check_role(?ADMIN_ID, [admin, moderator])),
?assertEqual(false, admin_utils:check_role(?ADMIN_ID, [superadmin, support])),
?assertEqual(true, admin_utils:check_role(?SUPER_ID, [admin, superadmin])),
?assertEqual(false, admin_utils:check_role(<<"missing">>, admin)),
?assertEqual(false, admin_utils:check_role(<<"missing">>, [admin])).
test_get_permissions() ->
Super = admin_utils:get_permissions(superadmin),
Admin = admin_utils:get_permissions(admin),
Mod = admin_utils:get_permissions(moderator),
Support = admin_utils:get_permissions(support),
?assert(lists:member(<<"manage_admins">>, Super)),
?assert(lists:member(<<"view_audit">>, Super)),
?assertEqual(false, lists:member(<<"manage_admins">>, Admin)),
?assert(lists:member(<<"manage_users">>, Admin)),
?assert(lists:member(<<"manage_tickets">>, Mod)),
?assertEqual(false, lists:member(<<"view_audit">>, Mod)),
?assertEqual([<<"manage_tickets">>, <<"view_stats">>], Support),
?assertEqual([], admin_utils:get_permissions(unknown)).
+30 -41
View File
@@ -2,32 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, calendar, event, booking]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, user)},
{ram_copies, [node()]}
]),
mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
mnesia:create_table(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(booking, [
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(booking), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(event), eh_test_support:stop_mnesia(),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
booking_integration_test_() -> booking_integration_test_() ->
@@ -35,7 +19,7 @@ booking_integration_test_() ->
fun setup/0, fun setup/0,
fun cleanup/1, fun cleanup/1,
[ [
{"Full booking flow with auto confirmation", fun test_auto_booking_flow/0}, {"Booking create stays pending", fun test_pending_booking_flow/0},
{"Full booking flow with manual confirmation", fun test_manual_booking_flow/0}, {"Full booking flow with manual confirmation", fun test_manual_booking_flow/0},
{"Capacity management test", fun test_capacity_management/0}, {"Capacity management test", fun test_capacity_management/0},
{"Multiple bookings test", fun test_multiple_bookings/0} {"Multiple bookings test", fun test_multiple_bookings/0}
@@ -55,22 +39,26 @@ create_user() ->
mnesia:dirty_write(User), mnesia:dirty_write(User),
UserId. UserId.
test_auto_booking_flow() -> add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_pending_booking_flow() ->
OwnerId = create_user(), OwnerId = create_user(),
ParticipantId = create_user(), ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Auto">>, <<"">>, auto), {ok, Calendar} = core_calendar:create(OwnerId, <<"Auto">>, <<"">>, auto),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id), {ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id),
timer:sleep(100), ?assertEqual(pending, Booking#booking.status),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id), {ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(confirmed, Updated#booking.status), ?assertEqual(pending, Stored#booking.status),
{ok, EventBookings} = logic_booking:list_event_bookings(OwnerId, Event#event.id), {ok, EventBookings} = logic_booking:list_event_bookings(Event#event.id),
?assertEqual(1, length(EventBookings)), ?assertEqual(1, length(EventBookings)),
{ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId), {ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId),
@@ -82,7 +70,7 @@ test_manual_booking_flow() ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Manual">>, <<"">>, manual), {ok, Calendar} = core_calendar:create(OwnerId, <<"Manual">>, <<"">>, manual),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id), {ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id),
@@ -91,7 +79,7 @@ test_manual_booking_flow() ->
{ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status), ?assertEqual(confirmed, Confirmed#booking.status),
{ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id), {ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id, cancel),
?assertEqual(cancelled, Cancelled#booking.status). ?assertEqual(cancelled, Cancelled#booking.status).
test_capacity_management() -> test_capacity_management() ->
@@ -100,21 +88,21 @@ test_capacity_management() ->
Participant2Id = create_user(), Participant2Id = create_user(),
Participant3Id = create_user(), Participant3Id = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, auto), {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, _} = core_event:update(Event#event.id, [{capacity, 2}]), {ok, _} = core_event:update(Event#event.id, [{capacity, 2}]),
{ok, Booking1} = logic_booking:create_booking(Participant1Id, Event#event.id), {ok, Booking1} = logic_booking:create_booking(Participant1Id, Event#event.id),
{ok, _Booking2} = logic_booking:create_booking(Participant2Id, Event#event.id), {ok, Booking2} = logic_booking:create_booking(Participant2Id, Event#event.id),
{ok, _} = logic_booking:confirm_booking(OwnerId, Booking1#booking.id, confirm),
{ok, _} = logic_booking:confirm_booking(OwnerId, Booking2#booking.id, confirm),
{error, event_full} = logic_booking:create_booking(Participant3Id, Event#event.id), {error, full} = logic_booking:create_booking(Participant3Id, Event#event.id),
% Участник 1 отменяет своё бронирование {ok, _} = logic_booking:cancel_booking(Participant1Id, Booking1#booking.id, cancel),
{ok, _} = logic_booking:cancel_booking(Participant1Id, Booking1#booking.id),
% Теперь третий может записаться
{ok, _} = logic_booking:create_booking(Participant3Id, Event#event.id). {ok, _} = logic_booking:create_booking(Participant3Id, Event#event.id).
test_multiple_bookings() -> test_multiple_bookings() ->
@@ -123,9 +111,10 @@ test_multiple_bookings() ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
StartTime1 = {{2026, 6, 1}, {10, 0, 0}}, Base = eh_test_support:future_start(),
StartTime2 = {{2026, 6, 2}, {10, 0, 0}}, StartTime1 = Base,
StartTime3 = {{2026, 6, 3}, {10, 0, 0}}, StartTime2 = add_days(Base, 1),
StartTime3 = add_days(Base, 2),
{ok, Event1} = core_event:create(Calendar#calendar.id, <<"Event1">>, StartTime1, 60), {ok, Event1} = core_event:create(Calendar#calendar.id, <<"Event1">>, StartTime1, 60),
{ok, Event2} = core_event:create(Calendar#calendar.id, <<"Event2">>, StartTime2, 60), {ok, Event2} = core_event:create(Calendar#calendar.id, <<"Event2">>, StartTime2, 60),
+121
View File
@@ -0,0 +1,121 @@
-module(core_admin_audit_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin_audit]).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
ok.
cleanup(_) ->
eh_test_support:clear_tables(?TABLES),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
core_admin_audit_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"Log/7 without reason", fun test_log7/0},
{"Log/8 with reason", fun test_log8/0},
{"List all", fun test_list_all/0},
{"List with filters", fun test_list_filters/0},
{"Count actions by admin", fun test_count_actions/0}
]}.
test_log7() ->
{ok, Entry} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"create">>, <<"user">>, <<"u1">>, <<"127.0.0.1">>
),
?assertEqual(<<"adm1">>, Entry#admin_audit.admin_id),
?assertEqual(<<"a@test.local">>, Entry#admin_audit.email),
?assertEqual(admin, Entry#admin_audit.role),
?assertEqual(<<"create">>, Entry#admin_audit.action),
?assertEqual(<<"user">>, Entry#admin_audit.entity_type),
?assertEqual(<<"u1">>, Entry#admin_audit.entity_id),
?assertEqual(<<"127.0.0.1">>, Entry#admin_audit.ip),
?assertEqual(<<>>, Entry#admin_audit.reason),
?assert(is_binary(Entry#admin_audit.id)).
test_log8() ->
{ok, Entry} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, moderator,
<<"block">>, <<"user">>, <<"u2">>, <<"10.0.0.1">>, <<"abuse">>
),
?assertEqual(<<"block">>, Entry#admin_audit.action),
?assertEqual(<<"abuse">>, Entry#admin_audit.reason),
?assertEqual(moderator, Entry#admin_audit.role).
test_list_all() ->
?assertEqual([], core_admin_audit:list()),
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"create">>, <<"user">>, <<"u1">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
<<"adm2">>, <<"b@test.local">>, admin,
<<"update">>, <<"event">>, <<"e1">>, <<"127.0.0.1">>, <<"fix">>
),
?assertEqual(2, length(core_admin_audit:list())).
test_list_filters() ->
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"create">>, <<"user">>, <<"u1">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"block">>, <<"user">>, <<"u2">>, <<"127.0.0.1">>, <<"spam">>
),
{ok, _} = core_admin_audit:log(
<<"adm2">>, <<"b@test.local">>, support,
<<"create">>, <<"ticket">>, <<"t1">>, <<"10.0.0.1">>
),
ByAdmin = core_admin_audit:list([{admin_id, <<"adm1">>}]),
?assertEqual(2, length(ByAdmin)),
?assert(lists:all(fun(E) -> E#admin_audit.admin_id =:= <<"adm1">> end, ByAdmin)),
ByAction = core_admin_audit:list([{action, <<"create">>}]),
?assertEqual(2, length(ByAction)),
Combined = core_admin_audit:list([
{admin_id, <<"adm1">>},
{action, <<"block">>}
]),
?assertEqual(1, length(Combined)),
[Only] = Combined,
?assertEqual(<<"block">>, Only#admin_audit.action),
FarFuture = {{2099, 1, 1}, {0, 0, 0}},
?assertEqual([], core_admin_audit:list([{date_from, FarFuture}])),
FarPast = {{1970, 1, 1}, {0, 0, 0}},
?assertEqual(3, length(core_admin_audit:list([{date_from, FarPast}]))),
BeforeAll = {{1970, 1, 1}, {0, 0, 0}},
?assertEqual([], core_admin_audit:list([{date_to, BeforeAll}])).
test_count_actions() ->
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"create">>, <<"user">>, <<"u1">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"create">>, <<"event">>, <<"e1">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
<<"adm1">>, <<"a@test.local">>, admin,
<<"block">>, <<"user">>, <<"u2">>, <<"127.0.0.1">>
),
{ok, _} = core_admin_audit:log(
<<"adm2">>, <<"b@test.local">>, admin,
<<"create">>, <<"user">>, <<"u3">>, <<"127.0.0.1">>
),
?assertEqual(2, core_admin_audit:count_actions_by_admin(<<"adm1">>, <<"create">>)),
?assertEqual(1, core_admin_audit:count_actions_by_admin(<<"adm1">>, <<"block">>)),
?assertEqual(0, core_admin_audit:count_actions_by_admin(<<"adm1">>, <<"delete">>)),
?assertEqual(1, core_admin_audit:count_actions_by_admin(<<"adm2">>, <<"create">>)).
+109
View File
@@ -0,0 +1,109 @@
-module(core_admin_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [admin]).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
ok.
cleanup(_) ->
eh_test_support:clear_tables(?TABLES),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
core_admin_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"Create admin", fun test_create/0},
{"Create admin email exists", fun test_create_email_exists/0},
{"Get by id", fun test_get_by_id/0},
{"Get by email", fun test_get_by_email/0},
{"List all", fun test_list_all/0},
{"Update fields", fun test_update/0},
{"Update role", fun test_update_role/0},
{"Block and unblock", fun test_block_unblock/0},
{"Update last login", fun test_update_last_login/0},
{"Delete", fun test_delete/0}
]}.
test_create() ->
Email = <<"admin1@test.local">>,
{ok, Admin} = core_admin:create(Email, <<"secret123">>, admin),
?assertEqual(Email, Admin#admin.email),
?assertEqual(admin, Admin#admin.role),
?assertEqual(active, Admin#admin.status),
?assertEqual(<<"admin1">>, Admin#admin.nickname),
?assert(is_binary(Admin#admin.id)),
?assert(is_binary(Admin#admin.password_hash)),
?assert(Admin#admin.password_hash =/= <<"secret123">>).
test_create_email_exists() ->
Email = <<"dup@test.local">>,
{ok, _} = core_admin:create(Email, <<"pass1">>, admin),
?assertEqual({error, email_exists}, core_admin:create(Email, <<"pass2">>, moderator)).
test_get_by_id() ->
{ok, Admin} = core_admin:create(<<"byid@test.local">>, <<"pass">>, admin),
{ok, Found} = core_admin:get_by_id(Admin#admin.id),
?assertEqual(Admin#admin.id, Found#admin.id),
?assertEqual(Admin#admin.email, Found#admin.email),
?assertEqual({error, not_found}, core_admin:get_by_id(<<"missing">>)).
test_get_by_email() ->
Email = <<"byemail@test.local">>,
{ok, Admin} = core_admin:create(Email, <<"pass">>, support),
{ok, Found} = core_admin:get_by_email(Email),
?assertEqual(Admin#admin.id, Found#admin.id),
?assertEqual({error, not_found}, core_admin:get_by_email(<<"nope@test.local">>)).
test_list_all() ->
?assertEqual([], core_admin:list_all()),
{ok, _} = core_admin:create(<<"a1@test.local">>, <<"pass">>, admin),
{ok, _} = core_admin:create(<<"a2@test.local">>, <<"pass">>, moderator),
List = core_admin:list_all(),
?assertEqual(2, length(List)).
test_update() ->
{ok, Admin} = core_admin:create(<<"upd@test.local">>, <<"pass">>, admin),
{ok, Updated} = core_admin:update(Admin#admin.id, [
{nickname, <<"nick">>},
{phone, <<"+7999">>},
{timezone, <<"Europe/Moscow">>}
]),
?assertEqual(<<"nick">>, Updated#admin.nickname),
?assertEqual(<<"+7999">>, Updated#admin.phone),
?assertEqual(<<"Europe/Moscow">>, Updated#admin.timezone),
?assertEqual({error, not_found}, core_admin:update(<<"missing">>, [{nickname, <<"x">>}])).
test_update_role() ->
{ok, Admin} = core_admin:create(<<"role@test.local">>, <<"pass">>, admin),
{ok, Updated} = core_admin:update_role(Admin#admin.id, superadmin),
?assertEqual(superadmin, Updated#admin.role),
{ok, Loaded} = core_admin:get_by_id(Admin#admin.id),
?assertEqual(superadmin, Loaded#admin.role),
?assertEqual({error, not_found}, core_admin:update_role(<<"missing">>, admin)).
test_block_unblock() ->
{ok, Admin} = core_admin:create(<<"block@test.local">>, <<"pass">>, admin),
{ok, Blocked} = core_admin:block(Admin#admin.id),
?assertEqual(blocked, Blocked#admin.status),
{ok, Active} = core_admin:unblock(Admin#admin.id),
?assertEqual(active, Active#admin.status),
?assertEqual({error, not_found}, core_admin:block(<<"missing">>)),
?assertEqual({error, not_found}, core_admin:unblock(<<"missing">>)).
test_update_last_login() ->
{ok, Admin} = core_admin:create(<<"login@test.local">>, <<"pass">>, admin),
OldLogin = Admin#admin.last_login,
{ok, Updated} = core_admin:update_last_login(Admin#admin.id),
?assert(Updated#admin.last_login >= OldLogin),
?assertEqual({error, not_found}, core_admin:update_last_login(<<"missing">>)).
test_delete() ->
{ok, Admin} = core_admin:create(<<"del@test.local">>, <<"pass">>, admin),
?assertEqual({ok, deleted}, core_admin:delete(Admin#admin.id)),
?assertEqual({error, not_found}, core_admin:get_by_id(Admin#admin.id)),
?assertEqual({error, not_found}, core_admin:delete(<<"missing">>)).
+31 -72
View File
@@ -2,24 +2,20 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [booking]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(booking, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(booking), eh_test_support:delete_tables(?TABLES),
mnesia:stop(), eh_test_support:stop_mnesia(),
ok. ok.
core_booking_test_() -> core_booking_test_() ->
{foreach, {foreach, fun setup/0, fun cleanup/1, [
fun setup/0,
fun cleanup/1,
[
{"Create booking test", fun test_create_booking/0}, {"Create booking test", fun test_create_booking/0},
{"Get booking by id test", fun test_get_by_id/0}, {"Get booking by id test", fun test_get_by_id/0},
{"Get booking by event and user test", fun test_get_by_event_and_user/0}, {"Get booking by event and user test", fun test_get_by_event_and_user/0},
@@ -32,95 +28,58 @@ core_booking_test_() ->
test_create_booking() -> test_create_booking() ->
EventId = <<"event123">>, EventId = <<"event123">>,
UserId = <<"user123">>, UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId, pending),
{ok, Booking} = core_booking:create(EventId, UserId),
?assertEqual(EventId, Booking#booking.event_id), ?assertEqual(EventId, Booking#booking.event_id),
?assertEqual(UserId, Booking#booking.user_id), ?assertEqual(UserId, Booking#booking.user_id),
?assertEqual(pending, Booking#booking.status), ?assertEqual(pending, Booking#booking.status),
?assertEqual(undefined, Booking#booking.confirmed_at),
?assert(is_binary(Booking#booking.id)), ?assert(is_binary(Booking#booking.id)),
?assert(Booking#booking.created_at =/= undefined), ?assert(Booking#booking.created_at =/= undefined),
?assert(Booking#booking.updated_at =/= undefined). ?assert(Booking#booking.updated_at =/= undefined).
test_get_by_id() -> test_get_by_id() ->
EventId = <<"event123">>, {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending),
UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId),
{ok, Found} = core_booking:get_by_id(Booking#booking.id), {ok, Found} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(Booking#booking.id, Found#booking.id), ?assertEqual(Booking#booking.id, Found#booking.id),
{error, not_found} = core_booking:get_by_id(<<"nonexistent">>). {error, not_found} = core_booking:get_by_id(<<"nonexistent">>).
test_get_by_event_and_user() -> test_get_by_event_and_user() ->
EventId = <<"event123">>, EventId = <<"event123">>,
UserId1 = <<"user1">>, {ok, Booking1} = core_booking:create(EventId, <<"user1">>, pending),
UserId2 = <<"user2">>, {ok, _} = core_booking:create(EventId, <<"user2">>, pending),
{ok, Found} = core_booking:get_by_event_and_user(EventId, <<"user1">>),
{ok, Booking1} = core_booking:create(EventId, UserId1),
{ok, _Booking2} = core_booking:create(EventId, UserId2),
{ok, Found} = core_booking:get_by_event_and_user(EventId, UserId1),
?assertEqual(Booking1#booking.id, Found#booking.id), ?assertEqual(Booking1#booking.id, Found#booking.id),
{error, not_found} = core_booking:get_by_event_and_user(EventId, <<"user3">>). {error, not_found} = core_booking:get_by_event_and_user(EventId, <<"user3">>).
test_list_by_event() -> test_list_by_event() ->
EventId1 = <<"event1">>, {ok, _} = core_booking:create(<<"event1">>, <<"user123">>, pending),
EventId2 = <<"event2">>, {ok, _} = core_booking:create(<<"event1">>, <<"user2">>, pending),
UserId = <<"user123">>, {ok, _} = core_booking:create(<<"event2">>, <<"user123">>, pending),
{ok, Bookings1} = core_booking:list_by_event(<<"event1">>),
{ok, _} = core_booking:create(EventId1, UserId),
{ok, _} = core_booking:create(EventId1, <<"user2">>),
{ok, _} = core_booking:create(EventId2, UserId),
{ok, Bookings1} = core_booking:list_by_event(EventId1),
?assertEqual(2, length(Bookings1)), ?assertEqual(2, length(Bookings1)),
{ok, Bookings2} = core_booking:list_by_event(<<"event2">>),
{ok, Bookings2} = core_booking:list_by_event(EventId2),
?assertEqual(1, length(Bookings2)). ?assertEqual(1, length(Bookings2)).
test_list_by_user() -> test_list_by_user() ->
EventId = <<"event123">>, {ok, _} = core_booking:create(<<"event123">>, <<"user1">>, pending),
UserId1 = <<"user1">>, {ok, _} = core_booking:create(<<"event456">>, <<"user1">>, pending),
UserId2 = <<"user2">>, {ok, _} = core_booking:create(<<"event123">>, <<"user2">>, pending),
{ok, Bookings1} = core_booking:list_by_user(<<"user1">>),
{ok, _} = core_booking:create(EventId, UserId1),
{ok, _} = core_booking:create(EventId, UserId1),
{ok, _} = core_booking:create(EventId, UserId2),
{ok, Bookings1} = core_booking:list_by_user(UserId1),
?assertEqual(2, length(Bookings1)), ?assertEqual(2, length(Bookings1)),
{ok, Bookings2} = core_booking:list_by_user(<<"user2">>),
{ok, Bookings2} = core_booking:list_by_user(UserId2),
?assertEqual(1, length(Bookings2)). ?assertEqual(1, length(Bookings2)).
test_update_status() -> test_update_status() ->
EventId = <<"event123">>, {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending),
UserId = <<"user123">>, Now = calendar:universal_time(),
{ok, Booking} = core_booking:create(EventId, UserId), {ok, Confirmed} = core_booking:update(Booking#booking.id,
[{status, confirmed}, {confirmed_at, Now}]),
timer:sleep(2000), % 2 секунды
{ok, Confirmed} = core_booking:update_status(Booking#booking.id, confirmed),
?assertEqual(confirmed, Confirmed#booking.status), ?assertEqual(confirmed, Confirmed#booking.status),
?assert(Confirmed#booking.confirmed_at =/= undefined), ?assertEqual(Now, Confirmed#booking.confirmed_at),
?assert(Confirmed#booking.updated_at > Booking#booking.updated_at), {ok, Cancelled} = core_booking:update(Booking#booking.id, [{status, cancelled}]),
timer:sleep(2000), % 2 секунды
{ok, Cancelled} = core_booking:update_status(Booking#booking.id, cancelled),
?assertEqual(cancelled, Cancelled#booking.status), ?assertEqual(cancelled, Cancelled#booking.status),
?assert(Cancelled#booking.updated_at > Confirmed#booking.updated_at), {error, not_found} = core_booking:update(<<"nonexistent">>, [{status, confirmed}]).
{error, not_found} = core_booking:update_status(<<"nonexistent">>, confirmed).
test_delete_booking() -> test_delete_booking() ->
EventId = <<"event123">>, {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending),
UserId = <<"user123">>, ok = core_booking:delete(Booking#booking.id),
{ok, Booking} = core_booking:create(EventId, UserId),
{ok, deleted} = core_booking:delete(Booking#booking.id),
{error, not_found} = core_booking:get_by_id(Booking#booking.id). {error, not_found} = core_booking:get_by_id(Booking#booking.id).
+21 -24
View File
@@ -2,22 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [event, recurrence_exception]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(event, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(recurrence_exception, [
{attributes, record_info(fields, recurrence_exception)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(recurrence_exception), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(event), eh_test_support:stop_mnesia(),
mnesia:stop(),
ok. ok.
core_event_recurring_test_() -> core_event_recurring_test_() ->
@@ -31,10 +25,14 @@ core_event_recurring_test_() ->
{"Materialize non-recurring test", fun test_materialize_non_recurring/0} {"Materialize non-recurring test", fun test_materialize_non_recurring/0}
]}. ]}.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_create_recurring() -> test_create_recurring() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
Title = <<"Weekly Meeting">>, Title = <<"Weekly Meeting">>,
StartTime = {{2026, 4, 20}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
Duration = 60, Duration = 60,
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
@@ -46,7 +44,6 @@ test_create_recurring() ->
?assertEqual(false, Event#event.is_instance), ?assertEqual(false, Event#event.is_instance),
?assert(is_binary(Event#event.recurrence_rule)), ?assert(is_binary(Event#event.recurrence_rule)),
% Проверяем, что правило сохранилось
Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]), Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]),
RRuleMap = case Decoded of RRuleMap = case Decoded of
Map when is_map(Map) -> Map; Map when is_map(Map) -> Map;
@@ -56,8 +53,8 @@ test_create_recurring() ->
test_materialize_occurrence() -> test_materialize_occurrence() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
MasterId = create_recurring_event(CalendarId), {MasterId, StartTime} = create_recurring_event(CalendarId),
OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}}, OccurrenceStart = add_days(StartTime, 7),
SpecialistId = <<"specialist123">>, SpecialistId = <<"specialist123">>,
{ok, Instance} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId), {ok, Instance} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId),
@@ -71,15 +68,13 @@ test_materialize_occurrence() ->
test_materialize_existing() -> test_materialize_existing() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
MasterId = create_recurring_event(CalendarId), {MasterId, StartTime} = create_recurring_event(CalendarId),
OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}}, OccurrenceStart = add_days(StartTime, 7),
SpecialistId1 = <<"specialist1">>, SpecialistId1 = <<"specialist1">>,
SpecialistId2 = <<"specialist2">>, SpecialistId2 = <<"specialist2">>,
% Первая материализация
{ok, Instance1} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId1), {ok, Instance1} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId1),
% Вторая материализация - должна вернуть существующий экземпляр
{ok, Instance2} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId2), {ok, Instance2} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId2),
?assertEqual(Instance1#event.id, Instance2#event.id), ?assertEqual(Instance1#event.id, Instance2#event.id),
@@ -87,18 +82,20 @@ test_materialize_existing() ->
test_materialize_non_recurring() -> test_materialize_non_recurring() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
{ok, SingleEvent} = core_event:create(CalendarId, <<"Single">>, {{2026, 4, 20}, {10, 0, 0}}, 60), StartTime = eh_test_support:future_start(),
{ok, SingleEvent} = core_event:create(CalendarId, <<"Single">>, StartTime, 60),
{error, not_recurring} = core_event:materialize_occurrence( {error, not_recurring} = core_event:materialize_occurrence(
SingleEvent#event.id, {{2026, 4, 27}, {10, 0, 0}}, <<"spec">> SingleEvent#event.id, add_days(StartTime, 7), <<"spec">>
). ).
create_recurring_event(CalendarId) -> create_recurring_event(CalendarId) ->
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create_recurring( {ok, Event} = core_event:create_recurring(
CalendarId, CalendarId,
<<"Weekly Meeting">>, <<"Weekly Meeting">>,
{{2026, 4, 20}, {10, 0, 0}}, StartTime,
60, 60,
#{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1} #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}
), ),
Event#event.id. {Event#event.id, StartTime}.
+18 -15
View File
@@ -2,17 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [event]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(event, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(event), eh_test_support:delete_tables(?TABLES),
mnesia:stop(), eh_test_support:stop_mnesia(),
ok. ok.
core_event_test_() -> core_event_test_() ->
@@ -27,10 +26,14 @@ core_event_test_() ->
{"Delete event test", fun test_delete_event/0} {"Delete event test", fun test_delete_event/0}
]}. ]}.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_create_event() -> test_create_event() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
Title = <<"Test Event">>, Title = <<"Test Event">>,
StartTime = {{2026, 4, 25}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
Duration = 60, Duration = 60,
{ok, Event} = core_event:create(CalendarId, Title, StartTime, Duration), {ok, Event} = core_event:create(CalendarId, Title, StartTime, Duration),
@@ -48,7 +51,7 @@ test_create_event() ->
test_get_by_id() -> test_get_by_id() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Test">>, {{2026, 4, 25}, {10, 0, 0}}, 60), {ok, Event} = core_event:create(CalendarId, <<"Test">>, eh_test_support:future_start(), 60),
{ok, Found} = core_event:get_by_id(Event#event.id), {ok, Found} = core_event:get_by_id(Event#event.id),
?assertEqual(Event#event.id, Found#event.id), ?assertEqual(Event#event.id, Found#event.id),
@@ -58,10 +61,11 @@ test_get_by_id() ->
test_list_by_calendar() -> test_list_by_calendar() ->
CalendarId1 = <<"calendar1">>, CalendarId1 = <<"calendar1">>,
CalendarId2 = <<"calendar2">>, CalendarId2 = <<"calendar2">>,
Base = eh_test_support:future_start(),
{ok, _} = core_event:create(CalendarId1, <<"Event 1">>, {{2026, 4, 25}, {10, 0, 0}}, 60), {ok, _} = core_event:create(CalendarId1, <<"Event 1">>, Base, 60),
{ok, _} = core_event:create(CalendarId1, <<"Event 2">>, {{2026, 4, 26}, {11, 0, 0}}, 90), {ok, _} = core_event:create(CalendarId1, <<"Event 2">>, add_days(Base, 1), 90),
{ok, _} = core_event:create(CalendarId2, <<"Event 3">>, {{2026, 4, 27}, {12, 0, 0}}, 30), {ok, _} = core_event:create(CalendarId2, <<"Event 3">>, add_days(Base, 2), 30),
{ok, Events1} = core_event:list_by_calendar(CalendarId1), {ok, Events1} = core_event:list_by_calendar(CalendarId1),
?assertEqual(2, length(Events1)), ?assertEqual(2, length(Events1)),
@@ -74,7 +78,7 @@ test_list_by_calendar() ->
test_update_event() -> test_update_event() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Original">>, {{2026, 4, 25}, {10, 0, 0}}, 60), {ok, Event} = core_event:create(CalendarId, <<"Original">>, eh_test_support:future_start(), 60),
timer:sleep(2000), timer:sleep(2000),
Updates = [{title, <<"Updated">>}, {capacity, 100}, {description, <<"New desc">>}], Updates = [{title, <<"Updated">>}, {capacity, 100}, {description, <<"New desc">>}],
{ok, Updated} = core_event:update(Event#event.id, Updates), {ok, Updated} = core_event:update(Event#event.id, Updates),
@@ -88,11 +92,10 @@ test_update_event() ->
test_delete_event() -> test_delete_event() ->
CalendarId = <<"calendar123">>, CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Test">>, {{2026, 4, 25}, {10, 0, 0}}, 60), {ok, Event} = core_event:create(CalendarId, <<"Test">>, eh_test_support:future_start(), 60),
{ok, Deleted} = core_event:delete(Event#event.id), {ok, Deleted} = core_event:delete(Event#event.id),
?assertEqual(deleted, Deleted#event.status), ?assertEqual(deleted, Deleted#event.status),
% Удалённое событие не возвращается в списке активных
{ok, ActiveEvents} = core_event:list_by_calendar(CalendarId), {ok, ActiveEvents} = core_event:list_by_calendar(CalendarId),
?assertEqual(0, length(ActiveEvents)). ?assertEqual(0, length(ActiveEvents)).
+27 -57
View File
@@ -2,24 +2,20 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [report]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(report, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, report)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(report), eh_test_support:delete_tables(?TABLES),
mnesia:stop(), eh_test_support:stop_mnesia(),
ok. ok.
core_report_test_() -> core_report_test_() ->
{foreach, {foreach, fun setup/0, fun cleanup/1, [
fun setup/0,
fun cleanup/1,
[
{"Create report test", fun test_create_report/0}, {"Create report test", fun test_create_report/0},
{"Get report by id test", fun test_get_by_id/0}, {"Get report by id test", fun test_get_by_id/0},
{"List reports by target test", fun test_list_by_target/0}, {"List reports by target test", fun test_list_by_target/0},
@@ -29,73 +25,47 @@ core_report_test_() ->
]}. ]}.
test_create_report() -> test_create_report() ->
ReporterId = <<"user123">>, {ok, Report} = core_report:create(<<"user123">>, event, <<"event123">>,
TargetType = event, <<"Inappropriate content">>),
TargetId = <<"event123">>, ?assertEqual(<<"user123">>, Report#report.reporter_id),
Reason = <<"Inappropriate content">>, ?assertEqual(event, Report#report.target_type),
?assertEqual(<<"event123">>, Report#report.target_id),
{ok, Report} = core_report:create(ReporterId, TargetType, TargetId, Reason), ?assertEqual(<<"Inappropriate content">>, Report#report.reason),
?assertEqual(ReporterId, Report#report.reporter_id),
?assertEqual(TargetType, Report#report.target_type),
?assertEqual(TargetId, Report#report.target_id),
?assertEqual(Reason, Report#report.reason),
?assertEqual(pending, Report#report.status), ?assertEqual(pending, Report#report.status),
?assertEqual(undefined, Report#report.resolved_at),
?assertEqual(undefined, Report#report.resolved_by),
?assert(is_binary(Report#report.id)). ?assert(is_binary(Report#report.id)).
test_get_by_id() -> test_get_by_id() ->
ReporterId = <<"user123">>, {ok, Report} = core_report:create(<<"user123">>, event, <<"ev1">>, <<"Bad">>),
{ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"Bad">>),
{ok, Found} = core_report:get_by_id(Report#report.id), {ok, Found} = core_report:get_by_id(Report#report.id),
?assertEqual(Report#report.id, Found#report.id), ?assertEqual(Report#report.id, Found#report.id),
{error, not_found} = core_report:get_by_id(<<"nonexistent">>). {error, not_found} = core_report:get_by_id(<<"nonexistent">>).
test_list_by_target() -> test_list_by_target() ->
User1 = <<"user1">>,
User2 = <<"user2">>,
EventId = <<"event123">>, EventId = <<"event123">>,
{ok, _} = core_report:create(<<"user1">>, event, EventId, <<"Reason 1">>),
{ok, _} = core_report:create(User1, event, EventId, <<"Reason 1">>), {ok, _} = core_report:create(<<"user2">>, event, EventId, <<"Reason 2">>),
{ok, _} = core_report:create(User2, event, EventId, <<"Reason 2">>), {ok, _} = core_report:create(<<"user1">>, calendar, <<"cal1">>, <<"Reason 3">>),
{ok, _} = core_report:create(User1, calendar, <<"cal1">>, <<"Reason 3">>), Reports = core_report:list_by_target(event, EventId),
{ok, Reports} = core_report:list_by_target(event, EventId),
?assertEqual(2, length(Reports)). ?assertEqual(2, length(Reports)).
test_list_by_reporter() -> test_list_by_reporter() ->
User1 = <<"user1">>, {ok, _} = core_report:create(<<"user1">>, event, <<"ev1">>, <<"">>),
User2 = <<"user2">>, {ok, _} = core_report:create(<<"user1">>, event, <<"ev2">>, <<"">>),
{ok, _} = core_report:create(<<"user2">>, event, <<"ev3">>, <<"">>),
{ok, _} = core_report:create(User1, event, <<"ev1">>, <<"">>), Reports = core_report:list_by_reporter(<<"user1">>),
{ok, _} = core_report:create(User1, event, <<"ev2">>, <<"">>),
{ok, _} = core_report:create(User2, event, <<"ev3">>, <<"">>),
{ok, Reports} = core_report:list_by_reporter(User1),
?assertEqual(2, length(Reports)). ?assertEqual(2, length(Reports)).
test_update_status() -> test_update_status() ->
ReporterId = <<"user123">>, {ok, Report} = core_report:create(<<"user123">>, event, <<"ev1">>, <<"">>),
AdminId = <<"admin123">>, {ok, Updated} = core_report:update_status(Report#report.id, reviewed, <<"admin123">>),
{ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"">>),
{ok, Updated} = core_report:update_status(Report#report.id, reviewed, AdminId),
?assertEqual(reviewed, Updated#report.status), ?assertEqual(reviewed, Updated#report.status),
?assertEqual(AdminId, Updated#report.resolved_by), ?assertEqual(<<"admin123">>, Updated#report.resolved_by),
?assert(Updated#report.resolved_at =/= undefined). ?assert(Updated#report.resolved_at =/= undefined).
test_get_count_by_target() -> test_get_count_by_target() ->
User1 = <<"user1">>,
User2 = <<"user2">>,
EventId = <<"event123">>, EventId = <<"event123">>,
?assertEqual(0, core_report:get_count_by_target(event, EventId)), ?assertEqual(0, core_report:get_count_by_target(event, EventId)),
{ok, _} = core_report:create(<<"user1">>, event, EventId, <<"">>),
{ok, _} = core_report:create(User1, event, EventId, <<"">>),
?assertEqual(1, core_report:get_count_by_target(event, EventId)), ?assertEqual(1, core_report:get_count_by_target(event, EventId)),
{ok, _} = core_report:create(<<"user2">>, event, EventId, <<"">>),
{ok, _} = core_report:create(User2, event, EventId, <<"">>),
?assertEqual(2, core_report:get_count_by_target(event, EventId)). ?assertEqual(2, core_report:get_count_by_target(event, EventId)).
+11 -10
View File
@@ -2,17 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [review]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(review, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, review)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(review), eh_test_support:delete_tables(?TABLES),
mnesia:stop(), eh_test_support:stop_mnesia(),
ok. ok.
core_review_test_() -> core_review_test_() ->
@@ -103,11 +102,13 @@ test_hide_unhide() ->
UserId = <<"user123">>, UserId = <<"user123">>,
{ok, Review} = core_review:create(UserId, event, <<"ev1">>, 4, <<"">>), {ok, Review} = core_review:create(UserId, event, <<"ev1">>, 4, <<"">>),
{ok, Hidden} = core_review:hide(Review#review.id), {ok, Hidden} = core_review:hide(Review#review.id, <<"spam">>),
?assertEqual(hidden, Hidden#review.status), ?assertEqual(hidden, Hidden#review.status),
?assertEqual(<<"spam">>, Hidden#review.reason),
{ok, Unhidden} = core_review:unhide(Review#review.id), {ok, Unhidden} = core_review:unhide(Review#review.id, <<"restored">>),
?assertEqual(visible, Unhidden#review.status). ?assertEqual(visible, Unhidden#review.status),
?assertEqual(<<"restored">>, Unhidden#review.reason).
test_average_rating() -> test_average_rating() ->
UserId1 = <<"user1">>, UserId1 = <<"user1">>,
+161
View File
@@ -0,0 +1,161 @@
-module(core_user_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user]).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
case mnesia:add_table_index(user, email) of
{atomic, ok} -> ok;
{aborted, {already_exists, user, email}} -> ok;
{aborted, Reason} -> error({add_index_failed, Reason})
end,
ok.
cleanup(_) ->
eh_test_support:clear_tables(?TABLES),
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
core_user_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"Create user", fun test_create/0},
{"Create user email exists", fun test_create_email_exists/0},
{"Get by id", fun test_get_by_id/0},
{"Get by email / email_exists", fun test_get_by_email/0},
{"List all and list_users", fun test_list/0},
{"Update fields", fun test_update/0},
{"Update status", fun test_update_status/0},
{"Update last login", fun test_update_last_login/0},
{"Block and unblock", fun test_block_unblock/0},
{"Soft delete", fun test_delete/0},
{"Count helpers", fun test_counts/0},
{"Create and delete bot", fun test_bot/0}
]}.
test_create() ->
Email = <<"user1@test.local">>,
{ok, User} = core_user:create(Email, <<"secret123">>),
?assertEqual(Email, User#user.email),
?assertEqual(user, User#user.role),
?assertEqual(pending, User#user.status),
?assertEqual(<<"user1">>, User#user.nickname),
?assert(is_binary(User#user.id)),
?assert(is_binary(User#user.password_hash)),
?assert(User#user.password_hash =/= <<"secret123">>).
test_create_email_exists() ->
Email = <<"dup@test.local">>,
{ok, _} = core_user:create(Email, <<"pass1">>),
?assertEqual({error, email_exists}, core_user:create(Email, <<"pass2">>)).
test_get_by_id() ->
{ok, User} = core_user:create(<<"byid@test.local">>, <<"pass">>),
{ok, Found} = core_user:get_by_id(User#user.id),
?assertEqual(User#user.id, Found#user.id),
?assertEqual({error, not_found}, core_user:get_by_id(<<"missing">>)).
test_get_by_email() ->
Email = <<"byemail@test.local">>,
{ok, User} = core_user:create(Email, <<"pass">>),
{ok, Found} = core_user:get_by_email(Email),
?assertEqual(User#user.id, Found#user.id),
?assertEqual(true, core_user:email_exists(Email)),
?assertEqual(false, core_user:email_exists(<<"nope@test.local">>)),
?assertEqual({error, not_found}, core_user:get_by_email(<<"nope@test.local">>)).
test_list() ->
?assertEqual([], core_user:list_all()),
{ok, U1} = core_user:create(<<"a@test.local">>, <<"pass">>),
{ok, U2} = core_user:create(<<"b@test.local">>, <<"pass">>),
?assertEqual(2, length(core_user:list_all())),
{ok, _} = core_user:delete(U1#user.id),
All = core_user:list_all(),
?assertEqual(2, length(All)),
{ok, ActiveMaps} = core_user:list_users(),
?assertEqual(1, length(ActiveMaps)),
[Map] = ActiveMaps,
?assertEqual(U2#user.id, maps:get(id, Map)),
?assertEqual(<<"b@test.local">>, maps:get(email, Map)).
test_update() ->
{ok, User} = core_user:create(<<"upd@test.local">>, <<"pass">>),
{ok, Updated} = core_user:update(User#user.id, [
{nickname, <<"nick">>},
{phone, <<"+7000">>},
{timezone, <<"UTC">>},
{status, active}
]),
?assertEqual(<<"nick">>, Updated#user.nickname),
?assertEqual(<<"+7000">>, Updated#user.phone),
?assertEqual(<<"UTC">>, Updated#user.timezone),
?assertEqual(active, Updated#user.status),
?assertEqual({error, not_found}, core_user:update(<<"missing">>, [{nickname, <<"x">>}])).
test_update_status() ->
{ok, User} = core_user:create(<<"st@test.local">>, <<"pass">>),
{ok, Updated} = core_user:update_status(User#user.id, frozen, <<"review">>),
?assertEqual(frozen, Updated#user.status),
?assertEqual(<<"review">>, Updated#user.reason),
?assertEqual({error, not_found},
core_user:update_status(<<"missing">>, active, <<>>)).
test_update_last_login() ->
{ok, User} = core_user:create(<<"login@test.local">>, <<"pass">>),
OldLogin = User#user.last_login,
{ok, Updated} = core_user:update_last_login(User#user.id),
?assert(Updated#user.last_login >= OldLogin),
?assertEqual({error, not_found}, core_user:update_last_login(<<"missing">>)).
test_block_unblock() ->
{ok, User} = core_user:create(<<"block@test.local">>, <<"pass">>),
{ok, Blocked} = core_user:block(User#user.id, <<"spam">>),
?assertEqual(blocked, Blocked#user.status),
?assertEqual(<<"spam">>, Blocked#user.reason),
{ok, Active} = core_user:unblock(User#user.id, <<"ok">>),
?assertEqual(active, Active#user.status),
?assertEqual(<<"ok">>, Active#user.reason),
?assertEqual({error, not_found}, core_user:block(<<"missing">>, <<"x">>)),
?assertEqual({error, not_found}, core_user:unblock(<<"missing">>, <<"x">>)).
test_delete() ->
{ok, User} = core_user:create(<<"del@test.local">>, <<"pass">>),
{ok, Deleted} = core_user:delete(User#user.id),
?assertEqual(deleted, Deleted#user.status),
{ok, StillThere} = core_user:get_by_id(User#user.id),
?assertEqual(deleted, StillThere#user.status),
?assertEqual({error, not_found}, core_user:delete(<<"missing">>)).
test_counts() ->
{ok, U1} = core_user:create(<<"c1@test.local">>, <<"pass">>),
{ok, _} = core_user:create(<<"c2@test.local">>, <<"pass">>),
{ok, _} = core_user:update_status(U1#user.id, active, <<>>),
?assertEqual(2, core_user:count_users()),
?assertEqual(1, core_user:count_pending_users()),
ByRole = core_user:count_users_by_role(),
?assertEqual(2, proplists:get_value(user, ByRole)),
ByStatus = core_user:count_users_by_status(),
?assertEqual(1, proplists:get_value(pending, ByStatus)),
?assertEqual(1, proplists:get_value(active, ByStatus)),
Now = calendar:universal_time(),
{{Y, M, D}, _} = Now,
From = {{Y, M, D}, {0, 0, 0}},
To = {{Y, M, D}, {23, 59, 59}},
ByDate = core_user:count_users_by_date(From, To),
?assert(lists:keyfind({Y, M, D}, 1, ByDate) =/= false),
PendingByDate = core_user:count_pending_users_by_date(From, To),
{_, PendingCount} = lists:keyfind({Y, M, D}, 1, PendingByDate),
?assertEqual(1, PendingCount).
test_bot() ->
Email = <<"bot@test.local">>,
{ok, Bot} = core_user:create_bot(Email, <<"botpass">>),
?assertEqual(bot, Bot#user.role),
?assertEqual(active, Bot#user.status),
?assertEqual({error, email_exists}, core_user:create_bot(Email, <<"other">>)),
?assertEqual(ok, core_user:delete_bot(Bot#user.id)),
?assertEqual({error, not_found}, core_user:get_by_id(Bot#user.id)),
?assertEqual({error, not_found}, core_user:delete_bot(<<"missing">>)).
+356
View File
@@ -0,0 +1,356 @@
%%%-------------------------------------------------------------------
%%% Shared fixtures for EventHub EUnit tests.
%%% - In-memory Mnesia tables (ram_copies), isolated per suite
%%% - Thin cowboy_req fake via meck (HTTP boundary only)
%%% - Real JWT + admin seed for auth_admin without mocking domain
%%%-------------------------------------------------------------------
-module(eh_test_support).
-include("records.hrl").
-export([
start_mnesia/0,
stop_mnesia/0,
ensure_tables/1,
clear_tables/1,
delete_tables/1,
seed_admin/0,
seed_admin/1,
make_admin/1,
make_user/1,
ensure_jwt/0,
admin_token/1,
with_cowboy/1,
update_cowboy/1,
unload_cowboy/0,
last_reply/0,
last_status/0,
last_body/0,
last_json/0,
call/2,
days_from_now/1,
future_start/0
]).
-define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>).
-define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>).
-define(DEFAULT_ADMIN_ID, <<"adm_test_1">>).
-define(REQ_KEY, eh_test_req).
-define(REPLY_KEY, eh_test_reply).
%%%===================================================================
%%% Mnesia
%%%===================================================================
start_mnesia() ->
application:load(mnesia),
catch mnesia:stop(),
Base = case os:getenv("TMPDIR") of
false ->
case os:getenv("TEMP") of
false -> "/tmp";
Temp -> Temp
end;
TmpDir -> TmpDir
end,
Tmp = filename:join(Base, "eh_eunit_" ++ integer_to_list(erlang:unique_integer([positive]))),
ok = filelib:ensure_dir(filename:join(Tmp, "dummy")),
application:set_env(mnesia, dir, Tmp),
_ = mnesia:delete_schema([node()]),
ok = mnesia:create_schema([node()]),
case mnesia:start() of
ok -> ok;
{error, {already_started, mnesia}} -> ok;
Other -> error({mnesia_start_failed, Other})
end,
ok.
stop_mnesia() ->
catch mnesia:stop(),
catch mnesia:delete_schema([node()]),
ok.
ensure_tables(Tables) when is_list(Tables) ->
lists:foreach(fun ensure_table/1, Tables),
ok.
ensure_table(Table) ->
case lists:member(Table, mnesia:system_info(tables)) of
true ->
ok;
false ->
case mnesia:create_table(Table, table_opts(Table)) of
{atomic, ok} -> ok;
{aborted, {already_exists, Table}} -> ok;
{aborted, Reason} -> error({create_table_failed, Table, Reason})
end
end.
clear_tables(Tables) when is_list(Tables) ->
lists:foreach(fun(T) ->
case lists:member(T, mnesia:system_info(tables)) of
true -> {atomic, ok} = mnesia:clear_table(T);
false -> ok
end
end, Tables),
ok.
delete_tables(Tables) when is_list(Tables) ->
lists:foreach(fun(T) -> catch mnesia:delete_table(T) end, Tables),
ok.
table_opts(user) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, user)}];
table_opts(admin) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, admin)}];
table_opts(calendar) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar)}];
table_opts(calendar_share) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_share)}];
table_opts(calendar_specialist) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
table_opts(event) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, event)}];
table_opts(recurrence_exception) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, recurrence_exception)}];
table_opts(booking) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, booking)}];
table_opts(review) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, review)}];
table_opts(report) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, report)}];
table_opts(banned_word) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, banned_word)}];
table_opts(ticket) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, ticket)}];
table_opts(subscription) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, subscription)}];
table_opts(admin_audit) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, admin_audit)}];
table_opts(notification) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, notification)}];
table_opts(stats) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, stats)}];
table_opts(session) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, session)}];
table_opts(verification) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, verification)}];
table_opts(admin_session) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, admin_session)}];
table_opts(auth_session) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, auth_session)}];
table_opts(node_metric) ->
[{ram_copies, [node()]}, {local_content, true},
{attributes, record_info(fields, node_metric)}];
table_opts(schema_migration) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, schema_migration)}].
%%%===================================================================
%%% Domain seeds
%%%===================================================================
seed_admin() ->
seed_admin(#{}).
seed_admin(Opts) when is_map(Opts) ->
Admin = make_admin(Opts),
ok = mnesia:dirty_write(Admin),
Admin.
make_admin(Opts) when is_map(Opts) ->
Now = calendar:universal_time(),
#admin{
id = maps:get(id, Opts, ?DEFAULT_ADMIN_ID),
email = maps:get(email, Opts, <<"admin@test.local">>),
password_hash = maps:get(password_hash, Opts, <<"hash">>),
role = maps:get(role, Opts, admin),
status = maps:get(status, Opts, active),
nickname = maps:get(nickname, Opts, <<"admin">>),
avatar_url = maps:get(avatar_url, Opts, default),
timezone = maps:get(timezone, Opts, <<"UTC">>),
language = maps:get(language, Opts, <<"ru">>),
phone = maps:get(phone, Opts, <<>>),
preferences = maps:get(preferences, Opts, #{}),
last_login = maps:get(last_login, Opts, Now),
created_at = maps:get(created_at, Opts, Now),
updated_at = maps:get(updated_at, Opts, Now)
}.
make_user(Opts) when is_map(Opts) ->
Now = calendar:universal_time(),
#user{
id = maps:get(id, Opts, <<"user_test_1">>),
email = maps:get(email, Opts, <<"user@test.local">>),
password_hash = maps:get(password_hash, Opts, <<"hash">>),
role = maps:get(role, Opts, user),
status = maps:get(status, Opts, active),
reason = maps:get(reason, Opts, <<>>),
nickname = maps:get(nickname, Opts, <<"user">>),
avatar_url = maps:get(avatar_url, Opts, default),
timezone = maps:get(timezone, Opts, <<"UTC">>),
language = maps:get(language, Opts, <<"ru">>),
social_links = maps:get(social_links, Opts, []),
phone = maps:get(phone, Opts, <<>>),
preferences = maps:get(preferences, Opts, #{}),
last_login = maps:get(last_login, Opts, Now),
created_at = maps:get(created_at, Opts, Now),
updated_at = maps:get(updated_at, Opts, Now)
}.
%%%===================================================================
%%% JWT
%%%===================================================================
ensure_jwt() ->
application:set_env(eventhub, jwt_secret, ?JWT_SECRET),
application:set_env(eventhub, admin_jwt_secret, ?ADMIN_JWT_SECRET),
{ok, _} = application:ensure_all_started(jose),
ok.
admin_token(AdminId) when is_binary(AdminId) ->
ensure_jwt(),
eventhub_auth:generate_admin_token(AdminId, <<"admin">>).
%%%===================================================================
%%% Cowboy request fake (meck at HTTP boundary only)
%%%===================================================================
%% Opts:
%% method :: binary()
%% path :: binary()
%% bindings :: #{atom() => binary() | undefined}
%% qs :: [{binary(), binary()}]
%% body :: binary()
%% has_body :: boolean()
%% headers :: #{binary() => binary()}
%% peer :: inet:ip_address() | {inet:ip_address(), inet:port_number()}
%% auth :: binary() AdminId | {bearer, Token} | none
with_cowboy(Opts) when is_map(Opts) ->
unload_cowboy(),
ok = meck:new(cowboy_req, [non_strict]),
State = normalize_req(Opts),
put(?REQ_KEY, State),
erase(?REPLY_KEY),
install_cowboy_expects(),
req.
update_cowboy(Opts) when is_map(Opts) ->
Cur = case get(?REQ_KEY) of
undefined -> #{};
M when is_map(M) -> M
end,
put(?REQ_KEY, maps:merge(Cur, normalize_req(Opts))),
erase(?REPLY_KEY),
req.
unload_cowboy() ->
catch meck:unload(cowboy_req),
erase(?REQ_KEY),
erase(?REPLY_KEY),
ok.
last_reply() ->
case get(?REPLY_KEY) of
undefined -> error(no_reply);
Reply -> Reply
end.
last_status() ->
{Status, _, _} = last_reply(),
Status.
last_body() ->
{_, _, Body} = last_reply(),
Body.
last_json() ->
jsx:decode(last_body(), [return_maps]).
call(Module, Opts) when is_atom(Module), is_map(Opts) ->
Req = with_cowboy(Opts),
{ok, _, _} = Module:init(Req, []),
last_reply().
%%%===================================================================
%%% Internal
%%%===================================================================
normalize_req(Opts) ->
Auth = maps:get(auth, Opts, none),
Headers0 = maps:get(headers, Opts, #{<<"x-real-ip">> => <<"127.0.0.1">>}),
{AuthHdr, Token} = case Auth of
none ->
{undefined, undefined};
{bearer, T} when is_binary(T) ->
{{bearer, T}, T};
AdminId when is_binary(AdminId) ->
T = admin_token(AdminId),
{{bearer, T}, T}
end,
Headers = case AuthHdr of
undefined -> Headers0;
_ -> Headers0#{<<"authorization">> => <<"Bearer ", Token/binary>>}
end,
#{
method => maps:get(method, Opts, <<"GET">>),
path => maps:get(path, Opts, <<"/v1/admin">>),
bindings => maps:get(bindings, Opts, #{}),
qs => maps:get(qs, Opts, []),
body => maps:get(body, Opts, <<>>),
has_body => maps:get(has_body, Opts, maps:is_key(body, Opts)),
headers => Headers,
peer => maps:get(peer, Opts, {127, 0, 0, 1}),
auth_parsed => AuthHdr
}.
install_cowboy_expects() ->
ok = meck:expect(cowboy_req, method, fun(_) ->
maps:get(method, get(?REQ_KEY))
end),
ok = meck:expect(cowboy_req, path, fun(_) ->
maps:get(path, get(?REQ_KEY))
end),
ok = meck:expect(cowboy_req, binding, fun(Key, _) ->
maps:get(Key, maps:get(bindings, get(?REQ_KEY)), undefined)
end),
ok = meck:expect(cowboy_req, parse_qs, fun(_) ->
maps:get(qs, get(?REQ_KEY))
end),
ok = meck:expect(cowboy_req, read_body, fun(Req) ->
{ok, maps:get(body, get(?REQ_KEY)), Req}
end),
ok = meck:expect(cowboy_req, has_body, fun(_) ->
maps:get(has_body, get(?REQ_KEY))
end),
ok = meck:expect(cowboy_req, header, fun(Name, _) ->
maps:get(Name, maps:get(headers, get(?REQ_KEY)), undefined)
end),
ok = meck:expect(cowboy_req, parse_header, fun
(<<"authorization">>, _) ->
maps:get(auth_parsed, get(?REQ_KEY), undefined);
(_, _) ->
undefined
end),
ok = meck:expect(cowboy_req, peer, fun(_) ->
maps:get(peer, get(?REQ_KEY))
end),
ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) ->
put(?REPLY_KEY, {Code, Headers, Body}),
Req
end),
ok.
%%%===================================================================
%%% Time helpers
%%%===================================================================
%% @doc Calendar datetime N days from now (UTC).
days_from_now(Days) when is_integer(Days) ->
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time())
+ (Days * 86400),
calendar:gregorian_seconds_to_datetime(Sec).
%% @doc Convenient future start (~30 days ahead, noon UTC).
future_start() ->
{{Y, M, D}, _} = days_from_now(30),
{{Y, M, D}, {12, 0, 0}}.
+39 -74
View File
@@ -2,34 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TEST_EMAIL, <<"test@example.com">>). -define(TABLES, [user, calendar, event, booking]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [ eh_test_support:ensure_tables(?TABLES),
{attributes, record_info(fields, user)},
{ram_copies, [node()]}
]),
mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
mnesia:create_table(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(booking, [
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(booking), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(event), eh_test_support:stop_mnesia(),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
logic_booking_test_() -> logic_booking_test_() ->
@@ -37,16 +19,15 @@ logic_booking_test_() ->
fun setup/0, fun setup/0,
fun cleanup/1, fun cleanup/1,
[ [
{"Create booking with auto confirmation", fun test_create_booking_auto/0}, {"Create booking returns pending", fun test_create_booking_pending/0},
{"Create booking with manual confirmation", fun test_create_booking_manual/0},
{"Create booking with timeout confirmation", fun test_create_booking_timeout/0},
{"Create duplicate booking", fun test_create_duplicate_booking/0}, {"Create duplicate booking", fun test_create_duplicate_booking/0},
{"Create booking for inactive event", fun test_booking_inactive_event/0}, {"Create booking for missing event", fun test_booking_missing_event/0},
{"Create booking when event is full", fun test_booking_event_full/0}, {"Create booking when event is full", fun test_booking_event_full/0},
{"Confirm booking by owner", fun test_confirm_booking/0}, {"Pending bookings do not fill capacity", fun test_pending_does_not_fill/0},
{"Decline booking by owner", fun test_decline_booking/0}, {"Confirm booking", fun test_confirm_booking/0},
{"Confirm non-pending booking denied", fun test_confirm_non_pending/0},
{"Cancel booking by participant", fun test_cancel_booking/0}, {"Cancel booking by participant", fun test_cancel_booking/0},
{"Unauthorized confirm attempt", fun test_unauthorized_confirm/0}, {"Cancel booking access denied", fun test_cancel_access_denied/0},
{"List event bookings", fun test_list_event_bookings/0}, {"List event bookings", fun test_list_event_bookings/0},
{"List user bookings", fun test_list_user_bookings/0} {"List user bookings", fun test_list_user_bookings/0}
]}. ]}.
@@ -71,50 +52,28 @@ create_test_calendar(OwnerId, Confirmation) ->
Calendar#calendar.id. Calendar#calendar.id.
create_test_event(CalendarId) -> create_test_event(CalendarId) ->
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60), {ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
Event#event.id. Event#event.id.
create_test_event_with_capacity(CalendarId, Capacity) -> create_test_event_with_capacity(CalendarId, Capacity) ->
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60), {ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
{ok, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]), {ok, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]),
Updated#event.id. Updated#event.id.
%% Тесты %% Тесты
test_create_booking_auto() -> test_create_booking_pending() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto), CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
timer:sleep(100),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(confirmed, Updated#booking.status).
test_create_booking_manual() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status).
test_create_booking_timeout() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, {timeout, 1}),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status), ?assertEqual(pending, Booking#booking.status),
timer:sleep(1500), {ok, Stored} = core_booking:get_by_id(Booking#booking.id),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id), ?assertEqual(pending, Stored#booking.status).
?assertEqual(confirmed, Updated#booking.status).
test_create_duplicate_booking() -> test_create_duplicate_booking() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
@@ -125,25 +84,31 @@ test_create_duplicate_booking() ->
{ok, _} = logic_booking:create_booking(ParticipantId, EventId), {ok, _} = logic_booking:create_booking(ParticipantId, EventId),
{error, already_booked} = logic_booking:create_booking(ParticipantId, EventId). {error, already_booked} = logic_booking:create_booking(ParticipantId, EventId).
test_booking_inactive_event() -> test_booking_missing_event() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual), {error, not_found} = logic_booking:create_booking(ParticipantId, <<"nonexistent">>).
EventId = create_test_event(CalendarId),
{ok, _} = core_event:update(EventId, [{status, cancelled}]),
{error, event_not_active} = logic_booking:create_booking(ParticipantId, EventId).
test_booking_event_full() -> test_booking_event_full() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
Participant1Id = create_test_user(user), Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user), Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto), CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, B1} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:confirm_booking(Participant1Id, B1#booking.id, confirm),
{error, full} = logic_booking:create_booking(Participant2Id, EventId).
test_pending_does_not_fill() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_with_capacity(CalendarId, 1), EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId), {ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{error, event_full} = logic_booking:create_booking(Participant2Id, EventId). {ok, B2} = logic_booking:create_booking(Participant2Id, EventId),
?assertEqual(pending, B2#booking.status).
test_confirm_booking() -> test_confirm_booking() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
@@ -155,15 +120,15 @@ test_confirm_booking() ->
{ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status). ?assertEqual(confirmed, Confirmed#booking.status).
test_decline_booking() -> test_confirm_non_pending() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual), CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline), {ok, _} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(cancelled, Declined#booking.status). {error, access_denied} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm).
test_cancel_booking() -> test_cancel_booking() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
@@ -172,10 +137,10 @@ test_cancel_booking() ->
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id), {ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id, cancel),
?assertEqual(cancelled, Cancelled#booking.status). ?assertEqual(cancelled, Cancelled#booking.status).
test_unauthorized_confirm() -> test_cancel_access_denied() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(user),
OtherId = create_test_user(user), OtherId = create_test_user(user),
@@ -183,7 +148,7 @@ test_unauthorized_confirm() ->
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:confirm_booking(OtherId, Booking#booking.id, confirm). {error, access_denied} = logic_booking:cancel_booking(OtherId, Booking#booking.id, cancel).
test_list_event_bookings() -> test_list_event_bookings() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
@@ -195,7 +160,7 @@ test_list_event_bookings() ->
{ok, _} = logic_booking:create_booking(Participant1Id, EventId), {ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:create_booking(Participant2Id, EventId), {ok, _} = logic_booking:create_booking(Participant2Id, EventId),
{ok, Bookings} = logic_booking:list_event_bookings(OwnerId, EventId), {ok, Bookings} = logic_booking:list_event_bookings(EventId),
?assertEqual(2, length(Bookings)). ?assertEqual(2, length(Bookings)).
test_list_user_bookings() -> test_list_user_bookings() ->
+21 -33
View File
@@ -2,32 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, calendar, event, recurrence_exception]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [ eh_test_support:ensure_tables(?TABLES),
{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(recurrence_exception, [
{attributes, record_info(fields, recurrence_exception)},
{ram_copies, [node()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(recurrence_exception), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(event), eh_test_support:stop_mnesia(),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
logic_event_recurring_test_() -> logic_event_recurring_test_() ->
@@ -59,10 +43,14 @@ create_test_user_and_calendar() ->
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual),
{UserId, Calendar#calendar.id}. {UserId, Calendar#calendar.id}.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_create_recurring_event() -> test_create_recurring_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Weekly Meeting">>, Title = <<"Weekly Meeting">>,
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
Duration = 60, Duration = 60,
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
@@ -75,7 +63,7 @@ test_create_recurring_event() ->
test_create_recurring_invalid() -> test_create_recurring_invalid() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Invalid">>, Title = <<"Invalid">>,
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
Duration = 60, Duration = 60,
InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1}, InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1},
@@ -85,43 +73,43 @@ test_create_recurring_invalid() ->
test_get_occurrences() -> test_get_occurrences() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event( {ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
), ),
RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, RangeEnd = add_days(StartTime, 28),
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd), {ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
?assertEqual(5, length(Occurrences)). ?assertEqual(5, length(Occurrences)).
test_cancel_occurrence() -> test_cancel_occurrence() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event( {ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
), ),
CancelTime = {{2026, 6, 8}, {10, 0, 0}}, CancelTime = add_days(StartTime, 7),
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime). {ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime).
test_occurrences_with_cancelled() -> test_occurrences_with_cancelled() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event( {ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
), ),
CancelTime = {{2026, 6, 8}, {10, 0, 0}}, CancelTime = add_days(StartTime, 7),
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime), {ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime),
RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, RangeEnd = add_days(StartTime, 28),
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd), {ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
?assertEqual(4, length(Occurrences)), ?assertEqual(4, length(Occurrences)),
@@ -131,14 +119,14 @@ test_occurrences_with_cancelled() ->
test_materialize_for_booking() -> test_materialize_for_booking() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event( {ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
), ),
OccurrenceStart = {{2026, 6, 8}, {10, 0, 0}}, OccurrenceStart = add_days(StartTime, 7),
SpecialistId = <<"specialist123">>, SpecialistId = <<"specialist123">>,
{ok, Instance} = logic_event:materialize_for_booking( {ok, Instance} = logic_event:materialize_for_booking(
+22 -24
View File
@@ -2,27 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, calendar, event]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [ eh_test_support:ensure_tables(?TABLES),
{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()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(event), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(calendar), eh_test_support:stop_mnesia(),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
logic_event_test_() -> logic_event_test_() ->
@@ -54,10 +43,14 @@ create_test_user_and_calendar() ->
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual),
{UserId, Calendar#calendar.id}. {UserId, Calendar#calendar.id}.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_create_event() -> test_create_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Test Event">>, Title = <<"Test Event">>,
StartTime = {{2026, 5, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
Duration = 60, Duration = 60,
{ok, Event} = logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration), {ok, Event} = logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration),
@@ -67,22 +60,26 @@ test_create_event() ->
test_get_event() -> test_get_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, {{2026, 5, 1}, {10, 0, 0}}, 60), StartTime = eh_test_support:future_start(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, StartTime, 60),
{ok, Found} = logic_event:get_event(UserId, Event#event.id), {ok, Found} = logic_event:get_event(UserId, Event#event.id),
?assertEqual(Event#event.id, Found#event.id). ?assertEqual(Event#event.id, Found#event.id).
test_list_events() -> test_list_events() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 1">>, {{2026, 5, 1}, {10, 0, 0}}, 60), Start1 = eh_test_support:future_start(),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 2">>, {{2026, 5, 2}, {11, 0, 0}}, 90), Start2 = add_days(Start1, 1),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 1">>, Start1, 60),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 2">>, Start2, 90),
{ok, Events} = logic_event:list_events(UserId, CalendarId), {ok, Events} = logic_event:list_events(UserId, CalendarId),
?assertEqual(2, length(Events)). ?assertEqual(2, length(Events)).
test_update_event() -> test_update_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Original">>, {{2026, 5, 1}, {10, 0, 0}}, 60), StartTime = eh_test_support:future_start(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Original">>, StartTime, 60),
Updates = [{title, <<"Updated">>}, {capacity, 50}], Updates = [{title, <<"Updated">>}, {capacity, 50}],
{ok, Updated} = logic_event:update_event(UserId, Event#event.id, Updates), {ok, Updated} = logic_event:update_event(UserId, Event#event.id, Updates),
@@ -91,7 +88,8 @@ test_update_event() ->
test_delete_event() -> test_delete_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(), {UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, {{2026, 5, 1}, {10, 0, 0}}, 60), StartTime = eh_test_support:future_start(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, StartTime, 60),
{ok, Deleted} = logic_event:delete_event(UserId, Event#event.id), {ok, Deleted} = logic_event:delete_event(UserId, Event#event.id),
?assertEqual(deleted, Deleted#event.status). ?assertEqual(deleted, Deleted#event.status).
@@ -102,5 +100,5 @@ test_time_validation() ->
PastTime = {{2020, 1, 1}, {10, 0, 0}}, PastTime = {{2020, 1, 1}, {10, 0, 0}},
{error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60), {error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60),
FutureTime = {{2030, 1, 1}, {10, 0, 0}}, FutureTime = eh_test_support:future_start(),
?assertEqual(ok, logic_event:validate_event_time(FutureTime)). ?assertEqual(ok, logic_event:validate_event_time(FutureTime)).
+35 -52
View File
@@ -2,34 +2,20 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, admin, calendar, event, report, banned_word]).
%% ---------------------------------------------------------------- %% ----------------------------------------------------------------
%% Фикстуры %% Фикстуры
%% ---------------------------------------------------------------- %% ----------------------------------------------------------------
setup() -> setup() ->
catch mnesia:stop(), eh_test_support:start_mnesia(),
case mnesia:start() of eh_test_support:ensure_tables(?TABLES),
{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. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(banned_word), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(report), eh_test_support:stop_mnesia(),
mnesia:delete_table(event), ok.
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop().
%% ---------------------------------------------------------------- %% ----------------------------------------------------------------
%% Тесты %% Тесты
@@ -48,33 +34,30 @@ logic_moderation_test_() ->
]}. ]}.
%% Вспомогательные функции %% Вспомогательные функции
create_test_user(Role) -> create_test_user() ->
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{ User = eh_test_support:make_user(#{id => UserId, email => <<UserId/binary, "@test.com">>}),
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.
create_test_admin() ->
AdminId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
Admin = eh_test_support:seed_admin(#{id => AdminId, email => <<AdminId/binary, "@admin.test">>}),
Admin#admin.id.
create_test_calendar(OwnerId) -> create_test_calendar(OwnerId) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
Calendar#calendar.id. Calendar#calendar.id.
create_test_event(CalendarId) -> create_test_event(CalendarId) ->
{ok, Event} = core_event:create(CalendarId, <<"Event">>, {ok, Event} = core_event:create(CalendarId, <<"Event">>,
{{2026, 6, 1}, {10, 0, 0}}, 60), eh_test_support:future_start(), 60),
Event#event.id. Event#event.id.
%% Тесты %% Тесты
test_create_report() -> test_create_report() ->
ReporterId = create_test_user(user), ReporterId = create_test_user(),
OwnerId = create_test_user(user), OwnerId = create_test_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">>),
@@ -82,19 +65,19 @@ test_create_report() ->
?assertEqual(pending, Report#report.status). ?assertEqual(pending, Report#report.status).
test_get_reports() -> test_get_reports() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
ReporterId = create_test_user(user), ReporterId = create_test_user(),
OwnerId = create_test_user(user), OwnerId = create_test_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), Reports = logic_moderation:get_reports(AdminId),
?assertEqual(1, length(Reports)). ?assertEqual(1, length(Reports)).
test_resolve_report() -> test_resolve_report() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
ReporterId = create_test_user(user), ReporterId = create_test_user(),
OwnerId = create_test_user(user), OwnerId = create_test_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, <<"">>),
@@ -103,22 +86,22 @@ test_resolve_report() ->
?assertEqual(AdminId, Resolved#report.resolved_by). ?assertEqual(AdminId, Resolved#report.resolved_by).
test_add_banned_word() -> test_add_banned_word() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
{ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>), {ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
?assertEqual(<<"badword">>, BW#banned_word.word), ?assertEqual(<<"badword">>, BW#banned_word.word),
?assertEqual(AdminId, BW#banned_word.added_by). ?assertEqual(AdminId, BW#banned_word.added_by).
test_remove_banned_word() -> test_remove_banned_word() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>), {ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
{ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>), {ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
?assertEqual([], core_banned_words:list_banned_words()). ?assertEqual([], core_banned_words:list_banned_words()).
test_auto_freeze() -> test_auto_freeze() ->
Reporter1 = create_test_user(user), Reporter1 = create_test_user(),
Reporter2 = create_test_user(user), Reporter2 = create_test_user(),
Reporter3 = create_test_user(user), Reporter3 = create_test_user(),
OwnerId = create_test_user(user), OwnerId = create_test_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(Reporter1, event, EventId, <<"">>), {ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
@@ -128,8 +111,8 @@ test_auto_freeze() ->
?assertEqual(frozen, Event#event.status). ?assertEqual(frozen, Event#event.status).
test_freeze_calendar() -> test_freeze_calendar() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
OwnerId = create_test_user(user), OwnerId = create_test_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),
@@ -137,8 +120,8 @@ test_freeze_calendar() ->
?assertEqual(active, Unfrozen#calendar.status). ?assertEqual(active, Unfrozen#calendar.status).
test_freeze_event() -> test_freeze_event() ->
AdminId = create_test_user(admin), AdminId = create_test_admin(),
OwnerId = create_test_user(user), OwnerId = create_test_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),
@@ -147,7 +130,7 @@ test_freeze_event() ->
?assertEqual(active, Unfrozen#event.status). ?assertEqual(active, Unfrozen#event.status).
test_check_content() -> test_check_content() ->
AdminId = create_test_user(admin), AdminId = create_test_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">>)),
+7 -10
View File
@@ -2,19 +2,17 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [booking, calendar, event]).
setup() -> setup() ->
pg:start_link(), pg:start_link(),
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]), eh_test_support:ensure_tables(?TABLES),
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(event), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(calendar), eh_test_support:stop_mnesia(),
mnesia:delete_table(booking),
mnesia:stop(),
ok. ok.
logic_notification_test_() -> logic_notification_test_() ->
@@ -66,7 +64,7 @@ create_test_event() ->
title = <<"Test Event">>, title = <<"Test Event">>,
description = <<"">>, description = <<"">>,
event_type = single, event_type = single,
start_time = {{2026, 6, 1}, {10, 0, 0}}, start_time = eh_test_support:future_start(),
duration = 60, duration = 60,
recurrence_rule = undefined, recurrence_rule = undefined,
master_id = undefined, master_id = undefined,
@@ -89,7 +87,6 @@ test_notify_booking() ->
Booking = create_test_booking(), Booking = create_test_booking(),
UserId = <<"user123">>, UserId = <<"user123">>,
% Функция возвращает список пидов (может быть пустым)
Result = logic_notification:notify_booking(UserId, Booking), Result = logic_notification:notify_booking(UserId, Booking),
?assert(is_list(Result)). ?assert(is_list(Result)).
+39 -40
View File
@@ -2,22 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, admin, calendar, event, booking, review]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]), eh_test_support:ensure_tables(?TABLES),
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]),
mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(review), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(booking), eh_test_support:stop_mnesia(),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
logic_review_test_() -> logic_review_test_() ->
@@ -36,29 +30,35 @@ logic_review_test_() ->
{"Rating updates target", fun test_rating_updates_target/0} {"Rating updates target", fun test_rating_updates_target/0}
]}. ]}.
create_test_user(Role) -> create_test_user() ->
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 = eh_test_support:make_user(#{id => UserId, email => <<UserId/binary, "@test.com">>}),
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User), mnesia:dirty_write(User),
UserId. UserId.
create_test_admin() ->
AdminId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
Admin = eh_test_support:seed_admin(#{id => AdminId, email => <<AdminId/binary, "@admin.test">>}),
Admin#admin.id.
create_test_calendar(OwnerId) -> create_test_calendar(OwnerId) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
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">>, eh_test_support:future_start(), 60),
Event#event.id. Event#event.id.
create_booking(UserId, EventId) -> create_booking(UserId, EventId) ->
{ok, Booking} = core_booking:create(EventId, UserId), {ok, Booking} = core_booking:create(EventId, UserId, pending),
core_booking:update_status(Booking#booking.id, confirmed), Now = calendar:universal_time(),
{ok, _} = core_booking:update(Booking#booking.id,
[{status, confirmed}, {confirmed_at, Now}]),
Booking#booking.id. Booking#booking.id.
test_create_event_review() -> test_create_event_review() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -66,30 +66,29 @@ test_create_event_review() ->
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 5, <<"Great!">>), {ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 5, <<"Great!">>),
?assertEqual(5, Review#review.rating), ?assertEqual(5, Review#review.rating),
% Проверяем, что рейтинг события обновился
{ok, Event} = core_event:get_by_id(EventId), {ok, Event} = core_event:get_by_id(EventId),
?assertEqual(5.0, Event#event.rating_avg), ?assertEqual(5.0, Event#event.rating_avg),
?assertEqual(1, Event#event.rating_count). ?assertEqual(1, Event#event.rating_count).
test_create_calendar_review() -> test_create_calendar_review() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ReviewerId = create_test_user(user), ReviewerId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
{ok, Review} = logic_review:create_review(ReviewerId, calendar, CalendarId, 4, <<"Nice">>), {ok, Review} = logic_review:create_review(ReviewerId, calendar, CalendarId, 4, <<"Nice">>),
?assertEqual(4, Review#review.rating). ?assertEqual(4, Review#review.rating).
test_cannot_review_without_booking() -> test_cannot_review_without_booking() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
UserId = create_test_user(user), UserId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
{error, cannot_review} = logic_review:create_review(UserId, event, EventId, 5, <<"">>). {error, cannot_review} = logic_review:create_review(UserId, event, EventId, 5, <<"">>).
test_cannot_review_twice() -> test_cannot_review_twice() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -98,8 +97,8 @@ test_cannot_review_twice() ->
{error, already_reviewed} = logic_review:create_review(ParticipantId, event, EventId, 4, <<"">>). {error, already_reviewed} = logic_review:create_review(ParticipantId, event, EventId, 4, <<"">>).
test_update_own_review() -> test_update_own_review() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -109,9 +108,9 @@ test_update_own_review() ->
?assertEqual(5, Updated#review.rating). ?assertEqual(5, Updated#review.rating).
test_cannot_update_others() -> test_cannot_update_others() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
OtherId = create_test_user(user), OtherId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -120,8 +119,8 @@ test_cannot_update_others() ->
{error, access_denied} = logic_review:update_review(OtherId, Review#review.id, [{rating, 5}]). {error, access_denied} = logic_review:update_review(OtherId, Review#review.id, [{rating, 5}]).
test_delete_own_review() -> test_delete_own_review() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -130,9 +129,9 @@ test_delete_own_review() ->
{ok, deleted} = logic_review:delete_review(ParticipantId, Review#review.id). {ok, deleted} = logic_review:delete_review(ParticipantId, Review#review.id).
test_admin_hide_review() -> test_admin_hide_review() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
ParticipantId = create_test_user(user), ParticipantId = create_test_user(),
AdminId = create_test_user(admin), AdminId = create_test_admin(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId), create_booking(ParticipantId, EventId),
@@ -142,9 +141,9 @@ test_admin_hide_review() ->
?assertEqual(hidden, Hidden#review.status). ?assertEqual(hidden, Hidden#review.status).
test_rating_updates_target() -> test_rating_updates_target() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(),
P1 = create_test_user(user), P1 = create_test_user(),
P2 = create_test_user(user), P2 = create_test_user(),
CalendarId = create_test_calendar(OwnerId), CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId), EventId = create_test_event(CalendarId),
create_booking(P1, EventId), create_booking(P1, EventId),
+63 -71
View File
@@ -2,27 +2,16 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("records.hrl"). -include("records.hrl").
-define(TABLES, [user, calendar, event]).
setup() -> setup() ->
mnesia:start(), eh_test_support:start_mnesia(),
mnesia:create_table(user, [ eh_test_support:ensure_tables(?TABLES),
{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()]}
]),
ok. ok.
cleanup(_) -> cleanup(_) ->
mnesia:delete_table(event), eh_test_support:delete_tables(?TABLES),
mnesia:delete_table(calendar), eh_test_support:stop_mnesia(),
mnesia:delete_table(user),
mnesia:stop(),
ok. ok.
logic_search_test_() -> logic_search_test_() ->
@@ -75,89 +64,96 @@ create_test_event(CalendarId, Title, Description, StartTime, Tags, Location) ->
{ok, Updated} = core_event:get_by_id(Event#event.id), {ok, Updated} = core_event:get_by_id(Event#event.id),
Updated#event.id. Updated#event.id.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
events_from({ok, Total, #{<<"events">> := Events}}) ->
{Total, Events}.
calendars_from({ok, Total, #{<<"calendars">> := Calendars}}) ->
{Total, Calendars}.
%% Тесты %% Тесты
test_search_events_by_text() -> test_search_events_by_text() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [], undefined), create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [], undefined),
create_test_event(CalendarId, <<"JavaScript Conference">>, <<"JS for everyone">>, StartTime, [], undefined), create_test_event(CalendarId, <<"JavaScript Conference">>, <<"JS for everyone">>, StartTime, [], undefined),
create_test_event(CalendarId, <<"Yoga Class">>, <<"Morning yoga">>, StartTime, [], undefined), create_test_event(CalendarId, <<"Yoga Class">>, <<"Morning yoga">>, StartTime, [], undefined),
Params = #{}, Params = #{},
{ok, Total, Results} = logic_search:search(<<"event">>, <<"Python">>, OwnerId, Params), {Total, Results} = events_from(logic_search:search(<<"event">>, <<"Python">>, OwnerId, Params)),
?assertEqual(1, Total), ?assertEqual(1, Total),
?assertEqual(1, length(Results)), ?assertEqual(1, length(Results)),
{ok, Total2, _Results2} = logic_search:search(<<"event">>, <<"conference">>, OwnerId, Params), {Total2, _} = events_from(logic_search:search(<<"event">>, <<"conference">>, OwnerId, Params)),
?assertEqual(1, Total2). ?assertEqual(1, Total2).
test_search_events_by_tags() -> test_search_events_by_tags() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Event 1">>, <<"">>, StartTime, [<<"python">>, <<"workshop">>], undefined), create_test_event(CalendarId, <<"Event 1">>, <<"">>, StartTime, [<<"python">>, <<"workshop">>], undefined),
create_test_event(CalendarId, <<"Event 2">>, <<"">>, StartTime, [<<"javascript">>, <<"conference">>], undefined), create_test_event(CalendarId, <<"Event 2">>, <<"">>, StartTime, [<<"javascript">>, <<"conference">>], undefined),
create_test_event(CalendarId, <<"Event 3">>, <<"">>, StartTime, [<<"python">>, <<"advanced">>], undefined), create_test_event(CalendarId, <<"Event 3">>, <<"">>, StartTime, [<<"python">>, <<"advanced">>], undefined),
% Поиск по одному тегу {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"python">>})),
Params = #{tags => <<"python">>}, ?assertEqual(2, Total),
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(2, Total), % Event 1 и Event 3
% Поиск по тегу, который есть только у одного события {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"workshop">>})),
Params2 = #{tags => <<"workshop">>}, ?assertEqual(1, Total2),
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertEqual(1, Total2), % Только Event 1
% Поиск по тегу, которого нет {Total3, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"nonexistent">>})),
Params3 = #{tags => <<"nonexistent">>},
{ok, Total3, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
?assertEqual(0, Total3). ?assertEqual(0, Total3).
test_search_events_by_date() -> test_search_events_by_date() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
create_test_event(CalendarId, <<"June Event">>, <<"">>, {{2026, 6, 15}, {10, 0, 0}}, [], undefined), Base = eh_test_support:future_start(),
create_test_event(CalendarId, <<"July Event">>, <<"">>, {{2026, 7, 15}, {10, 0, 0}}, [], undefined), create_test_event(CalendarId, <<"Near Event">>, <<"">>, Base, [], undefined),
create_test_event(CalendarId, <<"August Event">>, <<"">>, {{2026, 8, 15}, {10, 0, 0}}, [], undefined), create_test_event(CalendarId, <<"Mid Event">>, <<"">>, add_days(Base, 30), [], undefined),
create_test_event(CalendarId, <<"Far Event">>, <<"">>, add_days(Base, 60), [], undefined),
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}}, Params = #{from => add_days(Base, -1), to => add_days(Base, 7)},
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params), {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, Params)),
?assertEqual(1, Total). ?assertEqual(1, Total).
test_search_events_by_location() -> test_search_events_by_location() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
MoscowLoc = #location{address = <<"Moscow">>, lat = 55.7558, lon = 37.6173}, MoscowLoc = #location{address = <<"Moscow">>, lat = 55.7558, lon = 37.6173},
SpbLoc = #location{address = <<"SPb">>, lat = 59.9343, lon = 30.3351}, SpbLoc = #location{address = <<"SPb">>, lat = 59.9343, lon = 30.3351},
create_test_event(CalendarId, <<"Moscow Event">>, <<"">>, StartTime, [], MoscowLoc), create_test_event(CalendarId, <<"Moscow Event">>, <<"">>, StartTime, [], MoscowLoc),
create_test_event(CalendarId, <<"SPb Event">>, <<"">>, StartTime, [], SpbLoc), create_test_event(CalendarId, <<"SPb Event">>, <<"">>, StartTime, [], SpbLoc),
Params = #{lat => 55.7558, lon => 37.6173, radius => 10}, {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId,
{ok, Total, _Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params), #{lat => 55.7558, lon => 37.6173, radius => 10})),
?assertEqual(1, Total), ?assertEqual(1, Total),
Params2 = #{lat => 59.9343, lon => 30.3351, radius => 10}, {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId,
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), #{lat => 59.9343, lon => 30.3351, radius => 10})),
?assertEqual(1, Total2). ?assertEqual(1, Total2).
test_combined_search() -> test_combined_search() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 15}, {10, 0, 0}}, Base = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [<<"python">>, <<"workshop">>], undefined), create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, Base,
create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, {{2026, 7, 15}, {10, 0, 0}}, [<<"python">>, <<"advanced">>], undefined), [<<"python">>, <<"workshop">>], undefined),
create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, add_days(Base, 30),
[<<"python">>, <<"advanced">>], undefined),
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}, tags => <<"python">>}, Params = #{from => add_days(Base, -1), to => add_days(Base, 7), tags => <<"python">>},
{ok, Total, _} = logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params), {Total, _} = events_from(logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params)),
?assertEqual(1, Total). ?assertEqual(1, Total).
test_search_calendars() -> test_search_calendars() ->
@@ -165,60 +161,57 @@ test_search_calendars() ->
create_test_calendar(OwnerId, personal, [<<"tech">>, <<"programming">>]), create_test_calendar(OwnerId, personal, [<<"tech">>, <<"programming">>]),
create_test_calendar(OwnerId, commercial, [<<"yoga">>, <<"wellness">>]), create_test_calendar(OwnerId, commercial, [<<"yoga">>, <<"wellness">>]),
Params = #{tags => <<"tech">>}, {Total, _} = calendars_from(logic_search:search(<<"calendar">>, undefined, OwnerId, #{tags => <<"tech">>})),
{ok, Total, _} = logic_search:search(<<"calendar">>, undefined, OwnerId, Params),
?assertEqual(1, Total), ?assertEqual(1, Total),
{ok, Total2, _} = logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{}), {Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})),
?assertEqual(2, Total2). ?assertEqual(2, Total2).
test_search_all() -> test_search_all() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Test Event">>, <<"">>, StartTime, [], undefined), create_test_event(CalendarId, <<"Test Event">>, <<"">>, StartTime, [], undefined),
{ok, Total, Results} = logic_search:search(undefined, <<"Test">>, OwnerId, #{}), {ok, Total, Results} = logic_search:search(undefined, <<"Test">>, OwnerId, #{}),
?assert(Total >= 2), ?assert(Total >= 2),
?assert(maps:is_key(events, Results)), ?assert(maps:is_key(<<"events">>, Results)),
?assert(maps:is_key(calendars, Results)). ?assert(maps:is_key(<<"calendars">>, Results)).
test_pagination() -> test_pagination() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
lists:foreach(fun(I) -> lists:foreach(fun(I) ->
Title = iolist_to_binary(["Event ", integer_to_binary(I)]), Title = iolist_to_binary(["Event ", integer_to_binary(I)]),
create_test_event(CalendarId, Title, <<"">>, StartTime, [], undefined) create_test_event(CalendarId, Title, <<"">>, StartTime, [], undefined)
end, [1, 2, 3, 4, 5]), end, [1, 2, 3, 4, 5]),
Params = #{limit => 2, offset => 0}, {Total, Results} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 0})),
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(5, Total), ?assertEqual(5, Total),
?assertEqual(2, length(Results)), ?assertEqual(2, length(Results)),
Params2 = #{limit => 2, offset => 2}, {_, Results2} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 2})),
{ok, _, Results2} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertEqual(2, length(Results2)), ?assertEqual(2, length(Results2)),
Params3 = #{limit => 2, offset => 4}, {_, Results3} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 4})),
{ok, _, Results3} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
?assertEqual(1, length(Results3)). ?assertEqual(1, length(Results3)).
test_sorting() -> test_sorting() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
create_test_event(CalendarId, <<"Early">>, <<"">>, {{2026, 6, 1}, {10, 0, 0}}, [], undefined), Base = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Late">>, <<"">>, {{2026, 6, 30}, {10, 0, 0}}, [], undefined), create_test_event(CalendarId, <<"Early">>, <<"">>, Base, [], undefined),
create_test_event(CalendarId, <<"Late">>, <<"">>, add_days(Base, 29), [], undefined),
Params = #{sort => <<"start_time">>, order => <<"asc">>}, {_, [First | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId,
{ok, _, [First | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params), #{sort => <<"start_time">>, order => <<"asc">>})),
?assertMatch(#{title := <<"Early">>}, First), ?assertMatch(#{title := <<"Early">>}, First),
Params2 = #{sort => <<"start_time">>, order => <<"desc">>}, {_, [First2 | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId,
{ok, _, [First2 | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), #{sort => <<"start_time">>, order => <<"desc">>})),
?assertMatch(#{title := <<"Late">>}, First2). ?assertMatch(#{title := <<"Late">>}, First2).
test_access_control() -> test_access_control() ->
@@ -228,12 +221,11 @@ test_access_control() ->
PersonalId = create_test_calendar(OwnerId, personal, []), PersonalId = create_test_calendar(OwnerId, personal, []),
CommercialId = create_test_calendar(OwnerId, commercial, []), CommercialId = create_test_calendar(OwnerId, commercial, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
create_test_event(PersonalId, <<"Personal Event">>, <<"">>, StartTime, [], undefined), create_test_event(PersonalId, <<"Personal Event">>, <<"">>, StartTime, [], undefined),
create_test_event(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined), create_test_event(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined),
% Другой пользователь видит только коммерческие события {Total, Results} = events_from(logic_search:search(<<"event">>, undefined, OtherId, #{})),
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OtherId, #{}),
?assertEqual(1, Total), ?assertEqual(1, Total),
[Event] = Results, [Event] = Results,
?assertMatch(#{title := <<"Public Event">>}, Event). ?assertMatch(#{title := <<"Public Event">>}, Event).
@@ -241,9 +233,9 @@ test_access_control() ->
test_empty_search() -> test_empty_search() ->
OwnerId = create_test_user(user), OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []), CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}}, StartTime = eh_test_support:future_start(),
create_test_event(CalendarId, <<"Test">>, <<"">>, StartTime, [], undefined), create_test_event(CalendarId, <<"Test">>, <<"">>, StartTime, [], undefined),
{ok, Total, Results} = logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{}), {Total, Results} = events_from(logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{})),
?assertEqual(0, Total), ?assertEqual(0, Total),
?assertEqual([], Results). ?assertEqual([], Results).
+6 -1
View File
@@ -25,7 +25,12 @@ ws_handler_test_() ->
test_websocket_info_booking() -> test_websocket_info_booking() ->
State = #state{user_id = <<"user123">>, subscriptions = []}, State = #state{user_id = <<"user123">>, subscriptions = []},
Data = #{booking_id => <<"book123">>, event_id => <<"ev123">>, status => confirmed}, Data = #{
user_id => <<"user123">>,
booking_id => <<"book123">>,
event_id => <<"ev123">>,
status => confirmed
},
Msg = {notification, booking_update, Data}, Msg = {notification, booking_update, Data},
case ws_handler:websocket_info(Msg, State) of case ws_handler:websocket_info(Msg, State) of