use super::*;
use crate::discord::VoiceScope;
#[test]
fn tracks_members_and_presences() {
let guild_id = Id::new(1);
let alice = Id::new(10);
let bob = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(100),
members: vec![member_info(alice, "alice"), member_info(bob, "bob")],
presences: vec![(alice, PresenceStatus::Online)],
..GuildCreateFixture::new(guild_id)
}));
let members = state.members_for_guild(guild_id);
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(100));
assert_eq!(members.len(), 2);
let alice_state = members.iter().find(|m| m.user_id == alice).unwrap();
assert_eq!(alice_state.status, PresenceStatus::Online);
let bob_state = members.iter().find(|m| m.user_id == bob).unwrap();
assert_eq!(bob_state.status, PresenceStatus::Unknown);
state.apply_event(&AppEvent::PresenceUpdate {
guild_id: Some(guild_id),
presence: crate::discord::PresenceEventFields {
user_id: bob,
status: PresenceStatus::Idle,
activities: Vec::new(),
},
});
assert_eq!(
state
.members_for_guild(guild_id)
.iter()
.find(|m| m.user_id == bob)
.unwrap()
.status,
PresenceStatus::Idle,
);
state.apply_event(&AppEvent::PresenceUpdate {
guild_id: None,
presence: crate::discord::PresenceEventFields {
user_id: bob,
status: PresenceStatus::DoNotDisturb,
activities: Vec::new(),
},
});
assert_eq!(
state.user_presence_for_guild(Some(guild_id), bob),
Some(PresenceStatus::DoNotDisturb)
);
assert_eq!(
state
.members_for_guild(guild_id)
.into_iter()
.find(|member| member.user_id == bob)
.map(|member| member.status),
Some(PresenceStatus::DoNotDisturb)
);
}
#[test]
fn user_identity_update_preserves_guild_member_avatar() {
let guild_id = Id::new(1);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(1),
members: vec![MemberInfo {
avatar_url: Some(
"https://cdn.discordapp.com/guilds/1/users/10/avatars/guild.png".to_owned(),
),
..member_info(user_id, "neo")
}],
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&user_identity_update_event(UserIdentityUpdateFixture {
user_id,
username: "neo".to_owned(),
global_name: Some("Neo".to_owned()),
avatar_url: Some("https://cdn.discordapp.com/avatars/10/global.png".to_owned()),
..UserIdentityUpdateFixture::new()
}));
let member = state
.members_for_guild(guild_id)
.into_iter()
.find(|member| member.user_id == user_id)
.expect("member should remain cached");
assert_eq!(
member.avatar_url.as_deref(),
Some("https://cdn.discordapp.com/guilds/1/users/10/avatars/guild.png")
);
}
#[test]
fn tracks_voice_participants_join_move_and_leave() {
let guild_id = Id::new(1);
let first_voice = Id::new(10);
let second_voice = Id::new(11);
let alice = Id::new(20);
let bob = Id::new(21);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Alice".to_owned(),
user_id: Some(alice),
});
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(2),
channels: vec![
guild_voice_channel(guild_id, first_voice),
ChannelInfo {
name: "Raid".to_owned(),
position: Some(1),
..guild_voice_channel(guild_id, second_voice)
},
],
..GuildCreateFixture::new(guild_id)
}));
let alice_member = member_with_username(alice, "Alice", "alice");
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
member: Some(alice_member),
self_mute: true,
self_stream: true,
..voice_state(guild_id, Some(first_voice), alice)
},
});
let first_voice_participants = state.voice_participants_for_channel(guild_id, first_voice);
assert_eq!(first_voice_participants[0].display_name, "Alice");
assert!(first_voice_participants[0].self_stream);
assert!(!first_voice_participants[0].speaking);
assert_eq!(
state.current_user_voice_connection(),
Some(CurrentVoiceConnectionState {
self_mute: true,
..CurrentVoiceConnectionState::test(guild_id, first_voice)
})
);
state.apply_event(&voice_speaking_update_event(VoiceSpeakingUpdateFixture {
scope: VoiceScope::Guild(guild_id),
channel_id: first_voice,
user_id: alice,
speaking: true,
}));
assert!(state.voice_participants_for_channel(guild_id, first_voice)[0].speaking);
assert!(state.current_user_voice_speaking());
assert!(state.user_voice_speaking_in_guild(guild_id, alice));
assert!(!state.user_voice_speaking_in_guild(Id::new(999), alice));
let bob_member = member_with_username(bob, "Bob", "bob");
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
member: Some(bob_member),
..voice_state(guild_id, Some(first_voice), bob)
},
});
state.apply_event(&voice_speaking_update_event(VoiceSpeakingUpdateFixture {
scope: VoiceScope::Guild(guild_id),
channel_id: first_voice,
user_id: bob,
speaking: true,
}));
let first_voice_participants = state.voice_participants_for_channel(guild_id, first_voice);
assert_eq!(first_voice_participants.len(), 2);
assert!(
first_voice_participants
.iter()
.any(|participant| participant.user_id == bob && participant.speaking)
);
state.apply_event(&AppEvent::VoiceStateUpdate {
state: voice_state(guild_id, Some(second_voice), alice),
});
let first_voice_participants = state.voice_participants_for_channel(guild_id, first_voice);
assert_eq!(first_voice_participants.len(), 1);
assert_eq!(first_voice_participants[0].user_id, bob);
assert!(!first_voice_participants[0].speaking);
assert_eq!(
state.voice_participants_for_channel(guild_id, second_voice)[0].user_id,
alice
);
assert!(!state.voice_participants_for_channel(guild_id, second_voice)[0].speaking);
assert!(!state.current_user_voice_speaking());
assert_eq!(
state.current_user_voice_connection(),
Some(CurrentVoiceConnectionState::test(guild_id, second_voice))
);
state.apply_event(&AppEvent::VoiceStateUpdate {
state: voice_state(guild_id, Some(second_voice), bob),
});
state.apply_event(&voice_speaking_update_event(VoiceSpeakingUpdateFixture {
scope: VoiceScope::Guild(guild_id),
channel_id: second_voice,
user_id: bob,
speaking: true,
}));
assert!(
state
.voice_participants_for_channel(guild_id, second_voice)
.iter()
.any(|participant| participant.user_id == bob && participant.speaking)
);
state.apply_event(&AppEvent::VoiceStateUpdate {
state: voice_state(guild_id, None, alice),
});
let second_voice_participants = state.voice_participants_for_channel(guild_id, second_voice);
assert_eq!(second_voice_participants.len(), 1);
assert_eq!(second_voice_participants[0].user_id, bob);
assert!(!second_voice_participants[0].speaking);
assert_eq!(state.current_user_voice_connection(), None);
}
#[test]
fn tracks_dm_call_participants_resolving_names_from_recipients() {
let dm_channel = Id::new(50);
let me = Id::new(20);
let friend = Id::new(21);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Me".to_owned(),
user_id: Some(me),
});
state.apply_event(&AppEvent::ChannelUpsert(dm_channel_with_recipients(
dm_channel,
"",
"group-dm",
vec![ChannelRecipientInfo::test(friend, "Friend")],
)));
for user_id in [me, friend] {
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(dm_channel), user_id)
},
});
}
let participants = state.voice_participants_for_private_channel(dm_channel);
assert_eq!(participants.len(), 2);
assert!(
participants
.iter()
.any(|participant| participant.user_id == me && participant.display_name == "Me")
);
assert!(
participants.iter().any(
|participant| participant.user_id == friend && participant.display_name == "Friend"
)
);
assert!(
state
.voice_participants_for_channel(Id::new(1), dm_channel)
.is_empty()
);
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), None, friend)
},
});
let participants = state.voice_participants_for_private_channel(dm_channel);
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].user_id, me);
}
#[test]
fn moving_between_dm_calls_does_not_leave_the_user_in_the_old_call() {
let first_dm = Id::new(50);
let second_dm = Id::new(51);
let me = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Me".to_owned(),
user_id: Some(me),
});
for dm_channel in [first_dm, second_dm] {
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(dm_channel), me)
},
});
}
assert!(
state
.voice_participants_for_private_channel(first_dm)
.is_empty(),
"the user should no longer appear in the call they left"
);
let current = state.voice_participants_for_private_channel(second_dm);
assert_eq!(current.len(), 1);
assert_eq!(current[0].user_id, me);
assert_eq!(
state
.current_user_voice_connection()
.map(|voice| voice.scope),
Some(VoiceScope::Private(second_dm))
);
}
#[test]
fn leaving_a_dm_call_clears_stale_speaking_in_the_old_call() {
let first_dm = Id::new(50);
let second_dm = Id::new(51);
let me = Id::new(20);
let friend = Id::new(21);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Me".to_owned(),
user_id: Some(me),
});
state.apply_event(&AppEvent::ChannelUpsert(dm_channel_with_recipients(
first_dm,
"",
"group-dm",
vec![ChannelRecipientInfo::test(friend, "Friend")],
)));
for user_id in [me, friend] {
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(first_dm), user_id)
},
});
}
state.apply_event(&voice_speaking_update_event(VoiceSpeakingUpdateFixture {
scope: VoiceScope::Private(first_dm),
channel_id: first_dm,
user_id: friend,
speaking: true,
}));
assert!(
state
.voice_participants_for_private_channel(first_dm)
.iter()
.any(|participant| participant.user_id == friend && participant.speaking)
);
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(second_dm), me)
},
});
assert!(
state
.voice_participants_for_private_channel(first_dm)
.iter()
.all(|participant| !participant.speaking)
);
}
#[test]
fn call_delete_clears_a_dm_calls_participants() {
let dm = Id::new(50);
let me = Id::new(20);
let friend = Id::new(21);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Me".to_owned(),
user_id: Some(me),
});
state.apply_event(&AppEvent::ChannelUpsert(dm_channel_with_recipients(
dm,
"",
"group-dm",
vec![ChannelRecipientInfo::test(friend, "Friend")],
)));
for user_id in [me, friend] {
state.apply_event(&AppEvent::VoiceStateUpdate {
state: VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(dm), user_id)
},
});
}
assert_eq!(state.voice_participants_for_private_channel(dm).len(), 2);
state.apply_event(&AppEvent::CallDelete { channel_id: dm });
assert!(state.voice_participants_for_private_channel(dm).is_empty());
}
#[test]
fn dm_call_join_and_leave_both_chime() {
use crate::discord::VoiceSoundKind;
let dm = Id::new(50);
let me = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::Ready {
user: "Me".to_owned(),
user_id: Some(me),
});
let join = VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), Some(dm), me)
};
assert_eq!(
state.voice_sound_for_state_update(&join),
Some(VoiceSoundKind::Join)
);
state.apply_event(&AppEvent::VoiceStateUpdate { state: join });
let leave = VoiceStateInfo {
guild_id: None,
..voice_state(Id::new(1), None, me)
};
assert_eq!(
state.voice_sound_for_state_update(&leave),
Some(VoiceSoundKind::Leave)
);
}
#[test]
fn guild_create_replaces_cached_voice_state_snapshot() {
let guild_id = Id::new(1);
let voice = Id::new(10);
let alice = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(1),
channels: vec![guild_voice_channel(guild_id, voice)],
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&AppEvent::VoiceStateUpdate {
state: voice_state(guild_id, Some(voice), alice),
});
assert_eq!(
state.voice_participants_for_channel(guild_id, voice)[0].user_id,
alice
);
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(1),
channels: vec![guild_voice_channel(guild_id, voice)],
..GuildCreateFixture::new(guild_id)
}));
assert!(
state
.voice_participants_for_channel(guild_id, voice)
.is_empty()
);
}
#[test]
fn presence_update_does_not_create_fallback_member() {
let guild_id = Id::new(1);
let user_id = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(100),
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&AppEvent::PresenceUpdate {
guild_id: Some(guild_id),
presence: crate::discord::PresenceEventFields {
user_id,
status: PresenceStatus::Idle,
activities: Vec::new(),
},
});
assert!(state.members_for_guild(guild_id).is_empty());
assert_eq!(state.user_presence(user_id), Some(PresenceStatus::Idle));
}
#[test]
fn real_member_add_and_remove_update_known_member_count() {
let guild_id = Id::new(1);
let alice = Id::new(10);
let bob = Id::new(20);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(1),
members: vec![member_info(alice, "alice")],
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&AppEvent::GuildMemberUpsert {
guild_id,
member: member_info(bob, "bob"),
});
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(1));
state.apply_event(&AppEvent::GuildMemberAdd {
guild_id,
member: member_info(bob, "bob"),
});
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(1));
state.apply_event(&AppEvent::GuildMemberAdd {
guild_id,
member: member_info(Id::new(30), "carol"),
});
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(2));
state.apply_event(&AppEvent::GuildMemberRemove {
guild_id,
user_id: Id::new(30),
});
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(1));
}
#[test]
fn guild_member_remove_decrements_known_count_for_unloaded_member() {
let guild_id = Id::new(1);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
member_count: Some(3),
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&AppEvent::GuildMemberRemove {
guild_id,
user_id: Id::new(99),
});
assert_eq!(state.guild(guild_id).unwrap().member_count, Some(2));
assert!(state.members_for_guild(guild_id).is_empty());
}
#[test]
fn guild_create_caches_roles_and_member_role_ids() {
let guild_id = Id::new(1);
let role_id = Id::new(90);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
members: vec![member_with_roles(user_id, "alice", vec![role_id])],
roles: vec![RoleInfo {
color: Some(0xFFAA00),
position: 10,
hoist: true,
..RoleInfo::test(role_id, "Admin")
}],
..GuildCreateFixture::new(guild_id)
}));
let roles = state.roles_for_guild(guild_id);
assert_eq!(roles.len(), 1);
assert_eq!(roles[0].name, "Admin");
let members = state.members_for_guild(guild_id);
assert_eq!(members[0].role_ids, vec![role_id]);
}
#[test]
fn guild_role_events_patch_cached_roles() {
let guild_id = Id::new(1);
let role_id = Id::new(90);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture::new(guild_id)));
state.apply_event(&AppEvent::GuildRoleUpsert {
guild_id,
role: RoleInfo {
color: Some(0xFFAA00),
position: 10,
hoist: true,
permissions: 1024,
..RoleInfo::test(role_id, "Admin")
},
});
let roles = state.roles_for_guild(guild_id);
assert_eq!(roles.len(), 1);
assert_eq!(roles[0].name, "Admin");
assert_eq!(roles[0].color, Some(0xFFAA00));
state.apply_event(&AppEvent::GuildRoleUpsert {
guild_id,
role: RoleInfo {
color: Some(0x00AAFF),
position: 20,
hoist: false,
permissions: 2048,
..RoleInfo::test(role_id, "Owner")
},
});
let roles = state.roles_for_guild(guild_id);
assert_eq!(roles.len(), 1);
assert_eq!(roles[0].name, "Owner");
assert_eq!(roles[0].color, Some(0x00AAFF));
assert_eq!(roles[0].permissions, 2048);
state.apply_event(&AppEvent::GuildRoleDelete { guild_id, role_id });
assert!(state.roles_for_guild(guild_id).is_empty());
}
#[test]
fn message_author_role_color_uses_history_author_roles_when_member_is_missing() {
let guild_id = Id::new(1);
let channel_id = Id::new(2);
let message_id = Id::new(3);
let role_id = Id::new(90);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
roles: vec![RoleInfo {
color: Some(0xCC0000),
position: 10,
hoist: true,
..RoleInfo::test(role_id, "Red")
}],
..GuildCreateFixture::new(guild_id)
}));
let mut message = message_info(channel_id, message_id.get(), "hello");
message.guild_id = Some(guild_id);
message.author_id = user_id;
message.author_role_ids = vec![role_id];
state.apply_event(&latest_history_loaded(channel_id, vec![message]));
assert_eq!(
state.message_author_role_color(guild_id, channel_id, message_id, user_id),
Some(0xCC0000)
);
}
#[test]
fn message_author_role_color_uses_live_author_roles_when_member_is_missing() {
let guild_id = Id::new(1);
let channel_id = Id::new(2);
let message_id = Id::new(3);
let role_id = Id::new(90);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
roles: vec![RoleInfo {
color: Some(0xCC0000),
position: 10,
hoist: true,
..RoleInfo::test(role_id, "Red")
}],
..GuildCreateFixture::new(guild_id)
}));
state.apply_event(&message_create_event(MessageCreateFixture {
guild_id: Some(guild_id),
channel_id,
message_id,
author_id: user_id,
author: "test-user".to_owned(),
author_role_ids: vec![role_id],
content: Some("hello".to_owned()),
..MessageCreateFixture::test_fixture_default()
}));
assert_eq!(
state.message_author_role_color(guild_id, channel_id, message_id, user_id),
Some(0xCC0000)
);
}
#[test]
fn message_author_role_color_uses_profile_roles_when_message_roles_are_missing() {
let guild_id = Id::new(1);
let channel_id = Id::new(2);
let message_id = Id::new(3);
let role_id = Id::new(90);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
roles: vec![RoleInfo {
color: Some(0xCC0000),
position: 10,
hoist: true,
..RoleInfo::test(role_id, "Red")
}],
..GuildCreateFixture::new(guild_id)
}));
let mut message = message_info(channel_id, message_id.get(), "hello");
message.guild_id = Some(guild_id);
message.author_id = user_id;
state.apply_event(&latest_history_loaded(channel_id, vec![message]));
let mut profile = profile_info(user_id.get(), Some("test-user"));
profile.role_ids = vec![role_id];
state.apply_event(&AppEvent::UserProfileLoaded {
guild_id: Some(guild_id),
profile,
});
assert_eq!(
state.message_author_role_color(guild_id, channel_id, message_id, user_id),
Some(0xCC0000)
);
}
#[test]
fn message_author_role_color_does_not_use_message_roles_when_member_is_cached() {
let guild_id = Id::new(1);
let channel_id = Id::new(2);
let message_id = Id::new(3);
let stale_role_id = Id::new(90);
let user_id = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&guild_create_event(GuildCreateFixture {
members: vec![member_info(user_id, "test-user")],
roles: vec![RoleInfo {
color: Some(0xCC0000),
position: 10,
hoist: true,
..RoleInfo::test(stale_role_id, "Old Red")
}],
..GuildCreateFixture::new(guild_id)
}));
let mut message = message_info(channel_id, message_id.get(), "hello");
message.guild_id = Some(guild_id);
message.author_id = user_id;
message.author_role_ids = vec![stale_role_id];
state.apply_event(&latest_history_loaded(channel_id, vec![message]));
assert_eq!(
state.message_author_role_color(guild_id, channel_id, message_id, user_id),
None
);
}
#[test]
fn chunk_style_member_upserts_populate_member_list() {
let guild_id = Id::new(1);
let alice = Id::new(10);
let bob = Id::new(20);
let mut state = DiscordState::default();
for (user_id, display_name) in [(alice, "alice"), (bob, "bob")] {
state.apply_event(&AppEvent::GuildMemberUpsert {
guild_id,
member: member_info(user_id, display_name.to_owned()),
});
}
state.apply_event(&AppEvent::PresenceUpdate {
guild_id: Some(guild_id),
presence: crate::discord::PresenceEventFields {
user_id: alice,
status: PresenceStatus::Online,
activities: Vec::new(),
},
});
let members = state.members_for_guild(guild_id);
assert_eq!(members.len(), 2);
assert_eq!(
members
.iter()
.find(|member| member.user_id == alice)
.map(|member| member.status),
Some(PresenceStatus::Online)
);
assert_eq!(
members
.iter()
.find(|member| member.user_id == bob)
.map(|member| member.status),
Some(PresenceStatus::Unknown)
);
}
#[test]
fn member_upsert_preserves_existing_status() {
let guild_id = Id::new(1);
let user = Id::new(10);
let mut state = DiscordState::default();
state.apply_event(&AppEvent::GuildMemberUpsert {
guild_id,
member: member_info(user, "alice"),
});
state.apply_event(&AppEvent::PresenceUpdate {
guild_id: Some(guild_id),
presence: crate::discord::PresenceEventFields {
user_id: user,
status: PresenceStatus::Online,
activities: Vec::new(),
},
});
state.apply_event(&AppEvent::GuildMemberUpsert {
guild_id,
member: member_info(user, "alice-renamed"),
});
let member = state
.members_for_guild(guild_id)
.into_iter()
.find(|m| m.user_id == user)
.unwrap();
assert_eq!(member.display_name, "alice-renamed");
assert_eq!(member.status, PresenceStatus::Online);
}