mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
680 lines
23 KiB
Erlang
680 lines
23 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(call_p2p_tests).
|
|
-typing([eqwalizer]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
agreed_joiners_form_a_mesh_without_pending_connections_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{1, true}, {2, true}], call_state()),
|
|
?assertEqual([true, true], p2p_flags(State)),
|
|
?assertEqual(#{}, maps:get(pending_connections, State)),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
sfu_join_keeps_its_pending_connection_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{1, false}], call_state()),
|
|
?assert(maps:is_key(connection_id(1), maps:get(pending_connections, State)))
|
|
end).
|
|
|
|
race_sfu_join_after_p2p_start_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{1, true}], call_state()),
|
|
?assertEqual(
|
|
{reply, {error, voice_p2p_consent_required}, State}, join(2, false, State)
|
|
),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
race_p2p_join_after_sfu_start_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{2, false}], call_state()),
|
|
?assertEqual(
|
|
{reply, {error, voice_p2p_consent_required}, State}, join(1, true, State)
|
|
),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
fifth_joiner_over_the_cap_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3, 4]], call_state()),
|
|
?assertEqual([true, true, true, true], p2p_flags(Mesh)),
|
|
?assertEqual({reply, {error, voice_channel_full}, Mesh}, join(5, true, Mesh)),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
race_join_past_the_configured_cap_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = join_all([{1, true}, {2, true}], call_state()),
|
|
{reply, ok, Full} = call:handle_call(capped_join(3, 3), from(), Mesh),
|
|
?assertEqual([true, true, true], p2p_flags(Full)),
|
|
?assertEqual(
|
|
{reply, {error, voice_channel_full}, Full},
|
|
call:handle_call(capped_join(4, 3), from(), Full)
|
|
),
|
|
?assertNot(maps:is_key(4, maps:get(voice_states, Full))),
|
|
?assertEqual(#{}, maps:get(pending_connections, Full)),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
join_without_a_cap_falls_back_to_the_hard_ceiling_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = join_all([{UserId, true} || UserId <- [1, 2, 3]], call_state()),
|
|
{reply, ok, Full} = call:handle_call(
|
|
{join, 4, voice_state(4, true), session_id(4), erlang:get(call_p2p_sink),
|
|
connection_id(4)},
|
|
from(),
|
|
Mesh
|
|
),
|
|
?assertEqual([true, true, true, true], p2p_flags(Full))
|
|
end).
|
|
|
|
session_p2p_grant_passes_the_cap_to_the_call_join_test() ->
|
|
with_stubs(fun() ->
|
|
meck:new(gateway_rpc_call_lookup, [passthrough, no_link]),
|
|
try
|
|
{reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]),
|
|
?assertMatch(
|
|
[{join, 10, #{<<"p2p">> := true}, <<"sess">>, _Pid, _ConnectionId, 3}],
|
|
[
|
|
Request
|
|
|| {_Caller, {gateway_rpc_call_lookup, safe_gen_server_call, [_, Request, _]},
|
|
_Result} <- meck:history(gateway_rpc_call_lookup),
|
|
element(1, Request) =:= join
|
|
]
|
|
)
|
|
after
|
|
meck:unload(gateway_rpc_call_lookup)
|
|
end
|
|
end).
|
|
|
|
rejected_join_rolls_back_without_retrying_test() ->
|
|
with_stubs(fun() ->
|
|
erlang:put(call_p2p_join_reply, {error, voice_p2p_consent_required}),
|
|
ok = dm_voice_token:join_or_create_call(
|
|
100, 10, voice_state(10, false), <<"sess">>, self(), undefined
|
|
),
|
|
?assertEqual(
|
|
[
|
|
{voice_rejected, voice_p2p_consent_required},
|
|
{call_force_disconnect, 100, connection_id(10)}
|
|
],
|
|
drain_session_casts()
|
|
),
|
|
?assertEqual(1, meck:num_calls(call_manager, lookup, '_'))
|
|
end).
|
|
|
|
explicit_p2p_false_update_converts_test() ->
|
|
with_stubs(fun() ->
|
|
State0 = join_all([{1, true}, {2, true}], call_state()),
|
|
{reply, ok, State1} = call:handle_call(
|
|
{update_voice_state, 1, voice_state(1, false)}, from(), State0
|
|
),
|
|
?assertEqual([false, false], p2p_flags(State1)),
|
|
?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(2))),
|
|
?assertEqual(
|
|
[connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates())
|
|
)
|
|
end).
|
|
|
|
update_keeping_p2p_keeps_the_mesh_test() ->
|
|
with_stubs(fun() ->
|
|
State0 = join_all([{1, true}, {2, true}], call_state()),
|
|
{reply, ok, State1} = call:handle_call(
|
|
{update_voice_state, 1, (voice_state(1, true))#{<<"self_mute">> => true}},
|
|
from(),
|
|
State0
|
|
),
|
|
?assertEqual([true, true], p2p_flags(State1)),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
stale_p2p_update_after_conversion_stays_sfu_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = join_all([{1, true}, {2, true}], call_state()),
|
|
{reply, ok, State0} = call:handle_call(
|
|
{update_voice_state, 2, voice_state(2, false)}, from(), Mesh
|
|
),
|
|
?assertEqual(2, length(drain_voice_server_updates())),
|
|
ok = flush_user_dispatches(),
|
|
{reply, ok, State1} = call:handle_call(
|
|
{update_voice_state, 1, voice_state(1, true)}, from(), State0
|
|
),
|
|
?assertEqual([false, false], p2p_flags(State1)),
|
|
?assertMatch(#{<<"p2p">> := false}, receive_voice_state_update(connection_id(1))),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
same_region_update_converts_a_mesh_test() ->
|
|
with_stubs(fun() ->
|
|
State0 = join_all([{1, true}, {2, true}], (call_state())#{region => <<"us-east">>}),
|
|
ok = flush_user_dispatches(),
|
|
{reply, ok, State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0),
|
|
?assertEqual([false, false], p2p_flags(State1)),
|
|
?assertEqual(
|
|
[connection_id(1), connection_id(2)], lists:sort(drain_voice_server_updates())
|
|
),
|
|
?assertEqual(
|
|
[<<"us-east">>, <<"us-east">>],
|
|
[maps:get(<<"rtc_region">>, Request) || Request <- token_requests()]
|
|
),
|
|
?assertMatch(
|
|
#{voice_states := [#{<<"p2p">> := false}, #{<<"p2p">> := false}]},
|
|
receive_user_dispatch(call_update)
|
|
)
|
|
end).
|
|
|
|
same_region_update_leaves_an_sfu_call_alone_test() ->
|
|
with_stubs(fun() ->
|
|
State0 = join_all([{1, false}, {2, false}], (call_state())#{region => <<"us-east">>}),
|
|
{reply, ok, _State1} = call:handle_call({update_region, <<"us-east">>}, from(), State0),
|
|
?assertEqual([], drain_voice_server_updates())
|
|
end).
|
|
|
|
call_event_and_handoff_keep_the_p2p_flag_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{1, true}], call_state()),
|
|
?assertMatch(
|
|
#{voice_states := [#{<<"p2p">> := true}]}, call_state:build_call_event(State)
|
|
),
|
|
Restored = call_handoff:restore_state(call_handoff:export_state(State)),
|
|
?assertEqual([true], p2p_flags(Restored))
|
|
end).
|
|
|
|
signal_relay_allowed_test() ->
|
|
with_stubs(fun() ->
|
|
State = join_all([{1, true}, {2, true}], call_state()),
|
|
Data = #{<<"type">> => <<"answer">>, <<"sdp">> => <<"v=0">>},
|
|
{noreply, State} = call:handle_cast(
|
|
{voice_signal, session_id(1), connection_id(2), Data}, State
|
|
),
|
|
?assertEqual(
|
|
#{
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"from">> => connection_id(1),
|
|
<<"user_id">> => <<"1">>,
|
|
<<"data">> => Data
|
|
},
|
|
receive_voice_signal()
|
|
)
|
|
end).
|
|
|
|
signal_relay_denied_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = join_all([{1, true}, {2, true}], call_state()),
|
|
Data = #{<<"type">> => <<"candidate">>},
|
|
Denied = [
|
|
{voice_signal, session_id(9), connection_id(2), Data},
|
|
{voice_signal, session_id(1), connection_id(1), Data},
|
|
{voice_signal, session_id(1), <<"unknown">>, Data}
|
|
],
|
|
[{noreply, Mesh} = call:handle_cast(Signal, Mesh) || Signal <- Denied],
|
|
{reply, ok, Sfu} = call:handle_call({update_region, <<"us-east">>}, from(), Mesh),
|
|
{noreply, Sfu} = call:handle_cast(
|
|
{voice_signal, session_id(1), connection_id(2), Data}, Sfu
|
|
),
|
|
assert_no_voice_signal()
|
|
end).
|
|
|
|
session_p2p_start_without_a_call_test() ->
|
|
with_stubs(fun() ->
|
|
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
|
|
true, not_found
|
|
),
|
|
?assertMatch(
|
|
#{
|
|
<<"p2p">> := true,
|
|
<<"p2p_initiator">> := true,
|
|
<<"p2p_participant_count">> := 1,
|
|
<<"country_code">> := <<"SE">>
|
|
},
|
|
lists:last(token_requests())
|
|
),
|
|
?assertEqual(
|
|
true, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
|
|
),
|
|
Update = receive_session_voice_server_update(),
|
|
?assertEqual(
|
|
#{
|
|
<<"p2p">> => true,
|
|
<<"ice_servers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}],
|
|
<<"connection_id">> => ConnectionId,
|
|
<<"channel_id">> => <<"100">>
|
|
},
|
|
Update
|
|
)
|
|
end).
|
|
|
|
session_p2p_join_of_an_existing_mesh_test() ->
|
|
with_stubs(fun() ->
|
|
{reply, #{success := true}, _State} = dm_join(true, [voice_state(20, true)]),
|
|
?assertMatch(
|
|
#{
|
|
<<"p2p">> := true,
|
|
<<"p2p_initiator">> := false,
|
|
<<"p2p_participant_count">> := 2
|
|
},
|
|
lists:last(token_requests())
|
|
),
|
|
?assertMatch(#{<<"p2p">> := true}, receive_session_voice_server_update())
|
|
end).
|
|
|
|
session_join_from_a_second_device_counts_the_user_once_test() ->
|
|
with_stubs(fun() ->
|
|
{reply, #{success := true}, _State} = dm_join(
|
|
true, [voice_state(20, true), voice_state(10, true)]
|
|
),
|
|
?assertMatch(
|
|
#{
|
|
<<"p2p">> := true,
|
|
<<"p2p_initiator">> := false,
|
|
<<"p2p_participant_count">> := 2
|
|
},
|
|
lists:last(token_requests())
|
|
)
|
|
end).
|
|
|
|
session_join_of_an_sfu_call_does_not_ask_for_p2p_test() ->
|
|
with_stubs(fun() ->
|
|
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
|
|
true, [voice_state(20, false)]
|
|
),
|
|
?assertNot(maps:is_key(<<"p2p">>, lists:last(token_requests()))),
|
|
?assertEqual(
|
|
false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
|
|
),
|
|
Update = receive_session_voice_server_update(),
|
|
?assertEqual(<<"tok">>, maps:get(<<"token">>, Update)),
|
|
?assertNot(maps:is_key(<<"p2p">>, Update))
|
|
end).
|
|
|
|
session_join_of_a_mesh_without_agreement_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
?assertMatch(
|
|
{reply, {error, permission_denied, voice_p2p_consent_required}, #{
|
|
dm_voice_states := #{}
|
|
}},
|
|
dm_join_reply(undefined, [voice_state(20, true)])
|
|
),
|
|
?assertEqual([], token_requests()),
|
|
assert_no_session_voice_server_update()
|
|
end).
|
|
|
|
session_bot_join_of_a_mesh_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
erlang:put(call_p2p_call_voice_states, [voice_state(20, true)]),
|
|
State = dm_session_state(#{}),
|
|
?assertMatch(
|
|
{reply, {error, permission_denied, voice_p2p_consent_required}, _State},
|
|
dm_voice_token:get_dm_voice_token_and_create_state(
|
|
(dm_request(true, State))#{bot => true}
|
|
)
|
|
)
|
|
end).
|
|
|
|
session_join_of_a_full_mesh_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
Mesh = [voice_state(UserId, true) || UserId <- [20, 21, 22, 23]],
|
|
?assertMatch(
|
|
{reply, {error, permission_denied, voice_channel_full}, _State},
|
|
dm_join_reply(true, Mesh)
|
|
),
|
|
?assertEqual([], token_requests())
|
|
end).
|
|
|
|
session_join_past_the_configured_cap_is_rejected_as_full_test() ->
|
|
with_stubs(fun() ->
|
|
erlang:put(call_p2p_stub_mode, channel_full),
|
|
?assertMatch(
|
|
{reply, {error, permission_denied, voice_channel_full}, #{dm_voice_states := #{}}},
|
|
dm_join_reply(true, [voice_state(20, true), voice_state(21, true)])
|
|
),
|
|
?assertMatch(
|
|
#{
|
|
<<"p2p">> := true,
|
|
<<"p2p_initiator">> := false,
|
|
<<"p2p_participant_count">> := 3
|
|
},
|
|
lists:last(token_requests())
|
|
),
|
|
assert_no_session_voice_server_update()
|
|
end).
|
|
|
|
session_declined_grant_into_a_mesh_is_rejected_test() ->
|
|
with_stubs(fun() ->
|
|
erlang:put(call_p2p_stub_mode, decline),
|
|
?assertMatch(
|
|
{reply, {error, voice_error, voice_p2p_unavailable}, #{dm_voice_states := #{}}},
|
|
dm_join_reply(true, [voice_state(20, true)])
|
|
),
|
|
?assertMatch(
|
|
#{<<"p2p">> := true, <<"p2p_initiator">> := false}, lists:last(token_requests())
|
|
),
|
|
assert_no_session_voice_server_update()
|
|
end).
|
|
|
|
session_declined_grant_without_a_call_joins_sfu_test() ->
|
|
with_stubs(fun() ->
|
|
erlang:put(call_p2p_stub_mode, decline),
|
|
{reply, #{success := true, connection_id := ConnectionId}, State} = dm_join(
|
|
true, not_found
|
|
),
|
|
?assertEqual(
|
|
false, maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, State)))
|
|
),
|
|
?assertEqual(<<"tok">>, maps:get(<<"token">>, receive_session_voice_server_update()))
|
|
end).
|
|
|
|
session_update_with_explicit_p2p_false_clears_the_flag_test() ->
|
|
with_stubs(fun() ->
|
|
?assertEqual(false, dm_updated_p2p(false)),
|
|
?assertEqual(true, dm_updated_p2p(undefined)),
|
|
?assertEqual(true, dm_updated_p2p(true))
|
|
end).
|
|
|
|
dm_updated_p2p(P2p) ->
|
|
Existing = (voice_state(10, true))#{
|
|
<<"channel_id">> => <<"100">>, <<"session_id">> => <<"sess">>
|
|
},
|
|
ConnectionId = connection_id(10),
|
|
State = dm_session_state(#{ConnectionId => Existing}),
|
|
{reply, #{success := true}, NewState} = dm_voice_connect:handle_dm_connect_or_update(
|
|
(dm_request(P2p, State))#{
|
|
connection_id => ConnectionId, viewer_stream_keys => undefined
|
|
}
|
|
),
|
|
maps:get(<<"p2p">>, maps:get(ConnectionId, maps:get(dm_voice_states, NewState))).
|
|
|
|
dm_join_reply(P2p, CallVoiceStates) ->
|
|
erlang:put(call_p2p_call_voice_states, CallVoiceStates),
|
|
State = dm_session_state(#{}),
|
|
dm_voice_token:get_dm_voice_token_and_create_state(dm_request(P2p, State)).
|
|
|
|
dm_join(P2p, CallVoiceStates) ->
|
|
Reply = dm_join_reply(P2p, CallVoiceStates),
|
|
receive
|
|
{'$gen_cast', {call_monitor, 100, _CallPid}} -> Reply
|
|
after 1000 ->
|
|
error(call_not_joined)
|
|
end.
|
|
|
|
dm_session_state(VoiceStates) ->
|
|
#{
|
|
dm_voice_states => VoiceStates,
|
|
channels => #{100 => #{<<"type">> => 1, <<"recipient_ids">> => [20]}},
|
|
user_id => 10,
|
|
id => <<"sess">>,
|
|
session_pid => erlang:get(call_p2p_sink)
|
|
}.
|
|
|
|
dm_request(P2p, State) ->
|
|
#{
|
|
user_id => 10,
|
|
channel_id => 100,
|
|
session_id => <<"sess">>,
|
|
connection_id => null,
|
|
self_mute => false,
|
|
self_deaf => false,
|
|
self_video => false,
|
|
self_stream => false,
|
|
viewer_stream_keys => [],
|
|
is_mobile => false,
|
|
latitude => null,
|
|
longitude => null,
|
|
e2ee_capable => false,
|
|
bot => false,
|
|
p2p => P2p,
|
|
country_code => <<"SE">>,
|
|
voice_states => maps:get(dm_voice_states, State),
|
|
state => State
|
|
}.
|
|
|
|
join_all(Joins, State0) ->
|
|
lists:foldl(
|
|
fun({UserId, P2p}, State) ->
|
|
{reply, ok, NewState} = join(UserId, P2p, State),
|
|
NewState
|
|
end,
|
|
State0,
|
|
Joins
|
|
).
|
|
|
|
join(UserId, P2p, State) ->
|
|
call_voice:handle_join_internal(
|
|
UserId,
|
|
voice_state(UserId, P2p),
|
|
session_id(UserId),
|
|
erlang:get(call_p2p_sink),
|
|
connection_id(UserId),
|
|
undefined,
|
|
State
|
|
).
|
|
|
|
capped_join(UserId, MaxParticipants) ->
|
|
{join, UserId, voice_state(UserId, true), session_id(UserId), erlang:get(call_p2p_sink),
|
|
connection_id(UserId), MaxParticipants}.
|
|
|
|
p2p_flags(State) ->
|
|
[
|
|
maps:get(<<"p2p">>, VoiceState)
|
|
|| {_UserId, VoiceState} <- lists:keysort(1, maps:to_list(maps:get(voice_states, State)))
|
|
].
|
|
|
|
voice_state(UserId, P2p) ->
|
|
#{
|
|
<<"user_id">> => integer_to_binary(UserId),
|
|
<<"channel_id">> => <<"1">>,
|
|
<<"connection_id">> => connection_id(UserId),
|
|
<<"session_id">> => session_id(UserId),
|
|
<<"p2p">> => P2p
|
|
}.
|
|
|
|
connection_id(UserId) ->
|
|
<<"conn-", (integer_to_binary(UserId))/binary>>.
|
|
|
|
session_id(UserId) ->
|
|
<<"sess-", (integer_to_binary(UserId))/binary>>.
|
|
|
|
from() ->
|
|
{self(), make_ref()}.
|
|
|
|
call_state() ->
|
|
#{
|
|
channel_id => 1,
|
|
message_id => 1,
|
|
region => undefined,
|
|
ringing => [],
|
|
pending_ringing => [],
|
|
recipients => [1],
|
|
voice_states => #{},
|
|
sessions => #{},
|
|
pending_connections => #{},
|
|
initiator_ready => true,
|
|
ringing_timers => #{},
|
|
idle_timer => undefined,
|
|
created_at => 0,
|
|
participants_history => sets:new(),
|
|
last_call_event => undefined
|
|
}.
|
|
|
|
token_requests() ->
|
|
[
|
|
Request
|
|
|| {_Pid, {rpc_client, call, [#{<<"type">> := <<"voice_get_token">>} = Request]}, _Result} <-
|
|
meck:history(rpc_client)
|
|
].
|
|
|
|
drain_voice_server_updates() ->
|
|
receive
|
|
{'$gen_cast', {dispatch, voice_server_update, Payload}} ->
|
|
?assertEqual(<<"tok">>, maps:get(<<"token">>, Payload)),
|
|
?assertNot(maps:is_key(<<"p2p">>, Payload)),
|
|
[maps:get(<<"connection_id">>, Payload) | drain_voice_server_updates()]
|
|
after 200 ->
|
|
[]
|
|
end.
|
|
|
|
receive_session_voice_server_update() ->
|
|
receive
|
|
{'$gen_cast', {dispatch, voice_server_update, Payload}} -> Payload
|
|
after 1000 ->
|
|
error(voice_server_update_not_dispatched)
|
|
end.
|
|
|
|
assert_no_session_voice_server_update() ->
|
|
receive
|
|
{'$gen_cast', {dispatch, voice_server_update, Payload}} ->
|
|
error({unexpected_voice_server_update, Payload})
|
|
after 100 ->
|
|
ok
|
|
end.
|
|
|
|
drain_session_casts() ->
|
|
receive
|
|
{'$gen_cast', Cast} -> [Cast | drain_session_casts()]
|
|
after 100 ->
|
|
[]
|
|
end.
|
|
|
|
receive_voice_state_update(ConnectionId) ->
|
|
case receive_user_dispatch(voice_state_update) of
|
|
#{<<"connection_id">> := ConnectionId} = VoiceState -> VoiceState;
|
|
_Other -> receive_voice_state_update(ConnectionId)
|
|
end.
|
|
|
|
receive_user_dispatch(Event) ->
|
|
receive
|
|
{user_dispatch, Event, Data} -> Data
|
|
after 1000 ->
|
|
error({user_dispatch_not_received, Event})
|
|
end.
|
|
|
|
flush_user_dispatches() ->
|
|
receive
|
|
{user_dispatch, _Event, _Data} -> flush_user_dispatches()
|
|
after 100 ->
|
|
ok
|
|
end.
|
|
|
|
receive_voice_signal() ->
|
|
receive
|
|
{'$gen_cast', {dispatch, voice_signal, Payload}} -> Payload
|
|
after 1000 ->
|
|
error(voice_signal_not_dispatched)
|
|
end.
|
|
|
|
assert_no_voice_signal() ->
|
|
receive
|
|
{'$gen_cast', {dispatch, voice_signal, Payload}} ->
|
|
error({unexpected_voice_signal, Payload})
|
|
after 100 ->
|
|
ok
|
|
end.
|
|
|
|
with_stubs(Fun) ->
|
|
Test = self(),
|
|
Sink = spawn(fun() -> sink(Test) end),
|
|
erlang:put(call_p2p_sink, Sink),
|
|
erlang:erase(call_p2p_call_voice_states),
|
|
erlang:erase(call_p2p_stub_mode),
|
|
erlang:erase(call_p2p_join_reply),
|
|
meck:new(rpc_client, [passthrough, no_link, non_strict]),
|
|
meck:expect(rpc_client, call, fun rpc_reply/1),
|
|
meck:new(presence_manager, [passthrough, no_link]),
|
|
meck:expect(presence_manager, dispatch_to_user, fun(_UserId, Event, Data) ->
|
|
Sink ! {user_dispatch, Event, Data},
|
|
ok
|
|
end),
|
|
CallPid = spawn(fun call_stub/0),
|
|
meck:new(call_manager, [passthrough, no_link]),
|
|
meck:expect(call_manager, lookup, fun(_ChannelId) ->
|
|
case erlang:get(call_p2p_call_voice_states) of
|
|
not_found ->
|
|
{error, not_found};
|
|
VoiceStates when is_list(VoiceStates) ->
|
|
CallPid ! {voice_states, VoiceStates},
|
|
{ok, CallPid};
|
|
undefined ->
|
|
CallPid ! {join_reply, erlang:get(call_p2p_join_reply)},
|
|
{ok, CallPid}
|
|
end
|
|
end),
|
|
try
|
|
Fun()
|
|
after
|
|
CallPid ! stop,
|
|
meck:unload(call_manager),
|
|
meck:unload(presence_manager),
|
|
meck:unload(rpc_client),
|
|
stop_sink(Sink)
|
|
end.
|
|
|
|
sink(Test) ->
|
|
receive
|
|
Message ->
|
|
Test ! Message,
|
|
sink(Test)
|
|
after 30000 ->
|
|
ok
|
|
end.
|
|
|
|
stop_sink(Sink) ->
|
|
exit(Sink, kill),
|
|
flush_mailbox().
|
|
|
|
flush_mailbox() ->
|
|
receive
|
|
_Message -> flush_mailbox()
|
|
after 0 ->
|
|
ok
|
|
end.
|
|
|
|
call_stub() ->
|
|
call_stub([], ok).
|
|
|
|
call_stub(VoiceStates, JoinReply) ->
|
|
receive
|
|
{voice_states, NewVoiceStates} ->
|
|
call_stub(NewVoiceStates, JoinReply);
|
|
{join_reply, undefined} ->
|
|
call_stub(VoiceStates, JoinReply);
|
|
{join_reply, NewJoinReply} ->
|
|
call_stub(VoiceStates, NewJoinReply);
|
|
{'$gen_call', From, {get_state}} ->
|
|
gen_server:reply(From, {ok, #{voice_states => VoiceStates, region => undefined}}),
|
|
call_stub(VoiceStates, JoinReply);
|
|
{'$gen_call', From, _Request} ->
|
|
gen_server:reply(From, JoinReply),
|
|
call_stub(VoiceStates, JoinReply);
|
|
stop ->
|
|
ok;
|
|
_Other ->
|
|
call_stub(VoiceStates, JoinReply)
|
|
after 30000 ->
|
|
ok
|
|
end.
|
|
|
|
rpc_reply(#{<<"type">> := <<"voice_get_token">>} = Request) ->
|
|
ConnectionId = maps:get(<<"connection_id">>, Request, <<"conn-new">>),
|
|
case {maps:get(<<"p2p">>, Request, false), erlang:get(call_p2p_stub_mode)} of
|
|
{true, channel_full} ->
|
|
{ok, #{<<"p2p">> => false, <<"declineReason">> => <<"channel_full">>}};
|
|
{true, Mode} when Mode =/= decline ->
|
|
{ok, #{
|
|
<<"p2p">> => true,
|
|
<<"connectionId">> => ConnectionId,
|
|
<<"iceServers">> => [#{<<"urls">> => [<<"stun:stun.example:3478">>]}],
|
|
<<"maxParticipants">> => 3
|
|
}};
|
|
_ ->
|
|
{ok, #{
|
|
<<"token">> => <<"tok">>,
|
|
<<"endpoint">> => <<"wss://voice.example">>,
|
|
<<"connectionId">> => ConnectionId
|
|
}}
|
|
end;
|
|
rpc_reply(_Request) ->
|
|
{ok, #{}}.
|