Files
openfang/tests/e2e_api_test.rs
iven 92e5def702
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
初始化提交
2026-03-01 16:24:24 +08:00

542 lines
16 KiB
Rust

//! Basic API endpoint tests for OpenFang.
//!
//! These tests verify the core API endpoints work correctly:
//! - Health and status endpoints
//! - Agent CRUD operations
//! - Session management
//! - Error handling
use super::e2e_common::*;
// ---------------------------------------------------------------------------
// Health & Status Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_health_endpoint_returns_ok() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/health").await;
assert_eq!(response["status"], "ok");
assert!(response["version"].is_string());
}
#[tokio::test]
async fn test_health_detail_endpoint() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/health/detail").await;
assert_eq!(response["status"], "ok");
// Detailed health includes more information
assert!(response["version"].is_string());
}
#[tokio::test]
async fn test_status_endpoint() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/status").await;
assert_eq!(response["status"], "running");
assert_eq!(response["agent_count"], 0);
assert!(response["uptime_seconds"].is_number());
assert!(response["default_provider"].is_string());
}
#[tokio::test]
async fn test_version_endpoint() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/version").await;
assert!(response["version"].is_string());
assert!(response["commit"].is_string() || response["commit"].is_null());
}
// ---------------------------------------------------------------------------
// Agent CRUD Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_spawn_agent() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
assert!(!agent.id.is_empty());
assert_eq!(agent.name, "test-agent");
}
#[tokio::test]
async fn test_list_agents_empty() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let agents = list_agents(&daemon).await;
assert_eq!(agents.len(), 0);
}
#[tokio::test]
async fn test_list_agents_with_agents() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
// Create two agents
let agent1 = create_named_test_agent(&daemon, "agent-1").await;
let agent2 = create_named_test_agent(&daemon, "agent-2").await;
let agents = list_agents(&daemon).await;
assert_eq!(agents.len(), 2);
// Verify both agents are present
let ids: Vec<&str> = agents.iter().filter_map(|a| a["id"].as_str()).collect();
assert!(ids.contains(&agent1.id.as_str()));
assert!(ids.contains(&agent2.id.as_str()));
}
#[tokio::test]
async fn test_get_agent_by_id() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
let response = get(&daemon, &format!("/api/agents/{}", agent.id)).await;
assert_eq!(response["id"], agent.id);
assert_eq!(response["name"], "test-agent");
}
#[tokio::test]
async fn test_kill_agent() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
assert_eq!(agent_count(&daemon).await, 1);
kill_agent(&daemon, &agent.id).await;
assert_eq!(agent_count(&daemon).await, 0);
}
#[tokio::test]
async fn test_kill_nonexistent_agent_returns_404() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let fake_id = uuid::Uuid::new_v4().to_string();
let client = reqwest::Client::new();
let resp = client
.delete(format!("{}/api/agents/{}", daemon.base_url(), fake_id))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn test_spawn_multiple_agents() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
// Create 5 agents
let mut agent_ids = Vec::new();
for i in 0..5 {
let agent = create_named_test_agent(&daemon, &format!("agent-{}", i)).await;
agent_ids.push(agent.id);
}
assert_eq!(agent_count(&daemon).await, 5);
// Kill all agents
for id in agent_ids {
kill_agent(&daemon, &id).await;
}
assert_eq!(agent_count(&daemon).await, 0);
}
// ---------------------------------------------------------------------------
// Error Handling Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_invalid_agent_id_returns_400() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
// Invalid (non-UUID) agent ID
let resp = client
.get(format!("{}/api/agents/not-a-uuid", daemon.base_url()))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 400);
let body: serde_json::Value = resp.json().await.unwrap();
assert!(body["error"].as_str().unwrap().contains("Invalid"));
}
#[tokio::test]
async fn test_invalid_manifest_returns_400() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/api/agents", daemon.base_url()))
.json(&serde_json::json!({"manifest_toml": "invalid {{ toml"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 400);
let body: serde_json::Value = resp.json().await.unwrap();
assert!(body["error"].as_str().unwrap().contains("Invalid manifest"));
}
#[tokio::test]
async fn test_message_to_nonexistent_agent_returns_404() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
let fake_id = uuid::Uuid::new_v4().to_string();
let resp = client
.post(format!(
"{}/api/agents/{}/message",
daemon.base_url(),
fake_id
))
.json(&serde_json::json!({"message": "hello"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
// ---------------------------------------------------------------------------
// Session Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_agent_session_empty_initially() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
let response = get(&daemon, &format!("/api/agents/{}/session", agent.id)).await;
assert_eq!(response["message_count"], 0);
assert_eq!(response["messages"].as_array().unwrap().len(), 0);
}
// ---------------------------------------------------------------------------
// Auth Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_health_is_public_without_auth() {
// Start daemon with auth enabled
let config = DaemonConfig::default().with_auth_key("secret-key-123");
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
// Health endpoint should be accessible without auth
let response = get(&daemon, "/api/health").await;
assert_eq!(response["status"], "ok");
}
#[tokio::test]
async fn test_protected_endpoint_requires_auth() {
// Start daemon with auth enabled
let config = DaemonConfig::default().with_auth_key("secret-key-123");
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
// Protected endpoint without auth header should return 401
let resp = client
.get(format!("{}/api/models", daemon.base_url()))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 401);
}
#[tokio::test]
async fn test_protected_endpoint_with_correct_auth() {
// Start daemon with auth enabled
let config = DaemonConfig::default().with_auth_key("secret-key-123");
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
// Protected endpoint with correct auth header should succeed
let resp = client
.get(format!("{}/api/models", daemon.base_url()))
.header("authorization", "Bearer secret-key-123")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn test_protected_endpoint_rejects_wrong_auth() {
// Start daemon with auth enabled
let config = DaemonConfig::default().with_auth_key("secret-key-123");
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
// Protected endpoint with wrong auth header should return 401
let resp = client
.get(format!("{}/api/models", daemon.base_url()))
.header("authorization", "Bearer wrong-key")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 401);
}
// ---------------------------------------------------------------------------
// Request ID Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_request_id_header_is_present() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let client = reqwest::Client::new();
let resp = client
.get(format!("{}/api/health", daemon.base_url()))
.send()
.await
.unwrap();
let request_id = resp
.headers()
.get("x-request-id")
.expect("x-request-id header should be present");
let id_str = request_id.to_str().unwrap();
assert!(
uuid::Uuid::parse_str(id_str).is_ok(),
"x-request-id should be a valid UUID, got: {}",
id_str
);
}
// ---------------------------------------------------------------------------
// Config Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_get_config() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/config").await;
// Config should have basic fields
assert!(response.is_object());
}
// ---------------------------------------------------------------------------
// Provider & Model Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_list_providers() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/providers").await;
assert!(response.is_array());
// Should have at least the default provider
let providers = response.as_array().unwrap();
assert!(!providers.is_empty());
}
#[tokio::test]
async fn test_list_models() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/models").await;
assert!(response.is_array());
}
// ---------------------------------------------------------------------------
// Workflow Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_workflow_crud() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
// Create agent for workflow
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
// Create workflow
let workflow = create_test_workflow(&agent.name);
let response = post(&daemon, "/api/workflows", &workflow).await;
assert!(response["workflow_id"].is_string());
let workflow_id = response["workflow_id"].as_str().unwrap();
// List workflows
let workflows = get(&daemon, "/api/workflows").await;
assert_eq!(workflows.as_array().unwrap().len(), 1);
assert_eq!(workflows[0]["name"], "test-workflow");
}
// ---------------------------------------------------------------------------
// Trigger Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_trigger_crud() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
// Create agent for trigger
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
// Create trigger
let trigger = create_test_trigger(&agent.id);
let response = post(&daemon, "/api/triggers", &trigger).await;
assert!(response["trigger_id"].is_string());
assert_eq!(response["agent_id"], agent.id);
// List triggers
let triggers = get(&daemon, "/api/triggers").await;
assert_eq!(triggers.as_array().unwrap().len(), 1);
assert_eq!(triggers[0]["enabled"], true);
}
// ---------------------------------------------------------------------------
// Budget Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_budget_status() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/budget").await;
// Budget should have basic fields
assert!(response["enabled"].is_boolean() || response["daily_limit"].is_number());
}
// ---------------------------------------------------------------------------
// Usage Tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_usage_stats() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/usage").await;
assert!(response.is_object());
}
#[tokio::test]
async fn test_usage_summary() {
let daemon = spawn_daemon().await;
wait_for_health(daemon.base_url()).await;
let response = get(&daemon, "/api/usage/summary").await;
assert!(response.is_object());
}
// ---------------------------------------------------------------------------
// LLM Integration Tests (require API key)
// ---------------------------------------------------------------------------
#[tokio::test]
#[ignore = "Requires GROQ_API_KEY environment variable"]
async fn test_send_message_with_real_llm() {
if !can_run_llm_tests() {
eprintln!("{}", llm_skip_reason());
return;
}
let config = DaemonConfig::groq();
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
let agent = create_test_agent(&daemon, LLM_MANIFEST).await;
let response = send_message(&daemon, &agent.id, "Say hello in exactly 3 words.").await;
assert!(!response.response.is_empty(), "LLM response should not be empty");
assert!(response.input_tokens > 0, "Should have input tokens");
assert!(response.output_tokens > 0, "Should have output tokens");
// Verify session has messages
let session = get(&daemon, &format!("/api/agents/{}/session", agent.id)).await;
assert!(session["message_count"].as_u64().unwrap() > 0);
}
#[tokio::test]
#[ignore = "Requires GROQ_API_KEY environment variable"]
async fn test_budget_increases_after_llm_call() {
if !can_run_llm_tests() {
eprintln!("{}", llm_skip_reason());
return;
}
let config = DaemonConfig::groq();
let daemon = spawn_daemon_with_config(config).await;
wait_for_health(daemon.base_url()).await;
// Get initial budget
let initial_budget = get(&daemon, "/api/budget").await;
// Make an LLM call
let agent = create_test_agent(&daemon, LLM_MANIFEST).await;
let _ = send_message(&daemon, &agent.id, "Hello").await;
// Get updated budget
let updated_budget = get(&daemon, "/api/budget").await;
// Budget tracking should reflect the usage
// (The exact structure depends on the implementation)
assert!(updated_budget.is_object());
}