cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output
- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization. - Create parent directories when an explicit --database path is provided. - Redact printed generated administrator passwords; announce file path or redact instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
2ac6c81dfe
140 files changed
+31342
No files matched your search
@@ -0,0 +1,236 @@
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use nx9_wg_api::auth::{BootstrapOptions, bootstrap_admin};
|
||||
use nx9_wg_api::routes::build_api_router;
|
||||
use nx9_wg_api::state::AppState;
|
||||
use nx9_wg_core::config::AppConfig;
|
||||
use nx9_wg_db::Store;
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
||||
async fn setup_test_app() -> (axum::Router, String) {
|
||||
let store = Store::connect_in_memory().await.expect("connect in-memory");
|
||||
store.migrate().await.expect("migrate");
|
||||
|
||||
let config = AppConfig::default();
|
||||
let opts = BootstrapOptions {
|
||||
cli_password: Some("AdminSecret123!".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
bootstrap_admin(&store, &config, &opts)
|
||||
.await
|
||||
.expect("bootstrap");
|
||||
|
||||
let state = AppState::new(store);
|
||||
let app = build_api_router(state.clone());
|
||||
|
||||
// Login to get session ID
|
||||
let login_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/auth/login")
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"username": "admin",
|
||||
"password": "AdminSecret123!"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(login_req).await.expect("login request");
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let cookie_header = resp
|
||||
.headers()
|
||||
.get(header::SET_COOKIE)
|
||||
.expect("set-cookie")
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let session_cookie = cookie_header.split(';').next().unwrap().to_string();
|
||||
|
||||
(app, session_cookie)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_public_health_and_version_endpoints() {
|
||||
let (app, _) = setup_test_app().await;
|
||||
|
||||
// Health
|
||||
let req = Request::builder()
|
||||
.uri("/api/v1/system/health")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let val: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(val["status"], "healthy");
|
||||
assert_eq!(val["database"], "connected");
|
||||
|
||||
// Version
|
||||
let req = Request::builder()
|
||||
.uri("/api/v1/system/version")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let val: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(val["name"], "nx9-wg");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_protected_route_unauthenticated_rejection() {
|
||||
let (app, _) = setup_test_app().await;
|
||||
|
||||
// Request protected route without auth
|
||||
let req = Request::builder()
|
||||
.uri("/api/v1/system")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_interfaces_and_peers_rest_lifecycle() {
|
||||
let (app, cookie) = setup_test_app().await;
|
||||
|
||||
// 1. Create interface
|
||||
let create_iface_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/interfaces")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"name": "wg0",
|
||||
"listen_port": 51820,
|
||||
"address_v4": "10.0.0.1/24",
|
||||
"dns": "1.1.1.1"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(create_iface_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let iface_val: Value = serde_json::from_slice(&body).unwrap();
|
||||
let iface_id = iface_val["id"].as_str().unwrap();
|
||||
assert_eq!(iface_val["name"], "wg0");
|
||||
|
||||
// 2. List interfaces
|
||||
let list_req = Request::builder()
|
||||
.uri("/api/v1/interfaces")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(list_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// 3. Create peer under interface
|
||||
let create_peer_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(format!("/api/v1/interfaces/{iface_id}/peers"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"name": "laptop-alice",
|
||||
"peer_type": "road_warrior",
|
||||
"profile": "full_tunnel",
|
||||
"allowed_ips": "10.0.0.2/32"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(create_peer_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let peer_val: Value = serde_json::from_slice(&body).unwrap();
|
||||
let peer_id = peer_val["id"].as_str().unwrap();
|
||||
assert_eq!(peer_val["name"], "laptop-alice");
|
||||
|
||||
// 4. Disable peer
|
||||
let disable_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(format!("/api/v1/peers/{peer_id}/disable"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(disable_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// 5. Get peer and verify state
|
||||
let get_peer_req = Request::builder()
|
||||
.uri(format!("/api/v1/peers/{peer_id}"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(get_peer_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let peer_val: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(peer_val["state"], "disabled");
|
||||
|
||||
// 6. Delete interface (cascades peer)
|
||||
let del_iface_req = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(format!("/api/v1/interfaces/{iface_id}"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(del_iface_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_networks_and_firewall_rest_lifecycle() {
|
||||
let (app, cookie) = setup_test_app().await;
|
||||
|
||||
// Create network
|
||||
let net_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/networks")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"name": "Management Network",
|
||||
"cidr": "10.10.0.0/16",
|
||||
"description": "Internal management"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(net_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// Create firewall rule
|
||||
let fw_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/firewall/rules")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"name": "Allow HTTPS",
|
||||
"direction": "in",
|
||||
"action": "accept",
|
||||
"protocol": "tcp",
|
||||
"destination_port": 443,
|
||||
"priority": 10
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(fw_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let rule_val: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(rule_val["name"], "Allow HTTPS");
|
||||
assert_eq!(rule_val["priority"], 10);
|
||||
}
|
||||
Reference in new issue
Block a user