//! 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, 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(), private_key: srv_priv, public_key: srv_pub, listen_port: 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 = 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 = 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("