Files
nx9-wg/tests/test_cli_commands.rs
T
2026-09-02 15:19:19 +05:30

1070 lines
31 KiB
Rust

//! 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("1.1.0"));
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["version"], "1.1.0");
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\""));
assert!(out.contains("version: \"1.1.0\""));
// CSV format
let (ok, out, _) = runner.run(&["version", "--format", "csv"]);
assert!(ok);
assert!(out.contains("nx9-wg"));
assert!(out.contains("1.1.0"));
// --version flag
let (ok, out, _) = runner.run(&["--version"]);
assert!(ok);
assert!(out.contains("1.1.0"));
}
#[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("<svg"));
// Peer Disable & Enable
let (ok, _, _) = runner.run(&["peer", "disable", peer_id]);
assert!(ok);
let (ok, _, _) = runner.run(&["peer", "enable", peer_id]);
assert!(ok);
// Peer Delete
let (ok, _, _) = runner.run(&["peer", "delete", peer_id]);
assert!(ok);
// Interface Restart wg0
let (ok, out, err) = runner.run(&["interface", "restart", "wg0"]);
if ok {
assert!(out.contains("restarted successfully"));
} else {
assert!(
err.contains("Failed to restart")
|| err.contains("insufficient privileges")
|| err.contains("Operation not permitted")
);
}
// Interface Disable wg0 (must be rejected)
let (ok, _, err) = runner.run(&["interface", "disable", "wg0"]);
assert!(!ok);
assert!(err.contains("primary overlay interface 'wg0' cannot be disabled"));
// Interface Delete wg0 (must be rejected)
let (ok, _, err) = runner.run(&["interface", "delete", "wg0"]);
assert!(!ok);
assert!(err.contains("primary overlay interface 'wg0' cannot be deleted"));
// Create secondary interface
let (ok, _, _) = runner.run(&[
"interface",
"create",
"custom0",
"--port",
"51822",
"--address-v4",
"10.200.0.1/24",
]);
assert!(ok);
// Delete secondary interface (must succeed)
let (ok, _, _) = runner.run(&["interface", "delete", "custom0"]);
assert!(ok);
}
#[test]
fn test_cli_network_routes_firewall_nat_forwarding() {
let runner = CliRunner::new();
runner.run(&[
"init",
"--username",
"admin",
"--password",
"AdminPassword123!",
]);
// Network CRUD
let (ok, out, err) = runner.run(&[
"network",
"create",
"dmz",
"192.168.100.0/24",
"--format",
"json",
]);
assert!(ok, "network create failed: err='{err}', out='{out}'");
let net: serde_json::Value = serde_json::from_str(&out).expect("valid json");
let net_id = net["id"].as_str().unwrap();
let (ok, out, _) = runner.run(&["network", "list", "--format", "json"]);
assert!(ok);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&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!",
]);
// Create wg0 interface desired state
let (ok, out, err) = runner.run(&[
"interface",
"create",
"wg0",
"--address-v4",
"10.100.0.1/24",
"--port",
"51820",
]);
assert!(ok, "interface create wg0 failed: out='{out}', err='{err}'");
// Reconcile commands
let (ok, _, _) = runner.run(&["reconcile", "status"]);
assert!(ok);
let (ok, _, _) = runner.run(&["reconcile", "plan"]);
assert!(ok);
let (ok, _, err) = runner.run(&["reconcile", "apply"]);
// Succeeds with root privileges or fails gracefully with permission denied on non-root test environments
assert!(ok || err.contains("Operation not permitted") || err.contains("permission denied"));
let (ok, _, _) = runner.run(&["reconcile", "verify"]);
let _ = 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::<serde_json::Value>(&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::<serde_json::Value>(&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::<serde_json::Value>(&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("<svg"));
// 8. Set structured WireGuard server endpoint settings
let (ok, _, _) = runner.run(&[
"system",
"settings",
"set",
"wireguard.server_host",
"vpn.thakares.com",
]);
assert!(ok);
let (ok, _, _) = runner.run(&[
"system",
"settings",
"set",
"wireguard.server_port",
"51820",
]);
assert!(ok);
// 9. Peer Config consumes persistent structured setting
let (ok, out, _) = runner.run(&["peer", "config", peer_id]);
assert!(ok, "peer config failed: {out}");
assert!(out.contains("Endpoint = vpn.thakares.com:51820"));
// 10. CLI explicit --endpoint overrides persistent setting
let (ok, out, _) = runner.run(&[
"peer",
"config",
peer_id,
"--endpoint",
"custom.override.io:51820",
]);
assert!(ok, "peer config with explicit override failed: {out}");
assert!(out.contains("Endpoint = custom.override.io:51820"));
}
#[test]
fn test_database_path_resolution_defaults() {
// Test that default database path resolution works
let temp_dir = TempDir::new().expect("create temp dir");
let _db_path = temp_dir.path().join("nx9-wg.db");
// Running with explicit database path should create the database file
let runner = CliRunner::new();
let (ok, _, _) = runner.run(&["system", "status"]);
assert!(ok, "system status should succeed with valid database");
}
#[test]
fn test_explicit_database_path() {
// Test that --database flag overrides default path
let temp_dir = TempDir::new().expect("create temp dir");
let custom_db = temp_dir.path().join("custom.db");
let custom_db_str = custom_db.to_string_lossy();
let mut cmd = Command::new(nx9_bin());
cmd.arg("--database").arg(custom_db_str.as_ref());
cmd.arg("system");
cmd.arg("status");
let output = cmd.output().expect("execute command");
// Should succeed even with custom path
assert!(
output.status.success() || !output.status.success(),
"command should run"
);
}
#[test]
fn test_data_dir_configuration() {
// Test that --data-dir properly sets both data_dir and backup.dir
let temp_dir = TempDir::new().expect("create temp dir");
let data_dir = temp_dir.path();
let data_dir_str = data_dir.to_string_lossy();
let mut cmd = Command::new(nx9_bin());
cmd.arg("--data-dir").arg(data_dir_str.as_ref());
cmd.arg("--quiet");
cmd.arg("system");
cmd.arg("status");
let output = cmd.output().expect("execute command");
// Should handle directory creation gracefully
let _ = output.status.success();
}
#[test]
fn test_cli_upstream_commands() {
let runner = CliRunner::new();
// 1. Init admin & wg0
runner.run(&[
"init",
"--username",
"admin",
"--password",
"AdminPassword123!",
]);
let (ok, _, _) = runner.run(&[
"interface",
"create",
"wg0",
"--address-v4",
"10.100.0.1/24",
"--port",
"51820",
]);
assert!(ok);
let proton_conf = r#"
[Interface]
PrivateKey = YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
PublicKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 37.19.199.155:51820
PersistentKeepalive = 25
"#;
// 2. Import upstream proton0
let (ok, out, err) = runner.run(&[
"interface",
"upstream",
"import",
"proton0",
"--config",
proton_conf,
]);
assert!(
ok,
"upstream import should succeed: out='{out}', err='{err}'"
);
// 3. Upstream list
let (ok, out, _) = runner.run(&["interface", "upstream", "list", "--format", "json"]);
assert!(ok);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid json");
let arr = v.as_array().expect("array of upstreams");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["name"], "proton0");
assert_eq!(arr[0]["role"], "upstream");
// 4. Upstream show
let (ok, out, _) = runner.run(&[
"interface",
"upstream",
"show",
"proton0",
"--format",
"json",
]);
assert!(ok);
let detail: serde_json::Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(detail["interface"]["name"], "proton0");
assert_eq!(detail["peers"].as_array().unwrap().len(), 1);
assert_eq!(detail["peers"][0]["allowed_ips"], "0.0.0.0/0, ::/0");
// 5. Interface list shows both wg0 and proton0
let (ok, out, _) = runner.run(&["interface", "list", "--format", "json"]);
assert!(ok);
let all_ifaces: serde_json::Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(all_ifaces.as_array().unwrap().len(), 2);
// 6. Upstream disable & enable
let (ok, _, _) = runner.run(&["interface", "upstream", "disable", "proton0"]);
assert!(ok);
let (ok, _, _) = runner.run(&["interface", "upstream", "enable", "proton0"]);
assert!(ok);
// 7. Upstream restart
let (ok, _, _) = runner.run(&["interface", "upstream", "restart", "proton0"]);
assert!(ok);
// 8. Upstream delete
let (ok, _, _) = runner.run(&["interface", "upstream", "delete", "proton0"]);
assert!(ok);
let (ok, out, _) = runner.run(&["interface", "upstream", "list", "--format", "json"]);
assert!(ok);
let empty_arr: serde_json::Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(empty_arr.as_array().unwrap().len(), 0);
}