libchat/crates/client/tests/alice_and_bob.rs
osmaczko 0a6e833b53
feat: implement Client crate and C FFI bindings
Implement a `client` crate that wraps the `libchat` context behind a
simple `ChatClient<D>` API. The delivery strategy is pluggable via a
`DeliveryService` trait, with two implementations provided:

- `InProcessDelivery` — shared `MessageBus` for single-process tests
- `CDelivery` — C function-pointer callback for the FFI layer

Add a `client-ffi` crate that exposes the client as a C API via
`safer-ffi`. A `generate-headers` binary produces the companion C
header.

Include two runnable examples:
- `examples/in-process` — Alice/Bob exchange using in-process delivery
- `examples/c-ffi` — same exchange written entirely in C; smoketested
under valgrind (to catch memory leaks) in CI

iterates: #71
2026-03-30 21:24:29 +02:00

72 lines
2.4 KiB
Rust

use client::{
ChatClient, ContentData, ConversationIdOwned, Cursor, InProcessDelivery, StorageConfig,
};
use std::sync::Arc;
fn receive(receiver: &mut ChatClient<InProcessDelivery>, cursor: &mut Cursor) -> ContentData {
let raw = cursor.next().expect("expected envelope");
receiver
.receive(&raw)
.expect("receive failed")
.expect("expected content")
}
#[test]
fn alice_bob_message_exchange() {
let (delivery, bus) = InProcessDelivery::new();
let mut cursor = bus.subscribe_tail("delivery_address");
let mut alice = ChatClient::new("alice", delivery.clone());
let mut bob = ChatClient::new("bob", delivery);
let bob_bundle = bob.create_intro_bundle().unwrap();
let alice_convo_id = alice
.create_conversation(&bob_bundle, b"hello bob")
.unwrap();
let content = receive(&mut bob, &mut cursor);
assert_eq!(content.data, b"hello bob");
assert!(content.is_new_convo);
let bob_convo_id: ConversationIdOwned = Arc::from(content.conversation_id.as_str());
bob.send_message(&bob_convo_id, b"hi alice").unwrap();
let content = receive(&mut alice, &mut cursor);
assert_eq!(content.data, b"hi alice");
assert!(!content.is_new_convo);
for i in 0u8..5 {
let msg = format!("msg {i}");
alice.send_message(&alice_convo_id, msg.as_bytes()).unwrap();
let content = receive(&mut bob, &mut cursor);
assert_eq!(content.data, msg.as_bytes());
let reply = format!("reply {i}");
bob.send_message(&bob_convo_id, reply.as_bytes()).unwrap();
let content = receive(&mut alice, &mut cursor);
assert_eq!(content.data, reply.as_bytes());
}
assert_eq!(alice.list_conversations().unwrap().len(), 1);
assert_eq!(bob.list_conversations().unwrap().len(), 1);
}
#[test]
fn open_persistent_client() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test.db").to_string_lossy().to_string();
let config = StorageConfig::File(db_path);
let client1 = ChatClient::open("alice", config.clone(), InProcessDelivery::default()).unwrap();
let name1 = client1.installation_name().to_string();
drop(client1);
let client2 = ChatClient::open("alice", config, InProcessDelivery::default()).unwrap();
let name2 = client2.installation_name().to_string();
assert_eq!(
name1, name2,
"installation name should persist across restarts"
);
}