mod common; use common::*; use rustproxy::RustProxy; use rustproxy_config::RustProxyOptions; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::test] async fn test_start_and_stop() { let port = next_port(); let options = RustProxyOptions { routes: vec![make_test_route(port, None, "127.0.0.1", 8080)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); // Not listening before start assert!(!wait_for_port(port, 200).await); proxy.start().await.unwrap(); assert!(wait_for_port(port, 2000).await, "Port should be listening after start"); proxy.stop().await.unwrap(); // Give the OS a moment to release the port tokio::time::sleep(std::time::Duration::from_millis(100)).await; assert!(!wait_for_port(port, 200).await, "Port should not be listening after stop"); } #[tokio::test] async fn test_double_start_fails() { let port = next_port(); let options = RustProxyOptions { routes: vec![make_test_route(port, None, "127.0.0.1", 8080)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); // Second start should fail let result = proxy.start().await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("already started")); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_update_routes_hot_reload() { let port = next_port(); let options = RustProxyOptions { routes: vec![make_test_route(port, Some("old.example.com"), "127.0.0.1", 8080)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); // Update routes atomically let new_routes = vec![ make_test_route(port, Some("new.example.com"), "127.0.0.1", 9090), ]; let result = proxy.update_routes(new_routes).await; assert!(result.is_ok()); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_add_remove_listening_port() { let port1 = next_port(); let port2 = next_port(); let options = RustProxyOptions { routes: vec![make_test_route(port1, None, "127.0.0.1", 8080)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); assert!(wait_for_port(port1, 2000).await); // Add a new port proxy.add_listening_port(port2).await.unwrap(); assert!(wait_for_port(port2, 2000).await, "New port should be listening"); // Remove the port proxy.remove_listening_port(port2).await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(100)).await; assert!(!wait_for_port(port2, 200).await, "Removed port should not be listening"); // Original port should still be listening assert!(wait_for_port(port1, 200).await, "Original port should still be listening"); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_get_statistics() { let port = next_port(); let options = RustProxyOptions { routes: vec![make_test_route(port, None, "127.0.0.1", 8080)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); let stats = proxy.get_statistics(); assert_eq!(stats.routes_count, 1); assert!(stats.listening_ports.contains(&port)); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_invalid_routes_rejected() { let options = RustProxyOptions { routes: vec![{ let mut route = make_test_route(80, None, "127.0.0.1", 8080); route.action.targets = None; // Invalid: forward without targets route }], ..Default::default() }; let result = RustProxy::new(options); assert!(result.is_err()); } #[tokio::test] async fn test_metrics_track_connections() { let backend_port = next_port(); let proxy_port = next_port(); let _backend = start_echo_server(backend_port).await; let options = RustProxyOptions { routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); assert!(wait_for_port(proxy_port, 2000).await); // No connections yet let stats = proxy.get_statistics(); assert_eq!(stats.total_connections, 0); // Make a connection and send data { let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port)) .await .unwrap(); stream.write_all(b"hello").await.unwrap(); let mut buf = vec![0u8; 16]; let _ = stream.read(&mut buf).await; } // Small delay for metrics to update tokio::time::sleep(std::time::Duration::from_millis(100)).await; let stats = proxy.get_statistics(); assert!(stats.total_connections > 0, "Expected total_connections > 0, got {}", stats.total_connections); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_metrics_track_bytes() { let backend_port = next_port(); let proxy_port = next_port(); let _backend = start_http_echo_backend(backend_port, "metrics-test").await; let options = RustProxyOptions { routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); assert!(wait_for_port(proxy_port, 2000).await); // Send HTTP request through proxy { let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port)) .await .unwrap(); let request = b"GET /test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"; stream.write_all(request).await.unwrap(); let mut response = Vec::new(); stream.read_to_end(&mut response).await.unwrap(); assert!(!response.is_empty(), "Expected non-empty response"); } // Small delay for metrics to update tokio::time::sleep(std::time::Duration::from_millis(100)).await; let stats = proxy.get_statistics(); assert!(stats.total_connections > 0, "Expected some connections tracked, got {}", stats.total_connections); proxy.stop().await.unwrap(); } #[tokio::test] async fn test_hot_reload_port_changes() { let port1 = next_port(); let port2 = next_port(); let backend_port = next_port(); let _backend = start_echo_server(backend_port).await; // Start with port1 let options = RustProxyOptions { routes: vec![make_test_route(port1, None, "127.0.0.1", backend_port)], ..Default::default() }; let mut proxy = RustProxy::new(options).unwrap(); proxy.start().await.unwrap(); assert!(wait_for_port(port1, 2000).await); assert!(!wait_for_port(port2, 200).await, "port2 should not be listening yet"); // Update routes to use port2 instead let new_routes = vec![ make_test_route(port2, None, "127.0.0.1", backend_port), ]; proxy.update_routes(new_routes).await.unwrap(); // Port2 should now be listening, port1 should be closed assert!(wait_for_port(port2, 2000).await, "port2 should be listening after reload"); tokio::time::sleep(std::time::Duration::from_millis(100)).await; assert!(!wait_for_port(port1, 200).await, "port1 should be closed after reload"); // Verify port2 works let ports = proxy.get_listening_ports(); assert!(ports.contains(&port2), "Expected port2 in listening ports: {:?}", ports); assert!(!ports.contains(&port1), "port1 should not be in listening ports: {:?}", ports); proxy.stop().await.unwrap(); }