iris-chat 0.1.66

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;

#[test]
fn first_contact_send_queues_text_until_peer_protocol_state_is_ready() {
    let alice_owner = Keys::generate();
    let alice_device = Keys::generate();
    let bob_owner = Keys::generate();
    let bob_device = Keys::generate();

    let mut alice = logged_in_test_core(
        "receiver-first-contact-bootstrap-alice",
        &alice_owner,
        &alice_device,
    );
    alice.pending_relay_publishes.clear();
    publish_personal_invite_for_test(&mut alice);
    let invite_url = alice
        .state
        .public_invite
        .as_ref()
        .expect("alice invite")
        .legacy_url
        .clone();

    let mut bob = logged_in_test_core(
        "receiver-first-contact-bootstrap-bob",
        &bob_owner,
        &bob_device,
    );
    bob.pending_relay_publishes.clear();
    let alice_app_keys =
        signed_app_keys_authorization_event(&alice_owner, alice_device.public_key(), 10);
    bob.handle_relay_event(alice_app_keys.clone());
    bob.handle_action(AppAction::AcceptInvite {
        invite_input: invite_url,
    });
    ingest_invite_owner_app_keys(&mut bob, alice_owner.public_key(), vec![alice_app_keys]);
    bob.handle_action(AppAction::SendMessage {
        chat_id: alice_owner.public_key().to_hex(),
        text: "payload before bootstrap".to_string(),
    });

    let _response_event = pending_events_with_kind(&bob, INVITE_RESPONSE_KIND)
        .into_iter()
        .next()
        .expect("invite response event");
    let payload_event = bob.pending_relay_publishes.values().find_map(|pending| {
        let event = serde_json::from_str::<Event>(&pending.event_json).ok()?;
        (event.kind.as_u16() as u32 == MESSAGE_EVENT_KIND
            && pending.inner_event_id.is_some()
            && pending.chat_id.is_some())
        .then_some(event)
    });
    assert!(
        payload_event.is_none(),
        "first-contact text must not publish before Alice's AppKeys make the chat send-ready"
    );
    let chat_id = alice_owner.public_key().to_hex();
    let message = bob
        .threads
        .get(&chat_id)
        .and_then(|thread| thread.messages.first())
        .expect("queued message");
    assert_eq!(message.body, "payload before bootstrap");
    assert_eq!(message.delivery, DeliveryState::Queued);
    assert!(message.delivery_trace.outer_event_ids.is_empty());
}

#[test]
fn cold_app_key_first_direct_message_names_no_recipient() {
    let alice_owner = Keys::generate();
    let alice_device = Keys::generate();
    let bob_owner = Keys::generate();
    let bob_device = Keys::generate();

    let mut alice = logged_in_test_core(
        "cold-appkey-first-message-alice",
        &alice_owner,
        &alice_device,
    );
    publish_personal_invite_for_test(&mut alice);
    let invite_url = alice
        .state
        .public_invite
        .as_ref()
        .expect("alice invite")
        .legacy_url
        .clone();

    let mut bob_acceptor = logged_in_test_core(
        "cold-appkey-first-message-bob-acceptor",
        &bob_owner,
        &bob_device,
    );
    let alice_app_keys =
        signed_app_keys_authorization_event(&alice_owner, alice_device.public_key(), 10);
    bob_acceptor.handle_relay_event(alice_app_keys.clone());
    bob_acceptor.handle_action(AppAction::AcceptInvite {
        invite_input: invite_url,
    });
    ingest_invite_owner_app_keys(
        &mut bob_acceptor,
        alice_owner.public_key(),
        vec![alice_app_keys],
    );
    let response = pending_events_with_kind(&bob_acceptor, INVITE_RESPONSE_KIND)
        .into_iter()
        .next()
        .expect("bob invite response");
    let bob_bootstrap_messages = pending_events_with_kind(&bob_acceptor, MESSAGE_EVENT_KIND);
    let bob_app_keys = signed_app_keys_authorization_event(&bob_owner, bob_device.public_key(), 10);
    alice.handle_relay_event(bob_app_keys.clone());
    alice.handle_relay_event(response);
    ingest_invite_owner_app_keys(&mut alice, bob_owner.public_key(), vec![bob_app_keys]);
    for event in bob_bootstrap_messages {
        alice.handle_relay_event(event);
    }
    assert!(
        alice.protocol_engine.as_ref().is_some_and(|engine| {
            engine.active_session_count_for_owner(bob_owner.public_key()) > 0
        }),
        "Alice should have a Bob session before sending"
    );

    let mut bob = logged_in_test_core(
        "cold-appkey-first-message-bob-receiver",
        &bob_owner,
        &bob_device,
    );
    alice.pending_relay_publishes.clear();
    bob.pending_relay_publishes.clear();

    let alice_app_keys = AppKeys::new(vec![DeviceEntry::new(alice_device.public_key(), 1)]);
    let bob_app_keys = AppKeys::new(vec![DeviceEntry::new(bob_device.public_key(), 1)]);
    for (core, owner, app_keys) in [
        (&mut alice, bob_owner.public_key(), bob_app_keys.clone()),
        (&mut bob, alice_owner.public_key(), alice_app_keys.clone()),
    ] {
        let batch = core
            .protocol_engine
            .as_mut()
            .expect("protocol engine")
            .ingest_app_keys_snapshot(owner, app_keys.clone(), 1)
            .expect("ingest peer app keys");
        core.process_protocol_engine_retry_batch("test_app_keys", batch);
        core.app_keys
            .insert(owner.to_hex(), known_app_keys_from_ndr(owner, &app_keys, 1));
    }

    bob.active_chat_id = Some(alice_owner.public_key().to_hex());
    alice.handle_action(AppAction::SendMessage {
        chat_id: bob_owner.public_key().to_hex(),
        text: "cold app-key hello".to_string(),
    });

    let message_events = pending_events_with_kind(&alice, MESSAGE_EVENT_KIND);
    assert!(
        !message_events.is_empty(),
        "cold app-key direct send should publish at least one message event; pending={} debug={:?}",
        alice.pending_relay_publishes.len(),
        alice
            .debug_log
            .iter()
            .map(|entry| format!("{}:{}", entry.category, entry.detail))
            .collect::<Vec<_>>()
    );
    // Message events name nobody: a relay must not learn which device a
    // delivery, or a fanout burst, is for. Bob finds them through the sender
    // ratchet keys he learns from the invite response instead.
    for event in &message_events {
        assert_eq!(
            event
                .tags
                .iter()
                .map(|tag| tag.as_slice()[0].as_str())
                .collect::<Vec<_>>(),
            ["v", "header"],
            "cold app-key direct sends must not tag a relay-visible recipient"
        );
        let wire = serde_json::to_string(event).expect("event json");
        for identity in [&alice_owner, &alice_device, &bob_owner, &bob_device] {
            assert!(!wire.contains(&identity.public_key().to_hex()));
        }
        assert_eq!(
            alice.queued_pairwise_device(event),
            Some(bob_device.public_key()),
            "the target device stays in local delivery metadata"
        );
    }
    let filters = bob.recent_protocol_filters(UnixSeconds(1_777_159_500));
    assert!(
        !filters.iter().any(|filter| filter
            .kinds
            .as_ref()
            .is_some_and(|kinds| kinds.contains(&Kind::from(MESSAGE_EVENT_KIND as u16)))
            && !filter.generic_tags.is_empty()),
        "Bob must not ask relays for messages addressed to his device; filters={:?}",
        filters
            .iter()
            .map(|filter| serde_json::to_value(filter).expect("filter json"))
            .collect::<Vec<_>>()
    );
    assert!(
        has_filter_with_kind_pubkey_for_test(
            &filters,
            INVITE_RESPONSE_KIND,
            bob.protocol_engine
                .as_ref()
                .and_then(ProtocolEngine::local_invite_response_pubkey)
                .expect("bob invite key"),
        ),
        "the invite response, addressed to the public invite key, opens a cold session"
    );
}

fn has_filter_with_kind_pubkey_for_test(filters: &[Filter], kind: u32, pubkey: PublicKey) -> bool {
    let pubkey_hex = pubkey.to_hex();
    filters
        .iter()
        .map(|filter| serde_json::to_value(filter).expect("filter json"))
        .any(|filter| {
            let has_kind = filter
                .get("kinds")
                .and_then(|kinds| kinds.as_array())
                .is_some_and(|kinds| {
                    kinds
                        .iter()
                        .any(|value| value.as_u64() == Some(kind as u64))
                });
            let has_pubkey = filter
                .get("#p")
                .and_then(|pubkeys| pubkeys.as_array())
                .is_some_and(|pubkeys| {
                    pubkeys
                        .iter()
                        .any(|value| value.as_str() == Some(pubkey_hex.as_str()))
                });
            has_kind && has_pubkey
        })
}