#![cfg(feature = "management-api")]
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::{Method, Request, StatusCode};
use hyper_util::rt::TokioIo;
use serde_json::Value;
use tokio::net::UnixStream;
use tokio::time::sleep;
use aivpn_common::network_config::VpnNetworkConfig;
use aivpn_server::client_db::ClientDatabase;
use aivpn_server::management_api;
static SOCKET_COUNTER: AtomicU32 = AtomicU32::new(0);
fn test_network_config() -> VpnNetworkConfig {
VpnNetworkConfig {
server_vpn_ip: "10.99.0.1".parse().unwrap(),
prefix_len: 24,
mtu: 1400,
}
}
fn make_temp_db(test_name: &str) -> (tempfile::TempDir, Arc<ClientDatabase>) {
let dir = tempfile::Builder::new()
.prefix(&format!("aivpn_api_test_{}_", test_name))
.tempdir()
.expect("tempdir");
let db_path = dir.path().join("clients.json");
let db = ClientDatabase::load(&db_path, test_network_config()).expect("load db");
(dir, Arc::new(db))
}
fn unique_socket_path() -> String {
let n = SOCKET_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("/tmp/aivpn_mgmt_test_{}_{}.sock", std::process::id(), n)
}
async fn spawn_server(db: Arc<ClientDatabase>, socket_path: String) {
spawn_server_with_key(db, socket_path, None, None).await;
}
async fn spawn_server_with_key(
db: Arc<ClientDatabase>,
socket_path: String,
server_pub_key: Option<[u8; 32]>,
server_addr: Option<String>,
) {
let db_clone = db.clone();
let path_clone = socket_path.clone();
tokio::spawn(async move {
management_api::serve(
Some(db_clone),
Some(path_clone),
server_pub_key,
server_addr,
)
.await;
});
for _ in 0..40 {
if std::path::Path::new(&socket_path).exists() {
return;
}
sleep(Duration::from_millis(50)).await;
}
panic!("Management API socket did not appear: {}", socket_path);
}
async fn connect(socket_path: &str) -> hyper::client::conn::http1::SendRequest<Full<Bytes>> {
let stream = UnixStream::connect(socket_path)
.await
.expect("connect to unix socket");
let io = TokioIo::new(stream);
let (sender, conn) = hyper::client::conn::http1::handshake(io)
.await
.expect("http1 handshake");
tokio::spawn(conn);
sender
}
async fn send(
sender: &mut hyper::client::conn::http1::SendRequest<Full<Bytes>>,
method: Method,
path: &str,
body: Option<&str>,
) -> (StatusCode, Value) {
let body_bytes = body.map(|s| Bytes::from(s.to_owned())).unwrap_or_default();
let req = Request::builder()
.method(method)
.uri(path)
.header("Host", "localhost")
.header("Content-Type", "application/json")
.header("Content-Length", body_bytes.len().to_string())
.body(Full::new(body_bytes))
.expect("build request");
let res = sender.send_request(req).await.expect("send request");
let status = res.status();
let bytes = res
.into_body()
.collect()
.await
.expect("collect body")
.to_bytes();
let json: Value = serde_json::from_slice(&bytes).unwrap_or(Value::Null);
(status, json)
}
#[tokio::test]
async fn test_get_status_returns_200() {
let (_dir, db) = make_temp_db("status");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (status, body) = send(&mut sender, Method::GET, "/api/v1/status", None).await;
assert_eq!(status, StatusCode::OK);
assert!(
body["version"].is_string(),
"version must be a string: {:?}",
body
);
assert!(
body["uptime_secs"].is_number(),
"uptime_secs must be a number: {:?}",
body
);
}
#[tokio::test]
async fn test_list_clients_empty() {
let (_dir, db) = make_temp_db("list_empty");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (status, body) = send(&mut sender, Method::GET, "/api/v1/clients", None).await;
assert_eq!(status, StatusCode::OK);
assert!(body.is_array(), "body must be an array: {:?}", body);
assert_eq!(body.as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_add_client_returns_201_without_psk() {
let (_dir, db) = make_temp_db("add_client");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let payload = r#"{"name": "alice"}"#;
let (status, body) = send(&mut sender, Method::POST, "/api/v1/clients", Some(payload)).await;
assert_eq!(
status,
StatusCode::CREATED,
"expected 201, got {:?}: {:?}",
status,
body
);
assert!(body["id"].is_string(), "id must be string: {:?}", body);
assert!(body["name"].is_string(), "name must be string: {:?}", body);
assert!(
body["vpn_ip"].is_string(),
"vpn_ip must be string: {:?}",
body
);
assert!(
body["enabled"].is_boolean(),
"enabled must be bool: {:?}",
body
);
assert!(
body["created_at"].is_string(),
"created_at must be string: {:?}",
body
);
assert!(
body.get("psk").is_none(),
"PSK must not appear in API response: {:?}",
body
);
assert_eq!(body["name"], "alice");
assert_eq!(body["enabled"], true);
}
#[tokio::test]
async fn test_add_client_duplicate_returns_409() {
let (_dir, db) = make_temp_db("add_dup");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let payload = r#"{"name": "bob"}"#;
let mut sender = connect(&sock).await;
let (s1, _) = send(&mut sender, Method::POST, "/api/v1/clients", Some(payload)).await;
assert_eq!(s1, StatusCode::CREATED);
let mut sender2 = connect(&sock).await;
let (s2, body) = send(&mut sender2, Method::POST, "/api/v1/clients", Some(payload)).await;
assert_eq!(
s2,
StatusCode::CONFLICT,
"duplicate name must return 409: {:?}",
body
);
assert!(
body["error"].is_string(),
"error field expected: {:?}",
body
);
}
#[tokio::test]
async fn test_get_client_by_id() {
let (_dir, db) = make_temp_db("get_by_id");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"charlie"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
let mut sender2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}", id);
let (status, body) = send(&mut sender2, Method::GET, &path, None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body["id"], id.as_str());
assert_eq!(body["name"], "charlie");
assert!(body.get("psk").is_none(), "PSK must not appear: {:?}", body);
}
#[tokio::test]
async fn test_get_client_not_found_returns_404() {
let (_dir, db) = make_temp_db("get_404");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (status, body) = send(
&mut sender,
Method::GET,
"/api/v1/clients/nonexistent",
None,
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert!(
body["error"].is_string(),
"error field expected: {:?}",
body
);
}
#[tokio::test]
async fn test_delete_client_returns_204() {
let (_dir, db) = make_temp_db("delete_client");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"dave"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
let mut sender2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}", id);
let (status, _) = send(&mut sender2, Method::DELETE, &path, None).await;
assert_eq!(status, StatusCode::NO_CONTENT, "DELETE must return 204");
let mut sender3 = connect(&sock).await;
let (status2, _) = send(&mut sender3, Method::GET, &path, None).await;
assert_eq!(
status2,
StatusCode::NOT_FOUND,
"deleted client must return 404"
);
}
#[tokio::test]
async fn test_delete_nonexistent_client_returns_404() {
let (_dir, db) = make_temp_db("delete_404");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (status, body) = send(
&mut sender,
Method::DELETE,
"/api/v1/clients/no-such-id",
None,
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert!(
body["error"].is_string(),
"error field expected: {:?}",
body
);
}
#[tokio::test]
async fn test_reload_endpoint() {
let (_dir, db) = make_temp_db("reload");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (status, body) = send(&mut sender, Method::POST, "/api/v1/reload", None).await;
assert_eq!(status, StatusCode::OK, "reload must return 200: {:?}", body);
assert!(
body["reloaded"].is_boolean(),
"`reloaded` must be a bool: {:?}",
body
);
}
#[tokio::test]
async fn test_list_clients_after_add() {
let (_dir, db) = make_temp_db("list_after_add");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
for name in &["eve", "frank"] {
let mut s = connect(&sock).await;
let payload = format!(r#"{{"name":"{}"}}"#, name);
let (status, _) = send(&mut s, Method::POST, "/api/v1/clients", Some(&payload)).await;
assert_eq!(status, StatusCode::CREATED);
}
let mut sender = connect(&sock).await;
let (status, body) = send(&mut sender, Method::GET, "/api/v1/clients", None).await;
assert_eq!(status, StatusCode::OK);
let arr = body.as_array().expect("body must be array");
assert_eq!(
arr.len(),
2,
"expected 2 clients, got {}: {:?}",
arr.len(),
body
);
for item in arr {
assert!(
item.get("psk").is_none(),
"PSK must not appear in list: {:?}",
item
);
assert!(item["id"].is_string());
assert!(item["name"].is_string());
assert!(item["vpn_ip"].is_string());
}
}
#[tokio::test]
async fn test_client_response_has_stats() {
let (_dir, db) = make_temp_db("stats_fields");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"grace"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
let mut sender2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}", id);
let (status, body) = send(&mut sender2, Method::GET, &path, None).await;
assert_eq!(status, StatusCode::OK);
let stats = &body["stats"];
assert!(stats.is_object(), "stats must be an object: {:?}", body);
assert!(stats["bytes_in"].is_number(), "bytes_in: {:?}", stats);
assert!(stats["bytes_out"].is_number(), "bytes_out: {:?}", stats);
assert!(
stats["total_connections"].is_number(),
"total_connections: {:?}",
stats
);
}
#[tokio::test]
async fn test_psk_never_exposed() {
let (_dir, db) = make_temp_db("psk_never");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"henry"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
assert!(
created.get("psk").is_none(),
"PSK in POST response: {:?}",
created
);
let mut s2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}", id);
let (_, single) = send(&mut s2, Method::GET, &path, None).await;
assert!(
single.get("psk").is_none(),
"PSK in GET single: {:?}",
single
);
let mut s3 = connect(&sock).await;
let (_, list) = send(&mut s3, Method::GET, "/api/v1/clients", None).await;
for item in list.as_array().unwrap() {
assert!(item.get("psk").is_none(), "PSK in list item: {:?}", item);
}
}
#[tokio::test]
async fn test_connection_key_without_server_config_returns_503() {
let (_dir, db) = make_temp_db("conn_key_503");
let sock = unique_socket_path();
spawn_server(db, sock.clone()).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"ivan"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
let mut s2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}/connection-key", id);
let (status, body) = send(&mut s2, Method::GET, &path, None).await;
assert_eq!(
status,
StatusCode::SERVICE_UNAVAILABLE,
"expected 503: {:?}",
body
);
assert!(
body["error"].is_string(),
"error field expected: {:?}",
body
);
}
#[tokio::test]
async fn test_connection_key_returns_aivpn_url() {
let (_dir, db) = make_temp_db("conn_key_200");
let sock = unique_socket_path();
let pub_key = [0x42u8; 32];
let server_addr = "1.2.3.4:4443".to_string();
spawn_server_with_key(db, sock.clone(), Some(pub_key), Some(server_addr)).await;
let mut sender = connect(&sock).await;
let (_, created) = send(
&mut sender,
Method::POST,
"/api/v1/clients",
Some(r#"{"name":"judy"}"#),
)
.await;
let id = created["id"].as_str().unwrap().to_string();
let mut s2 = connect(&sock).await;
let path = format!("/api/v1/clients/{}/connection-key", id);
let (status, body) = send(&mut s2, Method::GET, &path, None).await;
assert_eq!(status, StatusCode::OK, "expected 200: {:?}", body);
let key = body["connection_key"]
.as_str()
.expect("connection_key must be a string");
assert!(
key.starts_with("aivpn://"),
"key must start with aivpn://: {}",
key
);
let mut s3 = connect(&sock).await;
let (status404, _) = send(
&mut s3,
Method::GET,
"/api/v1/clients/no-such-id/connection-key",
None,
)
.await;
assert_eq!(status404, StatusCode::NOT_FOUND);
}