iris-chat 0.1.49

Iris Chat command line client and shared encrypted chat core
Documentation
fn deferred_registration_core(label: &str) -> (AppCore, Keys, Keys) {
    let owner = Keys::generate();
    let device = Keys::generate();
    let mut core = logged_in_test_core(label, &owner, &device);
    core.app_keys.remove(&owner.public_key().to_hex());
    core.defer_owner_app_keys_publish = true;
    (core, owner, device)
}

#[test]
fn restored_account_registers_after_completed_empty_server_lookup() {
    let (mut core, owner, device) = deferred_registration_core("empty-registration-recovery");
    core.complete_owner_registration_lookup(
        core.relay_status_watch_generation,
        owner.public_key(),
        device.public_key(),
        1,
        1,
        vec![],
    );
    assert!(!core.defer_owner_app_keys_publish);
    let own = &core.app_keys[&owner.public_key().to_hex()];
    assert_eq!(own.devices.len(), 1);
    assert_eq!(
        own.devices[0].identity_pubkey_hex,
        device.public_key().to_hex()
    );
    let published = core
        .pending_relay_publishes
        .values()
        .find(|pending| pending.label == "app-keys")
        .expect("registration must be queued durably");
    let event: Event = serde_json::from_str(&published.event_json).unwrap();
    assert!(event.verify().is_ok());
    assert!(AppKeys::from_event(&event)
        .unwrap()
        .get_device(&device.public_key())
        .is_some());
}

#[test]
fn incomplete_registration_lookup_never_publishes_an_empty_replacement() {
    let (mut core, owner, device) = deferred_registration_core("incomplete-registration-recovery");
    for (completed, queried) in [(0, 0), (0, 1), (1, 2)] {
        core.complete_owner_registration_lookup(
            core.relay_status_watch_generation,
            owner.public_key(),
            device.public_key(),
            completed,
            queried,
            vec![],
        );
        assert!(core.defer_owner_app_keys_publish);
        assert!(!core.app_keys.contains_key(&owner.public_key().to_hex()));
    }
}

#[test]
fn registration_recovery_migrates_signed_legacy_devices_without_reviving_current_revocations() {
    let (mut core, owner, device) = deferred_registration_core("legacy-registration-recovery");
    let old_device = Keys::generate();
    let legacy = EventBuilder::new(Kind::Custom(30078), "")
        .tags([
            nostr::Tag::parse(["d", "double-ratchet/app-keys"]).unwrap(),
            nostr::Tag::parse(vec![
                "device".to_string(),
                old_device.public_key().to_hex(),
                "10".to_string(),
            ])
            .unwrap(),
        ])
        .custom_created_at(Timestamp::from_secs(10))
        .sign_with_keys(&owner)
        .unwrap();
    core.complete_owner_registration_lookup(
        core.relay_status_watch_generation,
        owner.public_key(),
        device.public_key(),
        1,
        1,
        vec![legacy.clone()],
    );
    assert_eq!(core.app_keys[&owner.public_key().to_hex()].devices.len(), 2);

    let current = AppKeys::new(vec![])
        .get_encrypted_event_at(&owner, 20)
        .unwrap()
        .sign_with_keys(&owner)
        .unwrap();
    core.app_keys.remove(&owner.public_key().to_hex());
    core.defer_owner_app_keys_publish = true;
    core.complete_owner_registration_lookup(
        core.relay_status_watch_generation,
        owner.public_key(),
        device.public_key(),
        1,
        1,
        vec![legacy, current],
    );
    let devices = &core.app_keys[&owner.public_key().to_hex()].devices;
    assert_eq!(
        devices.len(),
        1,
        "current revocation must win over a legacy device list"
    );
    assert_eq!(devices[0].identity_pubkey_hex, device.public_key().to_hex());
}

#[test]
fn stale_registration_lookup_cannot_register_a_different_session() {
    let (mut core, owner, device) = deferred_registration_core("stale-registration-recovery");
    core.complete_owner_registration_lookup(
        core.relay_status_watch_generation.wrapping_add(1),
        owner.public_key(),
        device.public_key(),
        1,
        1,
        vec![],
    );
    assert!(core.defer_owner_app_keys_publish);
    core.complete_owner_registration_lookup(
        core.relay_status_watch_generation,
        owner.public_key(),
        Keys::generate().public_key(),
        1,
        1,
        vec![],
    );
    assert!(core.defer_owner_app_keys_publish);
}

#[test]
fn restored_account_invite_waits_for_local_approval_then_resumes_automatically() {
    for same_key_peer in [false, true] {
        let (mut sender, owner, device) = deferred_registration_core("restore-invite-approval");
        let peer_owner = Keys::generate();
        let peer_device = if same_key_peer { peer_owner.clone() } else { Keys::generate() };
        let mut peer = logged_in_test_core("restore-invite-peer", &peer_owner, &peer_device);
        let invite = create_private_invite_for_test(&mut peer);
        prove_invite_owner(&mut sender, &peer_owner, &peer_device, 10);
        sender.pending_relay_publishes.clear();
        sender.handle_action(AppAction::AcceptInvite { invite_input: invite });
        assert!(pending_events_with_kind(&sender, INVITE_RESPONSE_KIND).is_empty(),
            "an imported account must not send an unprovable device claim");
        assert!(sender.pending_outgoing_invite_acceptance.is_some());
        sender.complete_owner_registration_lookup(
            sender.relay_status_watch_generation, owner.public_key(), device.public_key(), 1, 1, vec![],
        );
        assert!(sender.pending_outgoing_invite_acceptance.is_none());
        let response = pending_events_with_kind(&sender, INVITE_RESPONSE_KIND).into_iter().next().unwrap();
        assert!(response.tags.iter().any(|tag| tag.as_slice()[0] == "owner-proof"));
        peer.handle_relay_event(response);
        assert_eq!(active_session_device_pubkeys(&peer, owner.public_key()), vec![device.public_key()]);
    }
}

#[test]
fn restored_account_publishes_verifiable_registration_to_empty_local_relay() {
    let relay = crate::local_relay::TestRelay::start();
    let temp = tempfile::TempDir::new().unwrap();
    let owner = Keys::generate();
    let device = Keys::generate();
    let (mut core, rx) = profile_restart_core(temp.path(), &relay);
    core.start_primary_session(owner.clone(), device.clone(), true, false)
        .unwrap();
    assert!(core.defer_owner_app_keys_publish);
    let receiver_owner = Keys::generate();
    let receiver_device = Keys::generate();
    let mut receiver = logged_in_test_core("registration-recovery-recipient", &receiver_owner, &receiver_device);
    let invite = receiver.protocol_engine.as_ref().unwrap().local_invite().unwrap();
    let (mut sending_session, response) = invite.accept_with_owner(
        device.public_key(), device.secret_key().to_secret_bytes(),
        Some(device.public_key().to_hex()), Some(owner.public_key()),
    ).unwrap();
    receiver.handle_relay_event(invite_response_event(&response).unwrap());
    let plan = sending_session.plan_send(b"previously missing message", NdrUnixSeconds(unix_now().get())).unwrap();
    let message = message_event(&sending_session.apply_send(plan).envelope).unwrap();
    receiver.handle_relay_event(message);
    assert!(!receiver.threads.get(&owner.public_key().to_hex()).is_some_and(|thread| thread.messages.iter().any(|message| message.body == "previously missing message")));
    wait_for_profile_restart(&mut core, &rx, |core| {
        !core.defer_owner_app_keys_publish
            && relay_events(&relay).iter().any(|event| {
                event.pubkey == owner.public_key() && AppKeys::from_event(event).is_ok()
            })
    });
    let events = relay_events(&relay);
    let registration = events
        .iter()
        .find(|event| event.pubkey == owner.public_key() && AppKeys::from_event(event).is_ok())
        .unwrap();
    assert!(registration.verify().is_ok());
    assert!(AppKeys::from_event(registration)
        .unwrap()
        .get_device(&device.public_key())
        .is_some());
    receiver.handle_relay_event(registration.clone());
    assert!(receiver.threads[&owner.public_key().to_hex()].messages.iter().any(|message| message.body == "previously missing message"),
        "verified registration must release the previously blocked chat message without resending it");
}