Rewrite and expand EUnit suite; run unit tests in CI from shared test image. Refs EventHub/EventHubBack#36 [skip ci]
This commit is contained in:
@@ -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}}.
|
||||
Reference in New Issue
Block a user