iris-chat 0.1.58

Iris Chat command line client and shared encrypted chat core
Documentation
fn deliver_sync_snapshot(sender: &AppCore, receiver: &mut AppCore, device: &Keys) {
    for packet in sender.build_device_sync_packets_for_test(100, true) {
        receiver.handle_device_sync_packet(&device.public_key().to_hex(), 7369, &packet);
    }
}

fn deletion_packet(chat_id: &str, deleted_at: u64) -> Vec<u8> {
    serde_json::to_vec(&serde_json::json!({
        "type": "snapshot", "v": 1, "rosterAt": 100,
        "deletedChats": [{"id": chat_id, "deletedAt": deleted_at}]
    }))
    .unwrap()
}

#[test]
fn chat_deletion_sync_removes_sibling_history_and_survives_stale_snapshots() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let peer = Keys::generate();
    let chat_id = peer.public_key().to_hex();
    let (mut a, _, _a_dir) = logged_in_test_core_with_updates("delete-sync-a", &owner, &alice);
    let (mut b, _, _b_dir) = logged_in_test_core_with_updates("delete-sync-b", &owner, &bob);
    configure_test_device_sync_profile(&mut a, &owner, &alice, &bob, None);
    configure_test_device_sync_profile(&mut b, &owner, &bob, &alice, None);
    a.apply_runtime_text_message(
        peer.public_key(),
        None,
        "old history".into(),
        200,
        None,
        Some("old".into()),
        None,
    );
    deliver_sync_snapshot(&a, &mut b, &alice);
    assert!(b.threads.contains_key(&chat_id));
    let stale = b.build_device_sync_packets_for_test(100, true);
    b.active_chat_id = Some(chat_id.clone());
    b.screen_stack = vec![Screen::Chat {
        chat_id: chat_id.clone(),
    }];

    a.handle_action(AppAction::DeleteChat {
        chat_id: chat_id.clone(),
    });
    deliver_sync_snapshot(&a, &mut b, &alice);

    assert!(
        !b.threads.contains_key(&chat_id),
        "deletion must reach the sibling"
    );
    assert!(b.active_chat_id.is_none());
    assert!(b.screen_stack.is_empty());
    assert!(!b
        .app_store
        .message_exists(&chat_id, Some("old"), None)
        .unwrap());
    for packet in stale {
        a.handle_device_sync_packet(&bob.public_key().to_hex(), 7369, &packet);
        b.handle_device_sync_packet(&alice.public_key().to_hex(), 7369, &packet);
    }
    assert!(
        !a.threads.contains_key(&chat_id),
        "stale sibling history cannot undo deletion"
    );
    assert!(!b.threads.contains_key(&chat_id));
}

#[test]
fn chat_deletion_sync_is_durable_and_offline_siblings_learn_it_after_restart() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let peer = Keys::generate();
    let chat_id = peer.public_key().to_hex();
    let (mut a, _, a_dir) = logged_in_test_core_with_updates("delete-restart-a", &owner, &alice);
    let (mut b, _, _b_dir) = logged_in_test_core_with_updates("delete-restart-b", &owner, &bob);
    configure_test_device_sync_profile(&mut a, &owner, &alice, &bob, None);
    configure_test_device_sync_profile(&mut b, &owner, &bob, &alice, None);
    a.apply_runtime_text_message(
        peer.public_key(),
        None,
        "old history".into(),
        200,
        None,
        Some("old".into()),
        None,
    );
    deliver_sync_snapshot(&a, &mut b, &alice);
    a.delete_chat(&chat_id);
    drop(a);
    let mut restarted = logged_in_test_core_at_data_dir(
        &owner,
        &alice,
        a_dir.path().to_string_lossy().into_owned(),
    );
    restarted.load_persisted().unwrap();
    configure_test_device_sync_profile(&mut restarted, &owner, &alice, &bob, None);

    deliver_sync_snapshot(&restarted, &mut b, &alice);
    assert!(
        !b.threads.contains_key(&chat_id),
        "offline sibling must learn persisted deletion"
    );
    deliver_sync_snapshot(&b, &mut restarted, &bob);
    assert!(!restarted.threads.contains_key(&chat_id));
}

#[test]
fn chat_deletion_sync_keeps_newer_messages_and_rejects_old_protocol_replays() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let peer = Keys::generate();
    let chat_id = peer.public_key().to_hex();
    let (mut core, _, _dir) = logged_in_test_core_with_updates("delete-ordering", &owner, &alice);
    configure_test_device_sync_profile(&mut core, &owner, &alice, &bob, None);
    for (id, at) in [("old", 200), ("new", 400)] {
        core.apply_runtime_text_message(
            peer.public_key(),
            None,
            id.into(),
            at,
            None,
            Some(id.into()),
            None,
        );
    }
    core.persist_best_effort();
    let packet = deletion_packet(&chat_id, 300);
    core.handle_device_sync_packet(&bob.public_key().to_hex(), 7369, &packet);
    core.handle_device_sync_packet(&bob.public_key().to_hex(), 7369, &packet);
    core.apply_runtime_text_message(
        peer.public_key(),
        None,
        "delayed old".into(),
        250,
        None,
        Some("delayed".into()),
        None,
    );

    let ids = core.threads[&chat_id]
        .messages
        .iter()
        .map(|m| m.id.as_str())
        .collect::<Vec<_>>();
    assert_eq!(ids, ["new"]);
    assert!(!core
        .app_store
        .message_exists(&chat_id, Some("old"), None)
        .unwrap());
    assert!(core
        .app_store
        .message_exists(&chat_id, Some("new"), None)
        .unwrap());
}

#[test]
fn chat_deletion_sync_rejects_unregistered_devices_and_future_deletions() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let peer = Keys::generate();
    let chat_id = peer.public_key().to_hex();
    let (mut core, _, _dir) = logged_in_test_core_with_updates("delete-auth", &owner, &alice);
    configure_test_device_sync_profile(&mut core, &owner, &alice, &bob, None);
    core.ensure_thread_record(&chat_id, 200);
    core.handle_device_sync_packet(
        &peer.public_key().to_hex(),
        7369,
        &deletion_packet(&chat_id, 300),
    );
    core.handle_device_sync_packet(
        &bob.public_key().to_hex(),
        7369,
        &deletion_packet(&chat_id, u64::MAX),
    );
    assert!(core.threads.contains_key(&chat_id));
}

#[test]
fn chat_deletion_sync_allows_explicit_recreation_without_restoring_history() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let peer = Keys::generate();
    let chat_id = peer.public_key().to_hex();
    let (mut a, _, _a_dir) = logged_in_test_core_with_updates("delete-recreate-a", &owner, &alice);
    let (mut b, _, _b_dir) = logged_in_test_core_with_updates("delete-recreate-b", &owner, &bob);
    configure_test_device_sync_profile(&mut a, &owner, &alice, &bob, None);
    configure_test_device_sync_profile(&mut b, &owner, &bob, &alice, None);
    a.ensure_thread_record(&chat_id, 200);
    a.delete_chat(&chat_id);
    let deletion = a.build_device_sync_packets_for_test(100, false);

    a.create_chat(&chat_id);
    deliver_sync_snapshot(&a, &mut b, &alice);
    assert!(
        b.threads.contains_key(&chat_id),
        "explicitly recreated chat must sync"
    );
    for packet in deletion {
        b.handle_device_sync_packet(&alice.public_key().to_hex(), 7369, &packet);
    }
    assert!(
        b.threads.contains_key(&chat_id),
        "duplicate deletion must not undo recreation"
    );
    a.send_message(&chat_id, "new start", None);
    assert!(a.threads[&chat_id]
        .messages
        .iter()
        .all(|m| !a.chat_activity_is_deleted(&chat_id, m.created_at_secs)));
}

#[test]
fn chat_deletion_sync_does_not_restore_groups_from_stale_metadata() {
    let owner = Keys::generate();
    let alice = Keys::generate();
    let bob = Keys::generate();
    let (mut core, _, _dir) = logged_in_test_core_with_updates("delete-group", &owner, &alice);
    configure_test_device_sync_profile(&mut core, &owner, &alice, &bob, None);
    let packet = serde_json::to_vec(&serde_json::json!({
        "type": "snapshot", "v": 1, "rosterAt": 100,
        "groups": [{ "id": "friends", "name": "Friends", "createdBy": owner.public_key().to_hex(),
            "members": [owner.public_key().to_hex()], "admins": [owner.public_key().to_hex()],
            "revision": 1, "createdAt": 100, "updatedAt": 200 }]
    }))
    .unwrap();
    core.handle_device_sync_packet(&bob.public_key().to_hex(), 7369, &packet);
    let group = core.groups["friends"].clone();
    core.delete_chat("group:friends");
    core.handle_device_sync_packet(&bob.public_key().to_hex(), 7369, &packet);
    core.apply_group_decrypted_event(GroupIncomingEvent::MetadataUpdated(group));
    assert!(!core.threads.contains_key("group:friends"));
    assert!(!core.groups.contains_key("friends"));
    let restored = core.load_persisted().unwrap().unwrap();
    assert!(restored.groups.is_empty());
    assert!(restored.threads.is_empty());
}