384 lines
13 KiB
Erlang
384 lines
13 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% 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_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}}.
|