//! Integration tests for the v3.3 signed-bridges manifest: //! //! * Parses a synthetic `client.toml` with `[client.bridges_discovery]` and asserts the section //! round-trips through the config layer. //! * Drives [`BridgesDiscoveryWatcher`] end-to-end against an on-disk manifest, swaps the file, //! asks the watcher to refresh, and verifies the snapshot picks the new list up while keeping //! the static `[client] bridges` baseline. //! //! Lives next to the existing `cli_bridges.rs` test so the v3.3 watcher coverage stays close to //! the v3.2 static-list test. use std::path::PathBuf; use std::time::Duration; use aura_cli::bridges::{BridgeManifest, BridgesDiscoveryWatcher}; use aura_cli::config::ClientConfigFile; use aura_pki::AuraCa; use uuid::Uuid; /// Helper: build a fresh CA on disk and return `(cert_pem, key_pem, cert_path, key_path)`. The /// caller is responsible for cleaning up the files on the temp dir. fn fresh_ca() -> (String, String, PathBuf, PathBuf) { let ca = AuraCa::generate("Aura Test").unwrap(); let cert_pem = ca.ca_cert_pem(); let cert_path = std::env::temp_dir().join(format!("aura-bridges-it-{}-ca.crt", Uuid::new_v4())); let key_path = std::env::temp_dir().join(format!("aura-bridges-it-{}-ca.key", Uuid::new_v4())); ca.save(&cert_path, &key_path).unwrap(); let key_pem = std::fs::read_to_string(&key_path).unwrap(); (cert_pem, key_pem, cert_path, key_path) } const CLIENT_TOML_WITH_DISCOVERY: &str = r#" [client] name = "laptop" server_addr = "203.0.113.10:443" sni = "vpn.example.com" bridges = ["203.0.113.11:443"] [client.bridges_discovery] enabled = true manifest_path = "/tmp/aura-bridges-it.signed" refresh_interval_secs = 200 [pki] ca_cert = "ca.crt" cert = "client.crt" key = "client.key" [tunnel] local_ip = "10.7.0.2" "#; #[test] fn parses_bridges_discovery_section() { let cfg = ClientConfigFile::parse(CLIENT_TOML_WITH_DISCOVERY).expect("parse"); assert!(cfg.client.bridges_discovery.enabled); assert_eq!( cfg.client.bridges_discovery.manifest_path.to_string_lossy(), "/tmp/aura-bridges-it.signed" ); assert_eq!(cfg.client.bridges_discovery.refresh_interval_secs, 200); } #[test] fn bridges_discovery_section_optional() { let minimal = r#" [client] name = "x" server_addr = "1.2.3.4:443" sni = "vpn.example.com" [pki] ca_cert = "a" cert = "b" key = "c" [tunnel] local_ip = "10.7.0.2" "#; let cfg = ClientConfigFile::parse(minimal).expect("parse minimal"); assert!( !cfg.client.bridges_discovery.enabled, "default is enabled = false (back-compat)" ); assert_eq!(cfg.client.bridges_discovery.refresh_interval_secs, 3600); } /// End-to-end watcher path: sign a manifest with one CA, hand it to the watcher with a static /// bridges baseline, then rewrite the file with a different list and ensure `refresh_once` picks /// it up. The merged snapshot must always contain the static baseline. #[tokio::test] async fn watcher_picks_up_file_replacement() { let (cert_pem, key_pem, cert_path, key_path) = fresh_ca(); let manifest_path = std::env::temp_dir().join(format!("aura-bridges-it-{}.signed", Uuid::new_v4())); let statics: Vec = vec!["203.0.113.10:443".parse().unwrap()]; // Generation 1: one extra bridge in the manifest. let gen1 = BridgeManifest::with_ttl( vec!["198.51.100.20:443".to_string()], Duration::from_secs(3600), ); gen1.save_signed(&manifest_path, &key_pem).expect("save"); let watcher = BridgesDiscoveryWatcher::new( manifest_path.clone(), cert_pem.clone(), // No background timer — drive refresh manually so the test is deterministic. 0, statics.clone(), ) .await; let snap = watcher.current().await; assert_eq!(snap.len(), 2, "static + one from manifest"); assert!( snap.iter().any(|sa| sa.to_string() == "198.51.100.20:443"), "manifest bridge present" ); // Generation 2: two bridges, one of them duplicating the static baseline. let gen2 = BridgeManifest::with_ttl( vec![ "203.0.113.10:443".to_string(), // dup of static "192.0.2.5:443".to_string(), ], Duration::from_secs(3600), ); gen2.save_signed(&manifest_path, &key_pem).expect("save2"); watcher.refresh_once().await; let snap = watcher.current().await; assert_eq!(snap.len(), 2, "dedup: static + one new"); assert_eq!(snap[0].to_string(), "203.0.113.10:443"); assert_eq!(snap[1].to_string(), "192.0.2.5:443"); // Clean up. let _ = std::fs::remove_file(&manifest_path); let _ = std::fs::remove_file(&cert_path); let _ = std::fs::remove_file(&key_path); } /// Sanity check: a `spawn_refresh` with a non-zero interval picks up a file replacement /// asynchronously. The interval here is 200 ms so the test is fast. #[tokio::test] async fn watcher_background_refresh_picks_up_change() { let (cert_pem, key_pem, cert_path, key_path) = fresh_ca(); let manifest_path = std::env::temp_dir().join(format!("aura-bridges-it-bg-{}.signed", Uuid::new_v4())); let statics: Vec = vec!["203.0.113.10:443".parse().unwrap()]; let gen1 = BridgeManifest::with_ttl( vec!["198.51.100.20:443".to_string()], Duration::from_secs(3600), ); gen1.save_signed(&manifest_path, &key_pem).expect("save"); // Use a background refresher with a 1 s tick. The initial load (in `new`) already pulled // generation 1 in synchronously, so we only need to wait for the *next* tick after we drop a // new manifest into place. let watcher = BridgesDiscoveryWatcher::new(manifest_path.clone(), cert_pem.clone(), 1, statics.clone()) .await; let _bg = watcher.spawn_refresh().expect("background task"); assert_eq!(watcher.current().await.len(), 2, "static + gen1"); // Swap to a manifest with three new bridges. The first tick the background loop runs (after // the discard-first-tick) must observe the new file. let gen2 = BridgeManifest::with_ttl( vec![ "192.0.2.5:443".to_string(), "192.0.2.6:443".to_string(), "192.0.2.7:443".to_string(), ], Duration::from_secs(3600), ); gen2.save_signed(&manifest_path, &key_pem).expect("save2"); // The background task ticks once per second; allow some slack on slow CI. tokio::time::sleep(Duration::from_millis(2500)).await; let snap = watcher.current().await; assert_eq!(snap.len(), 4, "static + three new"); assert_eq!(snap[0].to_string(), "203.0.113.10:443"); assert!(snap.iter().any(|sa| sa.to_string() == "192.0.2.5:443")); assert!(snap.iter().any(|sa| sa.to_string() == "192.0.2.7:443")); let _ = std::fs::remove_file(&manifest_path); let _ = std::fs::remove_file(&cert_path); let _ = std::fs::remove_file(&key_path); }