//! 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()); }