//! Integration tests for nx9-wg CLI commands across all 16 command groups. use std::process::Command; use tempfile::TempDir; fn nx9_bin() -> &'static str { env!("CARGO_BIN_EXE_nx9-wg") } struct CliRunner { _temp_dir: TempDir, db_path: String, } impl CliRunner { fn new() -> Self { let temp_dir = TempDir::new().expect("create temp dir"); let db_path = temp_dir .path() .join("test-nx9.db") .to_string_lossy() .to_string(); Self { _temp_dir: temp_dir, db_path, } } fn run(&self, args: &[&str]) -> (bool, String, String) { let mut cmd = Command::new(nx9_bin()); cmd.arg("--database").arg(&self.db_path); cmd.arg("--quiet"); for arg in args { cmd.arg(arg); } let output = cmd.output().expect("execute command"); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); (output.status.success(), stdout, stderr) } } #[test] fn test_cli_version_and_formats() { let runner = CliRunner::new(); // Table / text format let (ok, out, _) = runner.run(&["version"]); assert!(ok); assert!(out.contains("nx9-wg")); assert!(out.contains("single_admin_security")); // JSON format let (ok, out, _) = runner.run(&["version", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(v["name"], "nx9-wg"); assert_eq!(v["single_admin_security"], true); // YAML format let (ok, out, _) = runner.run(&["version", "--format", "yaml"]); assert!(ok); assert!(out.contains("name: \"nx9-wg\"")); // CSV format let (ok, out, _) = runner.run(&["version", "--format", "csv"]); assert!(ok); assert!(out.contains("nx9-wg")); } #[test] fn test_cli_admin_init_and_management() { let runner = CliRunner::new(); // Init admin let (ok, out, _) = runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); assert!(ok, "init succeeded"); assert!(out.contains("Administrator initialized successfully")); // Verify admin status let (ok, out, _) = runner.run(&["admin", "status", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(v["username"], "admin"); assert_eq!(v["totp_enabled"], false); // Test password update let (ok, out, _) = runner.run(&["admin", "password", "--new-password", "NewSecretPass456!"]); assert!(ok); assert!(out.contains("Administrator password updated successfully")); // Test API token creation (write one-time token to a restricted file) let token_file = runner ._temp_dir .path() .join("admin_token.txt") .to_string_lossy() .to_string(); let (ok, out, _) = runner.run(&[ "admin", "tokens", "create", "--name", "ci-deployer", "--days", "30", "--write-token-file", &token_file, ]); assert!(ok); assert!(out.contains("API Token Created")); assert!(out.contains("Token written to file")); // Verify token file exists and contains the token once let token_contents = std::fs::read_to_string(&token_file).expect("read token file"); assert!(token_contents.starts_with("nx9_")); // verify restrictive permissions on unix systems #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = std::fs::metadata(&token_file) .expect("stat") .permissions() .mode() & 0o777; assert_eq!(mode, 0o600); } // List tokens let (ok, out, _) = runner.run(&["admin", "tokens", "list", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(!v.as_array().unwrap().is_empty()); let token_id = v[0]["id"].as_str().unwrap(); // Revoke token let (ok, out, _) = runner.run(&["admin", "tokens", "revoke", token_id]); assert!(ok); assert!(out.contains("revoked")); // Sessions list let (ok, _, _) = runner.run(&["admin", "sessions", "list", "--format", "json"]); assert!(ok); // Sessions revoke-all let (ok, out, _) = runner.run(&["admin", "sessions", "revoke-all"]); assert!(ok); assert!(out.contains("All active sessions revoked")); } #[test] fn test_cli_system_commands() { let runner = CliRunner::new(); // Init first runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); // Health let (ok, out, _) = runner.run(&["system", "health", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(v["status"], "healthy"); assert_eq!(v["database"], "connected"); // Status let (ok, out, _) = runner.run(&["system", "status", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(v["admin_initialized"], true); // Info let (ok, out, _) = runner.run(&["system", "info", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(v["version"].is_string()); // Settings CRUD let (ok, _, _) = runner.run(&["system", "settings", "set", "banner", "Welcome to NX9"]); assert!(ok); let (ok, out, _) = runner.run(&["system", "settings", "get", "banner", "--format", "json"]); assert!(ok); let v: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(v["value"], "Welcome to NX9"); let (ok, _, _) = runner.run(&["system", "settings", "delete", "banner"]); assert!(ok); } #[test] fn test_cli_interface_and_peer_lifecycle() { let runner = CliRunner::new(); runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); // Create interface let (ok, out, _) = runner.run(&[ "interface", "create", "wg0", "--port", "51820", "--address-v4", "10.100.0.1/24", "--dns", "1.1.1.1", "--format", "json", ]); assert!(ok); let iface: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(iface["name"], "wg0"); let iface_id = iface["id"].as_str().unwrap(); // Show interface let (ok, out, _) = runner.run(&["interface", "show", "wg0", "--format", "json"]); assert!(ok); let iface_show: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(iface_show["id"], iface_id); // List interfaces let (ok, out, _) = runner.run(&["interface", "list", "--format", "json"]); assert!(ok); let ifaces: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(ifaces.as_array().unwrap().len(), 1); // Create Peer let (ok, out, _) = runner.run(&[ "peer", "create", "--interface", "wg0", "--name", "laptop-alice", "--address-v4", "10.100.0.2/32", "--format", "json", ]); assert!(ok); let peer: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(peer["name"], "laptop-alice"); let peer_id = peer["id"].as_str().unwrap(); // Show Peer let (ok, out, _) = runner.run(&["peer", "show", peer_id, "--format", "json"]); assert!(ok); let peer_show: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(peer_show["id"], peer_id); // List Peers let (ok, out, _) = runner.run(&["peer", "list", "--interface", "wg0", "--format", "json"]); assert!(ok); let peers: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(peers.as_array().unwrap().len(), 1); // Peer Config without endpoint or setting should fail with actionable error let (ok, _, err) = runner.run(&["peer", "config", peer_id]); assert!(!ok); assert!(err.contains("No reachable WireGuard server endpoint is configured")); // Peer Config with explicit --endpoint let (ok, out, _) = runner.run(&["peer", "config", peer_id, "--endpoint", "vpn.example.com"]); assert!(ok); assert!(out.contains("[Interface]")); assert!(out.contains("[Peer]")); assert!(out.contains("Endpoint = vpn.example.com:51820")); // Peer QR ASCII let (ok, out, _) = runner.run(&[ "peer", "qr", peer_id, "--endpoint", "vpn.example.com", "--qr-format", "terminal", ]); assert!(ok); assert!(!out.is_empty()); // Peer QR SVG let (ok, out, _) = runner.run(&[ "peer", "qr", peer_id, "--endpoint", "vpn.example.com", "--qr-format", "svg", ]); assert!(ok); assert!(out.contains("(&out) .unwrap() .as_array() .unwrap() .len(), 1 ); let (ok, _, _) = runner.run(&["network", "delete", net_id]); assert!(ok); // Route CRUD & Sync let (ok, out, _) = runner.run(&[ "route", "add", "--destination", "172.16.0.0/16", "--interface-name", "eth0", "--metric", "100", "--format", "json", ]); assert!(ok); let route: serde_json::Value = serde_json::from_str(&out).expect("valid json"); let route_id = route["id"].as_str().unwrap(); let (ok, _, _) = runner.run(&["route", "sync"]); assert!(ok); let (ok, _, _) = runner.run(&["route", "status"]); assert!(ok); let (ok, _, _) = runner.run(&["route", "delete", route_id]); assert!(ok); // Firewall CRUD & Sync let (ok, out, _) = runner.run(&[ "firewall", "add", "--name", "allow-ssh", "--protocol", "tcp", "--port", "22", "--action", "accept", "--priority", "10", "--format", "json", ]); assert!(ok); let fw: serde_json::Value = serde_json::from_str(&out).expect("valid json"); let fw_id = fw["id"].as_str().unwrap(); let (ok, _, _) = runner.run(&["firewall", "sync"]); assert!(ok); let (ok, _, _) = runner.run(&["firewall", "status"]); assert!(ok); let (ok, _, _) = runner.run(&["firewall", "delete", fw_id]); assert!(ok); // NAT management let (ok, _, _) = runner.run(&["nat", "enable"]); assert!(ok); let (ok, _, _) = runner.run(&["nat", "status"]); assert!(ok); let (ok, _, _) = runner.run(&["nat", "sync"]); assert!(ok); let (ok, _, _) = runner.run(&["nat", "disable"]); assert!(ok); // Forwarding management let (ok, _, _) = runner.run(&["forwarding", "status"]); assert!(ok); let (ok, _, _) = runner.run(&["forwarding", "enable"]); assert!(ok); let (ok, _, _) = runner.run(&["forwarding", "sync"]); assert!(ok); } #[test] fn test_cli_reconciliation_backup_audit_live() { let runner = CliRunner::new(); runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); // Reconcile commands let (ok, _, _) = runner.run(&["reconcile", "status"]); assert!(ok); let (ok, _, _) = runner.run(&["reconcile", "plan"]); assert!(ok); let (ok, _, _) = runner.run(&["reconcile", "apply"]); assert!(ok); let (ok, _, _) = runner.run(&["reconcile", "verify"]); assert!(ok); // Backup commands let (ok, out, _) = runner.run(&[ "backup", "create", "--description", "auto-cli-test", "--format", "json", ]); assert!(ok); let backup_meta: serde_json::Value = serde_json::from_str(&out).expect("valid json"); let backup_id = backup_meta["id"].as_str().unwrap(); let (ok, out, _) = runner.run(&["backup", "list", "--format", "json"]); assert!(ok); assert_eq!( serde_json::from_str::(&out) .unwrap() .as_array() .unwrap() .len(), 1 ); let (ok, out, _) = runner.run(&["backup", "show", backup_id, "--format", "json"]); assert!(ok); assert_eq!( serde_json::from_str::(&out).unwrap()["id"], backup_id ); let (ok, _, _) = runner.run(&["backup", "delete", backup_id]); assert!(ok); // Audit commands let (ok, out, _) = runner.run(&["audit", "list", "--format", "json"]); assert!(ok); let events: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(!events.as_array().unwrap().is_empty()); let event_id = events[0]["id"].as_i64().unwrap(); let (ok, out, _) = runner.run(&["audit", "show", &event_id.to_string(), "--format", "json"]); assert!(ok); assert_eq!( serde_json::from_str::(&out).unwrap()["id"], event_id ); // Live commands let (ok, _, _) = runner.run(&["live", "interface", "list"]); assert!(ok); let (ok, _, _) = runner.run(&["live", "routes"]); assert!(ok); let (ok, _, _) = runner.run(&["live", "firewall"]); assert!(ok); let (ok, _, _) = runner.run(&["live", "forwarding"]); assert!(ok); let (ok, _, _) = runner.run(&["live", "nat"]); assert!(ok); } #[test] fn test_cli_wiregui_capabilities() { let runner = CliRunner::new(); runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); // 1. Diagnostics subsystem let (ok, out, _) = runner.run(&["diagnostics", "all", "--format", "json"]); assert!(ok, "diagnostics all failed: {out}"); let diag_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(diag_json.as_array().unwrap().len() >= 5); let (ok, out, _) = runner.run(&["diagnostics", "system", "--format", "json"]); assert!(ok); let sys_diag: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(sys_diag[0]["subsystem"], "system"); // 2. Network create, available, and allocations let (ok, _, _) = runner.run(&[ "network", "create", "corp-lan", "10.88.0.0/24", "--description", "Corporate LAN", ]); assert!(ok); let (ok, _, _) = runner.run(&[ "interface", "create", "wg88", "--address-v4", "10.88.0.1/24", "--port", "51888", ]); assert!(ok); let (ok, out, _) = runner.run(&[ "network", "available", "corp-lan", "--interface", "wg88", "--format", "json", ]); assert!(ok, "network available failed: {out}"); let avail_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(avail_json[0]["available_ip"], "10.88.0.2"); // 3. Peer create with automatic IP allocation from network let (ok, out, _) = runner.run(&[ "peer", "create", "--interface", "wg88", "--name", "auto-ip-peer", "--network", "corp-lan", "--expires-at", "2030-01-01 00:00:00", "--format", "json", ]); assert!(ok, "peer create failed: {out}"); let peer_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); let peer_id = peer_json["id"].as_str().unwrap(); assert_eq!(peer_json["address_v4"], "10.88.0.2/32"); // Network allocations should now reflect the new peer let (ok, out, _) = runner.run(&["network", "allocations", "corp-lan", "--format", "json"]); assert!(ok); let alloc_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(alloc_json.as_array().unwrap().len(), 1); assert_eq!(alloc_json[0]["ip_address"], "10.88.0.2/32"); // Next available should now be 10.88.0.3 let (ok, out, _) = runner.run(&[ "network", "available", "corp-lan", "--interface", "wg88", "--format", "json", ]); assert!(ok); let avail_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(avail_json[0]["available_ip"], "10.88.0.3"); // Peer lifecycle inspection let (ok, out, _) = runner.run(&["peer", "lifecycle", peer_id, "--format", "json"]); assert!(ok); let life_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(life_json["state"], "active"); assert_eq!(life_json["is_expired"], false); // Peer expiration let (ok, _, _) = runner.run(&["peer", "expire", peer_id]); assert!(ok); let (ok, out, _) = runner.run(&["peer", "lifecycle", peer_id, "--format", "json"]); assert!(ok); let life_json2: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(life_json2["state"], "expired"); assert_eq!(life_json2["is_expired"], true); // 4. Firewall rule with peer association and port range let (ok, out, _) = runner.run(&[ "firewall", "add", "--name", "peer-app-range", "--peer", peer_id, "--protocol", "tcp_udp", "--port-range", "8000-8100", "--direction", "forward", "--action", "accept", "--priority", "20", "--format", "json", ]); assert!(ok, "firewall add failed: {out}"); let fw_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(fw_json["peer_id"], peer_id); assert_eq!(fw_json["port_range"], "8000-8100"); assert_eq!(fw_json["protocol"], "tcp_udp"); // List firewall rules filtered by peer let (ok, out, _) = runner.run(&["firewall", "list", "--peer", peer_id, "--format", "json"]); assert!(ok); let list_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(list_json.as_array().unwrap().len(), 1); } #[test] fn test_cli_client_profile_and_mtu_system() { let runner = CliRunner::new(); // 1. Initialize admin, interface, and peer runner.run(&[ "init", "--username", "admin", "--password", "AdminPassword123!", ]); runner.run(&[ "interface", "create", "wg77", "--address-v4", "10.77.0.1/24", ]); let (ok, out, _) = runner.run(&[ "peer", "create", "--interface", "wg77", "--name", "alice-phone", "--address-v4", "10.77.0.2/32", "--format", "json", ]); assert!(ok); let peer_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); let peer_id = peer_json["id"].as_str().unwrap(); // 2. Profile List let (ok, out, _) = runner.run(&["profile", "list", "--format", "json"]); assert!(ok, "profile list failed: {out}"); let profiles: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(profiles.as_array().unwrap().len() >= 10); // Filter profile list by connection let (ok, out, _) = runner.run(&[ "profile", "list", "--connection", "mobile", "--format", "json", ]); assert!(ok); let mob_profiles: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert!(mob_profiles.as_array().unwrap().len() >= 2); // 3. Profile Show let (ok, out, _) = runner.run(&["profile", "show", "default-mobile", "--format", "json"]); assert!(ok); let show_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(show_json["id"], "default-mobile"); assert_eq!(show_json["mtu"], 1280); assert_eq!(show_json["connection_type"], "mobile"); // 4. Profile Validate let (ok, out, _) = runner.run(&["profile", "validate", "1280", "--format", "json"]); assert!(ok); let val_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(val_json["valid"], true); assert_eq!(val_json["is_jumbo"], false); let (ok, _, err) = runner.run(&["profile", "validate", "1100"]); assert!(!ok); assert!(err.contains("below IPv6 minimum MTU")); // 5. Profile Resolve let (ok, out, _) = runner.run(&[ "profile", "resolve", "--connection", "mobile", "--device", "android", "--format", "json", ]); assert!(ok); let res_json: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(res_json["mtu"], 1280); assert_eq!(res_json["applied_profile_id"], "android-mobile"); let (ok, out, _) = runner.run(&[ "profile", "resolve", "--nat", "cgnat", "--provider", "starlink", "--format", "json", ]); assert!(ok); let res_json2: serde_json::Value = serde_json::from_str(&out).expect("valid json"); assert_eq!(res_json2["mtu"], 1360); assert_eq!(res_json2["applied_profile_id"], "starlink-cgnat"); // 6. Set server_endpoint setting let (ok, _, _) = runner.run(&[ "system", "settings", "set", "server_endpoint", "vpn.example.com", ]); assert!(ok); // 7. Peer Config with Profile Options let (ok, out, _) = runner.run(&[ "peer", "config", peer_id, "--connection", "mobile", "--device", "android", ]); assert!(ok, "peer config failed: {out}"); assert!(out.contains("MTU = 1280")); assert!(out.contains("PersistentKeepalive = 25")); let (ok, out, _) = runner.run(&[ "peer", "config", peer_id, "--nat", "cgnat", "--provider", "starlink", ]); assert!(ok); assert!(out.contains("MTU = 1360")); let (ok, out, _) = runner.run(&["peer", "config", peer_id, "--mtu", "1380"]); assert!(ok); assert!(out.contains("MTU = 1380")); // 7. Peer QR with Profile Options let (ok, out, _) = runner.run(&[ "peer", "qr", peer_id, "--connection", "mobile", "--qr-format", "svg", ]); assert!(ok); assert!(out.contains("