iris-chat 0.1.55

Iris Chat command line client and shared encrypted chat core
Documentation
#[test]
fn owner_device_rejects_app_key_only_approval_input() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let new_device = Keys::generate();
    let mut core = logged_in_test_core("manual-device-npub-appkeys", &owner, &device);
    core.upsert_local_app_key_device(owner.public_key(), device.public_key());
    core.sync_local_app_keys_to_protocol_engine("test_seed_appkeys");
    core.pending_relay_publishes.clear();

    core.handle_action(AppAction::AddAuthorizedDevice {
        device_input: new_device.public_key().to_bech32().expect("device npub"),
    });

    assert_eq!(core.state.toast.as_deref(), Some("Invalid device request."));
    let app_keys_events = pending_events_with_kind(&core, APP_KEYS_EVENT_KIND);
    assert!(app_keys_events.is_empty());

    let known = core
        .app_keys
        .get(&owner.public_key().to_hex())
        .expect("AppKeys projection");
    assert!(!known
        .devices
        .iter()
        .any(|device| device.identity_pubkey_hex == new_device.public_key().to_hex()));
}

#[test]
fn owner_device_approves_bootstrap_and_publishes_app_keys_snapshot() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let linked_device = Keys::generate();
    let request_keys = Keys::generate();
    let approval_relay = crate::local_relay::TestRelay::start();
    let linked_device_hex = linked_device.public_key().to_hex();
    let bootstrap = device_approval_bootstrap_for_test(
        &linked_device,
        &request_keys,
        TEST_DEVICE_APPROVAL_REQUEST_SECRET,
        Some("Safari on macOS"),
    );

    let mut core = logged_in_test_core("owner-full-appkeys-approval", &owner, &device);
    core.upsert_local_app_key_device(owner.public_key(), device.public_key());
    core.sync_local_app_keys_to_protocol_engine("test_seed_appkeys");
    core.pending_relay_publishes.clear();
    dispatch_device_approval_for_test(&mut core, approval_relay.url(), bootstrap);

    assert_eq!(core.state.toast.as_deref(), Some("Device added"));
    let approval_events = relay_events(&approval_relay);
    let app_keys_events = approval_events
        .iter()
        .filter(|event| event.kind.as_u16() as u32 == APP_KEYS_EVENT_KIND)
        .collect::<Vec<_>>();
    assert_eq!(
        app_keys_events.len(),
        1,
        "approval publishes an owner-signed AppKeys snapshot"
    );
    let app_keys_event = app_keys_events[0];
    assert!(is_app_keys_event(app_keys_event));
    assert_eq!(app_keys_event.pubkey, owner.public_key());
    assert!(event_has_tag_value(
        app_keys_event,
        "device",
        &linked_device_hex
    ));
    assert_eq!(
        approval_events
            .iter()
            .filter(|event| event.kind.as_u16() as u32 == INVITE_RESPONSE_KIND)
            .count(),
        1,
        "approval still responds to the deterministic NDR link invite"
    );
    let known = core
        .app_keys
        .get(&owner.public_key().to_hex())
        .expect("AppKeys projection");
    assert!(known
        .devices
        .iter()
        .any(|device| device.identity_pubkey_hex == linked_device_hex));
    let linked = known
        .devices
        .iter()
        .find(|device| device.identity_pubkey_hex == linked_device_hex)
        .expect("linked device");
    assert_eq!(linked.device_label.as_deref(), Some("Safari on macOS"));
    assert_eq!(linked.client_label.as_deref(), None);
}

#[test]
fn create_account_publishes_app_keys_snapshot() {
    let data_dir = tempfile::tempdir().expect("test data directory");
    let mut core = AppCore::new(
        flume::unbounded().0,
        flume::unbounded().0,
        data_dir.path().to_string_lossy().into_owned(),
        Arc::new(RwLock::new(AppState::empty())),
    );

    core.handle_action(AppAction::CreateAccount {
        name: "Alice".to_string(),
    });

    assert_eq!(core.state.toast, None);
    let app_keys_events = pending_events_with_kind(&core, APP_KEYS_EVENT_KIND);
    assert_eq!(
        app_keys_events.len(),
        1,
        "account bootstrap publishes the current owner AppKeys snapshot"
    );
    let owner_pubkey = core.logged_in.as_ref().expect("logged in").owner_pubkey;
    assert!(app_keys_events.iter().all(is_app_keys_event));
    assert!(app_keys_events
        .iter()
        .all(|event| event.pubkey == owner_pubkey));
}

#[test]
fn account_bootstrap_app_keys_snapshot_installs_peer_device_roster() {
    let bob_data_dir = tempfile::tempdir().expect("test data directory");
    let alice_owner = Keys::generate();
    let alice_device = Keys::generate();
    let mut alice = logged_in_test_core(
        "account-bootstrap-peer-appkeys",
        &alice_owner,
        &alice_device,
    );
    let mut bob = AppCore::new(
        flume::unbounded().0,
        flume::unbounded().0,
        bob_data_dir.path().to_string_lossy().into_owned(),
        Arc::new(RwLock::new(AppState::empty())),
    );

    bob.handle_action(AppAction::CreateAccount {
        name: "Bob".to_string(),
    });
    let bob_login = bob.logged_in.as_ref().expect("bob logged in");
    let bob_owner = bob_login.owner_pubkey;
    let bob_device = bob_login.device_keys.public_key();
    let app_keys_events = pending_events_with_kind(&bob, APP_KEYS_EVENT_KIND);

    for event in app_keys_events {
        alice.handle_relay_event(event);
    }

    let known = alice
        .app_keys
        .get(&bob_owner.to_hex())
        .expect("bob roster projected");
    assert!(known
        .devices
        .iter()
        .any(|device| device.identity_pubkey_hex == bob_device.to_hex()));
}

fn event_has_tag_value(event: &Event, tag_name: &str, value: &str) -> bool {
    event.tags.iter().any(|tag| {
        let values = tag.as_slice();
        values.first().map(|value| value.as_str()) == Some(tag_name)
            && values.get(1).map(|candidate| candidate.as_str()) == Some(value)
    })
}

#[test]
fn linked_device_authorization_follows_app_keys() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let other_device = Keys::generate();
    let mut core = AppCore::new(
        flume::unbounded().0,
        flume::unbounded().0,
        std::env::temp_dir()
            .join(format!("iris-chat-rs-test-{}", owner.public_key().to_hex()))
            .to_string_lossy()
            .to_string(),
        Arc::new(RwLock::new(AppState::empty())),
    );

    assert_eq!(
        core.local_authorization_state(None, owner.public_key(), device.public_key(), None),
        LocalAuthorizationState::AwaitingApproval
    );

    let other_keys = known_app_keys_from_ndr(
        owner.public_key(),
        &AppKeys::new(vec![DeviceEntry::new(other_device.public_key(), 10)]),
        10,
    );
    core.app_keys
        .insert(owner.public_key().to_hex(), other_keys);
    assert_eq!(
        core.local_authorization_state(
            None,
            owner.public_key(),
            device.public_key(),
            Some(LocalAuthorizationState::AwaitingApproval),
        ),
        LocalAuthorizationState::AwaitingApproval
    );
    assert_eq!(
        core.local_authorization_state(
            None,
            owner.public_key(),
            device.public_key(),
            Some(LocalAuthorizationState::Authorized),
        ),
        LocalAuthorizationState::Revoked
    );

    let approved_keys = known_app_keys_from_ndr(
        owner.public_key(),
        &AppKeys::new(vec![DeviceEntry::new(device.public_key(), 20)]),
        20,
    );
    core.app_keys
        .insert(owner.public_key().to_hex(), approved_keys);
    assert_eq!(
        core.local_authorization_state(
            None,
            owner.public_key(),
            device.public_key(),
            Some(LocalAuthorizationState::AwaitingApproval),
        ),
        LocalAuthorizationState::AwaitingApproval
    );

    core.start_session(owner.public_key(), None, device.clone(), false, false)
        .expect("linked session");
    let approved_keys = known_app_keys_from_ndr(
        owner.public_key(),
        &AppKeys::new(vec![DeviceEntry::new(device.public_key(), 20)]),
        20,
    );
    core.app_keys
        .insert(owner.public_key().to_hex(), approved_keys);
    install_local_sibling_session_for_test(&mut core, &owner, &device, &other_device);
    assert_eq!(
        core.local_authorization_state(
            None,
            owner.public_key(),
            device.public_key(),
            Some(LocalAuthorizationState::AwaitingApproval),
        ),
        LocalAuthorizationState::Authorized
    );
}