#[cfg(feature = "server")]
mod server_tests {
use std::path::PathBuf;
use std::time::Duration;
use synheart_sensor_agent::server::{run, ServerConfig};
fn test_state_dir() -> PathBuf {
std::env::temp_dir().join("synheart-server-test")
}
#[tokio::test]
async fn test_health_endpoint() {
let config = ServerConfig::new(0, test_state_dir());
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let client = reqwest::Client::new();
let response = client
.get(format!("http://{addr}/health"))
.send()
.await
.expect("Failed to send request");
assert!(response.status().is_success());
let body: serde_json::Value = response.json().await.expect("Failed to parse JSON");
assert_eq!(body["status"], "ok");
assert!(body["version"].as_str().is_some());
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_collect_endpoint_accepts_json() {
let config = ServerConfig::new(0, test_state_dir());
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let payload = serde_json::json!({
"session": {
"session_id": "TEST-123",
"device_id": "chrome-test",
"events": [
{ "timestamp": "2024-01-22T10:00:01Z", "event_type": "typing" }
]
}
});
let client = reqwest::Client::new();
let response = client
.post(format!("http://{addr}/collect"))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert!(response.status().is_success());
let body: serde_json::Value = response.json().await.expect("Failed to parse JSON");
assert_eq!(body["status"], "ok");
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_collect_rejects_non_object() {
let config = ServerConfig::new(0, test_state_dir());
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let payload = serde_json::json!({ "session": "not-an-object" });
let client = reqwest::Client::new();
let response = client
.post(format!("http://{addr}/collect"))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST);
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_collect_requires_token_when_configured() {
let config = ServerConfig::new(0, test_state_dir()).with_token(Some("s3cret".to_string()));
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let payload = serde_json::json!({ "session": { "events": [] } });
let client = reqwest::Client::new();
let unauthorized = client
.post(format!("http://{addr}/collect"))
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(unauthorized.status(), reqwest::StatusCode::UNAUTHORIZED);
let wrong = client
.post(format!("http://{addr}/collect"))
.header("Authorization", "Bearer nope")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(wrong.status(), reqwest::StatusCode::UNAUTHORIZED);
let ok = client
.post(format!("http://{addr}/collect"))
.header("Authorization", "Bearer s3cret")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert!(ok.status().is_success());
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_collect_rejects_oversized_body() {
let config = ServerConfig::new(0, test_state_dir());
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let big = "a".repeat(512 * 1024);
let payload = serde_json::json!({ "session": { "blob": big } });
let client = reqwest::Client::new();
let response = client
.post(format!("http://{addr}/collect"))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(response.status(), reqwest::StatusCode::PAYLOAD_TOO_LARGE);
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_token_same_length_mismatch_is_rejected() {
let config = ServerConfig::new(0, test_state_dir()).with_token(Some("s3cret".to_string()));
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let payload = serde_json::json!({ "session": { "events": [] } });
let client = reqwest::Client::new();
let same_len = client
.post(format!("http://{addr}/collect"))
.header("Authorization", "Bearer s3cseT")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(same_len.status(), reqwest::StatusCode::UNAUTHORIZED);
let prefix = client
.post(format!("http://{addr}/collect"))
.header("Authorization", "Bearer s3cre")
.json(&payload)
.send()
.await
.expect("Failed to send request");
assert_eq!(prefix.status(), reqwest::StatusCode::UNAUTHORIZED);
let _ = shutdown_tx.send(());
}
#[tokio::test]
async fn test_cors_headers() {
let config = ServerConfig::new(0, test_state_dir());
let (addr, shutdown_tx) = run(config).await.expect("Failed to start server");
tokio::time::sleep(Duration::from_millis(100)).await;
let client = reqwest::Client::new();
let response = client
.request(reqwest::Method::OPTIONS, format!("http://{addr}/collect"))
.header("Origin", "http://localhost")
.header("Access-Control-Request-Method", "POST")
.send()
.await
.expect("Failed to send request");
assert!(
response.status().is_success() || response.status() == reqwest::StatusCode::NO_CONTENT,
"CORS preflight failed: {}",
response.status()
);
let _ = shutdown_tx.send(());
}
}