iris-chat 0.1.67

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;
use nostr::{EventBuilder, Keys, Tag, Timestamp};
use tempfile::TempDir;

fn follow_event(keys: &Keys, created_at: u64, followed: &[PublicKey]) -> Event {
    let tags = followed
        .iter()
        .map(|owner| Tag::parse(vec!["p".to_string(), owner.to_hex()]).unwrap())
        .collect::<Vec<_>>();
    follow_event_with_tags(keys, created_at, tags)
}

fn follow_event_with_tags(keys: &Keys, created_at: u64, tags: Vec<Tag>) -> Event {
    EventBuilder::new(Kind::ContactList, "")
        .tags(tags)
        .custom_created_at(Timestamp::from(created_at))
        .sign_with_keys(keys)
        .unwrap()
}

fn record(owner: PublicKey, position: u32) -> DiscoveredUserRecord {
    DiscoveredUserRecord {
        owner_pubkey_hex: owner.to_hex(),
        follow_position: position,
        petname: None,
    }
}

fn positions(users: &BTreeMap<String, DiscoveredUserRecord>, owners: &[PublicKey]) -> Vec<u32> {
    owners
        .iter()
        .map(|owner| users[&owner.to_hex()].follow_position)
        .collect()
}

#[test]
fn direct_follow_opinions_change_order_without_unknown_authors() {
    let root = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let carol = Keys::generate();
    let owners = [alice.public_key(), bob.public_key(), carol.public_key()];
    let root_event = follow_event(&root, 100, &owners);

    let obsolete = follow_event(&alice, 150, &[bob.public_key()]);
    let mut newest_tags = (0..MAX_PEER_SOCIAL_EDGES + 1)
        .map(|_| Tag::public_key(Keys::generate().public_key()))
        .collect::<Vec<_>>();
    newest_tags.push(Tag::public_key(carol.public_key()));
    let newest = follow_event_with_tags(&alice, 200, newest_tags);
    let mut invalid_newest = follow_event(&alice, 300, &[bob.public_key()]);
    invalid_newest.content.push_str("tampered");
    let bob_opinion = follow_event(&bob, 200, &[carol.public_key()]);

    let ranked = build_verified_social_ranking(
        root.public_key(),
        &root_event,
        &owners,
        &owners,
        vec![obsolete, newest, invalid_newest, bob_opinion],
        1_000,
    )
    .unwrap()
    .followed_owners;

    assert_eq!(
        ranked,
        [carol.public_key(), alice.public_key(), bob.public_key()]
    );
}

#[test]
fn future_dated_peer_head_degrades_social_order() {
    let root = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let owners = [alice.public_key(), bob.public_key()];
    let root_event = follow_event(&root, 100, &owners);
    let future = follow_event(&alice, 1_601, &[bob.public_key()]);

    assert!(build_verified_social_ranking(
        root.public_key(),
        &root_event,
        &owners,
        &owners,
        vec![future],
        1_000,
    )
    .is_none());
}

#[test]
fn verified_empty_root_is_a_successful_empty_rank() {
    let root = Keys::generate();
    let root_event = follow_event(&root, 100, &[]);

    assert_eq!(
        build_verified_social_ranking(root.public_key(), &root_event, &[], &[], Vec::new(), 1_000,)
            .map(|ranking| ranking.followed_owners),
        Some(Vec::new())
    );
}

#[test]
fn verified_friend_lists_rank_nonfollowed_targets() {
    let root = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let global = Keys::generate();
    let owners = [alice.public_key(), bob.public_key()];
    let ranking = build_verified_social_ranking(
        root.public_key(),
        &follow_event(&root, 100, &owners),
        &owners,
        &owners,
        vec![
            follow_event(&alice, 101, &[global.public_key()]),
            follow_event(&bob, 102, &[global.public_key()]),
        ],
        1_000,
    )
    .unwrap();

    assert_eq!(ranking.friend_support[&global.public_key().to_hex()], 2);
    assert!(!ranking.followed_owners.contains(&global.public_key()));
}

#[test]
fn peer_edge_bound_counts_unique_effective_targets() {
    let author = Keys::generate();
    let targets = (0..MAX_PEER_SOCIAL_EDGES)
        .map(|_| Keys::generate().public_key())
        .collect::<Vec<_>>();
    let mut candidates = targets.iter().copied().collect::<HashSet<_>>();
    let mut tags = vec![
        Tag::parse(vec!["p".to_string(), author.public_key().to_hex()]).unwrap(),
        Tag::parse(["subject", "ignored"]).unwrap(),
    ];
    for target in &targets {
        let tag = Tag::parse(vec!["p".to_string(), target.to_hex()]).unwrap();
        tags.push(tag.clone());
        tags.push(tag);
    }
    let at_limit = follow_event_with_tags(&author, 100, tags.clone());
    assert_eq!(
        bounded_peer_targets(&at_limit, &candidates).unwrap().len(),
        MAX_PEER_SOCIAL_EDGES
    );

    let extra = Keys::generate().public_key();
    candidates.insert(extra);
    tags.push(Tag::parse(vec!["p".to_string(), extra.to_hex()]).unwrap());
    let over_limit = follow_event_with_tags(&author, 101, tags);
    assert!(bounded_peer_targets(&over_limit, &candidates).is_none());
}

#[test]
fn malformed_social_orders_restore_every_fresh_root_position() {
    let owners = (0..3)
        .map(|_| Keys::generate().public_key())
        .collect::<Vec<_>>();
    let foreign = Keys::generate().public_key();
    let mut users = owners
        .iter()
        .enumerate()
        .map(|(position, owner)| (owner.to_hex(), record(*owner, 90 + position as u32)))
        .collect::<BTreeMap<_, _>>();
    let valid = [owners[2], owners[0], owners[1]];
    apply_follow_order(&mut users, &owners, Some(&valid));
    assert_eq!(positions(&users, &owners), [1, 2, 0]);

    let malformed = [
        vec![owners[2], owners[0]],
        vec![owners[2], owners[2], owners[1]],
        vec![owners[2], owners[0], foreign],
    ];
    for order in malformed {
        for user in users.values_mut() {
            user.follow_position = 99;
        }
        apply_follow_order(&mut users, &owners, Some(&order));
        assert_eq!(positions(&users, &owners), [0, 1, 2]);
    }
}

#[test]
fn derived_order_persists_offline_and_degraded_refresh_restores_root() {
    let temp = TempDir::new().unwrap();
    let mut store = AppStore::new(open_database(temp.path()).unwrap());
    let owners = (0..3)
        .map(|_| Keys::generate().public_key())
        .collect::<Vec<_>>();
    let mut cache = UserDiscoveryCache {
        follow_event_json: None,
        owner_pubkey_hex: Some("root".to_string()),
        follow_event_id: Some("root".to_string()),
        follow_created_at_secs: 100,
        users: owners
            .iter()
            .enumerate()
            .map(|(position, owner)| (owner.to_hex(), record(*owner, position as u32)))
            .collect(),
        social_rank_ready: true,
        social_friend_support: BTreeMap::from([(owners[2].to_hex(), 2)]),
        social_graph: None,
        social_opinion_events: Vec::new(),
    };
    let social = [owners[2], owners[0], owners[1]];
    apply_follow_order(&mut cache.users, &owners, Some(&social));
    store.replace_user_discovery(&cache).unwrap();
    let mut restored = store.load_user_discovery().unwrap();
    assert_eq!(positions(&restored.users, &owners), [1, 2, 0]);

    apply_follow_order(&mut restored.users, &owners, Some(&social[..2]));
    store.replace_user_discovery(&restored).unwrap();
    let restored = store.load_user_discovery().unwrap();
    assert_eq!(positions(&restored.users, &owners), [0, 1, 2]);
}