//! 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()], direct_hosts: vec!["1.2.3.4".parse().unwrap()], ..Default::default() }; 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()); } /// v3.3: a Windows-style client.toml (with the operator's pre-detected gateway already pinned /// in `[tunnel.os_routes]`) still parses and the dry-run install renders the windows plan in /// the logs. We do not assert on the log contents here — that is covered by the inner /// `windows_plan_default_vpn` unit test in `os_routes.rs` — but we *do* verify that the API /// surface accepts the same hints on every host (no Windows-only fields). #[test] fn dry_run_install_windows_style_overrides_succeed_anywhere() { let split = SplitRoutes { default: DefaultAction::Vpn, direct_cidrs: vec!["192.168.0.0/16".parse().unwrap()], direct_hosts: vec!["1.2.3.4".parse().unwrap()], ..Default::default() }; // On Windows the "egress" hint is the upstream interface IP, not its display name. // The dry-run path renders this verbatim into the windows plan. let guard = OsRouteGuard::install( "Aura", &split, Some("192.168.1.1"), Some("192.168.1.42"), /* dry_run */ true, ) .expect("dry_run with Windows-style overrides must succeed on every host"); drop(guard); }