Files
fluxer/fluxer_gateway/test/session_connection_guild_tests.erl
T

402 lines
13 KiB
Erlang

%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(session_connection_guild_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
do_guild_connect_skips_session_connect_when_cached_unavailable_test() ->
GuildId = 2002,
Attempt = 3,
Parent = self(),
TestRef = make_ref(),
GuildPid = spawn(fun() -> guild_stub_loop(Parent, TestRef) end),
ManagerPid = spawn(fun() -> manager_stub_loop(GuildId, GuildPid) end),
true = register(guild_manager, ManagerPid),
Features = [<<"UNAVAILABLE_FOR_EVERYONE">>],
GuildData = #{<<"guild">> => #{<<"features">> => Features}},
CacheState = #{id => GuildId, data => GuildData},
_ = guild_availability:update_unavailability_cache_for_state(CacheState),
try
ok = session_connection_guild:do_guild_connect(
connect_context(Parent, GuildId, Attempt)
),
{ok, Resp} = await_unavailable_result(GuildId, Attempt),
?assertEqual(true, maps:get(<<"unavailable">>, Resp)),
?assertNot(saw_stub_call(TestRef, 200))
after
safe_unregister(guild_manager, ManagerPid),
ManagerPid ! stop,
GuildPid ! stop,
CleanupState = #{id => GuildId, data => #{<<"guild">> => #{<<"features">> => []}}},
guild_availability:update_unavailability_cache_for_state(CleanupState)
end.
do_guild_connect_uses_session_connect_async_cast_test() ->
GuildId = 2004,
Parent = self(),
TestRef = make_ref(),
GuildPid = spawn(fun() -> guild_stub_loop(Parent, TestRef) end),
ManagerPid = spawn(fun() -> manager_stub_loop(GuildId, GuildPid) end),
true = register(guild_manager, ManagerPid),
SessionPid = spawn(fun capture_loop/0),
try
ok = session_connection_guild:do_guild_connect(connect_context(SessionPid, GuildId, 0)),
?assertMatch(
{session_connect_async, _},
await_stub_cast(TestRef, 1000)
)
after
SessionPid ! stop,
safe_unregister(guild_manager, ManagerPid),
ManagerPid ! stop,
GuildPid ! stop
end.
do_guild_connect_start_or_lookup_after_lookup_miss_test() ->
GuildId = 2005,
Parent = self(),
TestRef = make_ref(),
GuildPid = spawn(fun() -> guild_stub_loop(Parent, TestRef) end),
ManagerPid = spawn(fun() -> manager_lookup_miss_stub_loop(GuildId, GuildPid) end),
true = register(guild_manager, ManagerPid),
SessionPid = spawn(fun capture_loop/0),
try
ok = session_connection_guild:do_guild_connect(connect_context(SessionPid, GuildId, 0)),
?assertMatch(
{session_connect_async, _},
await_stub_cast(TestRef, 1000)
)
after
SessionPid ! stop,
safe_unregister(guild_manager, ManagerPid),
ManagerPid ! stop,
GuildPid ! stop
end.
guild_connect_timeout_exhaustion_marks_unavailable_test() ->
GuildId = 9001,
Attempt = 25,
State0 = exhausted_connect_state(<<"st1">>, GuildId, Attempt),
{noreply, State1} = session_connection_guild:handle_guild_connect_timeout(
GuildId, Attempt, State0
),
[E] = maps:get(collected_guild_states, State1, []),
?assertEqual(true, maps:get(<<"unavailable">>, E)),
?assertEqual(unavailable, maps:get(GuildId, maps:get(guilds, State1))),
?assertNot(maps:is_key(GuildId, maps:get(guild_connect_inflight, State1, #{}))).
session_connect_failed_exhaustion_marks_unavailable_test() ->
GuildId = 9002,
Attempt = 25,
State0 = (exhausted_connect_state(<<"st2">>, GuildId, undefined))#{
presence_pid => undefined
},
{noreply, State1} = session_connection_guild:handle_result_internal(
GuildId,
Attempt,
{error, {session_connect_failed, worker_crashed}},
State0
),
[E] = maps:get(collected_guild_states, State1, []),
?assertEqual(true, maps:get(<<"unavailable">>, E)),
?assertEqual(unavailable, maps:get(GuildId, maps:get(guilds, State1))).
guild_connect_worker_down_exhaustion_clears_inflight_and_marks_unavailable_test() ->
GuildId = 9003,
Attempt = 25,
WorkerRef = make_ref(),
State0 = (exhausted_connect_state(<<"st3">>, GuildId, Attempt))#{
guild_connect_workers => #{WorkerRef => {GuildId, Attempt, self()}},
presence_pid => undefined
},
{guild_connect_worker, {noreply, State1}} =
session_connection_guild:handle_guild_connect_worker_down(WorkerRef, killed, State0),
?assertEqual(#{}, maps:get(guild_connect_inflight, State1)),
?assertEqual(#{}, maps:get(guild_connect_workers, State1)),
?assertEqual(unavailable, maps:get(GuildId, maps:get(guilds, State1))).
guild_connect_worker_normal_down_only_cleans_worker_monitor_test() ->
GuildId = 9004,
Attempt = 3,
WorkerRef = make_ref(),
State0 = (exhausted_connect_state(<<"st4">>, GuildId, Attempt))#{
guild_connect_workers => #{WorkerRef => {GuildId, Attempt, self()}}
},
{guild_connect_worker, {noreply, State1}} =
session_connection_guild:handle_guild_connect_worker_down(WorkerRef, normal, State0),
?assertEqual(#{GuildId => Attempt}, maps:get(guild_connect_inflight, State1)),
?assertEqual(#{}, maps:get(guild_connect_workers, State1)).
finalize_guild_connection_propagates_ready_stop_test() ->
GuildId = 4242,
GuildPid = spawn(fun wait_for_stop/0),
try
{stop, normal, S} = session_connection_guild:finalize_guild_connection(
GuildId,
GuildPid,
#{guilds => #{}},
fun(St) -> {stop, normal, St#{stopped => true}} end
),
?assert(maps:get(stopped, S))
after
GuildPid ! stop
end.
repair_stalled_guild_connects_requeues_unresolved_guilds_test() ->
State0 = #{
id => <<"repair1">>,
user_id => 100,
guilds => #{
1 => undefined,
2 => unavailable,
3 => cached_unavailable,
4 => {self(), make_ref()}
},
guild_connect_inflight => #{2 => 1}
},
State1 = session_connection_guild:repair_stalled_guild_connects(State0),
?assert(is_integer(maps:get(guild_connect_last_repair_at, State1))),
?assertEqual([{guild_connect, 1, 0}], collect_guild_connect_messages([])).
repair_stalled_guild_connects_throttles_repair_test() ->
Now = erlang:system_time(millisecond),
State0 = #{
id => <<"repair2">>,
user_id => 100,
guilds => #{1 => undefined},
guild_connect_last_repair_at => Now
},
State1 = session_connection_guild:repair_stalled_guild_connects(State0),
?assertEqual(Now, maps:get(guild_connect_last_repair_at, State1)),
?assertEqual([], collect_guild_connect_messages([])).
repair_stalled_guild_connects_limits_batch_test() ->
Guilds = maps:from_list([{Id, undefined} || Id <- lists:seq(1, 12)]),
State0 = #{
id => <<"repair3">>,
user_id => 100,
guilds => Guilds,
guild_connect_last_repair_at => 0
},
_State1 = session_connection_guild:repair_stalled_guild_connects(State0),
Expected = [{guild_connect, Id, 0} || Id <- lists:seq(1, 8)],
?assertEqual(Expected, collect_guild_connect_messages([])).
not_member_below_cap_schedules_retry_test() ->
GuildId = 9201,
Attempt = 0,
State0 = #{
id => <<"nm1">>,
user_id => 100,
guilds => #{GuildId => undefined},
ready => undefined
},
{noreply, State1} = session_connection_guild:handle_result_internal(
GuildId, Attempt, {error, not_member}, State0
),
?assert(maps:is_key(GuildId, maps:get(guilds, State1))),
receive
{guild_connect, GuildId, 1} -> ok
after 5000 ->
?assert(false, not_member_retry_not_scheduled)
end.
not_member_exhaustion_removes_guild_without_unavailable_marker_test() ->
GuildId = 9202,
OtherGuildId = 5550,
Attempt = 3,
State0 = (base_dispatch_state())#{
guilds => #{
GuildId => {self(), make_ref()},
OtherGuildId => {self(), make_ref()}
},
guild_subscription_state => #{GuildId => #{}},
active_guilds => sets:from_list([GuildId]),
collected_guild_states => [],
ready => undefined
},
{noreply, State1} = session_connection_guild:handle_result_internal(
GuildId, Attempt, {error, not_member}, State0
),
?assertNot(maps:is_key(GuildId, maps:get(guilds, State1))),
?assertEqual([], maps:get(collected_guild_states, State1)),
?assert(maps:is_key(OtherGuildId, maps:get(guilds, State1))),
?assertNot(maps:is_key(GuildId, maps:get(guild_subscription_state, State1))),
?assertNot(sets:is_element(GuildId, maps:get(active_guilds, State1))),
?assertEqual([], collect_guild_connect_messages([])).
not_member_removal_unblocks_initial_ready_test() ->
GuildId = 9203,
Attempt = 3,
State0 = (base_dispatch_state())#{
guilds => #{GuildId => undefined},
collected_guild_states => [],
ready => #{<<"guilds">> => []},
presence_pid => self(),
socket_pid => undefined
},
?assertMatch(
{stop, normal, _},
session_connection_guild:handle_result_internal(
GuildId, Attempt, {error, not_member}, State0
)
).
base_dispatch_state() ->
#{
id => <<"nm">>,
user_id => 100,
seq => 0,
buffer => [],
buffer_bytes => 0,
socket_pid => undefined,
channels => #{},
relationships => #{},
suppress_presence_updates => false,
pending_presences => [],
presence_pid => undefined,
ignored_events => #{},
debounce_reactions => false,
reaction_buffer => [],
reaction_buffer_timer => undefined
}.
connect_context(SessionPid, GuildId, Attempt) ->
#{
session_pid => SessionPid,
guild_id => GuildId,
attempt => Attempt,
session_id => <<"sa1">>,
user_id => 77,
bot => false,
is_staff => false,
initial_guild_id => undefined,
peer_ip => undefined,
user_data => #{<<"flags">> => <<"0">>}
}.
exhausted_connect_state(SessionId, GuildId, Attempt) ->
#{
id => SessionId,
user_id => 100,
guilds => #{GuildId => undefined},
guild_connect_inflight => maybe_inflight(GuildId, Attempt),
collected_guild_states => [],
ready => #{<<"guilds">> => []}
}.
maybe_inflight(_GuildId, undefined) ->
#{};
maybe_inflight(GuildId, Attempt) ->
#{GuildId => Attempt}.
wait_for_stop() ->
receive
stop -> ok
after 30000 -> ok
end.
capture_loop() ->
receive
stop -> ok;
_ -> capture_loop()
after 30000 -> ok
end.
await_stub_cast(Ref, T) ->
receive
{guild_stub_cast, Ref, M} -> M;
_ -> await_stub_cast(Ref, T)
after T -> timeout
end.
await_unavailable_result(G, A) ->
receive
{guild_connect_result, G, A, {ok_cached_unavailable, R}} -> {ok, R};
_ -> await_unavailable_result(G, A)
after 1000 -> timeout
end.
saw_stub_call(Ref, T) ->
receive
{guild_stub_called, Ref, _} -> true;
_ -> saw_stub_call(Ref, T)
after T -> false
end.
safe_unregister(N, P) ->
case whereis(N) of
P -> unregister(N);
_ -> ok
end,
ok.
manager_stub_loop(G, P) ->
receive
stop ->
ok;
{'$gen_call', F, {with_timeout, Request, _Timeout}} ->
reply_manager_stub_request(G, P, F, Request),
manager_stub_loop(G, P);
{'$gen_call', F, Request} ->
reply_manager_stub_request(G, P, F, Request),
manager_stub_loop(G, P);
_ ->
manager_stub_loop(G, P)
after 30000 -> ok
end.
reply_manager_stub_request(G, P, F, {lookup, G}) ->
gen_server:reply(F, {ok, P});
reply_manager_stub_request(G, P, F, {start_or_lookup, G}) ->
gen_server:reply(F, {ok, P});
reply_manager_stub_request(_G, _P, F, _Request) ->
gen_server:reply(F, {error, unsupported}).
manager_lookup_miss_stub_loop(G, P) ->
receive
stop ->
ok;
{'$gen_call', F, {with_timeout, Request, _Timeout}} ->
reply_lookup_miss_stub_request(G, P, F, Request),
manager_lookup_miss_stub_loop(G, P);
{'$gen_call', F, Request} ->
reply_lookup_miss_stub_request(G, P, F, Request),
manager_lookup_miss_stub_loop(G, P);
_ ->
manager_lookup_miss_stub_loop(G, P)
after 30000 -> ok
end.
reply_lookup_miss_stub_request(G, _P, F, {lookup, G}) ->
gen_server:reply(F, {error, not_found});
reply_lookup_miss_stub_request(G, P, F, {start_or_lookup, G}) ->
gen_server:reply(F, {ok, P});
reply_lookup_miss_stub_request(_G, _P, F, _Request) ->
gen_server:reply(F, {error, unsupported}).
collect_guild_connect_messages(Acc) ->
receive
{guild_connect, _GuildId, _Attempt} = Msg ->
collect_guild_connect_messages([Msg | Acc])
after 0 ->
lists:reverse(Acc)
end.
guild_stub_loop(Parent, Ref) ->
receive
stop ->
ok;
{'$gen_cast', M} ->
Parent ! {guild_stub_cast, Ref, M},
guild_stub_loop(Parent, Ref);
{'$gen_call', F, R} ->
Parent ! {guild_stub_called, Ref, R},
gen_server:reply(F, {ok, #{}}),
guild_stub_loop(Parent, Ref);
_ ->
guild_stub_loop(Parent, Ref)
after 30000 -> ok
end.