mirror of
https://github.com/valitydev/bouncer.git
synced 2024-11-06 02:15:18 +00:00
361ed7c79b
* TD-431: Return timeout error upon negative timeout leftover after endpoint resolution in document request API * Adds testcase endpoint_resolve_timeout_means_unavailable with mocked resolve_endpoint/2 to bouncer_tests_SUITE
717 lines
21 KiB
Erlang
717 lines
21 KiB
Erlang
-module(bouncer_tests_SUITE).
|
|
|
|
-include_lib("stdlib/include/assert.hrl").
|
|
|
|
-export([all/0]).
|
|
-export([groups/0]).
|
|
-export([init_per_suite/1]).
|
|
-export([end_per_suite/1]).
|
|
-export([init_per_group/2]).
|
|
-export([end_per_group/2]).
|
|
-export([init_per_testcase/2]).
|
|
-export([end_per_testcase/2]).
|
|
|
|
-export([missing_ruleset_notfound/1]).
|
|
-export([incorrect_ruleset_invalid1/1]).
|
|
-export([incorrect_ruleset_invalid2/1]).
|
|
-export([incorrect_ruleset_invalid3/1]).
|
|
-export([missing_content_invalid_context/1]).
|
|
-export([junk_content_invalid_context/1]).
|
|
-export([conflicting_context_invalid/1]).
|
|
-export([distinct_sets_context_valid/1]).
|
|
|
|
-export([restricted_search_invoices_shop_manager/1]).
|
|
-export([forbidden_w_empty_context/1]).
|
|
-export([forbidden_expired/1]).
|
|
-export([forbidden_blacklisted_ip/1]).
|
|
|
|
-export([connect_failed_means_unavailable/1]).
|
|
-export([connect_timeout_means_unavailable/1]).
|
|
-export([request_timeout_means_unknown/1]).
|
|
-export([endpoint_resolve_timeout_means_unavailable/1]).
|
|
|
|
-behaviour(bouncer_arbiter_pulse).
|
|
|
|
-export([handle_beat/3]).
|
|
|
|
-include_lib("bouncer_proto/include/bouncer_decision_thrift.hrl").
|
|
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
|
|
|
|
-type config() :: ct_helper:config().
|
|
-type group_name() :: atom().
|
|
-type test_case_name() :: atom().
|
|
|
|
-define(CONFIG(Key, C), (element(2, lists:keyfind(Key, 1, C)))).
|
|
|
|
%%
|
|
|
|
-define(OPA_HOST, "opa").
|
|
-define(OPA_ENDPOINT_RESOLVE, {{resolve, dns, ?OPA_HOST, #{pick => random}}, 8181}).
|
|
-define(OPA_ENDPOINT, {?OPA_HOST, 8181}).
|
|
-define(API_RULESET_ID, "service/authz/api").
|
|
|
|
-spec all() -> [{group, group_name()}].
|
|
all() ->
|
|
[
|
|
{group, general},
|
|
{group, rules_authz_api},
|
|
{group, network_error_mapping}
|
|
].
|
|
|
|
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
|
|
groups() ->
|
|
[
|
|
{general, [parallel], [
|
|
missing_ruleset_notfound,
|
|
incorrect_ruleset_invalid1,
|
|
incorrect_ruleset_invalid2,
|
|
incorrect_ruleset_invalid3,
|
|
missing_content_invalid_context,
|
|
junk_content_invalid_context,
|
|
conflicting_context_invalid,
|
|
distinct_sets_context_valid
|
|
]},
|
|
{rules_authz_api, [parallel], [
|
|
restricted_search_invoices_shop_manager,
|
|
forbidden_expired,
|
|
forbidden_blacklisted_ip,
|
|
forbidden_w_empty_context
|
|
]},
|
|
{network_error_mapping, [], [
|
|
connect_failed_means_unavailable,
|
|
connect_timeout_means_unavailable,
|
|
request_timeout_means_unknown,
|
|
endpoint_resolve_timeout_means_unavailable
|
|
]}
|
|
].
|
|
|
|
-spec init_per_suite(config()) -> config().
|
|
init_per_suite(C) ->
|
|
Apps =
|
|
genlib_app:start_application(woody) ++
|
|
genlib_app:start_application_with(scoper, [
|
|
{storage, scoper_storage_logger}
|
|
]),
|
|
[{suite_apps, Apps} | C].
|
|
|
|
-spec end_per_suite(config()) -> ok.
|
|
end_per_suite(C) ->
|
|
genlib_app:stop_unload_applications(?CONFIG(suite_apps, C)).
|
|
|
|
-spec init_per_group(group_name(), config()) -> config().
|
|
init_per_group(Name, C) when Name == general; Name == rules_authz_api ->
|
|
start_bouncer([], [{groupname, Name} | C]);
|
|
init_per_group(Name, C) ->
|
|
[{groupname, Name} | C].
|
|
|
|
start_bouncer(Env, C) ->
|
|
IP = "127.0.0.1",
|
|
Port = 8022,
|
|
ArbiterPath = <<"/v1/arbiter">>,
|
|
{ok, StashPid} = ct_stash:start(),
|
|
Apps = genlib_app:start_application_with(
|
|
bouncer,
|
|
[
|
|
{ip, IP},
|
|
{port, Port},
|
|
{services, #{
|
|
arbiter => #{
|
|
path => ArbiterPath,
|
|
pulse => [{?MODULE, StashPid}]
|
|
}
|
|
}},
|
|
{transport_opts, #{
|
|
max_connections => 1000,
|
|
num_acceptors => 4
|
|
}},
|
|
{opa, #{
|
|
endpoint => ?OPA_ENDPOINT_RESOLVE,
|
|
request_timeout => 1000,
|
|
pool_opts => #{
|
|
connection_opts => #{
|
|
transport => tcp
|
|
}
|
|
}
|
|
}}
|
|
] ++ Env
|
|
),
|
|
Services = #{
|
|
arbiter => mk_url(IP, Port, ArbiterPath)
|
|
},
|
|
[{group_apps, Apps}, {service_urls, Services}, {stash, StashPid} | C].
|
|
|
|
mk_url(IP, Port, Path) ->
|
|
iolist_to_binary(["http://", IP, ":", genlib:to_binary(Port), Path]).
|
|
|
|
-spec end_per_group(group_name(), config()) -> _.
|
|
end_per_group(_Name, C) ->
|
|
stop_bouncer(C).
|
|
|
|
stop_bouncer(C) ->
|
|
ct_helper:with_config(
|
|
group_apps,
|
|
C,
|
|
fun(Apps) -> genlib_app:stop_unload_applications(Apps) end
|
|
),
|
|
ct_helper:with_config(
|
|
stash,
|
|
C,
|
|
fun(Pid) -> ?assertEqual(ok, ct_stash:destroy(Pid)) end
|
|
).
|
|
|
|
-spec init_per_testcase(atom(), config()) -> config().
|
|
init_per_testcase(endpoint_resolve_timeout_means_unavailable = Name, C) ->
|
|
meck:new(gunner_resolver, [no_link, passthrough]),
|
|
[{testcase, Name} | C];
|
|
init_per_testcase(Name, C) ->
|
|
[{testcase, Name} | C].
|
|
|
|
-spec end_per_testcase(atom(), config()) -> ok.
|
|
end_per_testcase(endpoint_resolve_timeout_means_unavailable, _C) ->
|
|
meck:unload(gunner_resolver),
|
|
ok;
|
|
end_per_testcase(_Name, _C) ->
|
|
ok.
|
|
|
|
%%
|
|
|
|
-define(CONTEXT(Fragments), #decision_Context{fragments = Fragments}).
|
|
-define(JUDGEMENT(Resolution), #decision_Judgement{resolution = Resolution}).
|
|
|
|
-spec missing_ruleset_notfound(config()) -> ok.
|
|
-spec incorrect_ruleset_invalid1(config()) -> ok.
|
|
-spec incorrect_ruleset_invalid2(config()) -> ok.
|
|
-spec incorrect_ruleset_invalid3(config()) -> ok.
|
|
-spec missing_content_invalid_context(config()) -> ok.
|
|
-spec junk_content_invalid_context(config()) -> ok.
|
|
-spec conflicting_context_invalid(config()) -> ok.
|
|
-spec distinct_sets_context_valid(config()) -> ok.
|
|
|
|
missing_ruleset_notfound(C) ->
|
|
Client = mk_client(C),
|
|
MissingRulesetID = "missing_ruleset",
|
|
?assertThrow(
|
|
#decision_RulesetNotFound{},
|
|
call_judge(MissingRulesetID, ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {failed, ruleset_notfound}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
incorrect_ruleset_invalid1(C) ->
|
|
Client = mk_client(C),
|
|
?assertThrow(
|
|
#decision_InvalidRuleset{},
|
|
call_judge("trivial/incorrect1", ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement,
|
|
{failed,
|
|
{ruleset_invalid, [
|
|
{data_invalid, _, wrong_size, _, [<<"resolution">>]}
|
|
]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
incorrect_ruleset_invalid2(C) ->
|
|
Client = mk_client(C),
|
|
?assertThrow(
|
|
#decision_InvalidRuleset{},
|
|
call_judge("trivial/incorrect2", ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement,
|
|
{failed,
|
|
{ruleset_invalid, [
|
|
{data_invalid, _, wrong_type, _, [<<"resolution">>, _]}
|
|
]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
incorrect_ruleset_invalid3(C) ->
|
|
Client = mk_client(C),
|
|
?assertThrow(
|
|
#decision_InvalidRuleset{},
|
|
call_judge("trivial/incorrect3", ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement,
|
|
{failed,
|
|
{ruleset_invalid, [
|
|
{data_invalid, _, no_extra_items_allowed, [<<"forbidden">>, [#{}], #{}], _}
|
|
]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
missing_content_invalid_context(C) ->
|
|
Client = mk_client(C),
|
|
NoContentFragment = #ctx_ContextFragment{type = v1_thrift_binary},
|
|
Context = ?CONTEXT(#{<<"missing">> => NoContentFragment}),
|
|
?assertThrow(
|
|
#decision_InvalidContext{},
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement,
|
|
{failed,
|
|
{malformed_context, #{
|
|
<<"missing">> := {v1_thrift_binary, {unexpected, _, _, _}}
|
|
}}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
junk_content_invalid_context(C) ->
|
|
Client = mk_client(C),
|
|
Junk = <<"STOP RIGHT THERE YOU CRIMINAL SCUM!">>,
|
|
JunkFragment = #ctx_ContextFragment{type = v1_thrift_binary, content = Junk},
|
|
Context = ?CONTEXT(#{<<"missing">> => JunkFragment}),
|
|
?assertThrow(
|
|
#decision_InvalidContext{},
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement,
|
|
{failed,
|
|
{malformed_context, #{
|
|
<<"missing">> := {v1_thrift_binary, {unexpected, _, _, _}}
|
|
}}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
conflicting_context_invalid(C) ->
|
|
Client = mk_client(C),
|
|
Fragment1 = #{
|
|
user => #{
|
|
id => <<"joeblow">>,
|
|
email => Email1 = <<"deadinside69@example.org">>
|
|
},
|
|
requester => #{
|
|
ip => <<"1.2.3.4">>
|
|
}
|
|
},
|
|
Fragment2 = #{
|
|
user => #{
|
|
id => <<"joeblow">>,
|
|
email => <<"deadinside420@example.org">>
|
|
},
|
|
requester => #{
|
|
ip => <<"1.2.3.4">>
|
|
}
|
|
},
|
|
Context = ?CONTEXT(#{
|
|
<<"frag1">> => mk_ctx_v1_fragment(Fragment1),
|
|
<<"frag2">> => mk_ctx_v1_fragment(Fragment2)
|
|
}),
|
|
?assertThrow(
|
|
#decision_InvalidContext{},
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertEqual(
|
|
{judgement,
|
|
{failed,
|
|
{conflicting_context, #{
|
|
<<"frag2">> => #{user => #{email => Email1}}
|
|
}}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
distinct_sets_context_valid(C) ->
|
|
Client = mk_client(C),
|
|
Fragment1 = #{
|
|
user => #{
|
|
id => <<"joeblow">>,
|
|
orgs => mk_ordset([
|
|
#{
|
|
id => <<"hoolie">>,
|
|
roles => mk_ordset([#{id => <<"Administrator">>}])
|
|
},
|
|
#{
|
|
id => <<"weewrok">>,
|
|
roles => mk_ordset([#{id => <<"Administrator">>}])
|
|
}
|
|
])
|
|
}
|
|
},
|
|
Fragment2 = #{
|
|
user => #{
|
|
id => <<"joeblow">>,
|
|
orgs => mk_ordset([
|
|
#{
|
|
id => <<"hoolie">>,
|
|
roles => mk_ordset([#{id => <<"Nobody">>}])
|
|
},
|
|
#{
|
|
id => <<"blooply">>,
|
|
roles => mk_ordset([#{id => <<"Nobody">>}])
|
|
}
|
|
])
|
|
}
|
|
},
|
|
Context = ?CONTEXT(#{
|
|
<<"frag1">> => mk_ctx_v1_fragment(Fragment1),
|
|
<<"frag2">> => mk_ctx_v1_fragment(Fragment2)
|
|
}),
|
|
?assertMatch(
|
|
#decision_Judgement{},
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, _}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
%%
|
|
|
|
-spec restricted_search_invoices_shop_manager(config()) -> ok.
|
|
-spec forbidden_expired(config()) -> ok.
|
|
-spec forbidden_blacklisted_ip(config()) -> ok.
|
|
-spec forbidden_w_empty_context(config()) -> ok.
|
|
|
|
restricted_search_invoices_shop_manager(C) ->
|
|
Client = mk_client(C),
|
|
Fragment = lists:foldl(fun maps:merge/2, #{}, [
|
|
mk_auth_session_token(),
|
|
mk_env(),
|
|
mk_op_search_invoices(<<"SHOP">>, <<"PARTY">>),
|
|
mk_user(
|
|
<<"USER">>,
|
|
mk_ordset([
|
|
mk_user_org(
|
|
<<"PARTY">>,
|
|
<<"OWNER">>,
|
|
mk_ordset([
|
|
mk_role(<<"Manager">>, <<"SHOP">>)
|
|
])
|
|
)
|
|
])
|
|
)
|
|
]),
|
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
|
?assertMatch(
|
|
?JUDGEMENT({restricted, #decision_ResolutionRestricted{}}),
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, {{restricted, _}, [{<<"org_role_allows_operation">>, _}]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
forbidden_expired(C) ->
|
|
Client = mk_client(C),
|
|
% Would be funny if this fails on some system too deep in the past.
|
|
Fragment = maps:merge(mk_env(), #{
|
|
auth => #{
|
|
method => <<"AccessToken">>,
|
|
% ☭😢
|
|
expiration => <<"1991-12-26T17:00:00Z">>
|
|
}
|
|
}),
|
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
|
?assertMatch(
|
|
?JUDGEMENT({forbidden, #decision_ResolutionForbidden{}}),
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, {forbidden, [{<<"auth_expired">>, _}]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
forbidden_blacklisted_ip(C) ->
|
|
Client = mk_client(C),
|
|
Fragment = lists:foldl(fun maps:merge/2, #{}, [
|
|
mk_auth_session_token(),
|
|
mk_env(),
|
|
% See test/policies/authz/blacklists/source-ip-range/data.json#L42
|
|
#{requester => #{ip => <<"91.41.147.55">>}}
|
|
]),
|
|
Context = ?CONTEXT(#{<<"root">> => mk_ctx_v1_fragment(Fragment)}),
|
|
?assertMatch(
|
|
?JUDGEMENT({forbidden, #decision_ResolutionForbidden{}}),
|
|
call_judge(?API_RULESET_ID, Context, Client)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, {forbidden, [{<<"ip_range_blacklisted">>, _}]}}},
|
|
lists:last(flush_beats(Client, C))
|
|
).
|
|
|
|
forbidden_w_empty_context(C) ->
|
|
Client1 = mk_client(C),
|
|
EmptyFragment = mk_ctx_v1_fragment(#{}),
|
|
?assertMatch(
|
|
?JUDGEMENT({forbidden, #decision_ResolutionForbidden{}}),
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client1)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, {forbidden, [{<<"auth_required">>, _}]}}},
|
|
lists:last(flush_beats(Client1, C))
|
|
),
|
|
Client2 = mk_client(C),
|
|
?assertMatch(
|
|
?JUDGEMENT({forbidden, #decision_ResolutionForbidden{}}),
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{<<"empty">> => EmptyFragment}), Client2)
|
|
),
|
|
?assertMatch(
|
|
{judgement, {completed, {forbidden, [{<<"auth_required">>, _}]}}},
|
|
lists:last(flush_beats(Client2, C))
|
|
).
|
|
|
|
mk_user(UserID, UserOrgs) ->
|
|
#{
|
|
user => #{
|
|
id => UserID,
|
|
orgs => UserOrgs
|
|
}
|
|
}.
|
|
|
|
mk_user_org(OrgID, OwnerID, Roles) ->
|
|
#{
|
|
id => OrgID,
|
|
owner => #{id => OwnerID},
|
|
roles => Roles
|
|
}.
|
|
|
|
mk_role(RoleID, ShopID) ->
|
|
#{id => RoleID, scope => #{shop => #{id => ShopID}}}.
|
|
|
|
mk_auth_session_token() ->
|
|
mk_auth_session_token(erlang:system_time(second) + 3600).
|
|
|
|
mk_auth_session_token(ExpiresAt) ->
|
|
#{
|
|
auth => #{
|
|
method => <<"SessionToken">>,
|
|
expiration => format_ts(ExpiresAt, second)
|
|
}
|
|
}.
|
|
|
|
mk_op_search_invoices(ShopID, PartyID) ->
|
|
#{
|
|
anapi => #{
|
|
op => #{
|
|
id => <<"SearchInvoices">>,
|
|
shop => #{id => ShopID},
|
|
party => #{id => PartyID}
|
|
}
|
|
}
|
|
}.
|
|
|
|
mk_env() ->
|
|
#{
|
|
env => #{
|
|
now => format_now()
|
|
}
|
|
}.
|
|
|
|
format_now() ->
|
|
USec = erlang:system_time(second),
|
|
format_ts(USec, second).
|
|
|
|
format_ts(Ts, Unit) ->
|
|
Str = calendar:system_time_to_rfc3339(Ts, [{unit, Unit}, {offset, "Z"}]),
|
|
erlang:list_to_binary(Str).
|
|
|
|
%%
|
|
|
|
-spec connect_failed_means_unavailable(config()) -> ok.
|
|
-spec connect_timeout_means_unavailable(config()) -> ok.
|
|
-spec request_timeout_means_unknown(config()) -> ok.
|
|
-spec endpoint_resolve_timeout_means_unavailable(config()) -> ok.
|
|
|
|
connect_failed_means_unavailable(C) ->
|
|
C1 = start_bouncer(
|
|
[
|
|
{opa, #{
|
|
endpoint => {?OPA_HOST, 65535},
|
|
pool_opts => #{
|
|
connection_opts => #{
|
|
transport => tcp,
|
|
event_handler => {ct_gun_event_h, []}
|
|
}
|
|
}
|
|
}}
|
|
],
|
|
C
|
|
),
|
|
Client = mk_client(C1),
|
|
try
|
|
?assertError(
|
|
{woody_error, {external, resource_unavailable, _}},
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
[
|
|
{judgement, started},
|
|
{judgement, {failed, {unavailable, {connection_failed, {shutdown, econnrefused}}}}}
|
|
],
|
|
flush_beats(Client, C1)
|
|
)
|
|
after
|
|
stop_bouncer(C1)
|
|
end.
|
|
|
|
connect_timeout_means_unavailable(C) ->
|
|
{ok, Proxy} = ct_proxy:start_link(?OPA_ENDPOINT, #{listen => ignore}),
|
|
C1 = start_proxy_bouncer(Proxy, C),
|
|
Client = mk_client(C1),
|
|
try
|
|
?assertError(
|
|
% NOTE
|
|
% Turns out it's somewhat hard to simulate connection timeout when connecting to the
|
|
% localhost. This is why we expect `result_unknown` here instead of
|
|
% `resource_unavailable`.
|
|
{woody_error, {external, result_unknown, _}},
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
[
|
|
{judgement, started},
|
|
{judgement, {failed, {unknown, timeout}}}
|
|
],
|
|
flush_beats(Client, C1)
|
|
)
|
|
after
|
|
stop_bouncer(C1)
|
|
end.
|
|
|
|
request_timeout_means_unknown(C) ->
|
|
{ok, Proxy} = ct_proxy:start_link(?OPA_ENDPOINT),
|
|
C1 = start_proxy_bouncer(Proxy, C),
|
|
Client = mk_client(C1),
|
|
ok = change_proxy_mode(Proxy, connection, ignore, C1),
|
|
try
|
|
?assertError(
|
|
{woody_error, {external, result_unknown, _}},
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
[
|
|
{judgement, started},
|
|
{judgement, {failed, {unknown, timeout}}}
|
|
],
|
|
flush_beats(Client, C1)
|
|
)
|
|
after
|
|
stop_bouncer(C1)
|
|
end.
|
|
|
|
endpoint_resolve_timeout_means_unavailable(C) ->
|
|
ok = meck:expect(gunner_resolver, resolve_endpoint, fun(Endpoint, Opts) ->
|
|
Timeout = maps:get(timeout, Opts),
|
|
timer:sleep(Timeout + 10),
|
|
meck:passthrough([Endpoint, Opts])
|
|
end),
|
|
%% Don't need actual OPA
|
|
C1 = start_bouncer(
|
|
[
|
|
{opa, #{
|
|
%% But we need an endpoint resolution try
|
|
endpoint => ?OPA_ENDPOINT_RESOLVE,
|
|
pool_opts => #{
|
|
connection_opts => #{
|
|
transport => tcp,
|
|
event_handler => {ct_gun_event_h, []}
|
|
}
|
|
}
|
|
}}
|
|
],
|
|
C
|
|
),
|
|
Client = mk_client(C1),
|
|
try
|
|
?assertError(
|
|
{woody_error, {external, resource_unavailable, <<"timeout">>}},
|
|
call_judge(?API_RULESET_ID, ?CONTEXT(#{}), Client)
|
|
),
|
|
?assertMatch(
|
|
[
|
|
{judgement, started},
|
|
{judgement, {failed, {unavailable, timeout}}}
|
|
],
|
|
flush_beats(Client, C1)
|
|
)
|
|
after
|
|
stop_bouncer(C1)
|
|
end.
|
|
|
|
start_proxy_bouncer(Proxy, C) ->
|
|
start_bouncer(
|
|
[
|
|
{opa, #{
|
|
endpoint => ct_proxy:endpoint(Proxy),
|
|
pool_opts => #{
|
|
connection_opts => #{
|
|
transport => tcp,
|
|
event_handler => {ct_gun_event_h, []}
|
|
}
|
|
}
|
|
}}
|
|
],
|
|
C
|
|
).
|
|
|
|
change_proxy_mode(Proxy, Scope, Mode, C) ->
|
|
ModeWas = ct_proxy:mode(Proxy, Scope, Mode),
|
|
_ = ct:pal(
|
|
debug,
|
|
"[~p] set proxy ~p from '~p' to '~p'",
|
|
[?CONFIG(testcase, C), Scope, ModeWas, Mode]
|
|
),
|
|
ok.
|
|
|
|
%%
|
|
|
|
mk_ordset(L) ->
|
|
ordsets:from_list(L).
|
|
|
|
mk_ctx_v1_fragment(Context) ->
|
|
{ok, Content} = bouncer_context_v1:encode(thrift, Context),
|
|
#ctx_ContextFragment{type = v1_thrift_binary, content = Content}.
|
|
|
|
%%
|
|
|
|
mk_client(C) ->
|
|
WoodyCtx = woody_context:new(genlib:to_binary(?CONFIG(testcase, C))),
|
|
ServiceURLs = ?CONFIG(service_urls, C),
|
|
{WoodyCtx, ServiceURLs}.
|
|
|
|
call_judge(RulesetID, Context, Client) ->
|
|
call(arbiter, 'Judge', {genlib:to_binary(RulesetID), Context}, Client).
|
|
|
|
call(ServiceName, Fn, Args, {WoodyCtx, ServiceURLs}) ->
|
|
Service = get_service_spec(ServiceName),
|
|
Opts = #{
|
|
url => maps:get(ServiceName, ServiceURLs),
|
|
event_handler => scoper_woody_event_handler
|
|
},
|
|
case woody_client:call({Service, Fn, Args}, Opts, WoodyCtx) of
|
|
{ok, Response} ->
|
|
Response;
|
|
{exception, Exception} ->
|
|
throw(Exception)
|
|
end.
|
|
|
|
get_service_spec(arbiter) ->
|
|
{bouncer_decision_thrift, 'Arbiter'}.
|
|
|
|
%%
|
|
|
|
-spec handle_beat(bouncer_arbiter_pulse:beat(), bouncer_arbiter_pulse:metadata(), pid()) -> ok.
|
|
handle_beat(Beat, Metadata, StashPid) ->
|
|
_ = stash_beat(Beat, Metadata, StashPid),
|
|
ct:pal("~p [arbiter] ~0p:~nmetadata=~p", [self(), Beat, Metadata]).
|
|
|
|
%%
|
|
|
|
stash_beat(Beat, Metadata = #{woody_ctx := WoodyCtx}, StashPid) ->
|
|
ct_stash:append(StashPid, get_trace_id(WoodyCtx), {Beat, Metadata}).
|
|
|
|
flush_beats({WoodyCtx, _}, C) ->
|
|
StashPid = ?CONFIG(stash, C),
|
|
{ok, Entries} = ct_stash:flush(StashPid, get_trace_id(WoodyCtx)),
|
|
[Beat || {Beat, _Metadata} <- Entries].
|
|
|
|
get_trace_id(WoodyCtx) ->
|
|
RpcID = woody_context:get_rpc_id(WoodyCtx),
|
|
maps:get(trace_id, RpcID).
|