883 lines
25 KiB
Rust
883 lines
25 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("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);
|
|
|
|
// Status
|
|
let (ok, _, _) = runner.run(&["interface", "status", "wg0"]);
|
|
assert!(ok);
|
|
|
|
// 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 Delete
|
|
let (ok, _, _) = runner.run(&["interface", "delete", "wg0"]);
|
|
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!",
|
|
]);
|
|
|
|
// 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::<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"));
|
|
}
|
|
|
|
#[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();
|
|
}
|