252 lines
9.1 KiB
Rust
252 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, 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<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")
|
|
);
|
|
}
|