Files

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
).