use std::{path::PathBuf, process::Command, time::Duration}; use assert_cmd::cargo::cargo_bin; use portpicker::pick_unused_port; use tempfile::TempDir; use tokio::time::sleep; use quicnprotochat_client::{ cmd_create_group, cmd_invite, cmd_join, cmd_ping, cmd_register_state, cmd_send, ClientAuth, connect_node, fetch_wait, init_auth, }; use quicnprotochat_core::IdentityKeypair; fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } #[derive(serde::Deserialize)] struct StoredStateCompat { identity_seed: [u8; 32], #[allow(dead_code)] group: Option>, } async fn wait_for_health(server: &str, ca_cert: &PathBuf, server_name: &str) -> anyhow::Result<()> { let local = tokio::task::LocalSet::new(); for _ in 0..30 { if local .run_until(cmd_ping(server, ca_cert, server_name)) .await .is_ok() { return Ok(()); } sleep(Duration::from_millis(200)).await; } anyhow::bail!("server health never became ready") } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { let temp = TempDir::new()?; let base = temp.path(); let port = pick_unused_port().expect("free port"); let listen = format!("127.0.0.1:{port}"); let server = listen.clone(); let ca_cert = base.join("server-cert.der"); let tls_key = base.join("server-key.der"); let data_dir = base.join("data"); let auth_token = "devtoken"; // Spawn server binary. let server_bin = cargo_bin("quicnprotochat-server"); let mut child = Command::new(server_bin) .arg("--listen") .arg(&listen) .arg("--data-dir") .arg(&data_dir) .arg("--tls-cert") .arg(&ca_cert) .arg("--tls-key") .arg(&tls_key) .arg("--auth-token") .arg(auth_token) .spawn() .expect("spawn server"); // Ensure we always terminate the child. struct ChildGuard(std::process::Child); impl Drop for ChildGuard { fn drop(&mut self) { let _ = self.0.kill(); } } let child_guard = ChildGuard(child); let _ = child_guard; // Wait for server to be healthy and certs to be generated. wait_for_health(&server, &ca_cert, "localhost").await?; // Set client auth context. init_auth(ClientAuth::from_parts(auth_token.to_string(), None)); // LocalSet for capnp !Send operations. let local = tokio::task::LocalSet::new(); let alice_state = base.join("alice.bin"); let bob_state = base.join("bob.bin"); local .run_until(cmd_register_state( &alice_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_register_state( &bob_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_create_group( &alice_state, &server, "test-group", None, )) .await?; // Load Bob identity key from persisted state to use as peer key. let bob_bytes = std::fs::read(&bob_state)?; let bob_state_compat: StoredStateCompat = bincode::deserialize(&bob_bytes)?; let bob_identity = IdentityKeypair::from_seed(bob_state_compat.identity_seed); let bob_pk_hex = hex_encode(&bob_identity.public_key_bytes()); local .run_until(cmd_invite( &alice_state, &server, &ca_cert, "localhost", &bob_pk_hex, None, )) .await?; local .run_until(cmd_join( &bob_state, &server, &ca_cert, "localhost", None, )) .await?; // Send Alice -> Bob. local .run_until(cmd_send( &alice_state, &server, &ca_cert, "localhost", &bob_pk_hex, "hello bob", None, )) .await?; // Confirm Bob can fetch at least one payload. local .run_until(async { let client = connect_node(&server, &ca_cert, "localhost").await?; let payloads = fetch_wait(&client, &bob_identity.public_key_bytes(), 1000).await?; anyhow::ensure!(!payloads.is_empty(), "no payloads delivered to Bob"); Ok::<(), anyhow::Error>(()) }) .await?; Ok(()) }