mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
459 lines
17 KiB
Erlang
459 lines
17 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(guild_voice_connection_tests).
|
|
-typing([eqwalizer]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
required_voice_perms() ->
|
|
constants:view_channel_permission() bor constants:connect_permission().
|
|
|
|
base_test_member(UserId) ->
|
|
#{<<"user">> => #{<<"id">> => integer_to_binary(UserId)}}.
|
|
|
|
base_test_channel(ChannelId) ->
|
|
#{<<"id">> => integer_to_binary(ChannelId), <<"type">> => 2, <<"user_limit">> => 0}.
|
|
|
|
base_test_state() ->
|
|
#{
|
|
id => 999,
|
|
data => #{
|
|
<<"channels">> => [base_test_channel(100)],
|
|
<<"members">> => [base_test_member(10)]
|
|
},
|
|
voice_states => #{},
|
|
test_perm_fun => fun(_) -> required_voice_perms() end
|
|
}.
|
|
|
|
replace_channels(State, Channels) ->
|
|
Data0 = maps:get(data, State, #{}),
|
|
maps:put(data, maps:put(<<"channels">>, Channels, Data0), State).
|
|
|
|
replace_guild_id(State, GuildIdValue) ->
|
|
Data0 = maps:get(data, State, #{}),
|
|
maps:put(data, maps:put(<<"id">>, GuildIdValue, Data0), State).
|
|
|
|
replace_guild_meta_id(State, GuildIdValue) ->
|
|
Data0 = maps:get(data, State, #{}),
|
|
Guild0 = maps:get(<<"guild">>, Data0, #{}),
|
|
Guild1 = maps:put(<<"id">>, GuildIdValue, Guild0),
|
|
maps:put(data, maps:put(<<"guild">>, Guild1, Data0), State).
|
|
|
|
voice_state_update_invalid_user_id_test() ->
|
|
{reply, {error, validation_error, voice_invalid_user_id}, _} =
|
|
guild_voice_connection:voice_state_update(#{channel_id => null}, #{}).
|
|
|
|
voice_state_update_member_not_found_test() ->
|
|
State = base_test_state(),
|
|
{reply, {error, not_found, voice_member_not_found}, _} =
|
|
guild_voice_connection:voice_state_update(#{user_id => 99, channel_id => null}, State).
|
|
|
|
voice_state_update_invalid_channel_id_test() ->
|
|
State = base_test_state(),
|
|
{reply, {error, validation_error, voice_invalid_channel_id}, _} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{user_id => 10, channel_id => undefined}, State
|
|
).
|
|
|
|
voice_state_update_guild_leave_null_connection_id_test() ->
|
|
State = connected_state(<<"10">>),
|
|
{reply, {error, validation_error, voice_missing_connection_id}, NewState} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{user_id => 10, channel_id => null, connection_id => null}, State
|
|
),
|
|
?assertEqual(connected_voice_states(<<"10">>), maps:get(voice_states, NewState)).
|
|
|
|
voice_state_update_guild_leave_omitted_connection_id_test() ->
|
|
State = connected_state(<<"10">>),
|
|
{reply, {error, validation_error, voice_missing_connection_id}, NewState} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{user_id => 10, channel_id => null}, State
|
|
),
|
|
?assertEqual(connected_voice_states(<<"10">>), maps:get(voice_states, NewState)).
|
|
|
|
voice_state_update_session_teardown_removes_that_sessions_states_test() ->
|
|
State = session_scoped_state(),
|
|
{reply, #{success := true}, NewState} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{
|
|
user_id => 10,
|
|
channel_id => null,
|
|
connection_id => null,
|
|
session_id => <<"sess-a">>
|
|
},
|
|
State
|
|
),
|
|
Remaining = maps:get(voice_states, NewState),
|
|
?assertNot(maps:is_key(<<"conn-a">>, Remaining)),
|
|
?assert(maps:is_key(<<"conn-b">>, Remaining)),
|
|
?assert(maps:is_key(<<"conn-c">>, Remaining)).
|
|
|
|
voice_state_update_session_teardown_works_for_former_members_test() ->
|
|
State = maps:put(
|
|
data,
|
|
#{<<"channels">> => [base_test_channel(100)], <<"members">> => []},
|
|
session_scoped_state()
|
|
),
|
|
{reply, #{success := true}, NewState} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{
|
|
user_id => 10,
|
|
channel_id => null,
|
|
connection_id => null,
|
|
session_id => <<"sess-a">>
|
|
},
|
|
State
|
|
),
|
|
?assertNot(maps:is_key(<<"conn-a">>, maps:get(voice_states, NewState))).
|
|
|
|
voice_state_update_leave_by_connection_works_for_former_members_test() ->
|
|
State = maps:put(
|
|
data,
|
|
#{<<"channels">> => [base_test_channel(100)], <<"members">> => []},
|
|
session_scoped_state()
|
|
),
|
|
{reply, #{success := true}, NewState} =
|
|
guild_voice_connection:voice_state_update(
|
|
#{user_id => 10, channel_id => null, connection_id => <<"conn-b">>}, State
|
|
),
|
|
?assertNot(maps:is_key(<<"conn-b">>, maps:get(voice_states, NewState))).
|
|
|
|
voice_state_update_guild_leave_blank_connection_id_test() ->
|
|
State = connected_state(<<"10">>),
|
|
assert_voice_state_update_error(
|
|
#{user_id => 10, channel_id => null, connection_id => <<>>},
|
|
State,
|
|
{error, validation_error, voice_missing_connection_id}
|
|
).
|
|
|
|
voice_state_update_channel_not_found_test() ->
|
|
State = replace_channels(base_test_state(), []),
|
|
{reply, {error, not_found, voice_channel_not_found}, _} =
|
|
guild_voice_connection:voice_state_update(#{user_id => 10, channel_id => 999}, State).
|
|
|
|
voice_state_update_connection_not_found_test() ->
|
|
State = base_test_state(),
|
|
Request = #{user_id => 10, channel_id => 100, connection_id => <<"missing-conn">>},
|
|
assert_voice_state_update_error(
|
|
Request, State, {error, not_found, voice_connection_not_found}
|
|
).
|
|
|
|
voice_state_update_invalid_viewer_stream_keys_test() ->
|
|
State = connected_state(<<"10">>),
|
|
Request = #{
|
|
user_id => 10,
|
|
channel_id => 100,
|
|
connection_id => <<"conn-1">>,
|
|
viewer_stream_keys => 123
|
|
},
|
|
assert_voice_state_update_error(
|
|
Request, State, {error, validation_error, voice_invalid_state}
|
|
).
|
|
|
|
voice_state_update_viewer_stream_keys_missing_connection_test() ->
|
|
State = connected_state(<<"10">>),
|
|
Request = #{
|
|
user_id => 10,
|
|
channel_id => 100,
|
|
connection_id => <<"conn-1">>,
|
|
viewer_stream_keys => [<<"999:100:missing-conn">>]
|
|
},
|
|
assert_voice_state_update_error(
|
|
Request, State, {error, not_found, voice_connection_not_found}
|
|
).
|
|
|
|
voice_state_update_rejects_dm_scope_viewer_stream_key_test() ->
|
|
VoiceStates = #{
|
|
<<"conn-1">> => connected_voice_state(<<"conn-1">>, <<"10">>, 100, []),
|
|
<<"conn-stream">> => connected_voice_state(<<"conn-stream">>, <<"20">>, 100, [])
|
|
},
|
|
State = maps:put(voice_states, VoiceStates, base_test_state()),
|
|
Request = #{
|
|
user_id => 10,
|
|
channel_id => 100,
|
|
connection_id => <<"conn-1">>,
|
|
viewer_stream_keys => [<<"dm:100:conn-stream">>]
|
|
},
|
|
assert_voice_state_update_error(
|
|
Request, State, {error, validation_error, voice_invalid_state}
|
|
).
|
|
|
|
voice_state_update_guild_id_missing_test() ->
|
|
State0 = base_test_state(),
|
|
State1 = replace_guild_id(State0, undefined),
|
|
State2 = replace_guild_meta_id(State1, undefined),
|
|
State = maps:remove(id, State2),
|
|
Request = #{user_id => 10, channel_id => 100},
|
|
{reply, {error, validation_error, voice_guild_id_missing}, _} =
|
|
guild_voice_connection:voice_state_update(Request, State).
|
|
|
|
voice_state_update_invalid_guild_id_test() ->
|
|
State0 = base_test_state(),
|
|
State1 = replace_guild_id(State0, <<"nope">>),
|
|
State2 = replace_guild_meta_id(State1, <<"nope">>),
|
|
State = maps:put(id, undefined, State2),
|
|
Request = #{user_id => 10, channel_id => 100},
|
|
{reply, {error, validation_error, voice_invalid_guild_id}, _} =
|
|
guild_voice_connection:voice_state_update(Request, State).
|
|
|
|
voice_state_update_connection_user_mismatch_test() ->
|
|
VoiceStates = connected_voice_states(<<"20">>),
|
|
State = maps:put(voice_states, VoiceStates, base_test_state()),
|
|
Request = #{user_id => 10, channel_id => 100, connection_id => <<"conn-1">>},
|
|
{reply, {error, validation_error, voice_user_mismatch}, _} =
|
|
guild_voice_connection:voice_state_update(Request, State).
|
|
|
|
voice_state_update_connection_owner_match_proceeds_test() ->
|
|
VoiceStates = connected_voice_states(<<"10">>),
|
|
State = maps:put(voice_states, VoiceStates, base_test_state()),
|
|
Request = #{user_id => 10, channel_id => 100, connection_id => <<"conn-1">>},
|
|
case guild_voice_connection:voice_state_update(Request, State) of
|
|
{reply, {error, validation_error, voice_user_mismatch}, _} ->
|
|
error(should_not_get_user_mismatch);
|
|
{reply, _, _} ->
|
|
ok
|
|
end.
|
|
|
|
voice_state_update_preserves_and_clears_viewer_keys_test() ->
|
|
StreamKey = <<"999:100:conn-stream">>,
|
|
VoiceStates = #{
|
|
<<"conn-1">> => connected_voice_state(<<"conn-1">>, <<"10">>, 100, []),
|
|
<<"conn-stream">> => connected_voice_state(<<"conn-stream">>, <<"20">>, 100, [])
|
|
},
|
|
State0 = maps:put(voice_states, VoiceStates, base_test_state()),
|
|
{reply, #{success := true}, State1} = guild_voice_connection:voice_state_update(
|
|
update_request(#{viewer_stream_keys => [StreamKey]}), State0
|
|
),
|
|
?assertEqual([StreamKey], viewer_keys(State1)),
|
|
{reply, #{success := true}, State2} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_mute => true}), State1
|
|
),
|
|
?assertEqual(true, maps:get(<<"self_mute">>, updated_voice_state(State2))),
|
|
?assertEqual([StreamKey], viewer_keys(State2)),
|
|
{reply, #{success := true}, State3} = guild_voice_connection:voice_state_update(
|
|
update_request(#{viewer_stream_keys => null}), State2
|
|
),
|
|
?assertEqual([], viewer_keys(State3)).
|
|
|
|
voice_state_update_stress_many_watch_unwatch_updates_test() ->
|
|
StreamKey = <<"999:100:conn-stream">>,
|
|
VoiceStates = #{
|
|
<<"conn-1">> => connected_voice_state(<<"conn-1">>, <<"10">>, 100, []),
|
|
<<"conn-stream">> => connected_voice_state(<<"conn-stream">>, <<"20">>, 100, [])
|
|
},
|
|
InitialState = maps:put(voice_states, VoiceStates, base_test_state()),
|
|
FinalState = lists:foldl(
|
|
fun(Index, State) ->
|
|
Keys =
|
|
case Index rem 4 of
|
|
0 -> [];
|
|
_ -> [StreamKey]
|
|
end,
|
|
SelfDeaf = Index rem 5 =:= 0,
|
|
{reply, #{success := true}, NextState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_deaf => SelfDeaf, viewer_stream_keys => Keys}), State
|
|
),
|
|
Updated = updated_voice_state(NextState),
|
|
?assertEqual(SelfDeaf, maps:get(<<"self_deaf">>, Updated)),
|
|
?assertEqual(Keys, maps:get(<<"viewer_stream_keys">>, Updated)),
|
|
NextState
|
|
end,
|
|
InitialState,
|
|
lists:seq(1, 80)
|
|
),
|
|
?assertMatch(#{<<"conn-1">> := #{}}, maps:get(voice_states, FinalState)).
|
|
|
|
voice_state_update_clears_self_stream_without_stream_permission_test() ->
|
|
State = connected_state(<<"10">>),
|
|
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_stream => true, self_video => true}), State
|
|
),
|
|
Updated = updated_voice_state(NewState),
|
|
?assertEqual(false, maps:get(<<"self_stream">>, Updated)),
|
|
?assertEqual(false, maps:get(<<"self_video">>, Updated)).
|
|
|
|
voice_state_update_keeps_self_stream_with_stream_permission_test() ->
|
|
State = maps:put(
|
|
voice_states,
|
|
connected_voice_states(<<"10">>),
|
|
with_stream_permission(base_test_state())
|
|
),
|
|
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_stream => true, self_video => true}), State
|
|
),
|
|
Updated = updated_voice_state(NewState),
|
|
?assertEqual(true, maps:get(<<"self_stream">>, Updated)),
|
|
?assertEqual(true, maps:get(<<"self_video">>, Updated)).
|
|
|
|
voice_state_update_allows_twenty_fifth_camera_sharer_test() ->
|
|
State = camera_state(24),
|
|
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_video => true}), State
|
|
),
|
|
?assertEqual(true, maps:get(<<"self_video">>, updated_voice_state(NewState))).
|
|
|
|
voice_state_update_blocks_twenty_sixth_camera_sharer_test() ->
|
|
State = camera_state(25),
|
|
assert_voice_state_update_error(
|
|
update_request(#{self_video => true}),
|
|
State,
|
|
{error, permission_denied, voice_camera_user_limit}
|
|
).
|
|
|
|
voice_state_update_camera_off_users_do_not_count_toward_camera_limit_test() ->
|
|
CameraOff = maps:from_list([
|
|
{camera_off_conn_id(N), camera_voice_state(camera_off_conn_id(N), 300 + N, false)}
|
|
|| N <- lists:seq(1, 25)
|
|
]),
|
|
VoiceStates = maps:merge(camera_test_voice_states(24), CameraOff),
|
|
State = maps:put(voice_states, VoiceStates, camera_base_state(24)),
|
|
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_video => true}), State
|
|
),
|
|
?assertEqual(true, maps:get(<<"self_video">>, updated_voice_state(NewState))).
|
|
|
|
voice_state_update_same_user_second_connection_does_not_change_camera_count_test() ->
|
|
VoiceStates = maps:put(
|
|
<<"conn-1b">>,
|
|
camera_voice_state(<<"conn-1b">>, 10, true),
|
|
camera_test_voice_states(24)
|
|
),
|
|
State = maps:put(voice_states, VoiceStates, camera_base_state(24)),
|
|
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_video => true}), State
|
|
),
|
|
?assertEqual(true, maps:get(<<"self_video">>, updated_voice_state(NewState))).
|
|
|
|
voice_state_update_camera_off_frees_slot_for_other_user_test() ->
|
|
State0 = camera_state(25),
|
|
assert_voice_state_update_error(
|
|
update_request(#{self_video => true}),
|
|
State0,
|
|
{error, permission_denied, voice_camera_user_limit}
|
|
),
|
|
{reply, #{success := true}, State1} = guild_voice_connection:voice_state_update(
|
|
update_request(#{
|
|
user_id => 201, connection_id => camera_conn_id(1), self_video => false
|
|
}),
|
|
State0
|
|
),
|
|
{reply, #{success := true}, State2} = guild_voice_connection:voice_state_update(
|
|
update_request(#{self_video => true}), State1
|
|
),
|
|
?assertEqual(true, maps:get(<<"self_video">>, updated_voice_state(State2))).
|
|
|
|
camera_conn_id(N) ->
|
|
<<"camera-conn-", (integer_to_binary(N))/binary>>.
|
|
|
|
camera_off_conn_id(N) ->
|
|
<<"camera-off-conn-", (integer_to_binary(N))/binary>>.
|
|
|
|
camera_voice_state(ConnId, UserId, SelfVideo) ->
|
|
(connected_voice_state(ConnId, integer_to_binary(UserId), 100, []))#{
|
|
<<"self_video">> => SelfVideo
|
|
}.
|
|
|
|
camera_test_voice_states(SharerCount) ->
|
|
Sharers = maps:from_list([
|
|
{camera_conn_id(N), camera_voice_state(camera_conn_id(N), 200 + N, true)}
|
|
|| N <- lists:seq(1, SharerCount)
|
|
]),
|
|
maps:put(<<"conn-1">>, camera_voice_state(<<"conn-1">>, 10, false), Sharers).
|
|
|
|
camera_base_state(SharerCount) ->
|
|
Members = [
|
|
base_test_member(10) | [base_test_member(200 + N) || N <- lists:seq(1, SharerCount)]
|
|
],
|
|
State = with_stream_permission(base_test_state()),
|
|
Data0 = maps:get(data, State, #{}),
|
|
maps:put(data, maps:put(<<"members">>, Members, Data0), State).
|
|
|
|
with_stream_permission(State) ->
|
|
Roles = [
|
|
#{
|
|
<<"id">> => <<"999">>,
|
|
<<"permissions">> => integer_to_binary(constants:stream_permission())
|
|
}
|
|
],
|
|
Data0 = maps:get(data, State, #{}),
|
|
maps:put(data, maps:put(<<"roles">>, Roles, Data0), State).
|
|
|
|
camera_state(SharerCount) ->
|
|
maps:put(
|
|
voice_states, camera_test_voice_states(SharerCount), camera_base_state(SharerCount)
|
|
).
|
|
|
|
connected_voice_states(UserId) ->
|
|
#{
|
|
<<"conn-1">> => #{
|
|
<<"guild_id">> => <<"999">>, <<"channel_id">> => <<"100">>, <<"user_id">> => UserId
|
|
}
|
|
}.
|
|
|
|
connected_state(UserId) ->
|
|
maps:put(voice_states, connected_voice_states(UserId), base_test_state()).
|
|
|
|
session_scoped_state() ->
|
|
VoiceStates = #{
|
|
<<"conn-a">> => #{
|
|
<<"guild_id">> => <<"999">>,
|
|
<<"channel_id">> => <<"100">>,
|
|
<<"user_id">> => <<"10">>,
|
|
<<"session_id">> => <<"sess-a">>
|
|
},
|
|
<<"conn-b">> => #{
|
|
<<"guild_id">> => <<"999">>,
|
|
<<"channel_id">> => <<"100">>,
|
|
<<"user_id">> => <<"10">>,
|
|
<<"session_id">> => <<"sess-b">>
|
|
},
|
|
<<"conn-c">> => #{
|
|
<<"guild_id">> => <<"999">>,
|
|
<<"channel_id">> => <<"100">>,
|
|
<<"user_id">> => <<"11">>,
|
|
<<"session_id">> => <<"sess-a">>
|
|
}
|
|
},
|
|
(base_test_state())#{
|
|
voice_states => VoiceStates,
|
|
test_force_disconnect_fun => fun(_, _, _, _) -> {ok, #{success => true}} end
|
|
}.
|
|
|
|
connected_voice_state(ConnectionId, UserId, ChannelId, ViewerKeys) ->
|
|
#{
|
|
<<"connection_id">> => ConnectionId,
|
|
<<"channel_id">> => integer_to_binary(ChannelId),
|
|
<<"user_id">> => UserId,
|
|
<<"version">> => 0,
|
|
<<"self_mute">> => false,
|
|
<<"self_deaf">> => false,
|
|
<<"self_video">> => false,
|
|
<<"self_stream">> => false,
|
|
<<"is_mobile">> => false,
|
|
<<"viewer_stream_keys">> => ViewerKeys
|
|
}.
|
|
|
|
update_request(Overrides) ->
|
|
maps:merge(
|
|
#{
|
|
user_id => 10,
|
|
channel_id => 100,
|
|
connection_id => <<"conn-1">>,
|
|
self_mute => false,
|
|
self_deaf => false,
|
|
self_video => false,
|
|
self_stream => false,
|
|
is_mobile => false
|
|
},
|
|
Overrides
|
|
).
|
|
|
|
updated_voice_state(State) ->
|
|
maps:get(<<"conn-1">>, maps:get(voice_states, State)).
|
|
|
|
viewer_keys(State) ->
|
|
maps:get(<<"viewer_stream_keys">>, updated_voice_state(State)).
|
|
|
|
assert_voice_state_update_error(Request, State, Error) ->
|
|
{reply, Error, _} = guild_voice_connection:voice_state_update(Request, State).
|