//! Integration tests for the OS-level split-tunnel helper (`aura_cli::os_routes::OsRouteGuard`). //! //! These tests only exercise the dry-run path: real `ip` / `route` programming needs root and a //! live network stack, which is inappropriate for the unit test runner. The dry-run path is //! platform-portable: it logs `would run: ...` for both the Linux and macOS plans (plus the //! Windows-stub notice) and never touches the host. use std::net::IpAddr; use aura_cli::config::{ClientConfigFile, OsRoutesSection, SplitRule, SplitSection}; use aura_cli::os_routes::{DefaultAction, OsRouteGuard, SplitRoutes}; /// Dry-run install must succeed on every host (Linux, macOS, Windows) regardless of which /// gateway / egress hints are provided. Drop must not panic. #[test] fn dry_run_install_succeeds_on_any_platform() { let split = SplitRoutes { default: DefaultAction::Vpn, direct_cidrs: vec!["192.168.0.0/16".parse().unwrap()], vpn_cidrs: Vec::new(), direct_hosts: vec!["1.2.3.4".parse().unwrap()], vpn_hosts: Vec::new(), }; let guard = OsRouteGuard::install("aura0", &split, None, None, true) .expect("dry_run install must succeed everywhere"); drop(guard); } /// Dry-run also accepts explicit gateway / egress overrides — they are rendered into the /// `would run: ...` lines without needing to invoke the host's `ip`/`route` binary. #[test] fn dry_run_install_accepts_explicit_overrides() { let split = SplitRoutes { default: DefaultAction::Direct, vpn_cidrs: vec!["10.7.0.0/24".parse().unwrap()], ..Default::default() }; let guard = OsRouteGuard::install( "utun4", &split, Some("10.0.0.1"), Some("en0"), /* dry_run */ true, ) .expect("dry_run install with explicit gateway/egress must succeed"); drop(guard); } /// `SplitRoutes::from_config` collects CIDRs from both branches and any resolved domain hosts. #[test] fn split_routes_from_config_collects_everything() { let split = SplitSection { default: "VPN".into(), direct: vec![SplitRule { cidr: Some("192.168.0.0/16".into()), domain: None, }], vpn: vec![SplitRule { cidr: Some("10.7.0.0/24".into()), domain: None, }], }; let resolved: Vec<(String, aura_tunnel::RouteAction, Vec)> = vec![( "intranet.example.com".into(), aura_tunnel::RouteAction::Direct, vec!["1.2.3.4".parse().unwrap()], )]; let r = SplitRoutes::from_config(&split, &resolved); assert_eq!(r.default, DefaultAction::Vpn); assert_eq!(r.direct_cidrs.len(), 1); assert_eq!(r.vpn_cidrs.len(), 1); assert_eq!(r.direct_hosts.len(), 1); assert!(r.vpn_hosts.is_empty()); } /// A `client.toml` without a `[tunnel.os_routes]` section still parses; the field is `None`. /// This is the explicit back-compat check — old configs do not need to know about the new /// section. #[test] fn client_toml_without_os_routes_section_parses() { let minimal = r#" [client] name = "x" server_addr = "1.2.3.4:443" sni = "a" [pki] ca_cert = "a" cert = "b" key = "c" [tunnel] local_ip = "10.7.0.2" [tunnel.split] default = "VPN" "#; let cfg = ClientConfigFile::parse(minimal).expect("parses minimal client.toml"); assert!( cfg.tunnel.os_routes.is_none(), "without the section, os_routes is None — runtime falls back to enabled = true default" ); } /// `[tunnel.os_routes]` with `enabled = true, dry_run = true` parses end-to-end and exposes the /// flags to the client startup path. #[test] fn client_toml_parses_os_routes_section() { let s = r#" [client] name = "x" server_addr = "1.2.3.4:443" sni = "a" [pki] ca_cert = "a" cert = "b" key = "c" [tunnel] local_ip = "10.7.0.2" [tunnel.os_routes] enabled = true dry_run = true gateway = "192.168.1.1" egress_iface = "en0" [tunnel.split] default = "VPN" "#; let cfg = ClientConfigFile::parse(s).expect("parses client.toml with [tunnel.os_routes]"); let os = cfg.tunnel.os_routes.expect("section present"); assert!(os.enabled); assert!(os.dry_run); assert_eq!(os.gateway.as_deref(), Some("192.168.1.1")); assert_eq!(os.egress_iface.as_deref(), Some("en0")); } /// `OsRoutesSection::default()` matches the documented v2 semantics: enabled by default with /// no dry_run and no explicit gateway/egress (auto-detected at runtime). #[test] fn os_routes_section_default_values() { let d = OsRoutesSection::default(); assert!( d.enabled, "default is enabled = true (v2 eliminates the v1 stub)" ); assert!(!d.dry_run); assert!(d.gateway.is_none()); assert!(d.egress_iface.is_none()); }