release: NX9-WG v1.0.0

This commit is contained in:
thakares committed 2026-08-18 17:32:56 +05:30
1 parent c8a9b7cde6
commit 4dfe42fe68
42 files changed
+4689 -336

No files matched your search

@@ -7,6 +7,24 @@ use nx9_wg_api::state::AppState;
use nx9_wg_db::Store;
use tower::ServiceExt;
async fn setup_test_app() -> (axum::Router, Store) {
let store = Store::connect_in_memory().await.expect("connect store");
store.migrate().await.expect("migrate store");
let config = nx9_wg_core::config::AppConfig::default();
let opts = nx9_wg_api::auth::BootstrapOptions {
cli_password: Some("TestAdminPassword123!".to_string()),
..Default::default()
};
nx9_wg_api::auth::bootstrap_admin(&store, &config, &opts)
.await
.expect("bootstrap admin");
let state = AppState::new(store.clone());
let app = build_api_router(state);
(app, store)
}
#[tokio::test]
async fn test_ui_spa_index_and_stylesheet_endpoints() {
let store = Store::connect_in_memory().await.expect("connect store");
@@ -121,6 +139,11 @@ async fn test_ui_api_complete_functional_loop() {
.await
.expect("bootstrap admin");
store
.set_setting("server_endpoint", "vpn.example.com", false)
.await
.expect("set server_endpoint");
let state = AppState::new(store.clone());
let app = build_api_router(state);
@@ -227,6 +250,26 @@ async fn test_ui_api_complete_functional_loop() {
let peer_json: serde_json::Value = serde_json::from_slice(&peer_body).unwrap();
let peer_id = peer_json["id"].as_str().unwrap();
// 4b. UI fetches collection of all peers (Peers page render: GET /api/v1/peers)
let res_all_peers = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/peers")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.expect("get all peers");
assert_eq!(res_all_peers.status(), StatusCode::OK);
let all_peers_bytes = to_bytes(res_all_peers.into_body(), 1024 * 1024)
.await
.unwrap();
let all_peers_json: Vec<serde_json::Value> = serde_json::from_slice(&all_peers_bytes).unwrap();
assert_eq!(all_peers_json.len(), 1);
assert_eq!(all_peers_json[0]["name"], "alice-phone");
// 5. UI downloads Client Config & SVG QR Code
let res_conf = app
.clone()
@@ -452,6 +495,7 @@ async fn test_ui_api_complete_functional_loop() {
// 13. UI Logout
let res_logout = app
.clone()
.oneshot(
Request::builder()
.method("POST")
@@ -463,4 +507,205 @@ async fn test_ui_api_complete_functional_loop() {
.await
.expect("logout request");
assert_eq!(res_logout.status(), StatusCode::OK);
// 14. Post-Logout: Session must be completely rejected on protected endpoints
let res_post_logout = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/auth/session")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.expect("post-logout session request");
assert_eq!(res_post_logout.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_logout_session_invalidation_and_idempotency() {
let (app, _store) = setup_test_app().await;
// 1. Initial login
let login_body = serde_json::to_vec(&serde_json::json!({
"username": "admin",
"password": "TestAdminPassword123!"
}))
.unwrap();
let res_login = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/login")
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(axum::body::Body::from(login_body))
.unwrap(),
)
.await
.expect("login request");
assert_eq!(res_login.status(), StatusCode::OK);
let cookie_header = res_login
.headers()
.get(axum::http::header::SET_COOKIE)
.expect("Set-Cookie header present")
.to_str()
.unwrap();
let session_cookie = cookie_header
.split(';')
.next()
.expect("nx9_session cookie")
.to_string();
// 2. Verified access before logout
let res_auth_session = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/auth/session")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res_auth_session.status(), StatusCode::OK);
let res_system = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/system")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res_system.status(), StatusCode::OK);
// 3. Perform Logout
let res_logout = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/logout")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res_logout.status(), StatusCode::OK);
let logout_cookie = res_logout
.headers()
.get(axum::http::header::SET_COOKIE)
.expect("Set-Cookie on logout")
.to_str()
.unwrap();
assert!(
logout_cookie.contains("Max-Age=0"),
"Logout must clear session cookie with Max-Age=0"
);
// 4. All protected endpoints must return 401 Unauthorized after logout
let endpoints = [
"/api/v1/auth/session",
"/api/v1/system",
"/api/v1/interfaces",
"/api/v1/networks",
"/api/v1/routes",
"/api/v1/firewall/rules",
"/api/v1/diagnostics/all",
"/api/v1/client-profiles",
"/api/v1/audit",
"/api/v1/backups",
"/api/v1/reconcile/plan",
];
for ep in endpoints {
let res_blocked = app
.clone()
.oneshot(
Request::builder()
.uri(ep)
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
res_blocked.status(),
StatusCode::UNAUTHORIZED,
"Endpoint {ep} must be blocked (401) after logout"
);
}
// 5. Repeated logout when already logged out is safe and idempotent
let res_logout_again = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/logout")
.header(axum::http::header::COOKIE, &session_cookie)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res_logout_again.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_ui_index_contains_login_view_and_hidden_app_layout() {
let (app, _store) = setup_test_app().await;
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/")
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.expect("index request");
assert_eq!(res.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(res.into_body(), 1024 * 1024)
.await
.unwrap();
let html = String::from_utf8(bytes.to_vec()).unwrap();
assert!(
html.contains("id=\"login-view\""),
"HTML must contain dedicated login-view container"
);
assert!(
html.contains("id=\"app-layout\" style=\"display: none;\""),
"app-layout must be initially hidden until authenticated"
);
assert!(
html.contains("id=\"login-username\""),
"HTML must contain login username input"
);
assert!(
html.contains("id=\"login-password\""),
"HTML must contain login password input"
);
assert!(
html.contains("id=\"login-submit-btn\""),
"HTML must contain login submit button"
);
assert!(
html.contains("handleLogout()"),
"HTML must contain handleLogout handler"
);
}