diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e7d3de7..de6deb4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: --sbom=false \ . - - name: Build API test image + - name: Build test image (EUnit + CT) run: | set -euo pipefail bash scripts/retry-docker-build.sh 5 60 -- \ @@ -101,14 +101,19 @@ jobs: --network=host \ --pull \ --file docker/ApiTests.Dockerfile \ - --tag eventhub-api-tests:latest \ + --tag eventhub-tests:latest \ --load \ --provenance=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) - run: docker run --rm eventhub-api-tests:latest + run: docker run --rm eventhub-tests:latest - name: Push tested eventhub image if: github.event_name == 'push' diff --git a/docker/ApiTests.Dockerfile b/docker/ApiTests.Dockerfile index 9e39542..9c2dfb5 100644 --- a/docker/ApiTests.Dockerfile +++ b/docker/ApiTests.Dockerfile @@ -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 FROM ${ERLANG_BUILDER_IMAGE} @@ -7,9 +7,12 @@ WORKDIR /app COPY rebar.config ./ COPY include/ include/ COPY src/ src/ +COPY test/unit/ test/unit/ COPY test/api_*_SUITE.erl test/ 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"] diff --git a/test/unit/admin_handler_admins_by_id_tests.erl b/test/unit/admin_handler_admins_by_id_tests.erl new file mode 100644 index 0000000..301e0c8 --- /dev/null +++ b/test/unit/admin_handler_admins_by_id_tests.erl @@ -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). diff --git a/test/unit/admin_handler_admins_tests.erl b/test/unit/admin_handler_admins_tests.erl new file mode 100644 index 0000000..8815e0f --- /dev/null +++ b/test/unit/admin_handler_admins_tests.erl @@ -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). diff --git a/test/unit/admin_handler_audit_tests.erl b/test/unit/admin_handler_audit_tests.erl new file mode 100644 index 0000000..240c56a --- /dev/null +++ b/test/unit/admin_handler_audit_tests.erl @@ -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). diff --git a/test/unit/admin_handler_banned_words_tests.erl b/test/unit/admin_handler_banned_words_tests.erl index a314b2c..0823495 100644 --- a/test/unit/admin_handler_banned_words_tests.erl +++ b/test/unit/admin_handler_banned_words_tests.erl @@ -2,157 +2,154 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, banned_word, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_banned_words, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_banned_words), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_banned_words_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/banned-words – success", fun test_list/0}, - {"GET /admin/banned-words – forbidden", fun test_list_forbidden/0}, - {"POST /admin/banned-words – success", fun test_add/0}, - {"POST /admin/banned-words – missing field", fun test_add_missing/0}, - {"POST /admin/banned-words – already exists", fun test_add_exists/0}, - {"DELETE /admin/banned-words/:word – success", fun test_delete/0}, - {"DELETE /admin/banned-words/:word – not found", fun test_delete_not_found/0}, - {"PUT /admin/banned-words/:word – success", fun test_update/0}, - {"PUT /admin/banned-words/:word – not found", fun test_update_not_found/0}, - {"PATCH /admin/banned-words – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"GET list – forbidden without admin", fun test_list_forbidden/0}, + {"POST add – success", fun test_add/0}, + {"POST add – missing field", fun test_add_missing/0}, + {"POST add – already exists", fun test_add_exists/0}, + {"DELETE – success", fun test_delete/0}, + {"DELETE – not found", fun test_delete_not_found/0}, + {"PUT – success", fun test_update/0}, + {"PUT – not found", fun test_update_not_found/0}, + {"POST batch – success", fun test_batch/0}, + {"PATCH – method not allowed", fun test_wrong_method/0} ]}. test_list() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - Words = [ - #banned_word{id = <<"bw1">>, word = <<"badword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}}, - #banned_word{id = <<"bw2">>, word = <<"spamword">>, added_by = <<"adm2">>, added_at = {{2026,4,27},{12,30,0}}} - ], - ok = meck:expect(core_banned_words, list_banned_words, fun() -> Words end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {ok, _} = core_banned_words:add_banned_word(<<"badword">>, ?ADMIN_ID), + {ok, _} = core_banned_words:add_banned_word(<<"spamword">>, ?ADMIN_ID), + {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"GET">>, + path => <<"/v1/admin/banned-words">>, + auth => ?ADMIN_ID + }), ?assertEqual(200, Status), - Result = jsx:decode(RespBody, [return_maps]), + Result = jsx:decode(Body, [return_maps]), ?assertEqual(2, length(Result)), - First = hd(Result), - ?assertEqual(<<"badword">>, maps:get(<<"word">>, First)), - ?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, First)). + Words = [maps:get(<<"word">>, W) || W <- Result], + ?assert(lists:member(<<"badword">>, Words)), + ?assert(lists:member(<<"spamword">>, Words)). test_list_forbidden() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"GET">>, + path => <<"/v1/admin/banned-words">>, + auth => none + }), + ?assertEqual(401, Status). test_add() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"banned">>}), Req} end), - NewBW = #banned_word{id = <<"bw_new">>, word = <<"banned">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{13,0,0}}}, - ok = meck:expect(core_banned_words, add_banned_word, fun(<<"banned">>, <<"adm1">>) -> {ok, NewBW} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"POST">>, + path => <<"/v1/admin/banned-words">>, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"word">> => <<"banned">>}) + }), ?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() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"POST">>, + path => <<"/v1/admin/banned-words">>, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"other">> => <<"data">>}) + }), ?assertEqual(400, Status). test_add_exists() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"already">>}), Req} end), - ok = meck:expect(core_banned_words, add_banned_word, fun(_, _) -> {error, already_exists} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, _, _} = erase(test_reply), + {ok, _} = core_banned_words:add_banned_word(<<"already">>, ?ADMIN_ID), + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"POST">>, + path => <<"/v1/admin/banned-words">>, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"word">> => <<"already">>}) + }), ?assertEqual(409, Status). test_delete() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"badword">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_banned_words, remove_banned_word, fun(<<"badword">>) -> {ok, deleted} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {ok, _} = core_banned_words:add_banned_word(<<"badword">>, ?ADMIN_ID), + {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/banned-words/badword">>, + bindings => #{word => <<"badword">>}, + auth => ?ADMIN_ID + }), ?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() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"unknown">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_banned_words, remove_banned_word, fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/banned-words/unknown">>, + bindings => #{word => <<"unknown">>}, + auth => ?ADMIN_ID + }), ?assertEqual(404, Status). test_update() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"oldword">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end), - UpdatedBW = #banned_word{id = <<"bw1">>, word = <<"newword">>, added_by = <<"adm1">>, added_at = {{2026,4,27},{12,0,0}}}, - ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {ok, UpdatedBW} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {ok, _} = core_banned_words:add_banned_word(<<"old">>, ?ADMIN_ID), + {Status, _, Body} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"PUT">>, + path => <<"/v1/admin/banned-words/old">>, + bindings => #{word => <<"old">>}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"word">> => <<"new">>}) + }), ?assertEqual(200, Status), - Resp = jsx:decode(RespBody, [return_maps]), - ?assertEqual(<<"newword">>, maps:get(<<"word">>, Resp)), - ?assertEqual(<<"adm1">>, maps:get(<<"added_by">>, Resp)). + #{<<"status">> := <<"updated">>} = jsx:decode(Body, [return_maps]), + ?assert(core_banned_words:is_word_banned(<<"new">>)), + ?assertEqual(false, core_banned_words:is_word_banned(<<"old">>)). test_update_not_found() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> <<"missing">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{<<"word">> => <<"newword">>}), Req} end), - ok = meck:expect(core_banned_words, update_banned_word, fun(_, _) -> {error, not_found} end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"PUT">>, + path => <<"/v1/admin/banned-words/missing">>, + bindings => #{word => <<"missing">>}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"word">> => <<"x">>}) + }), ?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() -> - ok = meck:expect(cowboy_req, binding, fun(word, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), - {ok, _, _} = admin_handler_banned_words:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_banned_words, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/banned-words">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_calendar_by_id_tests.erl b/test/unit/admin_handler_calendar_by_id_tests.erl new file mode 100644 index 0000000..e417685 --- /dev/null +++ b/test/unit/admin_handler_calendar_by_id_tests.erl @@ -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). diff --git a/test/unit/admin_handler_calendar_stats_tests.erl b/test/unit/admin_handler_calendar_stats_tests.erl new file mode 100644 index 0000000..094c5b5 --- /dev/null +++ b/test/unit/admin_handler_calendar_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_calendars_tests.erl b/test/unit/admin_handler_calendars_tests.erl new file mode 100644 index 0000000..8e5a793 --- /dev/null +++ b/test/unit/admin_handler_calendars_tests.erl @@ -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). diff --git a/test/unit/admin_handler_event_by_id_tests.erl b/test/unit/admin_handler_event_by_id_tests.erl new file mode 100644 index 0000000..84445ed --- /dev/null +++ b/test/unit/admin_handler_event_by_id_tests.erl @@ -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). diff --git a/test/unit/admin_handler_event_stats_tests.erl b/test/unit/admin_handler_event_stats_tests.erl new file mode 100644 index 0000000..673a359 --- /dev/null +++ b/test/unit/admin_handler_event_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_events_tests.erl b/test/unit/admin_handler_events_tests.erl new file mode 100644 index 0000000..0304cdf --- /dev/null +++ b/test/unit/admin_handler_events_tests.erl @@ -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). diff --git a/test/unit/admin_handler_health_tests.erl b/test/unit/admin_handler_health_tests.erl index b02dbf8..c3904c7 100644 --- a/test/unit/admin_handler_health_tests.erl +++ b/test/unit/admin_handler_health_tests.erl @@ -1,49 +1,37 @@ -module(admin_handler_health_tests). -include_lib("eunit/include/eunit.hrl"). -%% ------------------------------------------------------------------ -%% Фикстуры -%% ------------------------------------------------------------------ setup() -> - ok = meck:new(cowboy_req, [non_strict]), ok. cleanup(_) -> - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + ok. -%% ------------------------------------------------------------------ -%% Тесты -%% ------------------------------------------------------------------ -admin_handler_health_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/health returns 200 with status ok", - fun test_health_get/0}, - {"POST /admin/health returns 405 Method not allowed", - fun test_health_post/0} +admin_health_test_() -> + {foreach, fun setup/0, fun cleanup/1, [ + {"GET health – success", fun test_health_get/0}, + {"POST health – method not allowed", fun test_health_wrong_method/0} ]}. -%% ── Успешный GET ───────────────────────────────────────────── test_health_get() -> - % Мокаем method → <<"GET">> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - % reply/4 будем перехватывать - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_health:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {Status, _, Body} = eh_test_support:call(admin_handler_health, #{ + method => <<"GET">>, + path => <<"/admin/health">>, + auth => none + }), ?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_post() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_health:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_health_wrong_method() -> + {Status, _, Body} = eh_test_support:call(admin_handler_health, #{ + method => <<"POST">>, + path => <<"/admin/health">>, + auth => none + }), ?assertEqual(405, Status), - ?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])). \ No newline at end of file + #{<<"error">> := <<"Method not allowed">>} = jsx:decode(Body, [return_maps]). diff --git a/test/unit/admin_handler_login_tests.erl b/test/unit/admin_handler_login_tests.erl index a95f6d1..6a593f8 100644 --- a/test/unit/admin_handler_login_tests.erl +++ b/test/unit/admin_handler_login_tests.erl @@ -2,122 +2,134 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). --define(JWT_SECRET, <<"test-user-secret-key-32-byt!">>). --define(ADMIN_JWT_SECRET, <<"test-admin-secret-key-32-b">>). +-define(TABLES, [admin, admin_audit, auth_session, ticket]). +-define(EMAIL, <<"admin@test.local">>). +-define(PASSWORD, <<"SecretPass1!">>). setup() -> - ok = meck:new(logic_auth, [non_strict]), - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(core_admin, [non_strict]), - ok = meck:new(admin_utils, [non_strict]), - mnesia:start(), - 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). + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), + eh_test_support:ensure_jwt(), + catch application:ensure_all_started(argon2), + catch ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]), + ok. 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. + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. -admin_handler_login_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"Valid admin login returns 200 and token", fun test_valid_admin_login/0}, - {"Invalid credentials return 401", fun test_invalid_credentials/0}, - {"Non‑admin role returns 403", fun test_insufficient_permissions/0}, - {"Malformed JSON returns 400", fun test_malformed_json/0}, - {"Missing body returns 400", fun test_missing_body/0}, - {"Wrong HTTP method returns 405", fun test_wrong_method/0} - ]}. +admin_login_test_() -> + {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 ── -prepare_req(Method, HasBody, Body) -> - ok = meck:expect(cowboy_req, method, fun(_) -> Method end), - ok = meck:expect(cowboy_req, has_body, fun(_) -> HasBody end), - case {HasBody, Body} of - {true, undefined} -> ok; - {true, _} -> - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, Body, Req} end); - {false, _} -> ok - end, - % Устанавливаем мок на reply, который сохраняет ответ в словаре процесса - meck:expect(cowboy_req, reply, - fun(Code, Headers, RespBody, Req) -> - put(test_reply, {Code, Headers, RespBody}), - Req - end), - req. +%% argon2 is a Rust NIF; rebar hooks only build it on unix. Skip hash-dependent +%% cases when the NIF is unavailable (common on Windows without cargo). +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. -test_valid_admin_login() -> - UserMap = #{id => <<"adm1">>, email => <<"admin@test.com">>, role => <<"superadmin">>}, - ok = meck:expect(logic_auth, authenticate_admin, - fun(<<"admin@test.com">>, <<"pass">>) -> {ok, UserMap} end), - ok = meck:expect(core_admin, update_last_login, fun(_) -> ok end), - ok = meck:expect(admin_utils, log_admin_action, fun(_, _, _, _, _, _) -> ok end), - Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"admin@test.com">>, password => <<"pass">>})), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, Headers, Body} = get(test_reply), - ?assertEqual(200, Code), - ?assertEqual(<<"application/json">>, maps:get(<<"content-type">>, Headers)), +test_login_ok() -> + require_argon2(fun() -> + {ok, Admin} = core_admin:create(?EMAIL, ?PASSWORD, admin), + {Status, _, Body} = eh_test_support:call(admin_handler_login, #{ + method => <<"POST">>, + path => <<"/v1/admin/login">>, + auth => none, + body => jsx:encode(#{<<"email">> => ?EMAIL, <<"password">> => ?PASSWORD}) + }), + ?assertEqual(200, Status), Resp = jsx:decode(Body, [return_maps]), ?assert(is_map_key(<<"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() -> - ok = meck:expect(logic_auth, authenticate_admin, - fun(_, _) -> {error, bad_credentials} end), - Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"bad@test.com">>, password => <<"wrong">>})), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, _, Body} = get(test_reply), - ?assertEqual(401, Code), - #{<<"error">> := <<"bad_credentials">>} = jsx:decode(Body, [return_maps]). +test_login_bad_credentials() -> + %% Unknown email does not need password verification. + {Status, _, Body} = eh_test_support:call(admin_handler_login, #{ + method => <<"POST">>, + path => <<"/v1/admin/login">>, + auth => none, + body => jsx:encode(#{<<"email">> => <<"nobody@test.local">>, <<"password">> => <<"x">>}) + }), + ?assertEqual(401, Status), + #{<<"error">> := <<"invalid_credentials">>} = jsx:decode(Body, [return_maps]). -test_insufficient_permissions() -> - UserMap = #{id => <<"user1">>, email => <<"user@test.com">>, role => <<"user">>}, - ok = meck:expect(logic_auth, authenticate_admin, - fun(_, _) -> {ok, UserMap} end), - Req0 = prepare_req(<<"POST">>, true, jsx:encode(#{email => <<"user@test.com">>, password => <<"pass">>})), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, _, Body} = get(test_reply), - ?assertEqual(403, Code), - #{<<"error">> := <<"insufficient_permissions">>} = jsx:decode(Body, [return_maps]). +test_login_forbidden_role() -> + require_argon2(fun() -> + {ok, Hash} = logic_auth:hash_password(?PASSWORD), + eh_test_support:seed_admin(#{ + id => <<"adm_guest">>, + email => <<"guest@test.local">>, + password_hash => Hash, + role => guest + }), + {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() -> - Req0 = prepare_req(<<"POST">>, true, <<"not a json">>), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, _, Body} = get(test_reply), - ?assertEqual(400, Code), - #{<<"error">> := <<"Invalid JSON">>} = jsx:decode(Body, [return_maps]). +test_login_missing_fields() -> + {Status, _, _} = eh_test_support:call(admin_handler_login, #{ + method => <<"POST">>, + path => <<"/v1/admin/login">>, + auth => none, + body => jsx:encode(#{<<"email">> => ?EMAIL}) + }), + ?assertEqual(400, Status). -test_missing_body() -> - Req0 = prepare_req(<<"POST">>, false, undefined), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, _, Body} = get(test_reply), - ?assertEqual(400, Code), - #{<<"error">> := <<"Missing request body">>} = jsx:decode(Body, [return_maps]). +test_login_missing_body() -> + {Status, _, _} = eh_test_support:call(admin_handler_login, #{ + method => <<"POST">>, + path => <<"/v1/admin/login">>, + auth => none, + has_body => false + }), + ?assertEqual(400, Status). -test_wrong_method() -> - Req0 = prepare_req(<<"GET">>, false, undefined), - {ok, _, _} = admin_handler_login:init(Req0, []), - {Code, _, Body} = get(test_reply), - ?assertEqual(405, Code), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(Body, [return_maps]). \ No newline at end of file +test_login_invalid_json() -> + {Status, _, _} = eh_test_support:call(admin_handler_login, #{ + method => <<"POST">>, + path => <<"/v1/admin/login">>, + auth => none, + 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). diff --git a/test/unit/admin_handler_me_tests.erl b/test/unit/admin_handler_me_tests.erl new file mode 100644 index 0000000..b541599 --- /dev/null +++ b/test/unit/admin_handler_me_tests.erl @@ -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). diff --git a/test/unit/admin_handler_moderation_tests.erl b/test/unit/admin_handler_moderation_tests.erl index 685a3b3..a16a8a6 100644 --- a/test/unit/admin_handler_moderation_tests.erl +++ b/test/unit/admin_handler_moderation_tests.erl @@ -2,188 +2,114 @@ -include_lib("eunit/include/eunit.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() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_calendar, [non_strict]), - ok = meck:new(core_event, [non_strict]), - ok = meck:new(core_review, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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 = mnesia:dirty_write(eh_test_support:make_user(#{ + id => ?USER_ID, + email => <<"user@test.local">>, + status => active + })), ok. cleanup(_) -> - meck:unload(core_review), - meck:unload(core_event), - meck:unload(core_calendar), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_moderation_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"Freeze calendar – success", fun test_freeze_calendar/0}, - {"Freeze calendar – not found", fun test_freeze_calendar_not_found/0}, - {"Unfreeze calendar – success", fun test_unfreeze_calendar/0}, - {"Freeze event – success", fun test_freeze_event/0}, - {"Unfreeze event – success", fun test_unfreeze_event/0}, - {"Hide review – success", fun test_hide_review/0}, - {"Show review – success", fun test_show_review/0}, - {"Block user – success", fun test_block_user/0}, - {"Unblock user – success", fun test_unblock_user/0}, - {"Invalid target type", fun test_invalid_target/0}, - {"Invalid action", fun test_invalid_action/0}, - {"Missing action field", fun test_missing_action/0}, - {"Forbidden – non admin", fun test_forbidden/0}, - {"Wrong method – POST", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"PUT block user – success", fun test_block_user/0}, + {"PUT block user – not found", fun test_block_user_not_found/0}, + {"PUT freeze calendar – success", fun test_freeze_calendar/0}, + {"PUT – missing action", fun test_missing_action/0}, + {"PUT – invalid target", fun test_invalid_target/0}, + {"PUT – unauthorized", fun test_unauthorized/0}, + {"GET – method not allowed", fun test_wrong_method/0} ]}. -%% ── Вспомогательные функции ────────────────────────────── -prepare_req(TargetType, TargetId, Action) -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(target_type, _) -> TargetType; - (id, _) -> TargetId - end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"action">> => Action}), Req} end). - -%% ── Календари ─────────────────────────────────────────── -test_freeze_calendar() -> - prepare_req(<<"calendar">>, <<"c1">>, <<"freeze">>), - Frozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = frozen}, - ok = meck:expect(core_calendar, freeze, fun(<<"c1">>) -> {ok, Frozen} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_block_user() -> + {Status, _, Body} = eh_test_support:call(admin_handler_moderation, #{ + method => <<"PUT">>, + path => <<"/v1/admin/user/", ?USER_ID/binary>>, + bindings => #{target_type => <<"user">>, id => ?USER_ID}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"action">> => <<"block">>, <<"reason">> => <<"spam">>}) + }), ?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() -> - prepare_req(<<"calendar">>, <<"c99">>, <<"freeze">>), - ok = meck:expect(core_calendar, freeze, fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, _, _} = erase(test_reply), +test_block_user_not_found() -> + {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{ + method => <<"PUT">>, + path => <<"/v1/admin/user/missing">>, + bindings => #{target_type => <<"user">>, id => <<"missing">>}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"action">> => <<"block">>, <<"reason">> => <<"x">>}) + }), ?assertEqual(404, Status). -test_unfreeze_calendar() -> - prepare_req(<<"calendar">>, <<"c1">>, <<"unfreeze">>), - Unfrozen = #calendar{id = <<"c1">>, title = <<"Test">>, status = active}, - ok = meck:expect(core_calendar, unfreeze, fun(<<"c1">>) -> {ok, Unfrozen} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_freeze_calendar() -> + {ok, Cal} = core_calendar:create(?USER_ID, <<"Cal">>, <<"desc">>, auto), + CalId = Cal#calendar.id, + {Status, _, Body} = eh_test_support:call(admin_handler_moderation, #{ + method => <<"PUT">>, + 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), - #{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). - -%% ── События ───────────────────────────────────────────── -test_freeze_event() -> - prepare_req(<<"event">>, <<"e1">>, <<"freeze">>), - FrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = frozen}, - ok = meck:expect(core_event, freeze, fun(<<"e1">>) -> {ok, FrozenE} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]). - -test_unfreeze_event() -> - prepare_req(<<"event">>, <<"e1">>, <<"unfreeze">>), - UnfrozenE = #event{id = <<"e1">>, title = <<"Event1">>, status = active}, - ok = meck:expect(core_event, unfreeze, fun(<<"e1">>) -> {ok, UnfrozenE} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). - -%% ── Отзывы ────────────────────────────────────────────── -test_hide_review() -> - prepare_req(<<"review">>, <<"r1">>, <<"hide">>), - Hidden = #review{id = <<"r1">>, status = hidden}, - ok = meck:expect(core_review, hide, fun(<<"r1">>) -> {ok, Hidden} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"hidden">>} = jsx:decode(RespBody, [return_maps]). - -test_show_review() -> - prepare_req(<<"review">>, <<"r1">>, <<"show">>), - Visible = #review{id = <<"r1">>, status = active}, - ok = meck:expect(core_review, show, fun(<<"r1">>) -> {ok, Visible} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). - -%% ── Пользователи ──────────────────────────────────────── -test_block_user() -> - prepare_req(<<"user">>, <<"u1">>, <<"block">>), - Blocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = frozen}, - ok = meck:expect(core_user, block, fun(<<"u1">>) -> {ok, Blocked} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"frozen">>} = jsx:decode(RespBody, [return_maps]). - -test_unblock_user() -> - prepare_req(<<"user">>, <<"u1">>, <<"unblock">>), - Unblocked = #user{id = <<"u1">>, email = <<"user@test.com">>, status = active}, - ok = meck:expect(core_user, unblock, fun(<<"u1">>) -> {ok, Unblocked} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). - -%% ── Ошибки ────────────────────────────────────────────── -test_invalid_target() -> - prepare_req(<<"bad_type">>, <<"x">>, <<"freeze">>), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(400, Status). - -test_invalid_action() -> - prepare_req(<<"calendar">>, <<"c1">>, <<"delete">>), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(400, Status). + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(CalId, maps:get(<<"id">>, Result)), + ?assertEqual(<<"frozen">>, maps:get(<<"status">>, Result)). test_missing_action() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(target_type, _) -> <<"calendar">>; - (id, _) -> <<"c1">> - end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"other">> => <<"data">>}), Req} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, _, _} = erase(test_reply), + {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 => ?ADMIN_ID, + body => jsx:encode(#{<<"reason">> => <<"x">>}) + }), ?assertEqual(400, Status). -test_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). +test_invalid_target() -> + {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{ + method => <<"PUT">>, + path => <<"/v1/admin/unknown/x">>, + bindings => #{target_type => <<"unknown">>, id => <<"x">>}, + auth => ?ADMIN_ID, + 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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_moderation:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_moderation, #{ + method => <<"GET">>, + path => <<"/v1/admin/user/", ?USER_ID/binary>>, + bindings => #{target_type => <<"user">>, id => ?USER_ID}, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_node_metrics_tests.erl b/test/unit/admin_handler_node_metrics_tests.erl new file mode 100644 index 0000000..6003235 --- /dev/null +++ b/test/unit/admin_handler_node_metrics_tests.erl @@ -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). diff --git a/test/unit/admin_handler_refresh_tests.erl b/test/unit/admin_handler_refresh_tests.erl new file mode 100644 index 0000000..7d6a485 --- /dev/null +++ b/test/unit/admin_handler_refresh_tests.erl @@ -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). diff --git a/test/unit/admin_handler_report_by_id_tests.erl b/test/unit/admin_handler_report_by_id_tests.erl index eb0ce89..4b19847 100644 --- a/test/unit/admin_handler_report_by_id_tests.erl +++ b/test/unit/admin_handler_report_by_id_tests.erl @@ -2,144 +2,112 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, report, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_report, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_report), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_report_by_id_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/reports/:id – success", fun test_get_report/0}, - {"GET /admin/reports/:id – not found", fun test_get_report_not_found/0}, - {"GET /admin/reports/:id – forbidden", fun test_get_report_forbidden/0}, - {"PUT /admin/reports/:id – success", fun test_update_report/0}, - {"PUT /admin/reports/:id – not found", fun test_update_report_not_found/0}, - {"PUT /admin/reports/:id – bad JSON", fun test_update_report_bad_json/0}, - {"DELETE /admin/reports/:id – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET – success", fun test_get/0}, + {"GET – not found", fun test_get_not_found/0}, + {"PUT – update status", fun test_update/0}, + {"PUT – missing status", fun test_update_missing/0}, + {"PUT – not found", fun test_update_not_found/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"PATCH – method not allowed", fun test_wrong_method/0} ]}. -%% GET – успех -test_get_report() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"r1">> end), - Report = #report{ - id = <<"r1">>, - reporter_id = <<"u1">>, - target_type = <<"event">>, - target_id = <<"e1">>, - reason = <<"spam">>, - status = 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]). +seed_report() -> + {ok, Report} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>), + Report. -%% GET – не найдено -test_get_report_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"r99">> end), - ok = meck:expect(core_report, get_by_id, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_report_by_id:init(req, []), - {Status, _, _, _} = erase(test_reply), +test_get() -> + Report = seed_report(), + Id = Report#report.id, + {Status, _, Body} = eh_test_support:call(admin_handler_report_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/reports/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID + }), + ?assertEqual(200, Status), + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(Id, maps:get(<<"id">>, Result)), + ?assertEqual(<<"spam">>, maps:get(<<"reason">>, Result)). + +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). -%% GET – запрещён -test_get_report_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_report_by_id:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). - -%% PUT – успех -test_update_report() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"r1">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"reviewed">>}), Req} end), - Updated = #report{id = <<"r1">>, status = reviewed}, - ok = meck:expect(core_report, update_status, - fun(<<"r1">>, reviewed, <<"adm1">>) -> {ok, Updated} end), - {ok, _, _} = admin_handler_report_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_update() -> + Report = seed_report(), + Id = Report#report.id, + {Status, _, Body} = eh_test_support:call(admin_handler_report_by_id, #{ + method => <<"PUT">>, + path => <<"/v1/admin/reports/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"status">> => <<"reviewed">>}) + }), ?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_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_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), +test_update_missing() -> + Report = seed_report(), + Id = Report#report.id, + {Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{ + method => <<"PUT">>, + path => <<"/v1/admin/reports/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"other">> => <<"x">>}) + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - {ok, _, _} = admin_handler_report_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_report_by_id, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/reports/x">>, + bindings => #{id => <<"x">>}, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_report_stats_tests.erl b/test/unit/admin_handler_report_stats_tests.erl new file mode 100644 index 0000000..add718b --- /dev/null +++ b/test/unit/admin_handler_report_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_reports_tests.erl b/test/unit/admin_handler_reports_tests.erl index 56de3dc..154ce1c 100644 --- a/test/unit/admin_handler_reports_tests.erl +++ b/test/unit/admin_handler_reports_tests.erl @@ -2,127 +2,70 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, report, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_report, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_report), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_reports_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/reports – success", fun test_list_reports/0}, - {"GET /admin/reports – forbidden", fun test_list_reports_forbidden/0}, - {"PUT /admin/reports/:id – success", fun test_update_report/0}, - {"PUT /admin/reports/:id – missing status", fun test_update_report_bad_json/0}, - {"PUT /admin/reports/:id – not found", fun test_update_report_not_found/0}, - {"POST /admin/reports – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"GET list – filter by status", fun test_list_filter/0}, + {"GET list – unauthorized", fun test_list_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -%% GET – успех -test_list_reports() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - Report = #report{ - id = <<"r1">>, - reporter_id = <<"u1">>, - target_type = <<"event">>, - target_id = <<"e1">>, - reason = <<"spam">>, - status = 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), +test_list() -> + {ok, _} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>), + {ok, _} = core_report:create(<<"u2">>, event, <<"ev1">>, <<"abuse">>), + {Status, _, Body} = eh_test_support:call(admin_handler_reports, #{ + method => <<"GET">>, + path => <<"/v1/admin/reports">>, + auth => ?ADMIN_ID + }), ?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_reports_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_reports:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(403, Status), - #{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]). - -%% 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), +test_list_filter() -> + {ok, _} = core_report:create(<<"u1">>, calendar, <<"cal1">>, <<"spam">>), + {ok, R} = core_report:create(<<"u2">>, event, <<"ev1">>, <<"abuse">>), + {ok, _} = core_report:update_status(R#report.id, reviewed, ?ADMIN_ID), + {Status, _, Body} = eh_test_support:call(admin_handler_reports, #{ + method => <<"GET">>, + path => <<"/v1/admin/reports">>, + auth => ?ADMIN_ID, + qs => [{<<"status">>, <<"pending">>}] + }), ?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_update_report_bad_json() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"r1">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, <<"bad json">>, Req} end), - {ok, _, _} = admin_handler_reports:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(400, Status). +test_list_unauthorized() -> + {Status, _, _} = eh_test_support:call(admin_handler_reports, #{ + method => <<"GET">>, + path => <<"/v1/admin/reports">>, + auth => none + }), + ?assertEqual(401, 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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - {ok, _, _} = admin_handler_reports:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_reports, #{ + method => <<"POST">>, + path => <<"/v1/admin/reports">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_reviews_by_id_tests.erl b/test/unit/admin_handler_reviews_by_id_tests.erl new file mode 100644 index 0000000..d5a5b01 --- /dev/null +++ b/test/unit/admin_handler_reviews_by_id_tests.erl @@ -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). diff --git a/test/unit/admin_handler_reviews_stats_tests.erl b/test/unit/admin_handler_reviews_stats_tests.erl new file mode 100644 index 0000000..b1c6544 --- /dev/null +++ b/test/unit/admin_handler_reviews_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_reviews_tests.erl b/test/unit/admin_handler_reviews_tests.erl index eb90c28..a22cf2d 100644 --- a/test/unit/admin_handler_reviews_tests.erl +++ b/test/unit/admin_handler_reviews_tests.erl @@ -2,145 +2,85 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, review, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_review, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_review), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_reviews_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/reviews/:id – success", fun test_get_review/0}, - {"GET /admin/reviews/:id – not found", fun test_get_review_not_found/0}, - {"GET /admin/reviews/:id – forbidden", fun test_get_review_forbidden/0}, - {"PUT /admin/reviews/:id – success", fun test_update_review/0}, - {"PUT /admin/reviews/:id – not found", fun test_update_review_not_found/0}, - {"PUT /admin/reviews/:id – bad JSON", fun test_update_review_bad_json/0}, - {"DELETE /admin/reviews/:id – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"PATCH bulk – success", fun test_bulk/0}, + {"PATCH bulk – missing reason", fun test_bulk_missing/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -%% GET – успех -test_get_review() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"rv1">> end), - Review = #review{ - id = <<"rv1">>, - user_id = <<"u1">>, - target_type = <<"event">>, - target_id = <<"e1">>, - rating = 5, - comment = <<"Great!">>, - status = <<"active">>, - created_at = {{2026,4,26},{12,0,0}}, - updated_at = {{2026,4,26},{12,0,0}} - }, - ok = meck:expect(core_review, get_by_id, - fun(<<"rv1">>) -> {ok, Review} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +seed_review() -> + {ok, Review} = core_review:create(<<"u1">>, calendar, <<"cal1">>, 5, <<"great">>), + Review. + +test_list() -> + _ = seed_review(), + {ok, _} = core_review:create(<<"u2">>, event, <<"ev1">>, 3, <<"ok">>), + {Status, _, Body} = eh_test_support:call(admin_handler_reviews, #{ + method => <<"GET">>, + path => <<"/v1/admin/reviews">>, + auth => ?ADMIN_ID + }), ?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_get_review_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"rv99">> end), - ok = meck:expect(core_review, get_by_id, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(404, Status). - -%% GET – запрещён -test_get_review_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). - -%% PUT – успех -test_update_review() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"rv1">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end), - Updated = #review{id = <<"rv1">>, status = <<"hidden">>}, - ok = meck:expect(core_review, update_status, - fun(<<"rv1">>, <<"hidden">>) -> {ok, Updated} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_bulk() -> + Review = seed_review(), + Id = Review#review.id, + {Status, _, Body} = eh_test_support:call(admin_handler_reviews, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/reviews">>, + auth => ?ADMIN_ID, + body => jsx:encode(#{ + <<"operations">> => [#{<<"id">> => Id, <<"status">> => <<"hidden">>}], + <<"reason">> => <<"moderation">> + }) + }), ?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_update_review_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"rv99">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"hidden">>}), Req} end), - ok = meck:expect(core_review, update_status, - fun(_, _) -> {error, not_found} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(404, Status). - -%% PUT – невалидный JSON -test_update_review_bad_json() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"rv1">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, <<"bad json">>, Req} end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, _, _} = erase(test_reply), +test_bulk_missing() -> + {Status, _, _} = eh_test_support:call(admin_handler_reviews, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/reviews">>, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"operations">> => []}) + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - {ok, _, _} = admin_handler_reviews:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_reviews, #{ + method => <<"POST">>, + path => <<"/v1/admin/reviews">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_stats_tests.erl b/test/unit/admin_handler_stats_tests.erl index c7c23bd..7c18c15 100644 --- a/test/unit/admin_handler_stats_tests.erl +++ b/test/unit/admin_handler_stats_tests.erl @@ -2,101 +2,75 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, user, event, calendar, review, report, + ticket, subscription]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(admin_utils, [non_strict]), - ok = meck:new(core_admin, [non_strict]), - ok = meck:new(logic_stats, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, _, _, _) -> put(test_reply, Code) end), + 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}), ok. cleanup(_) -> - meck:unload(logic_stats), - meck:unload(core_admin), - meck:unload(admin_utils), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_stats_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/stats as superadmin returns 200 with system metrics", fun test_superadmin/0}, - {"GET /admin/stats as superadmin with date filter", fun test_superadmin_dates/0}, - {"GET /admin/stats as moderator returns 200 with own metrics", fun test_moderator/0}, - {"GET /admin/stats as support returns 200 with assigned tickets", fun test_support/0}, - {"GET /admin/stats with non‑admin token returns 403", fun test_forbidden/0}, - {"POST /admin/stats returns 405", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET stats – success empty", fun test_stats_empty/0}, + {"GET stats – with seeded data", fun test_stats_with_data/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -%% --- Суперадмин (без дат) --- -test_superadmin() -> - 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, <<"adm1">>, Req} end), - ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), - ok = meck:expect(core_admin, get_by_id, - fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end), - ok = meck:expect(logic_stats, get_stats, - fun(superadmin, _) -> #{users => 10, events => 25} end), - {ok, _, _} = admin_handler_stats:init(req, []), - ?assertEqual(200, erase(test_reply)). +test_stats_empty() -> + {Status, _, Body} = eh_test_support:call(admin_handler_stats, #{ + method => <<"GET">>, + path => <<"/v1/admin/stats">>, + auth => ?ADMIN_ID + }), + ?assertEqual(200, Status), + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(0, maps:get(<<"users_total">>, Result)), + ?assertEqual(0, maps:get(<<"events_total">>, Result)), + ?assertEqual(0, maps:get(<<"tickets_total">>, Result)), + ?assert(is_map_key(<<"registrations_by_day">>, Result)). -%% --- Суперадмин (с датами) --- -test_superadmin_dates() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(cowboy_req, parse_qs, fun(_) -> - [{<<"from">>, <<"2026-01-01T00:00:00">>}, {<<"to">>, <<"2026-06-01T00:00:00">>}] - end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), - ok = meck:expect(core_admin, get_by_id, - fun(<<"adm1">>) -> {ok, #admin{id = <<"adm1">>, role = superadmin}} end), - ok = meck:expect(logic_stats, get_stats, - fun(superadmin, _, _, _) -> #{users => 10} end), - {ok, _, _} = admin_handler_stats:init(req, []), - ?assertEqual(200, erase(test_reply)). +test_stats_with_data() -> + ok = mnesia:dirty_write(eh_test_support:make_user(#{ + id => <<"u1">>, + email => <<"u1@test.local">>, + status => active + })), + {ok, _} = core_report:create(<<"u1">>, calendar, <<"c1">>, <<"spam">>), + {ok, _} = core_ticket:create(<<"u1">>, <<"hash1">>, <<"boom">>, <<"stack">>), + {Status, _, Body} = eh_test_support:call(admin_handler_stats, #{ + method => <<"GET">>, + path => <<"/v1/admin/stats">>, + auth => ?ADMIN_ID + }), + ?assertEqual(200, Status), + 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_moderator() -> - 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, <<"mod1">>, Req} end), - ok = meck:expect(admin_utils, is_admin, fun(_) -> true end), - 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_unauthorized() -> + {Status, _, _} = eh_test_support:call(admin_handler_stats, #{ + method => <<"GET">>, + path => <<"/v1/admin/stats">>, + auth => none + }), + ?assertEqual(401, Status). -%% --- Поддержка --- -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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - {ok, _, _} = admin_handler_stats:init(req, []), - ?assertEqual(405, erase(test_reply)). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_stats, #{ + method => <<"POST">>, + path => <<"/v1/admin/stats">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_subscription_stats_tests.erl b/test/unit/admin_handler_subscription_stats_tests.erl new file mode 100644 index 0000000..5cbe1ba --- /dev/null +++ b/test/unit/admin_handler_subscription_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_subscriptions_by_id_tests.erl b/test/unit/admin_handler_subscriptions_by_id_tests.erl new file mode 100644 index 0000000..a6e3abe --- /dev/null +++ b/test/unit/admin_handler_subscriptions_by_id_tests.erl @@ -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). diff --git a/test/unit/admin_handler_subscriptions_tests.erl b/test/unit/admin_handler_subscriptions_tests.erl index 18a8960..43d85c1 100644 --- a/test/unit/admin_handler_subscriptions_tests.erl +++ b/test/unit/admin_handler_subscriptions_tests.erl @@ -2,218 +2,69 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, subscription, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_subscription, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_subscription), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_subscriptions_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/subscriptions – success", fun test_list/0}, - {"GET /admin/subscriptions – forbidden", fun test_list_forbidden/0}, - {"POST /admin/subscriptions – success", fun test_create/0}, - {"POST /admin/subscriptions – missing user_id", fun test_create_missing/0}, - {"GET /admin/subscriptions/:id – success", fun test_get/0}, - {"GET /admin/subscriptions/:id – not found", fun test_get_not_found/0}, - {"PUT /admin/subscriptions/:id – success", fun test_update/0}, - {"PUT /admin/subscriptions/:id – not found", fun test_update_not_found/0}, - {"DELETE /admin/subscriptions/:id – success", fun test_delete/0}, - {"DELETE /admin/subscriptions/:id – not found", fun test_delete_not_found/0}, - {"PATCH /admin/subscriptions – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"GET list – filter by plan", fun test_list_filter/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -%% GET список test_list() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - Sub1 = #subscription{ - id = <<"s1">>, - user_id = <<"u1">>, - plan = monthly, - status = active, - trial_used = false, - started_at = {{2026,4,27},{12,0,0}}, - expires_at = {{2026,5,27},{12,0,0}}, - created_at = {{2026,4,27},{12,0,0}}, - updated_at = {{2026,4,27},{12,0,0}} - }, - ok = meck:expect(core_subscription, list_subscriptions, fun() -> [Sub1] end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + {ok, _} = core_subscription:create(<<"u1">>, monthly, true), + {ok, _} = core_subscription:create(<<"u2">>, annual, false), + {Status, _, Body} = eh_test_support:call(admin_handler_subscriptions, #{ + method => <<"GET">>, + path => <<"/v1/admin/subscriptions">>, + auth => ?ADMIN_ID + }), ?assertEqual(200, Status), - [#{<<"id">> := <<"s1">>, <<"plan">> := <<"monthly">>, <<"status">> := <<"active">>}] = - jsx:decode(RespBody, [return_maps]). + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(2, length(Result)). -test_list_forbidden() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). - -%% POST создание -test_create() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - BodyMap = #{<<"user_id">> => <<"u1">>, <<"plan">> => <<"yearly">>}, - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(BodyMap), Req} end), - Created = #subscription{ - id = <<"s_new">>, user_id = <<"u1">>, plan = yearly, status = active, - trial_used = false, - started_at = {{2026,4,27},{14,0,0}}, expires_at = {{2027,4,27},{14,0,0}}, - created_at = {{2026,4,27},{14,0,0}}, updated_at = {{2026,4,27},{14,0,0}} - }, - ok = meck:expect(core_subscription, create_subscription, fun(_) -> {ok, Created} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(201, Status), - #{<<"plan">> := <<"yearly">>, <<"status">> := <<"active">>} = jsx:decode(RespBody, [return_maps]). - -test_create_missing() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"plan">> => <<"monthly">>}), Req} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(400, Status). - -%% GET по ID -test_get() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - Sub = #subscription{ - id = <<"s1">>, user_id = <<"u1">>, plan = monthly, status = active, - trial_used = false, - started_at = {{2026,4,27},{12,0,0}}, expires_at = {{2026,5,27},{12,0,0}}, - created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{12,0,0}} - }, - ok = meck:expect(core_subscription, get_by_id, - fun(<<"s1">>) -> {ok, Sub} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_list_filter() -> + {ok, _} = core_subscription:create(<<"u1">>, monthly, true), + {ok, _} = core_subscription:create(<<"u2">>, annual, false), + {Status, _, Body} = eh_test_support:call(admin_handler_subscriptions, #{ + method => <<"GET">>, + path => <<"/v1/admin/subscriptions">>, + auth => ?ADMIN_ID, + qs => [{<<"plan">>, <<"monthly">>}] + }), ?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() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_subscription, get_by_id, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(404, Status). +test_unauthorized() -> + {Status, _, _} = eh_test_support:call(admin_handler_subscriptions, #{ + method => <<"GET">>, + path => <<"/v1/admin/subscriptions">>, + auth => none + }), + ?assertEqual(401, Status). -%% PUT обновление -test_update() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end), - Updated = #subscription{id = <<"s1">>, status = cancelled}, - ok = meck:expect(core_subscription, update_subscription, - fun(<<"s1">>, _) -> {ok, Updated} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"cancelled">>} = jsx:decode(RespBody, [return_maps]). - -test_update_not_found() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"cancelled">>}), Req} end), - ok = meck:expect(core_subscription, update_subscription, - fun(_, _) -> {error, not_found} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(404, Status). - -%% DELETE -test_delete() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s1">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_subscription, delete_subscription, - fun(<<"s1">>) -> {ok, deleted} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]). - -test_delete_not_found() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"s99">> end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_subscription, delete_subscription, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(404, Status). - -%% Неверный метод test_wrong_method() -> - ok = meck:expect(cowboy_req, binding, fun(id, _) -> undefined end), - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), - {ok, _, _} = admin_handler_subscriptions:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_subscriptions, #{ + method => <<"POST">>, + path => <<"/v1/admin/subscriptions">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_ticket_by_id_tests.erl b/test/unit/admin_handler_ticket_by_id_tests.erl index 3c59961..7437960 100644 --- a/test/unit/admin_handler_ticket_by_id_tests.erl +++ b/test/unit/admin_handler_ticket_by_id_tests.erl @@ -2,186 +2,114 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_ticket, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_ticket), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_ticket_by_id_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/tickets/:id – success", fun test_get/0}, - {"GET /admin/tickets/:id – not found", fun test_get_not_found/0}, - {"GET /admin/tickets/:id – forbidden", fun test_get_forbidden/0}, - {"PUT /admin/tickets/:id – success", fun test_update/0}, - {"PUT /admin/tickets/:id – not found", fun test_update_not_found/0}, - {"PUT /admin/tickets/:id – bad JSON", fun test_update_bad_json/0}, - {"DELETE /admin/tickets/:id – success", fun test_delete/0}, - {"DELETE /admin/tickets/:id – not found", fun test_delete_not_found/0}, - {"POST /admin/tickets/:id – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET – success", fun test_get/0}, + {"GET – not found", fun test_get_not_found/0}, + {"PUT – resolve", fun test_resolve/0}, + {"DELETE – success", fun test_delete/0}, + {"DELETE – not found", fun test_delete_not_found/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"PATCH – method not allowed", fun test_wrong_method/0} ]}. -%% GET – успех +seed_ticket() -> + {ok, Ticket} = core_ticket:create(<<"u1">>, <<"hash_x">>, <<"boom">>, <<"stack">>), + Ticket. + test_get() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t1">> end), - Ticket = #ticket{ - id = <<"t1">>, - error_hash = <<"hash">>, - error_message = <<"msg">>, - stacktrace = <<"trace">>, - context = <<"ctx">>, - count = 5, - first_seen = {{2026,4,27},{12,0,0}}, - last_seen = {{2026,4,27},{13,0,0}}, - status = open, - assigned_to = <<"dev1">>, - resolution_note = undefined - }, - ok = meck:expect(core_ticket, get_by_id, - fun(<<"t1">>) -> {ok, Ticket} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + Ticket = seed_ticket(), + Id = Ticket#ticket.id, + {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t99">> end), - ok = meck:expect(core_ticket, get_by_id, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID + }), ?assertEqual(404, Status). -%% GET – запрещён -test_get_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). - -%% PUT – успех -test_update() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t1">> end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>, <<"assigned_to">> => <<"adm2">>}), Req} end), - Updated = #ticket{id = <<"t1">>, status = closed, assigned_to = <<"adm2">>}, - ok = meck:expect(core_ticket, update_ticket, - fun(<<"t1">>, _) -> {ok, Updated} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_resolve() -> + Ticket = seed_ticket(), + Id = Ticket#ticket.id, + {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"PUT">>, + path => <<"/v1/admin/tickets/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID, + body => jsx:encode(#{ + <<"status">> => <<"resolved">>, + <<"resolution_note">> => <<"fixed">> + }) + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t1">> end), - ok = meck:expect(core_ticket, delete_ticket, - fun(<<"t1">>) -> {ok, deleted} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), + Ticket = seed_ticket(), + Id = Ticket#ticket.id, + {Status, _, Body} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/tickets/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t99">> end), - ok = meck:expect(core_ticket, delete_ticket, - fun(_) -> {error, not_found} end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/tickets/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_ticket_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_ticket_by_id, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/tickets/x">>, + bindings => #{id => <<"x">>}, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_ticket_stats_tests.erl b/test/unit/admin_handler_ticket_stats_tests.erl index 3581998..d0a6a92 100644 --- a/test/unit/admin_handler_ticket_stats_tests.erl +++ b/test/unit/admin_handler_ticket_stats_tests.erl @@ -2,60 +2,70 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_ticket, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_ticket), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_ticket_stats_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/tickets/stats – success", fun test_stats/0}, - {"GET /admin/tickets/stats – forbidden", fun test_stats_forbidden/0}, - {"POST /admin/tickets/stats – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET stats – empty", fun test_stats_empty/0}, + {"GET stats – with tickets", fun test_stats_with_data/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -test_stats() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - StatsData = #{ - open => 5, - in_progress => 3, - resolved => 12, - closed => 20 - }, - ok = meck:expect(core_ticket, stats, fun() -> StatsData end), - {ok, _, _} = admin_handler_ticket_stats:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_stats_empty() -> + {Status, _, Body} = eh_test_support:call(admin_handler_ticket_stats, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets/stats">>, + auth => ?ADMIN_ID + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_ticket_stats:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). +test_stats_with_data() -> + {ok, T1} = core_ticket:create(<<"u1">>, <<"h1">>, <<"err1">>, <<"s">>), + {ok, _} = core_ticket:create(<<"u2">>, <<"h2">>, <<"err2">>, <<"s">>), + {ok, _} = logic_ticket:update_status(?ADMIN_ID, T1#ticket.id, in_progress), + {Status, _, Body} = eh_test_support:call(admin_handler_ticket_stats, #{ + method => <<"GET">>, + 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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - {ok, _, _} = admin_handler_ticket_stats:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_ticket_stats, #{ + method => <<"POST">>, + path => <<"/v1/admin/tickets/stats">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_tickets_tests.erl b/test/unit/admin_handler_tickets_tests.erl index 2ee0c50..fad84c3 100644 --- a/test/unit/admin_handler_tickets_tests.erl +++ b/test/unit/admin_handler_tickets_tests.erl @@ -2,230 +2,102 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [admin, admin_audit, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). + setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), - ok = meck:new(core_ticket, [non_strict]), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), + 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(_) -> - meck:unload(core_ticket), - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_tickets_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/tickets – success", fun test_list/0}, - {"GET /admin/tickets – forbidden", fun test_list_forbidden/0}, - {"POST /admin/tickets – success", fun test_create/0}, - {"POST /admin/tickets – missing error_message", fun test_create_missing/0}, - {"GET /admin/tickets/:id – success", fun test_get/0}, - {"GET /admin/tickets/:id – not found", fun test_get_not_found/0}, - {"PUT /admin/tickets/:id – success", fun test_update/0}, - {"PUT /admin/tickets/:id – not found", fun test_update_not_found/0}, - {"DELETE /admin/tickets/:id – success", fun test_delete/0}, - {"DELETE /admin/tickets/:id – not found", fun test_delete_not_found/0}, - {"PATCH /admin/tickets – method not allowed", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"GET list – filter by status", fun test_list_filter/0}, + {"PUT update – success", fun test_update/0}, + {"PUT update – not found", fun test_update_not_found/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – 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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> 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), - 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), + _ = seed_ticket(), + {ok, _} = core_ticket:create(<<"u2">>, <<"hash_b">>, <<"error B">>, <<"stack">>), + {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets">>, + auth => ?ADMIN_ID + }), ?assertEqual(200, Status), - [#{<<"id">> := <<"t1">>, <<"error_message">> := <<"Error message">>}] = - jsx:decode(RespBody, [return_maps]). + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(2, length(Result)). -%% GET – запрещён -test_list_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> undefined end), % для маршрута без id - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - {ok, _, _} = admin_handler_tickets:init(req, []), - {Status, _, _, _} = erase(test_reply), - ?assertEqual(403, Status). - -%% POST – создание тикета -test_create() -> - ok = meck:expect(cowboy_req, 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), +test_list_filter() -> + Ticket = seed_ticket(), + {ok, _} = logic_ticket:update_status(?ADMIN_ID, Ticket#ticket.id, in_progress), + {ok, _} = core_ticket:create(<<"u2">>, <<"hash_b">>, <<"error B">>, <<"stack">>), + {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets">>, + auth => ?ADMIN_ID, + qs => [{<<"status">>, <<"open">>}] + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t1">> end), % для маршрута с id - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end), - 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), + Ticket = seed_ticket(), + Id = Ticket#ticket.id, + {Status, _, Body} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"PUT">>, + path => <<"/v1/admin/tickets/", Id/binary>>, + bindings => #{id => Id}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"status">> => <<"in_progress">>}) + }), ?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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t99">> end), % для маршрута с id - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(cowboy_req, read_body, - fun(Req) -> {ok, jsx:encode(#{<<"status">> => <<"closed">>}), Req} end), - ok = meck:expect(core_ticket, update_ticket, - fun(_, _) -> {error, not_found} end), - {ok, _, _} = admin_handler_tickets:init(req, []), - {Status, _, _, _} = erase(test_reply), + {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"PUT">>, + path => <<"/v1/admin/tickets/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"status">> => <<"closed">>}) + }), ?assertEqual(404, Status). -%% DELETE – удаление тикета -test_delete() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"t1">> end), % для маршрута с id - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"adm1">>, Req} end), - AdminUser = #user{id = <<"adm1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"adm1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_ticket, delete_ticket, - fun(<<"t1">>) -> {ok, deleted} end), - {ok, _, _} = admin_handler_tickets:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - #{<<"status">> := <<"deleted">>} = jsx:decode(RespBody, [return_maps]). +test_unauthorized() -> + {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"GET">>, + path => <<"/v1/admin/tickets">>, + auth => none + }), + ?assertEqual(401, Status). -%% 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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PATCH">> end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> undefined end), % для маршрута без id - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_tickets:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_tickets, #{ + method => <<"POST">>, + path => <<"/v1/admin/tickets">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_user_by_id_tests.erl b/test/unit/admin_handler_user_by_id_tests.erl index 9b2c279..6965ea0 100644 --- a/test/unit/admin_handler_user_by_id_tests.erl +++ b/test/unit/admin_handler_user_by_id_tests.erl @@ -2,189 +2,125 @@ -include_lib("eunit/include/eunit.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() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), - ok = meck:new(core_user, [non_strict]), + 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 = mnesia:dirty_write(eh_test_support:make_user(#{ + id => ?USER_ID, + email => <<"target@test.local">>, + nickname => <<"target">>, + status => active + })), ok. cleanup(_) -> - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_user_by_id_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/users/:id – success", fun test_get_user/0}, - {"GET /admin/users/:id – not found", fun test_get_user_not_found/0}, - {"GET /admin/users/:id – forbidden", fun test_get_user_forbidden/0}, - {"PUT /admin/users/:id – success", fun test_update_user/0}, - {"PUT /admin/users/:id – not found", fun test_update_user_not_found/0}, - {"DELETE /admin/users/:id – success", fun test_delete_user/0}, - {"DELETE /admin/users/:id – not found", fun test_delete_user_not_found/0}, - {"POST /admin/users/:id – method not allowed", fun test_wrong_method/0}, - {"convert_updates/1", fun test_convert_updates/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET – success", fun test_get/0}, + {"GET – not found", fun test_get_not_found/0}, + {"PUT – update status", fun test_update/0}, + {"PUT – not found", fun test_update_not_found/0}, + {"DELETE – soft delete", fun test_delete/0}, + {"DELETE – not found", fun test_delete_not_found/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"PATCH – method not allowed", fun test_wrong_method/0} ]}. -%% ── GET – success ───────────────────────────────────────── -test_get_user() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser}; - (<<"user1">>) -> {ok, #user{id = <<"user1">>, email = <<"u@t.com">>, - role = user, status = active, - created_at = {{2026,4,27},{12,0,0}}, - updated_at = {{2026,4,27},{12,0,0}}}} - end), - ok = meck:expect(cowboy_req, binding, - fun(id, _) -> <<"user1">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_get() -> + {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/users/", ?USER_ID/binary>>, + bindings => #{id => ?USER_ID}, + auth => ?ADMIN_ID + }), ?assertEqual(200, Status), - #{<<"id">> := <<"user1">>, <<"email">> := <<"u@t.com">>, <<"role">> := <<"user">>} - = jsx:decode(RespBody, [return_maps]). + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(?USER_ID, maps:get(<<"id">>, Result)), + ?assertEqual(<<"target@test.local">>, maps:get(<<"email">>, Result)). -%% ── GET – not found ─────────────────────────────────────── -test_get_user_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser}; - (_) -> {error, not_found} - end), - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(404, Status), - #{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]). +test_get_not_found() -> + {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/users/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID + }), + ?assertEqual(404, Status). -%% ── GET – forbidden ─────────────────────────────────────── -test_get_user_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(403, Status), - #{<<"error">> := <<"Admin access required">>} = jsx:decode(RespBody, [return_maps]). - -%% ── PUT – success ───────────────────────────────────────── -test_update_user() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser} end), - User = #user{id = <<"user1">>, email = <<"u@t.com">>, role = user, status = frozen, - created_at = {{2026,4,27},{12,0,0}}, updated_at = {{2026,4,27},{13,0,0}}}, - ok = meck:expect(core_user, update, fun(<<"user1">>, _) -> {ok, User} end), - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, jsx:encode(#{status => <<"frozen">>}), Req} end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_update() -> + {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"PUT">>, + path => <<"/v1/admin/users/", ?USER_ID/binary>>, + bindings => #{id => ?USER_ID}, + auth => ?ADMIN_ID, + body => jsx:encode(#{ + <<"status">> => <<"frozen">>, + <<"reason">> => <<"abuse">> + }) + }), ?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_user_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"PUT">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_user, update, fun(_, _) -> {error, not_found} end), - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end), - ok = meck:expect(cowboy_req, read_body, fun(Req) -> {ok, <<"{}">>, Req} end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(404, Status), - #{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]). +test_update_not_found() -> + {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"PUT">>, + path => <<"/v1/admin/users/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID, + body => jsx:encode(#{<<"status">> => <<"frozen">>}) + }), + ?assertEqual(404, Status). -%% ── DELETE – success ───────────────────────────────────── -test_delete_user() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_user, delete, fun(<<"user1">>) -> {ok, deleted} end), - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"user1">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), +test_delete() -> + {Status, _, Body} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/users/", ?USER_ID/binary>>, + bindings => #{id => ?USER_ID}, + auth => ?ADMIN_ID + }), ?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_user_not_found() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"DELETE">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - AdminUser = #user{id = <<"admin1">>, role = admin}, - ok = meck:expect(core_user, get_by_id, - fun(<<"admin1">>) -> {ok, AdminUser} end), - ok = meck:expect(core_user, delete, fun(_) -> {error, not_found} end), - ok = meck:expect(cowboy_req, binding, fun(id, _) -> <<"missing">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(404, Status), - #{<<"error">> := <<"User not found">>} = jsx:decode(RespBody, [return_maps]). +test_delete_not_found() -> + {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"DELETE">>, + path => <<"/v1/admin/users/missing">>, + bindings => #{id => <<"missing">>}, + auth => ?ADMIN_ID + }), + ?assertEqual(404, Status). + +test_unauthorized() -> + {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"GET">>, + path => <<"/v1/admin/users/", ?USER_ID/binary>>, + bindings => #{id => ?USER_ID}, + auth => none + }), + ?assertEqual(401, Status). -%% ── Wrong method ───────────────────────────────────────── test_wrong_method() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, - fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_user_by_id:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - #{<<"error">> := <<"Method not allowed">>} = jsx:decode(RespBody, [return_maps]). - -%% ── convert_updates/1 ──────────────────────────────────── -test_convert_updates() -> - Updates = [ - {<<"status">>, <<"frozen">>}, - {<<"role">>, <<"admin">>}, - {<<"email">>, <<"test@test.com">>} - ], - 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)). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_user_by_id, #{ + method => <<"PATCH">>, + path => <<"/v1/admin/users/", ?USER_ID/binary>>, + bindings => #{id => ?USER_ID}, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_handler_user_stats_tests.erl b/test/unit/admin_handler_user_stats_tests.erl new file mode 100644 index 0000000..b583f27 --- /dev/null +++ b/test/unit/admin_handler_user_stats_tests.erl @@ -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). diff --git a/test/unit/admin_handler_user_verification_token_tests.erl b/test/unit/admin_handler_user_verification_token_tests.erl new file mode 100644 index 0000000..4c297f5 --- /dev/null +++ b/test/unit/admin_handler_user_verification_token_tests.erl @@ -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). diff --git a/test/unit/admin_handler_users_tests.erl b/test/unit/admin_handler_users_tests.erl index 602989f..6daac85 100644 --- a/test/unit/admin_handler_users_tests.erl +++ b/test/unit/admin_handler_users_tests.erl @@ -1,65 +1,83 @@ -module(admin_handler_users_tests). -include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +-define(TABLES, [admin, admin_audit, user, ticket]). +-define(ADMIN_ID, <<"adm_test_1">>). setup() -> - ok = meck:new(cowboy_req, [non_strict]), - ok = meck:new(handler_auth, [non_strict]), % вместо auth - ok = meck:new(core_user, [non_strict]), + 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(_) -> - meck:unload(core_user), - meck:unload(handler_auth), - meck:unload(cowboy_req). + eh_test_support:unload_cowboy(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. admin_users_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - {"GET /admin/users with valid admin token returns 200 and list of users", fun test_list_users/0}, - {"GET /admin/users with non-admin token returns 403", fun test_list_users_forbidden/0}, - {"POST /admin/users returns 405", fun test_wrong_method/0} + {foreach, fun setup/0, fun cleanup/1, [ + {"GET list – success", fun test_list/0}, + {"GET list – filter by status", fun test_list_filter/0}, + {"GET – unauthorized", fun test_unauthorized/0}, + {"POST – method not allowed", fun test_wrong_method/0} ]}. -test_list_users() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {ok, <<"admin1">>, Req} end), - User = #{ - id => <<"user1">>, - email => <<"user@test.com">>, - role => <<"user">>, - status => <<"active">>, - created_at => {{2025,4,27},{12,0,0}}, - updated_at => {{2025,4,27},{12,30,0}} - }, - ok = meck:expect(core_user, list_users, fun() -> {ok, [User]} end), - ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_users:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(200, Status), - Users = jsx:decode(RespBody, [return_maps]), - ?assertEqual(1, length(Users)), - ?assertEqual(<<"user1">>, maps:get(<<"id">>, hd(Users))). +seed_users() -> + ok = mnesia:dirty_write(eh_test_support:make_user(#{ + id => <<"u1">>, + email => <<"alice@test.local">>, + nickname => <<"alice">>, + status => active + })), + ok = mnesia:dirty_write(eh_test_support:make_user(#{ + id => <<"u2">>, + email => <<"bob@test.local">>, + nickname => <<"bob">>, + status => frozen + })), + ok. -test_list_users_forbidden() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"GET">> end), - ok = meck:expect(handler_auth, authenticate, - fun(Req) -> {error, 403, <<"Admin access required">>, Req} end), - ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_users:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(403, Status), - ?assertEqual(#{<<"error">> => <<"Admin access required">>}, jsx:decode(RespBody, [return_maps])). +test_list() -> + seed_users(), + {Status, _, Body} = eh_test_support:call(admin_handler_users, #{ + method => <<"GET">>, + path => <<"/v1/admin/users">>, + auth => ?ADMIN_ID + }), + ?assertEqual(200, Status), + Result = jsx:decode(Body, [return_maps]), + ?assertEqual(2, length(Result)). + +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() -> - ok = meck:expect(cowboy_req, method, fun(_) -> <<"POST">> end), - ok = meck:expect(cowboy_req, reply, fun(Code, Headers, Body, Req) -> - put(test_reply, {Code, Headers, Body, Req}) - end), - {ok, _, _} = admin_handler_users:init(req, []), - {Status, _, RespBody, _} = erase(test_reply), - ?assertEqual(405, Status), - ?assertEqual(#{<<"error">> => <<"Method not allowed">>}, jsx:decode(RespBody, [return_maps])). \ No newline at end of file + {Status, _, _} = eh_test_support:call(admin_handler_users, #{ + method => <<"POST">>, + path => <<"/v1/admin/users">>, + auth => ?ADMIN_ID + }), + ?assertEqual(405, Status). diff --git a/test/unit/admin_utils_tests.erl b/test/unit/admin_utils_tests.erl index ef12322..42c35c2 100644 --- a/test/unit/admin_utils_tests.erl +++ b/test/unit/admin_utils_tests.erl @@ -1,37 +1,110 @@ -module(admin_utils_tests). -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_() -> - {setup, - 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}, - {"Falls back to X-Real-IP", fun test_real_ip/0}, - {"Falls back to peer address", fun test_peer_fallback/0}, - {"ip_to_binary handles IPv4 tuple", fun test_ipv4_tuple/0} - ]}. + {foreach, fun client_ip_setup/0, fun client_ip_cleanup/1, [ + {"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 peer address", fun test_peer_fallback/0}, + {"ip_to_binary handles IPv4 tuple", fun test_ipv4_tuple/0} + ]}. test_forwarded_for() -> - ok = meck:expect(cowboy_req, header, fun - (<<"x-forwarded-for">>, _) -> <<"203.0.113.10, 10.0.0.1">>; - (_, _) -> undefined - end), - ok = meck:expect(cowboy_req, peer, fun(_) -> {10, 0, 0, 2} end), - ?assertEqual(<<"203.0.113.10">>, admin_utils:client_ip(req)). + Req = eh_test_support:with_cowboy(#{ + headers => #{<<"x-forwarded-for">> => <<"203.0.113.10, 10.0.0.1">>}, + peer => {10, 0, 0, 2} + }), + ?assertEqual(<<"203.0.113.10">>, admin_utils:client_ip(Req)). test_real_ip() -> - ok = meck:expect(cowboy_req, header, fun - (<<"x-real-ip">>, _) -> <<"198.51.100.5">>; - (_, _) -> undefined - end), - ok = meck:expect(cowboy_req, peer, fun(_) -> {10, 0, 0, 2} end), - ?assertEqual(<<"198.51.100.5">>, admin_utils:client_ip(req)). + Req = eh_test_support:with_cowboy(#{ + headers => #{<<"x-real-ip">> => <<"198.51.100.5">>}, + peer => {10, 0, 0, 2} + }), + ?assertEqual(<<"198.51.100.5">>, admin_utils:client_ip(Req)). test_peer_fallback() -> - ok = meck:expect(cowboy_req, header, fun(_, _) -> undefined end), - ok = meck:expect(cowboy_req, peer, fun(_) -> {127, 0, 0, 1} end), - ?assertEqual(<<"127.0.0.1">>, admin_utils:client_ip(req)). + Req = eh_test_support:with_cowboy(#{ + headers => #{}, + peer => {127, 0, 0, 1} + }), + ?assertEqual(<<"127.0.0.1">>, admin_utils:client_ip(Req)). test_ipv4_tuple() -> ?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)). diff --git a/test/unit/booking_integration_tests.erl b/test/unit/booking_integration_tests.erl index d493723..dd4a94e 100644 --- a/test/unit/booking_integration_tests.erl +++ b/test/unit/booking_integration_tests.erl @@ -2,32 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, calendar, event, booking]). + setup() -> - mnesia:start(), - mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(booking, [ - {attributes, record_info(fields, booking)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(booking), - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. booking_integration_test_() -> @@ -35,7 +19,7 @@ booking_integration_test_() -> fun setup/0, 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}, {"Capacity management test", fun test_capacity_management/0}, {"Multiple bookings test", fun test_multiple_bookings/0} @@ -55,22 +39,26 @@ create_user() -> mnesia:dirty_write(User), 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(), ParticipantId = create_user(), {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, 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), - ?assertEqual(confirmed, Updated#booking.status), + {ok, Stored} = core_booking:get_by_id(Booking#booking.id), + ?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)), {ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId), @@ -82,7 +70,7 @@ test_manual_booking_flow() -> {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, 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), ?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). test_capacity_management() -> @@ -100,21 +88,21 @@ test_capacity_management() -> Participant2Id = 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, _} = core_event:update(Event#event.id, [{capacity, 2}]), {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), + {ok, _} = logic_booking:cancel_booking(Participant1Id, Booking1#booking.id, cancel), - % Теперь третий может записаться {ok, _} = logic_booking:create_booking(Participant3Id, Event#event.id). test_multiple_bookings() -> @@ -123,9 +111,10 @@ test_multiple_bookings() -> {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), - StartTime1 = {{2026, 6, 1}, {10, 0, 0}}, - StartTime2 = {{2026, 6, 2}, {10, 0, 0}}, - StartTime3 = {{2026, 6, 3}, {10, 0, 0}}, + Base = eh_test_support:future_start(), + StartTime1 = Base, + StartTime2 = add_days(Base, 1), + StartTime3 = add_days(Base, 2), {ok, Event1} = core_event:create(Calendar#calendar.id, <<"Event1">>, StartTime1, 60), {ok, Event2} = core_event:create(Calendar#calendar.id, <<"Event2">>, StartTime2, 60), @@ -145,4 +134,4 @@ test_multiple_bookings() -> ?assertEqual(2, ConfirmedCount), PendingCount = length([B || B <- UserBookings, B#booking.status =:= pending]), - ?assertEqual(1, PendingCount). \ No newline at end of file + ?assertEqual(1, PendingCount). diff --git a/test/unit/core_admin_audit_tests.erl b/test/unit/core_admin_audit_tests.erl new file mode 100644 index 0000000..668f408 --- /dev/null +++ b/test/unit/core_admin_audit_tests.erl @@ -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">>)). diff --git a/test/unit/core_admin_tests.erl b/test/unit/core_admin_tests.erl new file mode 100644 index 0000000..650dad6 --- /dev/null +++ b/test/unit/core_admin_tests.erl @@ -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">>)). diff --git a/test/unit/core_booking_tests.erl b/test/unit/core_booking_tests.erl index 7b8abf5..c51e2c7 100644 --- a/test/unit/core_booking_tests.erl +++ b/test/unit/core_booking_tests.erl @@ -2,125 +2,84 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [booking]). + setup() -> - mnesia:start(), - mnesia:create_table(booking, [ - {attributes, record_info(fields, booking)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(booking), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. core_booking_test_() -> - {foreach, - fun setup/0, - fun cleanup/1, - [ - {"Create booking test", fun test_create_booking/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}, - {"List bookings by event test", fun test_list_by_event/0}, - {"List bookings by user test", fun test_list_by_user/0}, - {"Update booking status test", fun test_update_status/0}, - {"Delete booking test", fun test_delete_booking/0} - ]}. + {foreach, fun setup/0, fun cleanup/1, [ + {"Create booking test", fun test_create_booking/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}, + {"List bookings by event test", fun test_list_by_event/0}, + {"List bookings by user test", fun test_list_by_user/0}, + {"Update booking status test", fun test_update_status/0}, + {"Delete booking test", fun test_delete_booking/0} + ]}. test_create_booking() -> EventId = <<"event123">>, UserId = <<"user123">>, - - {ok, Booking} = core_booking:create(EventId, UserId), - + {ok, Booking} = core_booking:create(EventId, UserId, pending), ?assertEqual(EventId, Booking#booking.event_id), ?assertEqual(UserId, Booking#booking.user_id), ?assertEqual(pending, Booking#booking.status), - ?assertEqual(undefined, Booking#booking.confirmed_at), ?assert(is_binary(Booking#booking.id)), ?assert(Booking#booking.created_at =/= undefined), ?assert(Booking#booking.updated_at =/= undefined). test_get_by_id() -> - EventId = <<"event123">>, - UserId = <<"user123">>, - {ok, Booking} = core_booking:create(EventId, UserId), - + {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending), {ok, Found} = core_booking:get_by_id(Booking#booking.id), ?assertEqual(Booking#booking.id, Found#booking.id), - {error, not_found} = core_booking:get_by_id(<<"nonexistent">>). test_get_by_event_and_user() -> EventId = <<"event123">>, - UserId1 = <<"user1">>, - UserId2 = <<"user2">>, - - {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), + {ok, Booking1} = core_booking:create(EventId, <<"user1">>, pending), + {ok, _} = core_booking:create(EventId, <<"user2">>, pending), + {ok, Found} = core_booking:get_by_event_and_user(EventId, <<"user1">>), ?assertEqual(Booking1#booking.id, Found#booking.id), - {error, not_found} = core_booking:get_by_event_and_user(EventId, <<"user3">>). test_list_by_event() -> - EventId1 = <<"event1">>, - EventId2 = <<"event2">>, - UserId = <<"user123">>, - - {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), + {ok, _} = core_booking:create(<<"event1">>, <<"user123">>, pending), + {ok, _} = core_booking:create(<<"event1">>, <<"user2">>, pending), + {ok, _} = core_booking:create(<<"event2">>, <<"user123">>, pending), + {ok, Bookings1} = core_booking:list_by_event(<<"event1">>), ?assertEqual(2, length(Bookings1)), - - {ok, Bookings2} = core_booking:list_by_event(EventId2), + {ok, Bookings2} = core_booking:list_by_event(<<"event2">>), ?assertEqual(1, length(Bookings2)). test_list_by_user() -> - EventId = <<"event123">>, - UserId1 = <<"user1">>, - UserId2 = <<"user2">>, - - {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), + {ok, _} = core_booking:create(<<"event123">>, <<"user1">>, pending), + {ok, _} = core_booking:create(<<"event456">>, <<"user1">>, pending), + {ok, _} = core_booking:create(<<"event123">>, <<"user2">>, pending), + {ok, Bookings1} = core_booking:list_by_user(<<"user1">>), ?assertEqual(2, length(Bookings1)), - - {ok, Bookings2} = core_booking:list_by_user(UserId2), + {ok, Bookings2} = core_booking:list_by_user(<<"user2">>), ?assertEqual(1, length(Bookings2)). test_update_status() -> - EventId = <<"event123">>, - UserId = <<"user123">>, - {ok, Booking} = core_booking:create(EventId, UserId), - - timer:sleep(2000), % 2 секунды - - {ok, Confirmed} = core_booking:update_status(Booking#booking.id, confirmed), + {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending), + Now = calendar:universal_time(), + {ok, Confirmed} = core_booking:update(Booking#booking.id, + [{status, confirmed}, {confirmed_at, Now}]), ?assertEqual(confirmed, Confirmed#booking.status), - ?assert(Confirmed#booking.confirmed_at =/= undefined), - ?assert(Confirmed#booking.updated_at > Booking#booking.updated_at), - - timer:sleep(2000), % 2 секунды - - {ok, Cancelled} = core_booking:update_status(Booking#booking.id, cancelled), + ?assertEqual(Now, Confirmed#booking.confirmed_at), + {ok, Cancelled} = core_booking:update(Booking#booking.id, [{status, cancelled}]), ?assertEqual(cancelled, Cancelled#booking.status), - ?assert(Cancelled#booking.updated_at > Confirmed#booking.updated_at), - - {error, not_found} = core_booking:update_status(<<"nonexistent">>, confirmed). + {error, not_found} = core_booking:update(<<"nonexistent">>, [{status, confirmed}]). test_delete_booking() -> - EventId = <<"event123">>, - UserId = <<"user123">>, - {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). \ No newline at end of file + {ok, Booking} = core_booking:create(<<"event123">>, <<"user123">>, pending), + ok = core_booking:delete(Booking#booking.id), + {error, not_found} = core_booking:get_by_id(Booking#booking.id). diff --git a/test/unit/core_event_recurring_tests.erl b/test/unit/core_event_recurring_tests.erl index f354caa..b0b57f8 100644 --- a/test/unit/core_event_recurring_tests.erl +++ b/test/unit/core_event_recurring_tests.erl @@ -2,22 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [event, recurrence_exception]). + setup() -> - mnesia:start(), - 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()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(recurrence_exception), - mnesia:delete_table(event), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. core_event_recurring_test_() -> @@ -31,10 +25,14 @@ core_event_recurring_test_() -> {"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() -> CalendarId = <<"calendar123">>, Title = <<"Weekly Meeting">>, - StartTime = {{2026, 4, 20}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), Duration = 60, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, @@ -46,7 +44,6 @@ test_create_recurring() -> ?assertEqual(false, Event#event.is_instance), ?assert(is_binary(Event#event.recurrence_rule)), - % Проверяем, что правило сохранилось Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]), RRuleMap = case Decoded of Map when is_map(Map) -> Map; @@ -56,8 +53,8 @@ test_create_recurring() -> test_materialize_occurrence() -> CalendarId = <<"calendar123">>, - MasterId = create_recurring_event(CalendarId), - OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}}, + {MasterId, StartTime} = create_recurring_event(CalendarId), + OccurrenceStart = add_days(StartTime, 7), SpecialistId = <<"specialist123">>, {ok, Instance} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId), @@ -71,15 +68,13 @@ test_materialize_occurrence() -> test_materialize_existing() -> CalendarId = <<"calendar123">>, - MasterId = create_recurring_event(CalendarId), - OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}}, + {MasterId, StartTime} = create_recurring_event(CalendarId), + OccurrenceStart = add_days(StartTime, 7), SpecialistId1 = <<"specialist1">>, SpecialistId2 = <<"specialist2">>, - % Первая материализация {ok, Instance1} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId1), - % Вторая материализация - должна вернуть существующий экземпляр {ok, Instance2} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId2), ?assertEqual(Instance1#event.id, Instance2#event.id), @@ -87,18 +82,20 @@ test_materialize_existing() -> test_materialize_non_recurring() -> 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( - SingleEvent#event.id, {{2026, 4, 27}, {10, 0, 0}}, <<"spec">> + SingleEvent#event.id, add_days(StartTime, 7), <<"spec">> ). create_recurring_event(CalendarId) -> + StartTime = eh_test_support:future_start(), {ok, Event} = core_event:create_recurring( CalendarId, <<"Weekly Meeting">>, - {{2026, 4, 20}, {10, 0, 0}}, + StartTime, 60, #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1} ), - Event#event.id. \ No newline at end of file + {Event#event.id, StartTime}. diff --git a/test/unit/core_event_tests.erl b/test/unit/core_event_tests.erl index 50e5d66..c924c4a 100644 --- a/test/unit/core_event_tests.erl +++ b/test/unit/core_event_tests.erl @@ -2,17 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [event]). + setup() -> - mnesia:start(), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(event), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. core_event_test_() -> @@ -27,10 +26,14 @@ core_event_test_() -> {"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() -> CalendarId = <<"calendar123">>, Title = <<"Test Event">>, - StartTime = {{2026, 4, 25}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), Duration = 60, {ok, Event} = core_event:create(CalendarId, Title, StartTime, Duration), @@ -48,7 +51,7 @@ test_create_event() -> test_get_by_id() -> 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), ?assertEqual(Event#event.id, Found#event.id), @@ -58,10 +61,11 @@ test_get_by_id() -> test_list_by_calendar() -> CalendarId1 = <<"calendar1">>, 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 2">>, {{2026, 4, 26}, {11, 0, 0}}, 90), - {ok, _} = core_event:create(CalendarId2, <<"Event 3">>, {{2026, 4, 27}, {12, 0, 0}}, 30), + {ok, _} = core_event:create(CalendarId1, <<"Event 1">>, Base, 60), + {ok, _} = core_event:create(CalendarId1, <<"Event 2">>, add_days(Base, 1), 90), + {ok, _} = core_event:create(CalendarId2, <<"Event 3">>, add_days(Base, 2), 30), {ok, Events1} = core_event:list_by_calendar(CalendarId1), ?assertEqual(2, length(Events1)), @@ -74,7 +78,7 @@ test_list_by_calendar() -> test_update_event() -> 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), Updates = [{title, <<"Updated">>}, {capacity, 100}, {description, <<"New desc">>}], {ok, Updated} = core_event:update(Event#event.id, Updates), @@ -88,11 +92,10 @@ test_update_event() -> test_delete_event() -> 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), ?assertEqual(deleted, Deleted#event.status), - % Удалённое событие не возвращается в списке активных {ok, ActiveEvents} = core_event:list_by_calendar(CalendarId), - ?assertEqual(0, length(ActiveEvents)). \ No newline at end of file + ?assertEqual(0, length(ActiveEvents)). diff --git a/test/unit/core_report_tests.erl b/test/unit/core_report_tests.erl index 7a43ef5..8831d0f 100644 --- a/test/unit/core_report_tests.erl +++ b/test/unit/core_report_tests.erl @@ -2,100 +2,70 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [report]). + setup() -> - mnesia:start(), - mnesia:create_table(report, [ - {attributes, record_info(fields, report)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(report), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. core_report_test_() -> - {foreach, - fun setup/0, - fun cleanup/1, - [ - {"Create report test", fun test_create_report/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 reporter test", fun test_list_by_reporter/0}, - {"Update report status test", fun test_update_status/0}, - {"Get count by target test", fun test_get_count_by_target/0} - ]}. + {foreach, fun setup/0, fun cleanup/1, [ + {"Create report test", fun test_create_report/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 reporter test", fun test_list_by_reporter/0}, + {"Update report status test", fun test_update_status/0}, + {"Get count by target test", fun test_get_count_by_target/0} + ]}. test_create_report() -> - ReporterId = <<"user123">>, - TargetType = event, - TargetId = <<"event123">>, - Reason = <<"Inappropriate content">>, - - {ok, Report} = core_report:create(ReporterId, TargetType, TargetId, Reason), - - ?assertEqual(ReporterId, Report#report.reporter_id), - ?assertEqual(TargetType, Report#report.target_type), - ?assertEqual(TargetId, Report#report.target_id), - ?assertEqual(Reason, Report#report.reason), + {ok, Report} = core_report:create(<<"user123">>, event, <<"event123">>, + <<"Inappropriate content">>), + ?assertEqual(<<"user123">>, Report#report.reporter_id), + ?assertEqual(event, Report#report.target_type), + ?assertEqual(<<"event123">>, Report#report.target_id), + ?assertEqual(<<"Inappropriate content">>, Report#report.reason), ?assertEqual(pending, Report#report.status), - ?assertEqual(undefined, Report#report.resolved_at), - ?assertEqual(undefined, Report#report.resolved_by), ?assert(is_binary(Report#report.id)). test_get_by_id() -> - ReporterId = <<"user123">>, - {ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"Bad">>), - + {ok, Report} = core_report:create(<<"user123">>, event, <<"ev1">>, <<"Bad">>), {ok, Found} = core_report:get_by_id(Report#report.id), ?assertEqual(Report#report.id, Found#report.id), - {error, not_found} = core_report:get_by_id(<<"nonexistent">>). test_list_by_target() -> - User1 = <<"user1">>, - User2 = <<"user2">>, EventId = <<"event123">>, - - {ok, _} = core_report:create(User1, event, EventId, <<"Reason 1">>), - {ok, _} = core_report:create(User2, event, EventId, <<"Reason 2">>), - {ok, _} = core_report:create(User1, calendar, <<"cal1">>, <<"Reason 3">>), - - {ok, Reports} = core_report:list_by_target(event, EventId), + {ok, _} = core_report:create(<<"user1">>, event, EventId, <<"Reason 1">>), + {ok, _} = core_report:create(<<"user2">>, event, EventId, <<"Reason 2">>), + {ok, _} = core_report:create(<<"user1">>, calendar, <<"cal1">>, <<"Reason 3">>), + Reports = core_report:list_by_target(event, EventId), ?assertEqual(2, length(Reports)). test_list_by_reporter() -> - User1 = <<"user1">>, - User2 = <<"user2">>, - - {ok, _} = core_report:create(User1, event, <<"ev1">>, <<"">>), - {ok, _} = core_report:create(User1, event, <<"ev2">>, <<"">>), - {ok, _} = core_report:create(User2, event, <<"ev3">>, <<"">>), - - {ok, Reports} = core_report:list_by_reporter(User1), + {ok, _} = core_report:create(<<"user1">>, event, <<"ev1">>, <<"">>), + {ok, _} = core_report:create(<<"user1">>, event, <<"ev2">>, <<"">>), + {ok, _} = core_report:create(<<"user2">>, event, <<"ev3">>, <<"">>), + Reports = core_report:list_by_reporter(<<"user1">>), ?assertEqual(2, length(Reports)). test_update_status() -> - ReporterId = <<"user123">>, - AdminId = <<"admin123">>, - {ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"">>), - - {ok, Updated} = core_report:update_status(Report#report.id, reviewed, AdminId), + {ok, Report} = core_report:create(<<"user123">>, event, <<"ev1">>, <<"">>), + {ok, Updated} = core_report:update_status(Report#report.id, reviewed, <<"admin123">>), ?assertEqual(reviewed, Updated#report.status), - ?assertEqual(AdminId, Updated#report.resolved_by), + ?assertEqual(<<"admin123">>, Updated#report.resolved_by), ?assert(Updated#report.resolved_at =/= undefined). test_get_count_by_target() -> - User1 = <<"user1">>, - User2 = <<"user2">>, EventId = <<"event123">>, - ?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)), - - {ok, _} = core_report:create(User2, event, EventId, <<"">>), - ?assertEqual(2, core_report:get_count_by_target(event, EventId)). \ No newline at end of file + {ok, _} = core_report:create(<<"user2">>, event, EventId, <<"">>), + ?assertEqual(2, core_report:get_count_by_target(event, EventId)). diff --git a/test/unit/core_review_tests.erl b/test/unit/core_review_tests.erl index aa4e222..12ec3c4 100644 --- a/test/unit/core_review_tests.erl +++ b/test/unit/core_review_tests.erl @@ -2,17 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [review]). + setup() -> - mnesia:start(), - mnesia:create_table(review, [ - {attributes, record_info(fields, review)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(review), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. core_review_test_() -> @@ -103,11 +102,13 @@ test_hide_unhide() -> UserId = <<"user123">>, {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(<<"spam">>, Hidden#review.reason), - {ok, Unhidden} = core_review:unhide(Review#review.id), - ?assertEqual(visible, Unhidden#review.status). + {ok, Unhidden} = core_review:unhide(Review#review.id, <<"restored">>), + ?assertEqual(visible, Unhidden#review.status), + ?assertEqual(<<"restored">>, Unhidden#review.reason). test_average_rating() -> UserId1 = <<"user1">>, @@ -128,4 +129,4 @@ test_has_user_reviewed() -> ?assertNot(core_review:has_user_reviewed(UserId, event, EventId)), {ok, _} = core_review:create(UserId, event, EventId, 5, <<"">>), - ?assert(core_review:has_user_reviewed(UserId, event, EventId)). \ No newline at end of file + ?assert(core_review:has_user_reviewed(UserId, event, EventId)). diff --git a/test/unit/core_user_tests.erl b/test/unit/core_user_tests.erl new file mode 100644 index 0000000..53adf07 --- /dev/null +++ b/test/unit/core_user_tests.erl @@ -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">>)). diff --git a/test/unit/eh_test_support.erl b/test/unit/eh_test_support.erl new file mode 100644 index 0000000..c6dccb7 --- /dev/null +++ b/test/unit/eh_test_support.erl @@ -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}}. diff --git a/test/unit/logic_booking_tests.erl b/test/unit/logic_booking_tests.erl index 5f50e35..a517f4f 100644 --- a/test/unit/logic_booking_tests.erl +++ b/test/unit/logic_booking_tests.erl @@ -2,34 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). --define(TEST_EMAIL, <<"test@example.com">>). +-define(TABLES, [user, calendar, event, booking]). setup() -> - mnesia:start(), - mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(booking, [ - {attributes, record_info(fields, booking)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(booking), - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. logic_booking_test_() -> @@ -37,16 +19,15 @@ logic_booking_test_() -> fun setup/0, fun cleanup/1, [ - {"Create booking with auto confirmation", fun test_create_booking_auto/0}, - {"Create booking with manual confirmation", fun test_create_booking_manual/0}, - {"Create booking with timeout confirmation", fun test_create_booking_timeout/0}, + {"Create booking returns pending", fun test_create_booking_pending/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}, - {"Confirm booking by owner", fun test_confirm_booking/0}, - {"Decline booking by owner", fun test_decline_booking/0}, + {"Pending bookings do not fill capacity", fun test_pending_does_not_fill/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}, - {"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 user bookings", fun test_list_user_bookings/0} ]}. @@ -71,50 +52,28 @@ create_test_calendar(OwnerId, Confirmation) -> Calendar#calendar.id. 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), Event#event.id. 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, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]), Updated#event.id. %% Тесты -test_create_booking_auto() -> +test_create_booking_pending() -> OwnerId = create_test_user(user), ParticipantId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, auto), 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), ?assertEqual(pending, Booking#booking.status), - timer:sleep(1500), - {ok, Updated} = core_booking:get_by_id(Booking#booking.id), - ?assertEqual(confirmed, Updated#booking.status). + {ok, Stored} = core_booking:get_by_id(Booking#booking.id), + ?assertEqual(pending, Stored#booking.status). test_create_duplicate_booking() -> OwnerId = create_test_user(user), @@ -125,25 +84,31 @@ test_create_duplicate_booking() -> {ok, _} = logic_booking:create_booking(ParticipantId, EventId), {error, already_booked} = logic_booking:create_booking(ParticipantId, EventId). -test_booking_inactive_event() -> - OwnerId = create_test_user(user), +test_booking_missing_event() -> ParticipantId = create_test_user(user), - CalendarId = create_test_calendar(OwnerId, manual), - EventId = create_test_event(CalendarId), - - {ok, _} = core_event:update(EventId, [{status, cancelled}]), - - {error, event_not_active} = logic_booking:create_booking(ParticipantId, EventId). + {error, not_found} = logic_booking:create_booking(ParticipantId, <<"nonexistent">>). test_booking_event_full() -> OwnerId = create_test_user(user), Participant1Id = 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), {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() -> OwnerId = create_test_user(user), @@ -155,15 +120,15 @@ test_confirm_booking() -> {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), ?assertEqual(confirmed, Confirmed#booking.status). -test_decline_booking() -> +test_confirm_non_pending() -> 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), - {ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline), - ?assertEqual(cancelled, Declined#booking.status). + {ok, _} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), + {error, access_denied} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm). test_cancel_booking() -> OwnerId = create_test_user(user), @@ -172,10 +137,10 @@ test_cancel_booking() -> EventId = create_test_event(CalendarId), {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). -test_unauthorized_confirm() -> +test_cancel_access_denied() -> OwnerId = create_test_user(user), ParticipantId = create_test_user(user), OtherId = create_test_user(user), @@ -183,7 +148,7 @@ test_unauthorized_confirm() -> EventId = create_test_event(CalendarId), {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() -> 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(Participant2Id, EventId), - {ok, Bookings} = logic_booking:list_event_bookings(OwnerId, EventId), + {ok, Bookings} = logic_booking:list_event_bookings(EventId), ?assertEqual(2, length(Bookings)). test_list_user_bookings() -> @@ -209,4 +174,4 @@ test_list_user_bookings() -> {ok, _} = logic_booking:create_booking(ParticipantId, EventId2), {ok, Bookings} = logic_booking:list_user_bookings(ParticipantId), - ?assertEqual(2, length(Bookings)). \ No newline at end of file + ?assertEqual(2, length(Bookings)). diff --git a/test/unit/logic_event_recurring_tests.erl b/test/unit/logic_event_recurring_tests.erl index a21c21a..a9dbf26 100644 --- a/test/unit/logic_event_recurring_tests.erl +++ b/test/unit/logic_event_recurring_tests.erl @@ -2,32 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, calendar, event, recurrence_exception]). + setup() -> - mnesia:start(), - mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(recurrence_exception, [ - {attributes, record_info(fields, recurrence_exception)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(recurrence_exception), - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. logic_event_recurring_test_() -> @@ -59,10 +43,14 @@ create_test_user_and_calendar() -> {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {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() -> {UserId, CalendarId} = create_test_user_and_calendar(), Title = <<"Weekly Meeting">>, - StartTime = {{2026, 6, 1}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), Duration = 60, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, @@ -75,7 +63,7 @@ test_create_recurring_event() -> test_create_recurring_invalid() -> {UserId, CalendarId} = create_test_user_and_calendar(), Title = <<"Invalid">>, - StartTime = {{2026, 6, 1}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), Duration = 60, InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1}, @@ -85,43 +73,43 @@ test_create_recurring_invalid() -> test_get_occurrences() -> {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}, {ok, Event} = logic_event:create_recurring_event( 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), ?assertEqual(5, length(Occurrences)). test_cancel_occurrence() -> {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}, {ok, Event} = logic_event:create_recurring_event( 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). test_occurrences_with_cancelled() -> {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}, {ok, Event} = logic_event:create_recurring_event( 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), - RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, + RangeEnd = add_days(StartTime, 28), {ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd), ?assertEqual(4, length(Occurrences)), @@ -131,14 +119,14 @@ test_occurrences_with_cancelled() -> test_materialize_for_booking() -> {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}, {ok, Event} = logic_event:create_recurring_event( UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule ), - OccurrenceStart = {{2026, 6, 8}, {10, 0, 0}}, + OccurrenceStart = add_days(StartTime, 7), SpecialistId = <<"specialist123">>, {ok, Instance} = logic_event:materialize_for_booking( @@ -148,4 +136,4 @@ test_materialize_for_booking() -> ?assertEqual(Event#event.id, Instance#event.master_id), ?assertEqual(OccurrenceStart, Instance#event.start_time), ?assertEqual(SpecialistId, Instance#event.specialist_id), - ?assertEqual(true, Instance#event.is_instance). \ No newline at end of file + ?assertEqual(true, Instance#event.is_instance). diff --git a/test/unit/logic_event_tests.erl b/test/unit/logic_event_tests.erl index 71eb406..3d4bf4f 100644 --- a/test/unit/logic_event_tests.erl +++ b/test/unit/logic_event_tests.erl @@ -2,27 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, calendar, event]). + setup() -> - mnesia:start(), - mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. logic_event_test_() -> @@ -54,10 +43,14 @@ create_test_user_and_calendar() -> {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {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() -> {UserId, CalendarId} = create_test_user_and_calendar(), Title = <<"Test Event">>, - StartTime = {{2026, 5, 1}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), Duration = 60, {ok, Event} = logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration), @@ -67,22 +60,26 @@ test_create_event() -> test_get_event() -> {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), ?assertEqual(Event#event.id, Found#event.id). test_list_events() -> {UserId, CalendarId} = create_test_user_and_calendar(), - {ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 1">>, {{2026, 5, 1}, {10, 0, 0}}, 60), - {ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 2">>, {{2026, 5, 2}, {11, 0, 0}}, 90), + Start1 = eh_test_support:future_start(), + 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), ?assertEqual(2, length(Events)). test_update_event() -> {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}], {ok, Updated} = logic_event:update_event(UserId, Event#event.id, Updates), @@ -91,7 +88,8 @@ test_update_event() -> test_delete_event() -> {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), ?assertEqual(deleted, Deleted#event.status). @@ -102,5 +100,5 @@ test_time_validation() -> PastTime = {{2020, 1, 1}, {10, 0, 0}}, {error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60), - FutureTime = {{2030, 1, 1}, {10, 0, 0}}, - ?assertEqual(ok, logic_event:validate_event_time(FutureTime)). \ No newline at end of file + FutureTime = eh_test_support:future_start(), + ?assertEqual(ok, logic_event:validate_event_time(FutureTime)). diff --git a/test/unit/logic_moderation_tests.erl b/test/unit/logic_moderation_tests.erl index 8502bc8..ddea9ad 100644 --- a/test/unit/logic_moderation_tests.erl +++ b/test/unit/logic_moderation_tests.erl @@ -2,34 +2,20 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, admin, calendar, event, report, banned_word]). + %% ---------------------------------------------------------------- %% Фикстуры %% ---------------------------------------------------------------- setup() -> - catch mnesia:stop(), - case mnesia:start() of - {atomic, ok} -> ok; - ok -> ok - end, - {atomic, ok} = mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, {ram_copies, [node()]}]), - {atomic, ok} = mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]), - {atomic, ok} = mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, {ram_copies, [node()]}]), - {atomic, ok} = mnesia:create_table(report, [ - {attributes, record_info(fields, report)}, {ram_copies, [node()]}]), - {atomic, ok} = mnesia:create_table(banned_word, [ - {attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(banned_word), - mnesia:delete_table(report), - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(). + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), + ok. %% ---------------------------------------------------------------- %% Тесты @@ -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}), - User = #user{ - id = UserId, - email = <<>>, - password_hash = <<"hash">>, - role = Role, - status = active, - created_at = calendar:universal_time(), - updated_at = calendar:universal_time() - }, + User = eh_test_support:make_user(#{id => UserId, email => <>}), mnesia:dirty_write(User), 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 => <>}), + Admin#admin.id. + create_test_calendar(OwnerId) -> {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), Calendar#calendar.id. create_test_event(CalendarId) -> {ok, Event} = core_event:create(CalendarId, <<"Event">>, - {{2026, 6, 1}, {10, 0, 0}}, 60), + eh_test_support:future_start(), 60), Event#event.id. %% ── Тесты ───────────────────────────────────────────────── test_create_report() -> - ReporterId = create_test_user(user), - OwnerId = create_test_user(user), + ReporterId = create_test_user(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>), @@ -82,19 +65,19 @@ test_create_report() -> ?assertEqual(pending, Report#report.status). test_get_reports() -> - AdminId = create_test_user(admin), - ReporterId = create_test_user(user), - OwnerId = create_test_user(user), + AdminId = create_test_admin(), + ReporterId = create_test_user(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>), - {ok, Reports} = logic_moderation:get_reports(AdminId), + Reports = logic_moderation:get_reports(AdminId), ?assertEqual(1, length(Reports)). test_resolve_report() -> - AdminId = create_test_user(admin), - ReporterId = create_test_user(user), - OwnerId = create_test_user(user), + AdminId = create_test_admin(), + ReporterId = create_test_user(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>), @@ -103,22 +86,22 @@ test_resolve_report() -> ?assertEqual(AdminId, Resolved#report.resolved_by). test_add_banned_word() -> - AdminId = create_test_user(admin), + AdminId = create_test_admin(), {ok, BW} = logic_moderation:add_banned_word(AdminId, <<"badword">>), ?assertEqual(<<"badword">>, BW#banned_word.word), ?assertEqual(AdminId, BW#banned_word.added_by). test_remove_banned_word() -> - AdminId = create_test_user(admin), + AdminId = create_test_admin(), {ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>), {ok, deleted} = logic_moderation:remove_banned_word(AdminId, <<"badword">>), ?assertEqual([], core_banned_words:list_banned_words()). test_auto_freeze() -> - Reporter1 = create_test_user(user), - Reporter2 = create_test_user(user), - Reporter3 = create_test_user(user), - OwnerId = create_test_user(user), + Reporter1 = create_test_user(), + Reporter2 = create_test_user(), + Reporter3 = create_test_user(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>), @@ -128,8 +111,8 @@ test_auto_freeze() -> ?assertEqual(frozen, Event#event.status). test_freeze_calendar() -> - AdminId = create_test_user(admin), - OwnerId = create_test_user(user), + AdminId = create_test_admin(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), {ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId), ?assertEqual(frozen, Frozen#calendar.status), @@ -137,8 +120,8 @@ test_freeze_calendar() -> ?assertEqual(active, Unfrozen#calendar.status). test_freeze_event() -> - AdminId = create_test_user(admin), - OwnerId = create_test_user(user), + AdminId = create_test_admin(), + OwnerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId), @@ -147,9 +130,9 @@ test_freeze_event() -> ?assertEqual(active, Unfrozen#event.status). test_check_content() -> - AdminId = create_test_user(admin), + AdminId = create_test_admin(), {ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>), ?assertNot(logic_moderation:check_content(<<"Hello">>)), ?assert(logic_moderation:check_content(<<"This is bad">>)), ?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)), - ?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)). \ No newline at end of file + ?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)). diff --git a/test/unit/logic_notification_tests.erl b/test/unit/logic_notification_tests.erl index 81036e5..c0f504b 100644 --- a/test/unit/logic_notification_tests.erl +++ b/test/unit/logic_notification_tests.erl @@ -2,19 +2,17 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [booking, calendar, event]). + setup() -> pg:start_link(), - mnesia:start(), - mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {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()]}]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(booking), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. logic_notification_test_() -> @@ -66,7 +64,7 @@ create_test_event() -> title = <<"Test Event">>, description = <<"">>, event_type = single, - start_time = {{2026, 6, 1}, {10, 0, 0}}, + start_time = eh_test_support:future_start(), duration = 60, recurrence_rule = undefined, master_id = undefined, @@ -89,7 +87,6 @@ test_notify_booking() -> Booking = create_test_booking(), UserId = <<"user123">>, - % Функция возвращает список пидов (может быть пустым) Result = logic_notification:notify_booking(UserId, Booking), ?assert(is_list(Result)). @@ -105,4 +102,4 @@ test_notify_event_update() -> test_notify_admin() -> Result = logic_notification:notify_admin(report_created, #{report_id => <<"rep123">>}), - ?assertEqual(ok, Result). \ No newline at end of file + ?assertEqual(ok, Result). diff --git a/test/unit/logic_review_tests.erl b/test/unit/logic_review_tests.erl index 4eba104..9c49338 100644 --- a/test/unit/logic_review_tests.erl +++ b/test/unit/logic_review_tests.erl @@ -2,22 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, admin, calendar, event, booking, review]). + setup() -> - mnesia:start(), - mnesia:create_table(user, [{attributes, record_info(fields, user)}, {ram_copies, [node()]}]), - mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]), - mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]), - mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]), - mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(review), - mnesia:delete_table(booking), - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. logic_review_test_() -> @@ -36,29 +30,35 @@ logic_review_test_() -> {"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}), - User = #user{id = UserId, email = <>, password_hash = <<"hash">>, - role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()}, + User = eh_test_support:make_user(#{id => UserId, email => <>}), mnesia:dirty_write(User), 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 => <>}), + Admin#admin.id. + create_test_calendar(OwnerId) -> {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), Calendar#calendar.id. create_test_event(CalendarId) -> - {ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60), + {ok, Event} = core_event:create(CalendarId, <<"Event">>, eh_test_support:future_start(), 60), Event#event.id. create_booking(UserId, EventId) -> - {ok, Booking} = core_booking:create(EventId, UserId), - core_booking:update_status(Booking#booking.id, confirmed), + {ok, Booking} = core_booking:create(EventId, UserId, pending), + Now = calendar:universal_time(), + {ok, _} = core_booking:update(Booking#booking.id, + [{status, confirmed}, {confirmed_at, Now}]), Booking#booking.id. test_create_event_review() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(ParticipantId, EventId), @@ -66,30 +66,29 @@ test_create_event_review() -> {ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 5, <<"Great!">>), ?assertEqual(5, Review#review.rating), - % Проверяем, что рейтинг события обновился {ok, Event} = core_event:get_by_id(EventId), ?assertEqual(5.0, Event#event.rating_avg), ?assertEqual(1, Event#event.rating_count). test_create_calendar_review() -> - OwnerId = create_test_user(user), - ReviewerId = create_test_user(user), + OwnerId = create_test_user(), + ReviewerId = create_test_user(), CalendarId = create_test_calendar(OwnerId), {ok, Review} = logic_review:create_review(ReviewerId, calendar, CalendarId, 4, <<"Nice">>), ?assertEqual(4, Review#review.rating). test_cannot_review_without_booking() -> - OwnerId = create_test_user(user), - UserId = create_test_user(user), + OwnerId = create_test_user(), + UserId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), {error, cannot_review} = logic_review:create_review(UserId, event, EventId, 5, <<"">>). test_cannot_review_twice() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(ParticipantId, EventId), @@ -98,8 +97,8 @@ test_cannot_review_twice() -> {error, already_reviewed} = logic_review:create_review(ParticipantId, event, EventId, 4, <<"">>). test_update_own_review() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(ParticipantId, EventId), @@ -109,9 +108,9 @@ test_update_own_review() -> ?assertEqual(5, Updated#review.rating). test_cannot_update_others() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), - OtherId = create_test_user(user), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), + OtherId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), 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}]). test_delete_own_review() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(ParticipantId, EventId), @@ -130,9 +129,9 @@ test_delete_own_review() -> {ok, deleted} = logic_review:delete_review(ParticipantId, Review#review.id). test_admin_hide_review() -> - OwnerId = create_test_user(user), - ParticipantId = create_test_user(user), - AdminId = create_test_user(admin), + OwnerId = create_test_user(), + ParticipantId = create_test_user(), + AdminId = create_test_admin(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(ParticipantId, EventId), @@ -142,9 +141,9 @@ test_admin_hide_review() -> ?assertEqual(hidden, Hidden#review.status). test_rating_updates_target() -> - OwnerId = create_test_user(user), - P1 = create_test_user(user), - P2 = create_test_user(user), + OwnerId = create_test_user(), + P1 = create_test_user(), + P2 = create_test_user(), CalendarId = create_test_calendar(OwnerId), EventId = create_test_event(CalendarId), create_booking(P1, EventId), @@ -158,4 +157,4 @@ test_rating_updates_target() -> {ok, _} = logic_review:create_review(P2, event, EventId, 3, <<"">>), {ok, Event2} = core_event:get_by_id(EventId), ?assertEqual(4.0, Event2#event.rating_avg), - ?assertEqual(2, Event2#event.rating_count). \ No newline at end of file + ?assertEqual(2, Event2#event.rating_count). diff --git a/test/unit/logic_search_tests.erl b/test/unit/logic_search_tests.erl index 59d6584..1a4a4a0 100644 --- a/test/unit/logic_search_tests.erl +++ b/test/unit/logic_search_tests.erl @@ -2,27 +2,16 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). +-define(TABLES, [user, calendar, event]). + setup() -> - mnesia:start(), - mnesia:create_table(user, [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(calendar, [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]), - mnesia:create_table(event, [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]), + eh_test_support:start_mnesia(), + eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> - mnesia:delete_table(event), - mnesia:delete_table(calendar), - mnesia:delete_table(user), - mnesia:stop(), + eh_test_support:delete_tables(?TABLES), + eh_test_support:stop_mnesia(), ok. 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), 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() -> OwnerId = create_test_user(user), 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, <<"JavaScript Conference">>, <<"JS for everyone">>, StartTime, [], undefined), create_test_event(CalendarId, <<"Yoga Class">>, <<"Morning yoga">>, StartTime, [], undefined), 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, 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). test_search_events_by_tags() -> OwnerId = create_test_user(user), 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 2">>, <<"">>, StartTime, [<<"javascript">>, <<"conference">>], undefined), create_test_event(CalendarId, <<"Event 3">>, <<"">>, StartTime, [<<"python">>, <<"advanced">>], undefined), - % Поиск по одному тегу - Params = #{tags => <<"python">>}, - {ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params), - ?assertEqual(2, Total), % Event 1 и Event 3 + {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"python">>})), + ?assertEqual(2, Total), - % Поиск по тегу, который есть только у одного события - Params2 = #{tags => <<"workshop">>}, - {ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), - ?assertEqual(1, Total2), % Только Event 1 + {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"workshop">>})), + ?assertEqual(1, Total2), - % Поиск по тегу, которого нет - Params3 = #{tags => <<"nonexistent">>}, - {ok, Total3, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params3), + {Total3, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"nonexistent">>})), ?assertEqual(0, Total3). test_search_events_by_date() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), - create_test_event(CalendarId, <<"June Event">>, <<"">>, {{2026, 6, 15}, {10, 0, 0}}, [], undefined), - create_test_event(CalendarId, <<"July Event">>, <<"">>, {{2026, 7, 15}, {10, 0, 0}}, [], undefined), - create_test_event(CalendarId, <<"August Event">>, <<"">>, {{2026, 8, 15}, {10, 0, 0}}, [], undefined), + Base = eh_test_support:future_start(), + create_test_event(CalendarId, <<"Near Event">>, <<"">>, Base, [], 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}}}, - {ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params), + Params = #{from => add_days(Base, -1), to => add_days(Base, 7)}, + {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, Params)), ?assertEqual(1, Total). test_search_events_by_location() -> OwnerId = create_test_user(user), 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}, SpbLoc = #location{address = <<"SPb">>, lat = 59.9343, lon = 30.3351}, create_test_event(CalendarId, <<"Moscow Event">>, <<"">>, StartTime, [], MoscowLoc), create_test_event(CalendarId, <<"SPb Event">>, <<"">>, StartTime, [], SpbLoc), - Params = #{lat => 55.7558, lon => 37.6173, radius => 10}, - {ok, Total, _Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params), + {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, + #{lat => 55.7558, lon => 37.6173, radius => 10})), ?assertEqual(1, Total), - Params2 = #{lat => 59.9343, lon => 30.3351, radius => 10}, - {ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), + {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, + #{lat => 59.9343, lon => 30.3351, radius => 10})), ?assertEqual(1, Total2). test_combined_search() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), - StartTime = {{2026, 6, 15}, {10, 0, 0}}, - create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [<<"python">>, <<"workshop">>], undefined), - create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, {{2026, 7, 15}, {10, 0, 0}}, [<<"python">>, <<"advanced">>], undefined), + Base = eh_test_support:future_start(), + create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, Base, + [<<"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">>}, - {ok, Total, _} = logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params), + Params = #{from => add_days(Base, -1), to => add_days(Base, 7), tags => <<"python">>}, + {Total, _} = events_from(logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params)), ?assertEqual(1, Total). test_search_calendars() -> @@ -165,60 +161,57 @@ test_search_calendars() -> create_test_calendar(OwnerId, personal, [<<"tech">>, <<"programming">>]), create_test_calendar(OwnerId, commercial, [<<"yoga">>, <<"wellness">>]), - Params = #{tags => <<"tech">>}, - {ok, Total, _} = logic_search:search(<<"calendar">>, undefined, OwnerId, Params), + {Total, _} = calendars_from(logic_search:search(<<"calendar">>, undefined, OwnerId, #{tags => <<"tech">>})), ?assertEqual(1, Total), - {ok, Total2, _} = logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{}), + {Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})), ?assertEqual(2, Total2). test_search_all() -> OwnerId = create_test_user(user), 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), {ok, Total, Results} = logic_search:search(undefined, <<"Test">>, OwnerId, #{}), ?assert(Total >= 2), - ?assert(maps:is_key(events, Results)), - ?assert(maps:is_key(calendars, Results)). + ?assert(maps:is_key(<<"events">>, Results)), + ?assert(maps:is_key(<<"calendars">>, Results)). test_pagination() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), - StartTime = {{2026, 6, 1}, {10, 0, 0}}, + StartTime = eh_test_support:future_start(), lists:foreach(fun(I) -> Title = iolist_to_binary(["Event ", integer_to_binary(I)]), create_test_event(CalendarId, Title, <<"">>, StartTime, [], undefined) end, [1, 2, 3, 4, 5]), - Params = #{limit => 2, offset => 0}, - {ok, Total, Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params), + {Total, Results} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 0})), ?assertEqual(5, Total), ?assertEqual(2, length(Results)), - Params2 = #{limit => 2, offset => 2}, - {ok, _, Results2} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), + {_, Results2} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 2})), ?assertEqual(2, length(Results2)), - Params3 = #{limit => 2, offset => 4}, - {ok, _, Results3} = logic_search:search(<<"event">>, undefined, OwnerId, Params3), + {_, Results3} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 4})), ?assertEqual(1, length(Results3)). test_sorting() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), - create_test_event(CalendarId, <<"Early">>, <<"">>, {{2026, 6, 1}, {10, 0, 0}}, [], undefined), - create_test_event(CalendarId, <<"Late">>, <<"">>, {{2026, 6, 30}, {10, 0, 0}}, [], undefined), + Base = eh_test_support:future_start(), + create_test_event(CalendarId, <<"Early">>, <<"">>, Base, [], undefined), + create_test_event(CalendarId, <<"Late">>, <<"">>, add_days(Base, 29), [], undefined), - Params = #{sort => <<"start_time">>, order => <<"asc">>}, - {ok, _, [First | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params), + {_, [First | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, + #{sort => <<"start_time">>, order => <<"asc">>})), ?assertMatch(#{title := <<"Early">>}, First), - Params2 = #{sort => <<"start_time">>, order => <<"desc">>}, - {ok, _, [First2 | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params2), + {_, [First2 | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, + #{sort => <<"start_time">>, order => <<"desc">>})), ?assertMatch(#{title := <<"Late">>}, First2). test_access_control() -> @@ -228,12 +221,11 @@ test_access_control() -> PersonalId = create_test_calendar(OwnerId, personal, []), 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(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined), - % Другой пользователь видит только коммерческие события - {ok, Total, Results} = logic_search:search(<<"event">>, undefined, OtherId, #{}), + {Total, Results} = events_from(logic_search:search(<<"event">>, undefined, OtherId, #{})), ?assertEqual(1, Total), [Event] = Results, ?assertMatch(#{title := <<"Public Event">>}, Event). @@ -241,9 +233,9 @@ test_access_control() -> test_empty_search() -> OwnerId = create_test_user(user), 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), - {ok, Total, Results} = logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{}), + {Total, Results} = events_from(logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{})), ?assertEqual(0, Total), - ?assertEqual([], Results). \ No newline at end of file + ?assertEqual([], Results). diff --git a/test/unit/ws_handler_tests.erl b/test/unit/ws_handler_tests.erl index 90354a1..95bcbed 100644 --- a/test/unit/ws_handler_tests.erl +++ b/test/unit/ws_handler_tests.erl @@ -25,7 +25,12 @@ ws_handler_test_() -> test_websocket_info_booking() -> 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}, case ws_handler:websocket_info(Msg, State) of @@ -58,4 +63,4 @@ test_websocket_info_event() -> Decoded = jsx:decode(Reply, [return_maps]), ?assertEqual(<<"event_update">>, maps:get(<<"type">>, Decoded)); _ -> ?assert(false, "Expected reply") - end. \ No newline at end of file + end.