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

255 lines
9.1 KiB
Rust

//! Integration tests for Client Profiles REST API endpoints and resolver.
use axum::body::Body;
use axum::http::{Request, StatusCode};
use ipnet::IpNet;
use nx9_wg_api::state::AppState;
use nx9_wg_core::crypto::generate_keypair;
use nx9_wg_core::types::client_profile::{ClientProfile, ConnectionType, ResolvedClientProfile};
use nx9_wg_core::types::wireguard::{
Interface, InterfaceRole, Peer, PeerProfile, PeerState, PeerType,
};
use nx9_wg_db::Store;
use std::str::FromStr;
use tower::ServiceExt;
use uuid::Uuid;
async fn setup_test_app() -> (axum::Router, AppState, String, Interface, Peer) {
let store = Store::connect_in_memory().await.unwrap();
store.migrate().await.unwrap();
let now = chrono::Utc::now().naive_utc();
let hash = nx9_wg_core::crypto::hash_password("adminpassword123").unwrap();
store.create_admin("admin", &hash).await.unwrap();
// Create session token
let session = nx9_wg_core::types::auth::Session {
id: "test-session-id-12345".to_string(),
admin_id: 1,
created_at: now,
expires_at: now + chrono::Duration::hours(24),
last_seen_at: Some(now),
ip_address: Some("127.0.0.1".to_string()),
user_agent: Some("test-agent".to_string()),
};
store.create_session(&session).await.unwrap();
let (srv_priv, srv_pub) = generate_keypair();
let (peer_priv, peer_pub) = generate_keypair();
let interface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
role: InterfaceRole::Overlay,
private_key: srv_priv,
public_key: srv_pub,
listen_port: Some(51820),
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
dns: Some("1.1.1.1".to_string()),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
store.create_interface(&interface).await.unwrap();
let peer = Peer {
id: Uuid::new_v4(),
interface_id: interface.id,
name: "test-mobile-peer".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer_pub,
private_key: Some(peer_priv),
preshared_key: None,
endpoint: None,
allowed_ips: "10.0.0.2/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.2/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: None,
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
store.create_peer(&peer).await.unwrap();
store
.set_setting("server_endpoint", "vpn.example.com", false)
.await
.unwrap();
let state = AppState::new(store);
let app = nx9_wg_api::routes::build_api_router(state.clone());
(app, state, session.id, interface, peer)
}
#[tokio::test]
async fn test_client_profiles_endpoints() {
let (app, state, session_id, interface, peer) = setup_test_app().await;
// 1. List client profiles
let req = Request::builder()
.uri("/api/v1/client-profiles")
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let profiles: Vec<ClientProfile> = serde_json::from_slice(&body).unwrap();
assert!(profiles.len() >= 10);
// 2. List distinct providers
let req = Request::builder()
.uri("/api/v1/client-profiles/providers")
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let providers: Vec<String> = serde_json::from_slice(&body).unwrap();
assert!(providers.contains(&"tmobile".to_string()));
assert!(providers.contains(&"starlink".to_string()));
// 3. List device categories
let req = Request::builder()
.uri("/api/v1/client-profiles/devices")
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// 4. Resolve client profile via POST
let resolve_body = serde_json::json!({
"connection": "mobile",
"device": "android",
"nat": "cgnat"
});
let req = Request::builder()
.method("POST")
.uri("/api/v1/client-profiles/resolve")
.header("Cookie", format!("nx9_session={session_id}"))
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_vec(&resolve_body).unwrap()))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let resolved: ResolvedClientProfile = serde_json::from_slice(&body).unwrap();
assert_eq!(resolved.mtu, 1280);
assert_eq!(resolved.connection_type, ConnectionType::Mobile);
// 5. Download peer .conf with mobile profile parameters
let req = Request::builder()
.uri(format!(
"/api/v1/peers/{}/config?connection=mobile&device=android",
peer.id
))
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let conf_str = String::from_utf8(body.to_vec()).unwrap();
assert!(conf_str.contains("MTU = 1280"));
assert!(conf_str.contains("PersistentKeepalive = 25"));
// 6. Get QR code with CGNAT profile parameters
let req = Request::builder()
.uri(format!("/api/v1/peers/{}/qr?nat=cgnat", peer.id))
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let qr_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(qr_json["svg"].as_str().unwrap().contains("<svg"));
// 7. Delete server_endpoint setting and verify config export fails with actionable error
state.store.delete_setting("server_endpoint").await.unwrap();
let req = Request::builder()
.uri(format!("/api/v1/peers/{}/config", peer.id))
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let err_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
err_json["error"]["message"]
.as_str()
.unwrap()
.contains("No reachable WireGuard server endpoint is configured")
);
// 8. With query server_endpoint parameter, export succeeds even without DB setting
let req = Request::builder()
.uri(format!(
"/api/v1/peers/{}/config?server_endpoint=custom.vpn.io:51820",
peer.id
))
.header("Cookie", format!("nx9_session={session_id}"))
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let conf_str = String::from_utf8(body.to_vec()).unwrap();
assert!(conf_str.contains("Endpoint = custom.vpn.io:51820"));
// 9. Overlapping server-side AllowedIPs rejection
let overlap_peer = serde_json::json!({
"name": "overlapping-peer",
"peer_type": "road_warrior",
"address_v4": "10.0.0.2/32"
});
let req = Request::builder()
.method("POST")
.uri(format!("/api/v1/interfaces/{}/peers", interface.id))
.header("Cookie", format!("nx9_session={session_id}"))
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_vec(&overlap_peer).unwrap()))
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let err_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
err_json["error"]["message"]
.as_str()
.unwrap()
.contains("overlaps with active peer")
);
}