concord 2.5.1

A terminal user interface client for Discord
use super::*;
use crate::discord::{GuildOnboardingInfo, GuildOnboardingMode};
use serde_json::json;
use std::sync::Arc;

fn onboarding(guild_id: Id<GuildMarker>, enabled: bool) -> GuildOnboardingInfo {
    let raw = json!({
        "guild_id": guild_id.to_string(),
        "enabled": enabled,
        "mode": 0,
        "default_channel_ids": [],
        "prompts": [],
        "future_field": "kept"
    });
    GuildOnboardingInfo {
        guild_id,
        enabled: Some(enabled),
        mode: Some(GuildOnboardingMode::Default),
        default_channel_ids: Vec::new(),
        raw: Arc::new(raw),
    }
}

#[test]
fn guild_partial_updates_preserve_and_replace_optional_metadata() {
    let guild_id = Id::new(1);
    let mut state = DiscordState::default();
    state.apply_event(&guild_create_event(GuildCreateFixture {
        guild_id,
        onboarding: Some(onboarding(guild_id, false)),
        features: vec!["COMMUNITY".to_owned(), "FUTURE_FEATURE".to_owned()],
        ..GuildCreateFixture::new(guild_id)
    }));

    let cached = state
        .guild(guild_id)
        .and_then(|guild| guild.onboarding.as_ref())
        .expect("onboarding should be cached");
    assert_eq!(cached.enabled, Some(false));
    assert_eq!(cached.raw["future_field"], json!("kept"));
    assert!(state.guild_has_feature(guild_id, "COMMUNITY"));
    assert!(state.guild_has_feature(guild_id, "FUTURE_FEATURE"));

    state.apply_event(&guild_update_event(GuildUpdateFixture {
        guild_id,
        name: "renamed".to_owned(),
        ..GuildUpdateFixture::new()
    }));
    assert_eq!(
        state
            .guild(guild_id)
            .and_then(|guild| guild.onboarding.as_ref())
            .and_then(|onboarding| onboarding.enabled),
        Some(false)
    );
    assert!(state.guild_has_feature(guild_id, "COMMUNITY"));

    state.apply_event(&AppEvent::GuildOnboardingUpdate {
        guild_id,
        onboarding: onboarding(guild_id, true),
    });
    assert_eq!(
        state
            .guild(guild_id)
            .and_then(|guild| guild.onboarding.as_ref())
            .and_then(|onboarding| onboarding.enabled),
        Some(true)
    );

    state.apply_event(&guild_update_event(GuildUpdateFixture {
        guild_id,
        name: "renamed again".to_owned(),
        features: Some(vec!["MEMBER_VERIFICATION_GATE_ENABLED".to_owned()]),
        ..GuildUpdateFixture::new()
    }));
    assert!(!state.guild_has_feature(guild_id, "COMMUNITY"));
    assert!(state.guild_has_feature(guild_id, "MEMBER_VERIFICATION_GATE_ENABLED"));
}

#[test]
fn guild_outage_preserves_cache_and_membership_removal_clears_it() {
    let guild_id = Id::new(1);
    let mut state = DiscordState::default();

    state.apply_event(&guild_create_event(GuildCreateFixture {
        guild_id,
        emojis: vec![CustomEmojiInfo {
            animated: true,
            ..CustomEmojiInfo::test(Id::new(50), "party")
        }],
        ..GuildCreateFixture::new(guild_id)
    }));

    assert_eq!(state.custom_emojis_for_guild(guild_id).len(), 1);
    assert_eq!(state.custom_emojis_for_guild(guild_id)[0].name, "party");

    state.apply_event(&AppEvent::GuildUnavailable { guild_id });

    assert!(state.guild(guild_id).is_some());
    assert_eq!(state.custom_emojis_for_guild(guild_id).len(), 1);

    state.apply_event(&AppEvent::GuildDelete { guild_id });

    assert!(state.guild(guild_id).is_none());
    assert!(state.custom_emojis_for_guild(guild_id).is_empty());
}

#[test]
fn guild_emojis_update_replaces_cached_custom_emojis() {
    let guild_id = Id::new(1);
    let mut state = DiscordState::default();

    state.apply_event(&guild_create_event(GuildCreateFixture {
        guild_id,
        emojis: vec![CustomEmojiInfo::test(Id::new(50), "party")],
        ..GuildCreateFixture::new(guild_id)
    }));
    state.apply_event(&AppEvent::GuildEmojisUpdate {
        guild_id,
        emojis: vec![CustomEmojiInfo {
            animated: true,
            ..CustomEmojiInfo::test(Id::new(60), "wave")
        }],
    });

    let emojis = state.custom_emojis_for_guild(guild_id);
    assert_eq!(emojis.len(), 1);
    assert_eq!(emojis[0].id, Id::new(60));
    assert_eq!(emojis[0].name, "wave");
    assert!(emojis[0].animated);
}

#[test]
fn guild_update_replaces_custom_emojis_only_when_the_field_is_present() {
    let guild_id = Id::new(1);
    let cached = || {
        let mut state = DiscordState::default();
        state.apply_event(&guild_create_event(GuildCreateFixture {
            guild_id,
            emojis: vec![CustomEmojiInfo::test(Id::new(50), "party")],
            ..GuildCreateFixture::new(guild_id)
        }));
        state
    };

    let mut replaced = cached();
    replaced.apply_event(&guild_update_event(GuildUpdateFixture {
        guild_id,
        name: "guild renamed".to_owned(),
        emojis: Some(vec![CustomEmojiInfo {
            animated: true,
            ..CustomEmojiInfo::test(Id::new(70), "dance")
        }]),
        ..GuildUpdateFixture::new()
    }));
    let emojis = replaced.custom_emojis_for_guild(guild_id);
    assert_eq!(emojis.len(), 1);
    assert_eq!(emojis[0].id, Id::new(70));
    assert_eq!(emojis[0].name, "dance");

    // A rename that omits `emojis` must not be read as "the guild has none".
    let mut renamed_only = cached();
    renamed_only.apply_event(&guild_update_event(GuildUpdateFixture {
        guild_id,
        name: "guild renamed".to_owned(),
        ..GuildUpdateFixture::new()
    }));
    let emojis = renamed_only.custom_emojis_for_guild(guild_id);
    assert_eq!(emojis.len(), 1);
    assert_eq!(emojis[0].name, "party");
}