//! Admin socket roundtrip: start the admin listener on a temp Unix socket over a shared //! [`RouteTable`], connect a client, send `route_add` / `route_list` / `route_remove` / `status`, //! and assert the table changed and the responses are correct. //! //! Runs without root or network (an `AF_UNIX` socket in the temp dir). #![cfg(unix)] use std::path::PathBuf; use std::sync::Arc; use aura_cli::admin::{self, AdminState, Request, Stats}; use aura_tunnel::{RouteAction, RouteTable}; use tokio::sync::RwLock; /// A unique socket path for this test (Unix socket paths are length-limited; temp dir keeps it /// short enough on macOS/Linux). fn socket_path() -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( "aura-admin-{}-{}.sock", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); p } #[tokio::test] async fn admin_socket_route_roundtrip() { let routes = Arc::new(RwLock::new(RouteTable::new(RouteAction::Vpn))); let stats = Arc::new(Stats::new()); stats.set_peer_id(Some("client-test".to_string())); let state = AdminState::new( Arc::clone(&routes), Arc::clone(&stats), std::iter::empty(), std::iter::empty(), ); let path = socket_path(); let path_str = path.to_string_lossy().to_string(); // Spawn the listener. let serve_path = path_str.clone(); let listener = tokio::spawn(async move { let _ = admin::serve(&serve_path, state).await; }); // Wait until the socket file exists (the listener binds before serving). for _ in 0..200 { if path.exists() { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } assert!(path.exists(), "admin socket was not created"); // route_add (cidr, direct). let resp = admin::request( &path_str, &Request::RouteAdd { cidr: Some("8.8.8.0/24".into()), domain: None, action: "direct".into(), }, ) .await .expect("route_add request"); assert!(resp.ok, "route_add ok: {:?}", resp.error); // The shared table actually changed. assert_eq!( routes.read().await.classify("8.8.8.8".parse().unwrap()), RouteAction::Direct ); // route_add (domain, vpn). let resp = admin::request( &path_str, &Request::RouteAdd { cidr: None, domain: Some("example.com".into()), action: "vpn".into(), }, ) .await .expect("route_add domain"); assert!(resp.ok); // route_list reflects both rules and the default. let resp = admin::request(&path_str, &Request::RouteList) .await .expect("route_list"); assert!(resp.ok); assert_eq!(resp.default.as_deref(), Some("vpn")); let cidrs = resp.cidrs.expect("cidrs present"); assert_eq!(cidrs.len(), 1); assert_eq!(cidrs[0].cidr, "8.8.8.0/24"); assert_eq!(cidrs[0].action, "direct"); let domains = resp.domains.expect("domains present"); assert_eq!(domains.len(), 1); assert_eq!(domains[0].domain, "example.com"); // status reflects peer id + default + rule count. let resp = admin::request(&path_str, &Request::Status) .await .expect("status"); assert!(resp.ok); assert_eq!(resp.peer_id.as_deref(), Some("client-test")); assert_eq!(resp.default.as_deref(), Some("vpn")); assert_eq!(resp.rules, Some(2)); // route_remove the CIDR; classification falls back to default VPN. let resp = admin::request( &path_str, &Request::RouteRemove { cidr: "8.8.8.0/24".into(), }, ) .await .expect("route_remove"); assert_eq!(resp.removed, Some(true)); assert_eq!( routes.read().await.classify("8.8.8.8".parse().unwrap()), RouteAction::Vpn ); // A malformed CIDR yields an error response (not a panic / disconnect). let resp = admin::request( &path_str, &Request::RouteAdd { cidr: Some("nonsense".into()), domain: None, action: "vpn".into(), }, ) .await .expect("route_add bad cidr"); assert!(!resp.ok); assert!(resp.error.is_some()); listener.abort(); let _ = std::fs::remove_file(&path); }