mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
465 lines
15 KiB
Erlang
465 lines
15 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(call_tests).
|
|
-typing([eqwalizer]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
integer_list_to_binaries_test() ->
|
|
?assertEqual([<<"1">>, <<"2">>, <<"3">>], call_state:integer_list_to_binaries([1, 2, 3])),
|
|
?assertEqual([], call_state:integer_list_to_binaries([])).
|
|
|
|
find_session_by_pid_test() ->
|
|
Pid1 = self(),
|
|
Sessions = #{
|
|
<<"session1">> => {100, Pid1, make_ref()},
|
|
<<"session2">> => {200, spawn(fun() -> ok end), make_ref()}
|
|
},
|
|
?assertMatch({ok, <<"session1">>, 100}, call_state:find_session_by_pid(Pid1, Sessions)),
|
|
?assertEqual(not_found, call_state:find_session_by_pid(spawn(fun() -> ok end), Sessions)).
|
|
|
|
format_voice_state_test() ->
|
|
VoiceState = #{
|
|
<<"user_id">> => 123,
|
|
<<"channel_id">> => 456,
|
|
<<"guild_id">> => 789,
|
|
<<"mute">> => false
|
|
},
|
|
Result = call_state:format_voice_state(VoiceState),
|
|
?assertEqual(<<"123">>, maps:get(<<"user_id">>, Result)),
|
|
?assertEqual(<<"456">>, maps:get(<<"channel_id">>, Result)),
|
|
?assertEqual(<<"789">>, maps:get(<<"guild_id">>, Result)),
|
|
?assertEqual(false, maps:get(<<"mute">>, Result)).
|
|
|
|
format_pending_connections_normalizes_and_filters_malformed_entries_test() ->
|
|
PendingConnections = #{
|
|
<<"conn-a">> => #{user_id => <<"42">>, token_nonce => 123, joined_at => <<"1000">>},
|
|
"conn-b" => #{user_id => "84", token_nonce => "nonce", joined_at => "2000"},
|
|
<<"missing-user">> => #{token_nonce => <<"nonce">>, joined_at => 1000},
|
|
<<"bad-user">> => #{user_id => <<"bad">>, joined_at => 1000},
|
|
<<"leading-zero-user">> => #{user_id => <<"0042">>, joined_at => 1000},
|
|
<<"leading-zero-list-user">> => #{user_id => "0084", joined_at => 1000},
|
|
<<"bad-joined-at">> => #{user_id => 168, joined_at => <<"bad">>},
|
|
<<"not-a-map">> => not_a_map
|
|
},
|
|
Result = call_state:format_pending_connections(PendingConnections),
|
|
?assertEqual(3, length(Result)),
|
|
?assert(
|
|
has_pending_connection(
|
|
#{connection_id => <<"conn-a">>, user_id => <<"42">>, token_nonce => <<"123">>},
|
|
Result
|
|
)
|
|
),
|
|
?assert(
|
|
has_pending_connection(
|
|
#{connection_id => <<"conn-b">>, user_id => <<"84">>, token_nonce => <<"nonce">>},
|
|
Result
|
|
)
|
|
),
|
|
?assert(
|
|
has_pending_connection(
|
|
#{connection_id => <<"bad-joined-at">>, user_id => <<"168">>}, Result
|
|
)
|
|
),
|
|
?assertEqual([], call_state:format_pending_connections(not_a_map)).
|
|
|
|
remove_users_from_ringing_test() ->
|
|
State = new_call_test_state(#{
|
|
ringing => [1, 2, 3],
|
|
pending_ringing => [4, 5]
|
|
}),
|
|
Result = call_ringing:remove_users_from_ringing([2, 4], State),
|
|
?assertEqual([1, 3], maps:get(ringing, Result)),
|
|
?assertEqual([5], maps:get(pending_ringing, Result)).
|
|
|
|
join_from_ringing_adds_voice_state_test() ->
|
|
UserId = 42,
|
|
SessionId = <<"session-42">>,
|
|
ConnectionId = <<"conn-42">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => ConnectionId
|
|
},
|
|
State = new_call_test_state(#{
|
|
ringing => [UserId],
|
|
initiator_ready => true
|
|
}),
|
|
{reply, ok, NewState} = call_voice:handle_join_internal(
|
|
UserId, VoiceState, SessionId, self(), ConnectionId, State
|
|
),
|
|
?assert(maps:is_key(UserId, maps:get(voice_states, NewState))),
|
|
?assertNot(lists:member(UserId, maps:get(ringing, NewState))).
|
|
|
|
pending_connection_timeout_preserves_active_voice_state_test() ->
|
|
UserId = 42,
|
|
SessionId = <<"session-42">>,
|
|
ConnectionId = <<"conn-42">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => ConnectionId
|
|
},
|
|
State = new_call_test_state(#{
|
|
voice_states => #{UserId => VoiceState},
|
|
pending_connections => #{
|
|
ConnectionId => #{
|
|
user_id => UserId,
|
|
session_id => SessionId,
|
|
connection_id => ConnectionId,
|
|
channel_id => 1
|
|
}
|
|
},
|
|
initiator_ready => true
|
|
}),
|
|
{noreply, NewState} = call_voice:disconnect_user_after_pending_timeout(
|
|
ConnectionId, UserId, SessionId, State
|
|
),
|
|
?assert(maps:is_key(UserId, maps:get(voice_states, NewState))),
|
|
?assertEqual(
|
|
undefined,
|
|
maps:get(
|
|
ConnectionId, maps:get(pending_connections, NewState), undefined
|
|
)
|
|
).
|
|
|
|
leave_then_rejoin_keeps_voice_state_test() ->
|
|
UserId = 42,
|
|
OtherUserId = 84,
|
|
SessionId = <<"session-42">>,
|
|
OtherSessionId = <<"session-84">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-42">>
|
|
},
|
|
OtherVoiceState = #{
|
|
<<"user_id">> => <<"84">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-84">>
|
|
},
|
|
State = new_call_test_state(#{
|
|
voice_states => #{
|
|
UserId => VoiceState,
|
|
OtherUserId => OtherVoiceState
|
|
},
|
|
sessions => #{
|
|
SessionId => {UserId, self(), make_ref()},
|
|
OtherSessionId => {OtherUserId, self(), make_ref()}
|
|
},
|
|
initiator_ready => true
|
|
}),
|
|
{reply, ok, PostLeave} = call:handle_call(
|
|
{leave, SessionId}, {self(), make_ref()}, State
|
|
),
|
|
?assertEqual(
|
|
undefined,
|
|
maps:get(
|
|
UserId, maps:get(voice_states, PostLeave), undefined
|
|
)
|
|
),
|
|
?assertEqual(
|
|
undefined,
|
|
maps:get(
|
|
SessionId, maps:get(sessions, PostLeave), undefined
|
|
)
|
|
),
|
|
{reply, ok, Rejoined} = call_voice:handle_join_internal(
|
|
UserId, VoiceState, SessionId, self(), <<"conn-42">>, PostLeave
|
|
),
|
|
?assert(maps:is_key(UserId, maps:get(voice_states, Rejoined))).
|
|
|
|
disconnect_user_if_in_channel_notifies_session_cleanup_test() ->
|
|
UserId = 42,
|
|
OtherUserId = 84,
|
|
SessionId = <<"session-42">>,
|
|
OtherSessionId = <<"session-84">>,
|
|
ConnectionId = <<"conn-42">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => ConnectionId
|
|
},
|
|
OtherVoiceState = #{
|
|
<<"user_id">> => <<"84">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-84">>
|
|
},
|
|
State = new_call_test_state(#{
|
|
voice_states => #{
|
|
UserId => VoiceState,
|
|
OtherUserId => OtherVoiceState
|
|
},
|
|
sessions => #{
|
|
SessionId => {UserId, self(), make_ref()},
|
|
OtherSessionId => {OtherUserId, self(), make_ref()}
|
|
},
|
|
initiator_ready => true
|
|
}),
|
|
{reply, #{success := true}, NewState} = call:handle_call(
|
|
{disconnect_user_if_in_channel, UserId, 1, ConnectionId},
|
|
{self(), make_ref()},
|
|
State
|
|
),
|
|
?assertEqual(
|
|
undefined,
|
|
maps:get(
|
|
UserId, maps:get(voice_states, NewState), undefined
|
|
)
|
|
),
|
|
receive
|
|
{'$gen_cast', {call_force_disconnect, 1, ConnectionId}} ->
|
|
ok
|
|
after 1000 ->
|
|
?assert(false)
|
|
end.
|
|
|
|
disconnect_user_if_in_channel_ignores_a_stale_connection_id_test() ->
|
|
UserId = 42,
|
|
SessionId = <<"session-42">>,
|
|
LiveConnectionId = <<"conn-live">>,
|
|
StaleConnectionId = <<"conn-stale">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => LiveConnectionId
|
|
},
|
|
State = new_call_test_state(#{
|
|
voice_states => #{UserId => VoiceState},
|
|
sessions => #{SessionId => {UserId, self(), make_ref()}},
|
|
initiator_ready => true
|
|
}),
|
|
{reply, Reply, NewState} = call:handle_call(
|
|
{disconnect_user_if_in_channel, UserId, 1, StaleConnectionId},
|
|
{self(), make_ref()},
|
|
State
|
|
),
|
|
?assertMatch(#{ignored := true, reason := <<"not_in_call">>}, Reply),
|
|
?assertEqual(
|
|
VoiceState,
|
|
maps:get(UserId, maps:get(voice_states, NewState), undefined)
|
|
),
|
|
receive
|
|
{'$gen_cast', {call_force_disconnect, _, _}} ->
|
|
?assert(false, force_disconnected_a_live_connection)
|
|
after 200 ->
|
|
ok
|
|
end.
|
|
|
|
leave_removes_voice_state_count_without_full_rebuild_test() ->
|
|
UserId = 42,
|
|
SessionId = <<"session-count-leave">>,
|
|
ConnectionId = <<"conn-count-leave">>,
|
|
RegionId = <<"count-test-region">>,
|
|
ServerId = <<"count-test-server">>,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => ConnectionId,
|
|
<<"region_id">> => RegionId,
|
|
<<"server_id">> => ServerId
|
|
},
|
|
ok = voice_state_counts_cache:remove_connection(ConnectionId),
|
|
ok = voice_state_counts_cache:upsert_voice_state(VoiceState),
|
|
?assertEqual(1, voice_count(<<"servers">>, <<"server_id">>, ServerId)),
|
|
State = new_call_test_state(#{
|
|
voice_states => #{UserId => VoiceState},
|
|
sessions => #{SessionId => {UserId, self(), make_ref()}},
|
|
initiator_ready => true
|
|
}),
|
|
{stop, normal, ok, _PostLeave} = call:handle_call(
|
|
{leave, SessionId}, {self(), make_ref()}, State
|
|
),
|
|
?assertEqual(0, voice_count(<<"regions">>, <<"region_id">>, RegionId)),
|
|
?assertEqual(0, voice_count(<<"servers">>, <<"server_id">>, ServerId)),
|
|
ok = voice_state_counts_cache:remove_connection(ConnectionId).
|
|
|
|
terminate_abnormal_dispatches_call_delete_test() ->
|
|
State = new_call_test_state(#{
|
|
recipients => [100, 200],
|
|
voice_states => #{},
|
|
ringing => [],
|
|
initiator_ready => true
|
|
}),
|
|
?assertEqual(ok, call:terminate(killed, State)).
|
|
|
|
terminate_handoff_skips_cleanup_test() ->
|
|
State = new_call_test_state(#{
|
|
recipients => [100],
|
|
voice_states => #{42 => #{<<"connection_id">> => <<"conn-42">>}},
|
|
initiator_ready => true
|
|
}),
|
|
?assertEqual(ok, call:terminate({shutdown, handoff}, State)),
|
|
?assertEqual(ok, call:terminate(handoff, State)).
|
|
|
|
terminate_normal_cleans_counts_test() ->
|
|
State = new_call_test_state(#{
|
|
recipients => [],
|
|
voice_states => #{},
|
|
initiator_ready => true
|
|
}),
|
|
?assertEqual(ok, call:terminate(normal, State)).
|
|
|
|
call_manager_cleanup_call_pid_cache_on_down_test() ->
|
|
case ets:info(call_pid_cache) of
|
|
undefined ->
|
|
ets:new(call_pid_cache, [
|
|
named_table,
|
|
public,
|
|
set,
|
|
{read_concurrency, true},
|
|
{write_concurrency, true}
|
|
]);
|
|
_ ->
|
|
ok
|
|
end,
|
|
FakePid = spawn(fun() ->
|
|
receive
|
|
stop -> ok
|
|
after 30000 ->
|
|
ok
|
|
end
|
|
end),
|
|
ChannelId = 999888,
|
|
ets:insert(call_pid_cache, {ChannelId, FakePid}),
|
|
Ref = make_ref(),
|
|
Calls = #{ChannelId => {FakePid, Ref}},
|
|
State = #{calls => Calls},
|
|
{noreply, NewState} = call_manager:handle_info(
|
|
{'DOWN', Ref, process, FakePid, normal}, State
|
|
),
|
|
?assertEqual([], ets:lookup(call_pid_cache, ChannelId)),
|
|
?assertNot(maps:is_key(ChannelId, maps:get(calls, NewState))),
|
|
FakePid ! stop.
|
|
|
|
voice_count(CollectionKey, IdKey, Id) ->
|
|
Counts = voice_state_counts_cache:get_local_counts(),
|
|
Entries = maps:get(CollectionKey, Counts, []),
|
|
lists:foldl(
|
|
fun(Entry, Acc) ->
|
|
case
|
|
{maps:get(IdKey, Entry, undefined), maps:get(<<"voice_state_count">>, Entry, 0)}
|
|
of
|
|
{Id, Count} when is_integer(Count) -> Count;
|
|
_ -> Acc
|
|
end
|
|
end,
|
|
0,
|
|
Entries
|
|
).
|
|
|
|
has_pending_connection(Expected, Result) ->
|
|
Keys = maps:keys(Expected),
|
|
lists:any(fun(Connection) -> maps:with(Keys, Connection) =:= Expected end, Result).
|
|
|
|
leave_with_second_session_keeps_voice_state_test() ->
|
|
UserId = 42,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-42">>
|
|
},
|
|
State = new_call_test_state(#{
|
|
voice_states => #{UserId => VoiceState},
|
|
sessions => #{
|
|
<<"session-a">> => {UserId, self(), make_ref()},
|
|
<<"session-b">> => {UserId, self(), make_ref()}
|
|
},
|
|
initiator_ready => true
|
|
}),
|
|
{reply, ok, AfterFirstLeave} = call:handle_call(
|
|
{leave, <<"session-a">>}, {self(), make_ref()}, State
|
|
),
|
|
?assertEqual(VoiceState, maps:get(UserId, maps:get(voice_states, AfterFirstLeave))),
|
|
?assertNot(maps:is_key(<<"session-a">>, maps:get(sessions, AfterFirstLeave))),
|
|
?assert(maps:is_key(<<"session-b">>, maps:get(sessions, AfterFirstLeave))),
|
|
{stop, normal, ok, AfterSecondLeave} = call:handle_call(
|
|
{leave, <<"session-b">>}, {self(), make_ref()}, AfterFirstLeave
|
|
),
|
|
?assertNot(maps:is_key(UserId, maps:get(voice_states, AfterSecondLeave))),
|
|
?assertEqual(#{}, maps:get(sessions, AfterSecondLeave)).
|
|
|
|
session_down_with_second_session_keeps_voice_state_test() ->
|
|
UserId = 42,
|
|
VoiceState = #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-42">>
|
|
},
|
|
DownPid = spawn(fun() ->
|
|
receive
|
|
stop -> ok
|
|
after 30000 ->
|
|
ok
|
|
end
|
|
end),
|
|
State = new_call_test_state(#{
|
|
voice_states => #{UserId => VoiceState},
|
|
sessions => #{
|
|
<<"session-a">> => {UserId, DownPid, make_ref()},
|
|
<<"session-b">> => {UserId, self(), make_ref()}
|
|
},
|
|
initiator_ready => true
|
|
}),
|
|
{noreply, NewState} = call_voice:handle_session_down(DownPid, State),
|
|
?assertEqual(VoiceState, maps:get(UserId, maps:get(voice_states, NewState))),
|
|
?assertNot(maps:is_key(<<"session-a">>, maps:get(sessions, NewState))),
|
|
?assert(maps:is_key(<<"session-b">>, maps:get(sessions, NewState))),
|
|
DownPid ! stop.
|
|
|
|
disconnect_user_removes_all_user_sessions_test() ->
|
|
UserId = 42,
|
|
VoiceStates = #{
|
|
UserId => #{
|
|
<<"user_id">> => <<"42">>,
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => <<"conn-42">>
|
|
}
|
|
},
|
|
Sessions = #{
|
|
<<"session-a">> => {UserId, self(), make_ref()},
|
|
<<"session-b">> => {UserId, self(), make_ref()},
|
|
<<"session-other">> => {84, self(), make_ref()}
|
|
},
|
|
TestPid = self(),
|
|
CleanupFun = fun(CleanupUserId, SessionId) ->
|
|
TestPid ! {cleanup, CleanupUserId, SessionId},
|
|
ok
|
|
end,
|
|
{ok, NewVoiceStates, NewSessions} = voice_disconnect_common:disconnect_user_if_in_channel(
|
|
UserId, 1, VoiceStates, Sessions, CleanupFun
|
|
),
|
|
?assertEqual(#{}, NewVoiceStates),
|
|
?assertEqual([<<"session-other">>], maps:keys(NewSessions)),
|
|
CleanedSessions = lists:sort([
|
|
receive
|
|
{cleanup, UserId, SId} -> SId
|
|
after 1000 ->
|
|
error(cleanup_not_called)
|
|
end
|
|
|| _ <- [1, 2]
|
|
]),
|
|
?assertEqual([<<"session-a">>, <<"session-b">>], CleanedSessions).
|
|
|
|
new_call_test_state(Overrides) ->
|
|
maps:merge(
|
|
#{
|
|
channel_id => 1,
|
|
message_id => 1,
|
|
region => undefined,
|
|
ringing => [],
|
|
pending_ringing => [],
|
|
recipients => [],
|
|
voice_states => #{},
|
|
sessions => #{},
|
|
pending_connections => #{},
|
|
initiator_ready => false,
|
|
ringing_timers => #{},
|
|
idle_timer => undefined,
|
|
created_at => 0,
|
|
participants_history => sets:new(),
|
|
last_call_event => undefined
|
|
},
|
|
Overrides
|
|
).
|