mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367)
This commit is contained in:
1 parent
e5a568acca
commit
bef5e33eec
215 files changed
+15840
-2693
No files matched your search
@@ -0,0 +1,697 @@
|
||||
%% 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).
|
||||
|
||||
race_async_join_producing_mixed_states_is_rejected_test() ->
|
||||
with_stubs(fun() ->
|
||||
State = join_all([{1, true}], call_state()),
|
||||
?assertEqual(
|
||||
{noreply, State},
|
||||
call_voice:handle_join_async(
|
||||
2, voice_state(2, false), session_id(2), erlang:get(call_p2p_sink), State
|
||||
)
|
||||
),
|
||||
receive
|
||||
{call_join_result, 1, Result} ->
|
||||
?assertEqual({error, voice_p2p_consent_required}, Result)
|
||||
after 1000 ->
|
||||
error(call_join_result_not_sent)
|
||||
end,
|
||||
?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, #{}}.
|
||||
Reference in new issue
Block a user