// cargo_bin! only works for current package's binary; we spawn quicnprotochat-server from another package. #![allow(deprecated)] use std::{path::PathBuf, process::Command, time::Duration}; use assert_cmd::cargo::cargo_bin; use portpicker::pick_unused_port; use rand::RngCore; use tempfile::TempDir; use tokio::time::sleep; use hex; // Required by rustls 0.23 when QUIC/TLS is used from this process (e.g. client in test). fn ensure_rustls_provider() { let _ = rustls::crypto::ring::default_provider().install_default(); } use quicnprotochat_client::{ cmd_create_group, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_register_state, cmd_register_user, cmd_send, connect_node, enqueue, fetch_wait, init_auth, receive_pending_plaintexts, ClientAuth, }; 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") } /// Creator and joiner register; creator creates group and invites joiner; joiner joins; /// creator sends a message; assert joiner's mailbox receives it. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { ensure_rustls_provider(); 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 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) .arg("--allow-insecure-auth") .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)); let local = tokio::task::LocalSet::new(); let creator_state = base.join("creator.bin"); let joiner_state = base.join("joiner.bin"); local .run_until(cmd_register_state( &creator_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_register_state( &joiner_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_create_group(&creator_state, &server, "test-group", None)) .await?; let joiner_bytes = std::fs::read(&joiner_state)?; let joiner_state_compat: StoredStateCompat = bincode::deserialize(&joiner_bytes)?; let joiner_identity = IdentityKeypair::from_seed(joiner_state_compat.identity_seed); let joiner_pk_hex = hex_encode(&joiner_identity.public_key_bytes()); local .run_until(cmd_invite( &creator_state, &server, &ca_cert, "localhost", &joiner_pk_hex, None, )) .await?; local .run_until(cmd_join(&joiner_state, &server, &ca_cert, "localhost", None)) .await?; local .run_until(cmd_send( &creator_state, &server, &ca_cert, "localhost", Some(&joiner_pk_hex), false, "hello", None, )) .await?; local .run_until(async { let client = connect_node(&server, &ca_cert, "localhost").await?; let payloads = fetch_wait(&client, &joiner_identity.public_key_bytes(), 1000).await?; anyhow::ensure!(!payloads.is_empty(), "no payloads delivered to joiner"); Ok::<(), anyhow::Error>(()) }) .await?; Ok(()) } /// Three-party group: A creates group, invites B then C; B and C join; A sends, B and C receive; /// B sends, A and C receive. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn e2e_three_party_group_invite_join_send_recv() -> anyhow::Result<()> { ensure_rustls_provider(); 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"; let server_bin = cargo_bin("quicnprotochat-server"); let 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) .arg("--allow-insecure-auth") .spawn() .expect("spawn server"); struct ChildGuard(std::process::Child); impl Drop for ChildGuard { fn drop(&mut self) { let _ = self.0.kill(); } } let _child_guard = ChildGuard(child); wait_for_health(&server, &ca_cert, "localhost").await?; init_auth(ClientAuth::from_parts(auth_token.to_string(), None)); let local = tokio::task::LocalSet::new(); let creator_state = base.join("creator.bin"); let b_state = base.join("b.bin"); let c_state = base.join("c.bin"); local .run_until(cmd_register_state( &creator_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_register_state( &b_state, &server, &ca_cert, "localhost", None, )) .await?; local .run_until(cmd_register_state( &c_state, &server, &ca_cert, "localhost", None, )) .await?; let b_bytes = std::fs::read(&b_state)?; let b_compat: StoredStateCompat = bincode::deserialize(&b_bytes)?; let b_pk_hex = hex_encode(&IdentityKeypair::from_seed(b_compat.identity_seed).public_key_bytes()); let c_bytes = std::fs::read(&c_state)?; let c_compat: StoredStateCompat = bincode::deserialize(&c_bytes)?; let c_pk_hex = hex_encode(&IdentityKeypair::from_seed(c_compat.identity_seed).public_key_bytes()); local .run_until(cmd_create_group(&creator_state, &server, "test-group", None)) .await?; local .run_until(cmd_invite( &creator_state, &server, &ca_cert, "localhost", &b_pk_hex, None, )) .await?; local .run_until(cmd_invite( &creator_state, &server, &ca_cert, "localhost", &c_pk_hex, None, )) .await?; local .run_until(cmd_join(&b_state, &server, &ca_cert, "localhost", None)) .await?; local .run_until(cmd_join(&c_state, &server, &ca_cert, "localhost", None)) .await?; local .run_until(cmd_send( &creator_state, &server, &ca_cert, "localhost", None, true, "hello", None, )) .await?; sleep(Duration::from_millis(150)).await; let b_plaintexts = local .run_until(receive_pending_plaintexts( &b_state, &server, &ca_cert, "localhost", 1500, None, )) .await?; let c_plaintexts = local .run_until(receive_pending_plaintexts( &c_state, &server, &ca_cert, "localhost", 1500, None, )) .await?; anyhow::ensure!( b_plaintexts.iter().any(|p| p.as_slice() == b"hello"), "B did not receive 'hello', got {:?}", b_plaintexts ); anyhow::ensure!( c_plaintexts.iter().any(|p| p.as_slice() == b"hello"), "C did not receive 'hello', got {:?}", c_plaintexts ); local .run_until(cmd_send( &b_state, &server, &ca_cert, "localhost", None, true, "hi", None, )) .await?; sleep(Duration::from_millis(200)).await; let a_plaintexts = local .run_until(receive_pending_plaintexts( &creator_state, &server, &ca_cert, "localhost", 1500, None, )) .await?; let c_plaintexts2 = local .run_until(receive_pending_plaintexts( &c_state, &server, &ca_cert, "localhost", 1500, None, )) .await?; anyhow::ensure!( a_plaintexts.iter().any(|p| p.as_slice() == b"hi"), "A did not receive 'hi', got {:?}", a_plaintexts ); anyhow::ensure!( c_plaintexts2.iter().any(|p| p.as_slice() == b"hi"), "C did not receive 'hi', got {:?}", c_plaintexts2 ); Ok(()) } /// Login should refuse if the presented identity key does not match the registered key. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn e2e_login_rejects_mismatched_identity() -> anyhow::Result<()> { ensure_rustls_provider(); 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 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) .arg("--allow-insecure-auth") .spawn() .expect("spawn server"); 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_health(&server, &ca_cert, "localhost").await?; init_auth(ClientAuth::from_parts(auth_token.to_string(), None)); let local = tokio::task::LocalSet::new(); let state_path = base.join("user.bin"); // Register and persist state (includes identity key binding). local .run_until(cmd_register_state( &state_path, &server, &ca_cert, "localhost", None, )) .await?; // Register the user with the bound identity so login can enforce mismatches. let state_bytes = std::fs::read(&state_path)?; let stored_state: StoredStateCompat = bincode::deserialize(&state_bytes)?; let identity_hex = hex::encode( IdentityKeypair::from_seed(stored_state.identity_seed).public_key_bytes(), ); local .run_until(cmd_register_user( &server, &ca_cert, "localhost", "user1", "pass", Some(&identity_hex), )) .await?; // Craft an unrelated identity key and attempt login with it. let mut bogus_identity = [0u8; 32]; rand::thread_rng().fill_bytes(&mut bogus_identity); let bogus_hex = hex::encode(bogus_identity); let result = local .run_until(cmd_login( &server, &ca_cert, "localhost", "user1", "pass", Some(&bogus_hex), None, None, )) .await; match result { Ok(_) => anyhow::bail!("login unexpectedly succeeded with mismatched identity"), Err(e) => { // Show the full error chain so we can match the server's E016 response. let msg = format!("{e:#}"); anyhow::ensure!( msg.contains("identity") || msg.contains("E016"), "login failed but not for identity mismatch: {msg}" ); } } Ok(()) } /// Sealed Sender: enqueue with valid token (no identity binding) succeeds; recipient can fetch. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn e2e_sealed_sender_enqueue_then_fetch() -> anyhow::Result<()> { ensure_rustls_provider(); 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"; let server_bin = cargo_bin("quicnprotochat-server"); let 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) .arg("--allow-insecure-auth") .arg("--sealed-sender") .spawn() .expect("spawn server"); struct ChildGuard(std::process::Child); impl Drop for ChildGuard { fn drop(&mut self) { let _ = self.0.kill(); } } let _child_guard = ChildGuard(child); wait_for_health(&server, &ca_cert, "localhost").await?; init_auth(ClientAuth::from_parts(auth_token.to_string(), None)); let local = tokio::task::LocalSet::new(); let state_path = base.join("recipient.bin"); local .run_until(cmd_register_state( &state_path, &server, &ca_cert, "localhost", None, )) .await?; let state_bytes = std::fs::read(&state_path)?; let stored: StoredStateCompat = bincode::deserialize(&state_bytes)?; let recipient_key = IdentityKeypair::from_seed(stored.identity_seed).public_key_bytes(); let identity_hex = hex_encode(&recipient_key); local .run_until(cmd_register_user( &server, &ca_cert, "localhost", "recipient", "pass", Some(&identity_hex), )) .await?; local .run_until(cmd_login( &server, &ca_cert, "localhost", "recipient", "pass", Some(&identity_hex), None, None, )) .await?; let client = local.run_until(connect_node(&server, &ca_cert, "localhost")).await?; local .run_until(enqueue(&client, &recipient_key, b"sealed-payload")) .await?; let payloads = local .run_until(fetch_wait(&client, &recipient_key, 500)) .await?; anyhow::ensure!( payloads.len() == 1 && payloads[0].1.as_slice() == b"sealed-payload", "expected one payload 'sealed-payload', got {:?}", payloads ); Ok(()) }