Files
EventHubBack/test/unit/eh_test_support.erl
aleksey 59220b955e
CI / test (push) Failing after 6m39s
CI / deploy-ift (push) Has been skipped
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped
feat: calendar follow API (POST/DELETE follow, GET following). Refs EventHub/EventHubFront#9
2026-07-20 22:10:48 +03:00

386 lines
13 KiB
Erlang
Executable File

%%%-------------------------------------------------------------------
%%% 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),
maybe_add_admin_stats_indexes(Tables),
ok.
maybe_add_admin_stats_indexes(Tables) ->
case lists:member(report, Tables) of
true -> add_index(report, resolved_by);
false -> ok
end,
case lists:member(ticket, Tables) of
true -> add_index(ticket, assigned_to);
false -> ok
end.
add_index(Table, Attr) ->
case catch mnesia:add_table_index(Table, Attr) of
{atomic, ok} -> ok;
{aborted, {already_exists, _}} -> ok;
{aborted, {already_exists, _, _}} -> ok;
_ -> ok
end.
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_follow) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_follow)}];
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(review_vote) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, review_vote)}];
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(automod_settings) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, automod_settings)}];
table_opts(automod_hit) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, automod_hit)}];
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_counter) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, stats_counter)}];
table_opts(stats_daily) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, stats_daily)}];
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}}.