%% SPDX-License-Identifier: AGPL-3.0-or-later -module(session_ready_tests). -typing([eqwalizer]). -include_lib("eunit/include/eunit.hrl"). process_guild_state_unavailable_dispatches_guild_delete_test() -> State0 = base_state_for_guild_dispatch_test(), GuildState = #{<<"id">> => <<"123">>, <<"unavailable">> => true}, {noreply, State1} = session_ready:process_guild_state(GuildState, State0), Buffer = limited_deque:to_list(maps:get(buffer, State1)), ?assertEqual(1, length(Buffer)), [First | _] = Buffer, FirstEvent = maps:get(event, eqwalizer:dynamic_cast(First)), ?assertEqual(guild_delete, FirstEvent), ok. process_guild_state_available_dispatches_guild_create_test() -> State0 = base_state_for_guild_dispatch_test(), GuildState = #{ <<"id">> => <<"123">>, <<"unavailable">> => false, <<"channels">> => [], <<"members">> => [] }, {noreply, State1} = session_ready:process_guild_state(GuildState, State0), Buffer = limited_deque:to_list(maps:get(buffer, State1)), ?assertEqual(1, length(Buffer)), [First | _] = Buffer, FirstEvent = maps:get(event, eqwalizer:dynamic_cast(First)), ?assertEqual(guild_create, FirstEvent), ok. process_guild_state_bot_guild_create_marks_guild_available_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()}, GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State1} = session_ready:process_guild_state(GuildState, State0), receive {dispatch, guild_create, GuildCreateData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)), ?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end. process_guild_state_bot_guild_create_after_join_omits_unavailable_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()}, {noreply, State1} = session:handle_cast({guild_join, 123}, State0), GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, State2} = session_ready:process_guild_state(GuildState, State1), receive {dispatch, guild_create, JoinCreateData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, JoinCreateData)), ?assertNot(maps:is_key(<<"unavailable">>, JoinCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end, {noreply, _State3} = session_ready:process_guild_state(GuildState, State2), receive {dispatch, guild_create, LaterCreateData, 2} -> ?assertEqual(false, maps:get(<<"unavailable">>, LaterCreateData)) after 1000 -> ?assert(false, second_guild_create_not_dispatched) end, drain_mailbox(). process_guild_state_bot_guild_create_after_join_and_outage_omits_unavailable_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()}, {noreply, State1} = session:handle_cast({guild_join, 123}, State0), {noreply, State2} = session_ready:mark_guild_unavailable(123, State1), receive {dispatch, guild_delete, GuildDeleteData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildDeleteData)), ?assertEqual(true, maps:get(<<"unavailable">>, GuildDeleteData)) after 1000 -> ?assert(false, guild_delete_not_dispatched) end, GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State3} = session_ready:process_guild_state(GuildState, State2), receive {dispatch, guild_create, GuildCreateData, 2} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)), ?assertNot(maps:is_key(<<"unavailable">>, GuildCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end, drain_mailbox(). process_guild_state_bot_guild_create_after_join_and_removal_marks_guild_available_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{ bot => true, socket_pid => self(), user_id => 1 }, {noreply, State1} = session:handle_cast({guild_join, 123}, State0), {noreply, State2} = session_connection_guild:handle_result_internal( 123, 3, {error, not_member}, State1 ), receive {dispatch, guild_delete, GuildDeleteData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildDeleteData)), ?assertNot(maps:is_key(<<"unavailable">>, GuildDeleteData)) after 1000 -> ?assert(false, guild_delete_not_dispatched) end, GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State3} = session_ready:process_guild_state(GuildState, State2), receive {dispatch, guild_create, GuildCreateData, 2} -> ?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end, drain_mailbox(). relayed_guild_create_to_bot_marks_guild_available_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()}, GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State1} = session:handle_cast({dispatch, guild_create, GuildState}, State0), receive {dispatch, guild_create, GuildCreateData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)), ?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end. process_guild_state_user_guild_create_omits_unavailable_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{bot => false, socket_pid => self()}, GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State1} = session_ready:process_guild_state(GuildState, State0), receive {dispatch, guild_create, GuildCreateData, 1} -> ?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)), ?assertNot(maps:is_key(<<"unavailable">>, GuildCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end. mark_guild_unavailable_hidden_includes_flag_test() -> State0 = #{ collected_guild_states => [], ready => #{<<"guilds">> => []} }, {noreply, State1} = session_ready:mark_guild_unavailable(777, true, State0), Collected = maps:get(collected_guild_states, State1, []), ?assertEqual(1, length(Collected)), [UnavailableGuild] = Collected, ?assertEqual(<<"777">>, maps:get(<<"id">>, UnavailableGuild)), ?assertEqual(true, maps:get(<<"unavailable">>, UnavailableGuild)), ?assertEqual(true, maps:get(<<"unavailable_hidden">>, UnavailableGuild)). mark_guild_unavailable_after_ready_dispatches_guild_delete_test() -> drain_mailbox(), State0 = (base_state_for_guild_dispatch_test())#{ guilds => #{777 => undefined}, socket_pid => self() }, {noreply, State1} = session_ready:mark_guild_unavailable(777, true, State0), Collected = maps:get(collected_guild_states, State1, []), ?assertEqual(0, length(Collected)), ?assertEqual(unavailable, maps:get(777, maps:get(guilds, State1))), receive {dispatch, guild_delete, GuildDeleteData, _Seq} -> ?assertEqual(<<"777">>, maps:get(<<"id">>, GuildDeleteData)), ?assertEqual(true, maps:get(<<"unavailable">>, GuildDeleteData)), ?assertEqual(true, maps:get(<<"unavailable_hidden">>, GuildDeleteData)); Other -> ?assert(false, {unexpected_message, Other}) after 1000 -> ?assert(false, guild_delete_not_dispatched) end. update_ready_guilds_does_not_duplicate_collected_guild_state_test() -> GuildState = #{ <<"id">> => <<"777">>, <<"members">> => [#{<<"user">> => #{<<"id">> => <<"1">>}}] }, Ready = #{<<"guilds">> => [], <<"v">> => 9}, State = #{ready => Ready, bot => false}, Updated = session_ready:update_ready_guilds(GuildState, State), ?assertEqual(Ready, maps:get(ready, Updated)). dispatch_ready_data_bot_unavailable_dispatches_guild_delete_test() -> drain_mailbox(), UnavailableGuild = #{<<"id">> => <<"987">>, <<"unavailable">> => true}, State0 = base_ready_state( <<"session-ready-test">>, 42, true, #{987 => cached_unavailable}, [UnavailableGuild] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, ReadySeq} -> ?assertEqual(1, ReadySeq), ?assertEqual( [#{<<"id">> => <<"987">>, <<"unavailable">> => true}], maps:get(<<"guilds">>, ReadyData) ); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end, receive {dispatch, guild_delete, GuildDeleteData, GuildDeleteSeq} -> ?assertEqual(2, GuildDeleteSeq), ?assertEqual(<<"987">>, maps:get(<<"id">>, GuildDeleteData)), ?assertEqual(true, maps:get(<<"unavailable">>, GuildDeleteData)); OtherDelete -> ?assert(false, {unexpected_guild_event, OtherDelete}) after 1000 -> ?assert(false, guild_delete_not_dispatched) end, receive {dispatch, guild_create, _GuildCreateData, _GuildCreateSeq} -> ?assert(false, unexpected_guild_create_for_unavailable_guild) after 100 -> ok end. dispatch_ready_data_nonbot_includes_unavailable_guild_test() -> drain_mailbox(), UnavailableGuild = #{<<"id">> => <<"654">>, <<"unavailable">> => true}, State0 = base_ready_state( <<"session-ready-nonbot-test">>, 43, false, #{654 => cached_unavailable}, [UnavailableGuild] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> ReadyGuilds = maps:get(<<"guilds">>, ReadyData, []), ?assertEqual(1, length(ReadyGuilds)), ReadyGuild = hd(ReadyGuilds), ?assertEqual(<<"654">>, maps:get(<<"id">>, ReadyGuild)), ?assertEqual(true, maps:get(<<"unavailable">>, ReadyGuild)); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end, receive {dispatch, guild_create, _GuildCreateData, _GuildCreateSeq} -> ?assert(false, unexpected_guild_create_for_nonbot_ready) after 100 -> ok end, receive {dispatch, guild_delete, _GuildDeleteData, _GuildDeleteSeq} -> ?assert(false, unexpected_guild_delete_for_nonbot_ready) after 100 -> ok end. dispatch_ready_data_nonbot_includes_api_guild_ids_as_unavailable_test() -> drain_mailbox(), State0 = base_ready_state( <<"session-ready-pending-test">>, 44, false, #{111 => undefined, 222 => cached_unavailable}, [] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> ReadyGuilds = maps:get(<<"guilds">>, ReadyData, []), ?assertEqual(2, length(ReadyGuilds)), ReadyGuildIds = lists:sort([maps:get(<<"id">>, G) || G <- ReadyGuilds]), ?assertEqual([<<"111">>, <<"222">>], ReadyGuildIds), lists:foreach( fun(ReadyGuild) -> ?assertEqual(true, maps:get(<<"unavailable">>, ReadyGuild)) end, ReadyGuilds ); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end. dispatch_ready_data_nonbot_replaces_unavailable_placeholder_with_collected_guild_test() -> drain_mailbox(), CollectedGuild = #{ <<"id">> => <<"111">>, <<"name">> => <<"guild-111">>, <<"members">> => [#{<<"user">> => #{<<"id">> => <<"1">>}}], <<"channels">> => [] }, State0 = base_ready_state( <<"session-ready-replace-test">>, 46, false, #{111 => undefined, 222 => undefined}, [CollectedGuild] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> ReadyGuilds = maps:get(<<"guilds">>, ReadyData, []), ?assertEqual(2, length(ReadyGuilds)), FullGuild = find_ready_guild(<<"111">>, ReadyGuilds), PlaceholderGuild = find_ready_guild(<<"222">>, ReadyGuilds), ?assertEqual(<<"guild-111">>, maps:get(<<"name">>, FullGuild)), ?assertEqual(false, maps:is_key(<<"unavailable">>, FullGuild)), ?assertEqual(true, maps:get(<<"unavailable">>, PlaceholderGuild)); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end. dispatch_ready_data_bot_lists_every_guild_as_unavailable_test() -> drain_mailbox(), State0 = base_ready_state( <<"session-ready-bot-placeholder-test">>, 47, true, #{ 111 => undefined, 222 => cached_unavailable, 333 => unavailable, 444 => {self(), make_ref()} }, [] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, ReadySeq} -> ?assertEqual(1, ReadySeq), ?assertEqual( [ #{<<"id">> => <<"111">>, <<"unavailable">> => true}, #{<<"id">> => <<"222">>, <<"unavailable">> => true}, #{<<"id">> => <<"333">>, <<"unavailable">> => true}, #{<<"id">> => <<"444">>, <<"unavailable">> => true} ], lists:sort(maps:get(<<"guilds">>, ReadyData)) ); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end, receive {dispatch, guild_create, _GuildCreateData, _GuildCreateSeq} -> ?assert(false, unexpected_guild_create_for_bot_ready); {dispatch, guild_delete, _GuildDeleteData, _GuildDeleteSeq} -> ?assert(false, unexpected_guild_delete_for_bot_ready) after 100 -> ok end. dispatch_ready_data_bot_lists_collected_guild_as_unavailable_stub_test() -> drain_mailbox(), CollectedGuild = #{ <<"id">> => <<"111">>, <<"name">> => <<"guild-111">>, <<"members">> => [ #{<<"user">> => #{<<"id">> => <<"1">>, <<"username">> => <<"member-1">>}} ], <<"channels">> => [] }, State0 = base_ready_state( <<"session-ready-bot-collected-test">>, 50, true, #{111 => undefined, 222 => undefined}, [CollectedGuild] ), {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, ReadySeq} -> ?assertEqual(1, ReadySeq), ?assertEqual( [ #{<<"id">> => <<"111">>, <<"unavailable">> => true}, #{<<"id">> => <<"222">>, <<"unavailable">> => true} ], lists:sort(maps:get(<<"guilds">>, ReadyData)) ); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end, receive {dispatch, guild_create, GuildCreateData, GuildCreateSeq} -> ?assertEqual(2, GuildCreateSeq), ?assertEqual(<<"111">>, maps:get(<<"id">>, GuildCreateData)), ?assertEqual(<<"guild-111">>, maps:get(<<"name">>, GuildCreateData)), ?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData)), ?assertEqual( [#{<<"user">> => #{<<"id">> => <<"1">>}}], maps:get(<<"members">>, GuildCreateData) ); OtherGuild -> ?assert(false, {unexpected_guild_event, OtherGuild}) after 1000 -> ?assert(false, guild_create_not_dispatched) end, receive {dispatch, OtherEvent, _OtherData, _OtherSeq} -> ?assert(false, {unexpected_dispatch_after_burst, OtherEvent}) after 100 -> ok end. dispatch_ready_data_bot_marks_guilds_joined_before_ready_available_test() -> drain_mailbox(), CollectedGuild = #{<<"id">> => <<"111">>, <<"channels">> => [], <<"members">> => []}, UnavailableGuild = #{<<"id">> => <<"222">>, <<"unavailable">> => true}, State0 = base_ready_state( <<"session-ready-bot-join-test">>, 51, true, #{111 => {self(), make_ref()}, 222 => unavailable}, [UnavailableGuild, CollectedGuild] ), {noreply, State1} = session:handle_cast({guild_join, 111}, State0), {noreply, State2} = session:handle_cast({guild_join, 222}, State1), drain_mailbox(), {noreply, State3} = session_ready:dispatch_ready_data(State2), receive {dispatch, ready, ReadyData, 1} -> ?assertEqual( [ #{<<"id">> => <<"111">>, <<"unavailable">> => true}, #{<<"id">> => <<"222">>, <<"unavailable">> => true} ], lists:sort(maps:get(<<"guilds">>, ReadyData)) ) after 1000 -> ?assert(false, ready_not_dispatched) end, receive {dispatch, guild_create, BurstCreateData, 2} -> ?assertEqual(<<"111">>, maps:get(<<"id">>, BurstCreateData)), ?assertEqual(false, maps:get(<<"unavailable">>, BurstCreateData)) after 1000 -> ?assert(false, guild_create_not_dispatched) end, receive {dispatch, guild_delete, BurstDeleteData, 3} -> ?assertEqual(<<"222">>, maps:get(<<"id">>, BurstDeleteData)), ?assertEqual(true, maps:get(<<"unavailable">>, BurstDeleteData)) after 1000 -> ?assert(false, guild_delete_not_dispatched) end, RecoveredGuild = #{<<"id">> => <<"222">>, <<"channels">> => [], <<"members">> => []}, {noreply, _State4} = session_ready:process_guild_state(RecoveredGuild, State3), receive {dispatch, guild_create, RecoveredCreateData, 4} -> ?assertEqual(<<"222">>, maps:get(<<"id">>, RecoveredCreateData)), ?assertEqual(false, maps:get(<<"unavailable">>, RecoveredCreateData)) after 1000 -> ?assert(false, recovered_guild_create_not_dispatched) end, drain_mailbox(). dispatch_ready_data_includes_shard_metadata_test() -> drain_mailbox(), State0 = (base_ready_state( <<"session-ready-shard-test">>, 45, true, #{}, [] ))#{ shard => {1, 4} }, {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> ?assertEqual([1, 4], maps:get(<<"shard">>, ReadyData)); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end. dispatch_ready_data_includes_gateway_timings_test() -> drain_mailbox(), GwTimings = gateway_timings:record( test_gateway_step, gateway_timings:start() - 10, gateway_timings:new() ), State0 = (base_ready_state( <<"session-ready-gateway-timings-test">>, 48, false, #{}, [] ))#{ gw_timings => GwTimings, is_staff => true }, {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> Timings = maps:get(<<"_timings_gw">>, ReadyData), ?assertEqual(<<"microseconds">>, maps:get(<<"unit">>, Timings)), ?assert(is_binary(maps:get(<<"pod_name">>, Timings))), ?assertNot(maps:is_key(<<"node_name">>, Timings)), ?assertNot(maps:is_key(<<"erlang_node_name">>, Timings)), Trace = maps:get(<<"trace">>, Timings), TraceNames = [maps:get(<<"name">>, Span) || Span <- Trace], ?assert(lists:member(<<"test_gateway_step">>, TraceNames)), ReadySpan = find_trace_span( <<"session_ready_dispatch:dispatch_ready_to_socket/1">>, Trace ), ReadyChildren = maps:get(<<"children">>, ReadySpan), ReadyChildNames = [maps:get(<<"name">>, Span) || Span <- ReadyChildren], ?assert( lists:member( <<"session_ready_collect:collect_ready_presences/2">>, ReadyChildNames ) ), ?assert( lists:member( <<"session_ready_collect:collect_ready_users/2">>, ReadyChildNames ) ), ?assert( lists:member( <<"session_ready_dispatch:build_final_ready_data/9">>, ReadyChildNames ) ), ?assertNot(maps:is_key(<<"role">>, Timings)), ?assertNot(maps:is_key(<<"steps">>, Timings)), ?assertNot(maps:is_key(<<"nodes">>, Timings)); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end. dispatch_ready_data_omits_timings_for_non_staff_test() -> drain_mailbox(), GwTimings = gateway_timings:record( test_gateway_step, gateway_timings:start() - 10, gateway_timings:new() ), ApiTimings = #{ <<"unit">> => <<"microseconds">>, <<"total_us">> => 1234, <<"pod_name">> => <<"fluxer-api-abc123">> }, BaseState = base_ready_state( <<"session-ready-non-staff-timings-test">>, 49, false, #{}, [] ), State0 = BaseState#{ gw_timings => GwTimings, is_staff => false, ready => (maps:get(ready, BaseState))#{<<"_timings">> => ApiTimings} }, {noreply, _State1} = session_ready:dispatch_ready_data(State0), receive {dispatch, ready, ReadyData, _ReadySeq} -> ?assertNot(maps:is_key(<<"_timings_gw">>, ReadyData)), ?assertNot(maps:is_key(<<"_timings">>, ReadyData)); OtherReady -> ?assert(false, {unexpected_ready_message, OtherReady}) after 1000 -> ?assert(false, ready_not_dispatched) end. -spec find_trace_span(binary(), [map()]) -> map(). find_trace_span(Name, Trace) -> case [Span || Span <- Trace, maps:get(<<"name">>, Span, undefined) =:= Name] of [Span | _] -> Span; [] -> error({trace_span_not_found, Name}) end. -spec drain_mailbox() -> ok. drain_mailbox() -> receive _Message -> drain_mailbox() after 0 -> ok end. -spec base_ready_state(binary(), integer(), boolean(), map(), [map()]) -> map(). base_ready_state(SessionId, UserId, Bot, Guilds, CollectedGuildStates) -> #{ id => SessionId, user_id => UserId, version => 1, ready => #{<<"v">> => 9, <<"guilds">> => []}, bot => Bot, guilds => Guilds, channels => #{}, relationships => #{}, collected_guild_states => CollectedGuildStates, collected_sessions => [], seq => 0, buffer => limited_deque:new(4096, 16777216), socket_pid => self(), ignored_events => #{}, suppress_presence_updates => false, pending_presences => [], debounce_reactions => false, reaction_buffer => [], reaction_buffer_timer => undefined, presence_pid => undefined }. -spec base_state_for_guild_dispatch_test() -> map(). base_state_for_guild_dispatch_test() -> #{ guilds => #{}, ready => undefined, seq => 0, buffer => limited_deque:new(4096, 16777216), socket_pid => undefined, ignored_events => #{}, channels => #{}, relationships => #{}, suppress_presence_updates => false, pending_presences => [], presence_pid => undefined, debounce_reactions => false, reaction_buffer => [], reaction_buffer_timer => undefined, collected_guild_states => [] }. collect_ready_presences_does_not_add_one_to_one_dm_as_global_target_test() -> {ok, CachePid} = maybe_start_presence_cache(), OnlineDmUser = #{ <<"status">> => <<"online">>, <<"user">> => #{<<"id">> => <<"2">>} }, OnlineStranger = #{ <<"status">> => <<"online">>, <<"user">> => #{<<"id">> => <<"99">>} }, ok = presence_cache:put(2, OnlineDmUser), ok = presence_cache:put(99, OnlineStranger), _ = sys:get_state(CachePid), State = #{ bot => false, user_id => 1, ready => #{}, relationships => #{}, channels => #{ 100 => #{ <<"id">> => <<"100">>, <<"type">> => 1, <<"recipients">> => [#{<<"id">> => <<"2">>, <<"username">> => <<"dm-user">>}] } } }, Presences = session_ready_collect:collect_ready_presences(State, []), PresenceIds = [ maps:get(<<"id">>, maps:get(<<"user">>, P, #{}), undefined) || P <- Presences ], ?assertEqual([], PresenceIds), ?assertEqual(ok, gen_server:stop(CachePid)). collect_ready_presences_includes_friend_without_dm_recipients_test() -> {ok, CachePid} = maybe_start_presence_cache(), OnlineFriend = #{ <<"status">> => <<"online">>, <<"user">> => #{<<"id">> => <<"2">>} }, ok = presence_cache:put(2, OnlineFriend), _ = sys:get_state(CachePid), Ready = #{ <<"relationships">> => [ #{<<"id">> => <<"2">>, <<"type">> => 1, <<"user">> => #{<<"id">> => <<"2">>}} ], <<"private_channels">> => [ #{<<"id">> => <<"100">>, <<"type">> => 1} ] }, State = #{ bot => false, user_id => 1, ready => Ready, relationships => session_init:load_relationships(Ready), channels => session_init:load_private_channels(Ready) }, Presences = session_ready_collect:collect_ready_presences(State, []), PresenceIds = [ maps:get(<<"id">>, maps:get(<<"user">>, P, #{}), undefined) || P <- Presences ], ?assertEqual([<<"2">>], PresenceIds), ?assertEqual(ok, gen_server:stop(CachePid)). collect_ready_presences_uses_live_friend_presence_when_cache_missing_test() -> {ok, CachePid} = maybe_start_presence_cache(), {ManagerPid, ManagerStarted} = maybe_start_presence_manager(), TargetId = 32, {ok, TargetPid} = presence_manager:start_or_lookup(presence_request(TargetId)), {ok, _TargetSessions} = gen_server:call( TargetPid, {session_connect, presence_connect_req(<<"target-ready-session">>)}, 5000 ), _ = sys:get_state(CachePid), ok = presence_cache:delete(TargetId), _ = sys:get_state(CachePid), ?assertEqual(not_found, presence_cache:get(TargetId)), Ready = #{ <<"relationships">> => [ #{ <<"id">> => integer_to_binary(TargetId), <<"type">> => 1, <<"user">> => #{<<"id">> => integer_to_binary(TargetId)} } ] }, State = #{ bot => false, user_id => 1, ready => Ready, relationships => session_init:load_relationships(Ready), channels => #{} }, Presences = session_ready_collect:collect_ready_presences(State, []), ?assertEqual([TargetId], presence_ids(Presences)), _ = sys:get_state(CachePid), ?assertMatch({ok, _}, presence_cache:get(TargetId)), ok = gen_server:stop(TargetPid), stop_presence_manager(ManagerPid, ManagerStarted), ?assertEqual(ok, gen_server:stop(CachePid)). collect_ready_presences_skips_offline_dm_recipients_test() -> {ok, CachePid} = maybe_start_presence_cache(), State = #{ bot => false, user_id => 1, ready => #{}, relationships => #{}, channels => #{ 100 => #{ <<"id">> => <<"100">>, <<"type">> => 1, <<"recipients">> => [#{<<"id">> => <<"5">>, <<"username">> => <<"dm-user">>}] } } }, ?assertEqual([], session_ready_collect:collect_ready_presences(State, [])), ?assertEqual(ok, gen_server:stop(CachePid)). maybe_start_presence_cache() -> case whereis(presence_cache) of undefined -> presence_cache:start_link(); Existing when is_pid(Existing) -> {ok, Existing} end. maybe_start_presence_manager() -> process_registry:init(), case whereis(presence_manager) of undefined -> case presence_manager:start_link() of {ok, Pid} -> {Pid, true}; {error, {already_started, Pid}} -> {Pid, false} end; Existing when is_pid(Existing) -> {Existing, false} end. stop_presence_manager(_Pid, false) -> ok; stop_presence_manager(Pid, true) -> try gen_server:stop(Pid) of ok -> ok catch error:_ -> ok; exit:_ -> ok end. presence_request(UserId) -> #{ user_id => UserId, user_data => #{ <<"id">> => integer_to_binary(UserId), <<"username">> => <<"ready-target">>, <<"discriminator">> => <<"0001">>, <<"avatar">> => null, <<"flags">> => 0 }, guild_ids => [], friend_ids => [], group_dm_recipients => #{}, status => online, custom_status => null }. presence_connect_req(SessionId) -> #{ session_id => SessionId, status => online, afk => false, mobile => false, socket_pid => undefined }. presence_ids(Presences) -> [ snowflake_id:parse_maybe(maps:get(<<"id">>, maps:get(<<"user">>, P, #{}), undefined)) || P <- Presences ]. -spec find_ready_guild(binary(), [map()]) -> map(). find_ready_guild(GuildId, ReadyGuilds) -> case [G || G <- ReadyGuilds, maps:get(<<"id">>, G, undefined) =:= GuildId] of [Guild] -> Guild; [] -> error({missing_ready_guild, GuildId}) end.