use mirror_tool::{ config::{NetworkConfig, ReplicaPolicy, ServiceConfig, Team}, engine::Engine, }; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, time::Duration, }; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, task::JoinHandle, time::{sleep, timeout}, }; fn settings() -> NetworkConfig { NetworkConfig { max_buffered_bytes: 65536, connect_timeout_ms: 200, buffer_size: 4096, replica_queue_chunks: 4, max_connections: 32, drain_timeout_ms: 100, } } async fn free_address() -> SocketAddr { TcpListener::bind("127.0.0.1:0") .await .unwrap() .local_addr() .unwrap() } fn service(listen: SocketAddr, upstream: SocketAddr) -> ServiceConfig { ServiceConfig { custom: false, custom_ip: None, id: "service".into(), name: "Test".into(), listen, upstream_port: upstream.port(), primary: Some("primary".into()), replicas: vec![], enabled: true, auto_switch: false, replica_policy: ReplicaPolicy::Isolate, } } fn teams() -> Vec { vec![Team { hidden: false, id: "primary".into(), name: "Primary".into(), address: IpAddr::V4(Ipv4Addr::LOCALHOST), }] } async fn echo() -> (SocketAddr, JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let task = tokio::spawn(async move { loop { let (mut stream, _) = listener.accept().await.unwrap(); tokio::spawn(async move { let mut buffer = [0; 4096]; loop { let n = stream.read(&mut buffer).await.unwrap_or(0); if n == 0 { let _ = stream.shutdown().await; break; } if stream.write_all(&buffer[..n]).await.is_err() { break; } } }); } }); (address, task) } async fn exchange(address: SocketAddr, payload: &[u8]) -> Vec { let stream = TcpStream::connect(address).await.unwrap(); let (mut reader, mut writer) = stream.into_split(); let upload = async { writer.write_all(payload).await.unwrap(); writer.shutdown().await.unwrap(); }; let download = async { let mut response = vec![]; reader.read_to_end(&mut response).await.unwrap(); response }; let (_, response) = timeout(Duration::from_secs(5), async { tokio::join!(upload, download) }) .await .unwrap(); response } async fn settled(engine: &Engine) { timeout(Duration::from_secs(2), async { loop { if engine.snapshots()["service"].stats.active == 0 { break; } sleep(Duration::from_millis(5)).await; } }) .await .unwrap(); } #[tokio::test] async fn bulk_bidirectional_bytes_and_half_close() { let (upstream, server) = echo().await; let listen = free_address().await; let engine = Engine::new(settings()); engine .upsert(&service(listen, upstream), &teams()) .await .unwrap(); let payload: Vec = (0..2_000_000).map(|n| (n % 251) as u8).collect(); assert_eq!(exchange(listen, &payload).await, payload); settled(&engine).await; let stats = engine.snapshots()["service"].stats.clone(); assert_eq!(stats.bytes_up, payload.len() as u64); assert_eq!(stats.bytes_down, payload.len() as u64); assert_eq!(stats.completed, 1); engine.shutdown().await; server.abort(); } #[tokio::test] async fn response_after_request_eof_and_idle_over_three_seconds() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let upstream = listener.local_addr().unwrap(); let server = tokio::spawn(async move { let (mut stream, _) = listener.accept().await.unwrap(); let mut request = vec![]; stream.read_to_end(&mut request).await.unwrap(); assert_eq!(request, b"after idle"); stream.write_all(b"after eof").await.unwrap(); }); let listen = free_address().await; let engine = Engine::new(settings()); engine .upsert(&service(listen, upstream), &teams()) .await .unwrap(); let mut stream = TcpStream::connect(listen).await.unwrap(); sleep(Duration::from_millis(3200)).await; stream.write_all(b"after idle").await.unwrap(); stream.shutdown().await.unwrap(); let mut reply = vec![]; timeout(Duration::from_secs(2), stream.read_to_end(&mut reply)) .await .unwrap() .unwrap(); assert_eq!(reply, b"after eof"); server.await.unwrap(); engine.shutdown().await; } #[tokio::test] async fn retarget_only_new_sessions_and_reenable_without_port_leak() { let (a, server_a) = echo().await; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let b = listener.local_addr().unwrap(); let server_b = tokio::spawn(async move { let (mut stream, _) = listener.accept().await.unwrap(); let mut request = vec![]; stream.read_to_end(&mut request).await.unwrap(); stream.write_all(b"new target").await.unwrap(); }); let listen = free_address().await; let engine = Engine::new(settings()); let mut config = service(listen, a); engine.upsert(&config, &teams()).await.unwrap(); let mut old = TcpStream::connect(listen).await.unwrap(); old.write_all(b"before").await.unwrap(); let mut before = [0; 6]; old.read_exact(&mut before).await.unwrap(); config.upstream_port = b.port(); engine.upsert(&config, &teams()).await.unwrap(); old.write_all(b"old target").await.unwrap(); old.shutdown().await.unwrap(); let mut reply = vec![]; old.read_to_end(&mut reply).await.unwrap(); assert_eq!(reply, b"old target"); assert_eq!(exchange(listen, b"request").await, b"new target"); config.enabled = false; engine.upsert(&config, &teams()).await.unwrap(); assert!(!engine.snapshots()["service"].running); assert!(TcpStream::connect(listen).await.is_err()); config.enabled = true; config.upstream_port = a.port(); engine.upsert(&config, &teams()).await.unwrap(); assert_eq!(exchange(listen, b"enabled").await, b"enabled"); engine.shutdown().await; server_a.abort(); server_b.await.unwrap(); } #[tokio::test] async fn bind_error_is_reported_and_working_listener_preserved() { let (upstream, server) = echo().await; let listen = free_address().await; let occupied = TcpListener::bind("127.0.0.1:0").await.unwrap(); let engine = Engine::new(settings()); let mut config = service(listen, upstream); engine.upsert(&config, &teams()).await.unwrap(); config.listen = occupied.local_addr().unwrap(); assert!(engine.upsert(&config, &teams()).await.is_err()); assert_eq!(exchange(listen, b"still running").await, b"still running"); engine.shutdown().await; server.abort(); } #[tokio::test] async fn process_connection_limit_sheds_and_shutdown_cancels_active_sessions() { let (upstream, server) = echo().await; let listen = free_address().await; let engine = Engine::new(NetworkConfig { max_connections: 1, ..settings() }); engine .upsert(&service(listen, upstream), &teams()) .await .unwrap(); let mut first = TcpStream::connect(listen).await.unwrap(); first.write_all(b"a").await.unwrap(); let mut byte = [0]; first.read_exact(&mut byte).await.unwrap(); let second_listen = free_address().await; let mut second_config = service(second_listen, upstream); second_config.id = "second".into(); engine.upsert(&second_config, &teams()).await.unwrap(); let mut rejected = TcpStream::connect(second_listen).await.unwrap(); assert_eq!( timeout(Duration::from_secs(1), rejected.read(&mut byte)) .await .unwrap() .unwrap(), 0 ); assert_eq!(engine.snapshots()["second"].stats.rejected, 1); timeout(Duration::from_secs(1), engine.shutdown()) .await .unwrap(); assert_eq!(engine.snapshots()["service"].stats.active, 0); assert!(!engine.snapshots()["service"].running); assert_eq!( timeout(Duration::from_secs(1), first.read(&mut byte)) .await .unwrap() .unwrap(), 0 ); server.abort(); } #[tokio::test] async fn disable_keeps_existing_connection_and_remove_joins_it() { let (upstream, server) = echo().await; let listen = free_address().await; let engine = Engine::new(settings()); let mut config = service(listen, upstream); engine.upsert(&config, &teams()).await.unwrap(); let mut stream = TcpStream::connect(listen).await.unwrap(); stream.write_all(b"a").await.unwrap(); let mut byte = [0]; stream.read_exact(&mut byte).await.unwrap(); config.enabled = false; engine.upsert(&config, &teams()).await.unwrap(); stream.write_all(b"b").await.unwrap(); stream.read_exact(&mut byte).await.unwrap(); assert_eq!(&byte, b"b"); timeout(Duration::from_secs(1), engine.remove("service")) .await .unwrap() .unwrap(); assert!(engine.snapshots().is_empty()); assert_eq!(stream.read(&mut byte).await.unwrap(), 0); engine.shutdown().await; server.abort(); } // Distinct loopback addresses let primary and replica share the configured upstream port. async fn replica_fixture( policy: ReplicaPolicy, ) -> (Engine, SocketAddr, TcpListener, JoinHandle<()>) { let (upstream, server) = echo().await; let replica = TcpListener::bind(SocketAddr::new("::1".parse().unwrap(), upstream.port())) .await .unwrap(); let listen = free_address().await; let mut config = service(listen, upstream); config.replica_policy = policy; config.replicas = vec!["replica".into()]; let mut teams = teams(); teams.push(Team { hidden: false, id: "replica".into(), name: "Replica".into(), address: "::1".parse().unwrap(), }); let engine = Engine::new(settings()); engine.upsert(&config, &teams).await.unwrap(); (engine, listen, replica, server) } #[tokio::test] async fn strict_mirror_exact_bytes_and_drains_large_replica_replies() { let (engine, listen, replica, server) = replica_fixture(ReplicaPolicy::Strict).await; let payload: Vec = (0..1_000_000).map(|n| (n % 251) as u8).collect(); let expected = payload.clone(); let worker = tokio::spawn(async move { let (stream, _) = replica.accept().await.unwrap(); let (mut reader, mut writer) = stream.into_split(); let read = async { let mut bytes = vec![]; reader.read_to_end(&mut bytes).await.unwrap(); assert_eq!(bytes, expected); }; let write = async { writer.write_all(&vec![17; 2_000_000]).await.unwrap(); writer.shutdown().await.unwrap(); }; tokio::join!(read, write); }); assert_eq!(exchange(listen, &payload).await, payload); timeout(Duration::from_secs(3), worker) .await .unwrap() .unwrap(); settled(&engine).await; assert_eq!( engine.snapshots()["service"].stats.replica_bytes, payload.len() as u64 ); assert_eq!(engine.snapshots()["service"].stats.replica_disconnects, 0); engine.shutdown().await; server.abort(); } #[tokio::test] async fn isolate_unavailable_replica_preserves_primary_and_counts_failure() { let (engine, listen, replica, server) = replica_fixture(ReplicaPolicy::Isolate).await; drop(replica); // Give the worker time to fail before EOF rather than overflowing its bounded queue. let mut client = TcpStream::connect(listen).await.unwrap(); client.write_all(b"one").await.unwrap(); let mut bytes = [0; 3]; client.read_exact(&mut bytes).await.unwrap(); sleep(Duration::from_millis(50)).await; client.write_all(b"two").await.unwrap(); client.shutdown().await.unwrap(); let mut reply = vec![]; client.read_to_end(&mut reply).await.unwrap(); assert_eq!(reply, b"two"); settled(&engine).await; let stats = engine.snapshots()["service"].stats.clone(); assert_eq!(stats.replica_errors, 1); assert_eq!(stats.replica_disconnects, 1); assert_eq!(stats.io_errors, 0); engine.shutdown().await; server.abort(); } #[tokio::test] async fn strict_unavailable_replica_ends_session_before_primary_receives_payload() { let (engine, listen, replica, server) = replica_fixture(ReplicaPolicy::Strict).await; drop(replica); let mut client = TcpStream::connect(listen).await.unwrap(); let _ = client.write_all(b"must not forward").await; let mut bytes = vec![]; let _ = timeout(Duration::from_secs(1), client.read_to_end(&mut bytes)) .await .unwrap(); settled(&engine).await; let stats = engine.snapshots()["service"].stats.clone(); assert_eq!(stats.bytes_up, 0); assert_eq!(stats.io_errors, 1); assert_eq!(stats.replica_errors, 1); engine.shutdown().await; server.abort(); } #[tokio::test] async fn isolate_slow_replica_disconnects_instead_of_corrupting_primary_stream() { let (engine, listen, replica, server) = replica_fixture(ReplicaPolicy::Isolate).await; let slow = tokio::spawn(async move { let (_stream, _) = replica.accept().await.unwrap(); sleep(Duration::from_secs(20)).await; }); let payload = vec![93; 16_000_000]; assert_eq!(exchange(listen, &payload).await, payload); settled(&engine).await; assert!(engine.snapshots()["service"].stats.replica_disconnects >= 1); engine.shutdown().await; server.abort(); slow.abort(); } #[tokio::test] async fn strict_slow_replica_backpressures_and_shutdown_joins_workers() { let (engine, listen, replica, server) = replica_fixture(ReplicaPolicy::Strict).await; let slow = tokio::spawn(async move { let (_stream, _) = replica.accept().await.unwrap(); sleep(Duration::from_secs(20)).await; }); let mut client = TcpStream::connect(listen).await.unwrap(); let payload = vec![31; 32_000_000]; assert!( timeout(Duration::from_millis(200), client.write_all(&payload)) .await .is_err() ); let stats = engine.snapshots()["service"].stats.clone(); assert!(stats.bytes_up < payload.len() as u64); assert_eq!(stats.replica_disconnects, 0); timeout(Duration::from_secs(1), engine.shutdown()) .await .unwrap(); assert_eq!(engine.snapshots()["service"].stats.active, 0); server.abort(); slow.abort(); } #[tokio::test] async fn quiesce_keeps_existing_stream_but_route_rollback_cannot_reopen_listener() { let upstream = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = upstream.local_addr().unwrap(); let task = tokio::spawn(async move { let (mut stream, _) = upstream.accept().await.unwrap(); let mut bytes = vec![]; stream.read_to_end(&mut bytes).await.unwrap(); stream.write_all(&bytes).await.unwrap(); }); let engine = Engine::new(settings()); let listen = free_address().await; let original = service(listen, address); engine.upsert(&original, &teams()).await.unwrap(); let mut client = TcpStream::connect(listen).await.unwrap(); timeout(Duration::from_secs(1), async { while engine.snapshots()["service"].stats.active == 0 { tokio::task::yield_now().await; } }) .await .unwrap(); engine.quiesce().await; assert!(TcpStream::connect(listen).await.is_err()); engine.upsert(&original, &teams()).await.unwrap(); assert!(TcpStream::connect(listen).await.is_err()); assert!(!engine.snapshots()["service"].running); client .write_all(b"existing connection survives quiesce") .await .unwrap(); client.shutdown().await.unwrap(); let mut bytes = vec![]; timeout(Duration::from_secs(1), client.read_to_end(&mut bytes)) .await .unwrap() .unwrap(); assert_eq!(bytes, b"existing connection survives quiesce"); task.await.unwrap(); engine.shutdown().await; }