#[cfg(feature = "http-input")]
use std::sync::Arc;
#[cfg(feature = "http-input")]
use std::time::Duration;
#[cfg(feature = "http-input")]
use reqwest;
#[cfg(feature = "http-input")]
use serde_json::json;
#[cfg(feature = "http-input")]
use tokio::time::timeout;
#[cfg(feature = "http-input")]
use symbi_runtime::{
http_input::{HttpInputConfig, HttpInputServer},
AgentId, AgentRuntime, Capability, ExecutionMode, Priority, ResourceLimits, RuntimeConfig,
SecurityTier,
};
#[cfg(feature = "http-input")]
fn create_test_config(port: u16) -> HttpInputConfig {
HttpInputConfig {
bind_address: "127.0.0.1".to_string(),
port,
path: "/webhook".to_string(),
agent: AgentId::new(),
auth_header: Some("Bearer test-token-123".to_string()),
jwt_public_key_path: None,
max_body_bytes: 1024, concurrency: 5,
routing_rules: None,
response_control: None,
forward_headers: vec![],
cors_enabled: true,
audit_enabled: true,
}
}
#[cfg(feature = "http-input")]
async fn create_test_runtime() -> AgentRuntime {
let config = RuntimeConfig::default();
AgentRuntime::new(config)
.await
.expect("Failed to create runtime")
}
#[cfg(feature = "http-input")]
async fn find_available_port() -> u16 {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}
#[cfg(feature = "http-input")]
async fn start_test_server() -> (tokio::task::JoinHandle<()>, String, u16) {
let port = find_available_port().await;
let config = create_test_config(port);
let runtime = Arc::new(create_test_runtime().await);
let base_url = format!("http://127.0.0.1:{}", port);
let server = HttpInputServer::new(config).with_runtime(runtime);
let handle = tokio::spawn(async move {
if let Err(e) = server.start().await {
eprintln!("Server error: {:?}", e);
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
(handle, base_url, port)
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_valid_request_returns_200_ok() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let payload = json!({
"message": "Hello from webhook",
"data": {
"source": "test",
"timestamp": "2024-01-01T00:00:00Z"
}
});
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer test-token-123")
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 200);
assert!(response
.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.contains("application/json"));
let response_body: serde_json::Value = response
.json()
.await
.expect("Failed to parse JSON response");
assert!(response_body.get("status").is_some());
assert_eq!(response_body["status"], "invoked");
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_invalid_token_returns_401_unauthorized() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let payload = json!({
"message": "This should fail with wrong token"
});
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer wrong-token")
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 401);
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_missing_token_returns_401_unauthorized() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let payload = json!({
"message": "This should fail without token"
});
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 401);
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_payload_too_large_returns_413() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let large_data = "x".repeat(2048); let payload = json!({
"message": "Large payload test",
"large_data": large_data
});
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer test-token-123")
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 413);
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_malformed_json_returns_400_bad_request() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let malformed_json = r#"{"message": "incomplete json""#;
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer test-token-123")
.header("Content-Type", "application/json")
.body(malformed_json)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 400);
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_agent_interaction_and_invocation() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let payload = json!({
"action": "test_agent_invocation",
"parameters": {
"test_param": "test_value"
}
});
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer test-token-123")
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert_eq!(response.status(), 200);
let response_body: serde_json::Value = response
.json()
.await
.expect("Failed to parse JSON response");
assert_eq!(response_body["status"], "invoked");
assert!(response_body.get("agent_id").is_some());
assert!(response_body.get("timestamp").is_some());
let timestamp_str = response_body["timestamp"].as_str().unwrap();
assert!(chrono::DateTime::parse_from_rfc3339(timestamp_str).is_ok());
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_cors_headers_when_enabled() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let response = timeout(
Duration::from_secs(5),
client
.request(reqwest::Method::OPTIONS, &format!("{}/webhook", base_url))
.header("Origin", "https://example.com")
.header("Access-Control-Request-Method", "POST")
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert!(
response
.headers()
.contains_key("access-control-allow-origin")
|| response
.headers()
.contains_key("Access-Control-Allow-Origin")
);
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_content_type_enforcement() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let response = timeout(
Duration::from_secs(5),
client
.post(&format!("{}/webhook", base_url))
.header("Authorization", "Bearer test-token-123")
.body(r#"{"message": "test"}"#)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed");
assert!(response.status().is_client_error() || response.status().is_success());
}
#[cfg(feature = "http-input")]
#[tokio::test]
async fn test_concurrent_requests_within_limits() {
let (_handle, base_url, _port) = start_test_server().await;
let client = reqwest::Client::new();
let _payload = json!({
"message": "Concurrent request test"
});
let mut handles = vec![];
for i in 0..3 {
let client = client.clone();
let url = format!("{}/webhook", base_url);
let payload = json!({
"message": format!("Concurrent request {}", i)
});
let handle = tokio::spawn(async move {
timeout(
Duration::from_secs(5),
client
.post(&url)
.header("Authorization", "Bearer test-token-123")
.header("Content-Type", "application/json")
.json(&payload)
.send(),
)
.await
.expect("Request timeout")
.expect("Request failed")
});
handles.push(handle);
}
let responses = futures::future::join_all(handles).await;
for response in responses {
let response = response.expect("Task failed");
assert_eq!(response.status(), 200);
}
}
#[cfg(not(feature = "http-input"))]
#[tokio::test]
async fn test_http_input_feature_disabled() {
}