release: NX9-WG v1.0.0
This commit is contained in:
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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user