use bottest::prelude::*; use reqwest::{Client, StatusCode}; use serde_json::json; use std::time::Duration; fn test_client() -> Client { Client::builder() .timeout(Duration::from_secs(30)) .build() .expect("Failed to create HTTP client") } fn external_server_url() -> Option { std::env::var("BOTSERVER_URL").ok() } async fn get_test_server() -> Option<(Option, String)> { if let Some(url) = external_server_url() { let client = reqwest::Client::builder() .timeout(Duration::from_secs(2)) .build() .ok()?; if client.get(&url).send().await.is_ok() { return Some((None, url)); } } let ctx = TestHarness::quick().await.ok()?; let server = ctx.start_botserver().await.ok()?; if server.is_running() { Some((Some(ctx), server.url.clone())) } else { None } } fn is_server_available_sync() -> bool { if std::env::var("SKIP_INTEGRATION_TESTS").is_ok() { return false; } if let Some(url) = external_server_url() { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(2)) .build() .ok(); if let Some(client) = client { return client.get(&url).send().is_ok(); } } false } macro_rules! skip_if_no_server { ($base_url:expr) => { if $base_url.is_none() { eprintln!("Skipping API test: no server available"); return; } }; } #[tokio::test] async fn test_health_endpoint() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/health", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { assert!( resp.status().is_success(), "Health endpoint should return success status" ); } Err(e) => { eprintln!("Health check failed: {}", e); } } } #[tokio::test] async fn test_ready_endpoint() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/ready", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::OK || resp.status() == StatusCode::SERVICE_UNAVAILABLE, "Ready endpoint should return 200 or 503" ); } Err(e) => { eprintln!("Ready check failed: {}", e); } } } #[tokio::test] async fn test_version_endpoint() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/version", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { if resp.status().is_success() { let body = resp.text().await.unwrap_or_default(); assert!(!body.is_empty(), "Version should return non-empty body"); } } Err(e) => { eprintln!("Version check failed: {}", e); } } } #[tokio::test] async fn test_login_missing_credentials() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client.post(&url).json(&json!({})).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::BAD_REQUEST || resp.status() == StatusCode::UNPROCESSABLE_ENTITY, "Missing credentials should return 400 or 422" ); } Err(e) => { eprintln!("Login test failed: {}", e); } } } #[tokio::test] async fn test_login_invalid_credentials() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client .post(&url) .json(&json!({ "email": "invalid@example.com", "password": "wrongpassword" })) .send() .await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::UNAUTHORIZED || resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::NOT_FOUND, "Invalid credentials should return 401, 403, or 404" ); } Err(e) => { eprintln!("Login test failed: {}", e); } } } #[tokio::test] async fn test_protected_endpoint_without_auth() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/bots", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::UNAUTHORIZED || resp.status() == StatusCode::FORBIDDEN, "Protected endpoint without auth should return 401 or 403" ); } Err(e) => { eprintln!("Auth test failed: {}", e); } } } #[tokio::test] async fn test_list_bots_unauthorized() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/bots", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::UNAUTHORIZED || resp.status() == StatusCode::FORBIDDEN, "List bots without auth should return 401 or 403" ); } Err(e) => { eprintln!("Bots test failed: {}", e); } } } #[tokio::test] async fn test_get_nonexistent_bot() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let fake_id = Uuid::new_v4(); let url = format!("{}/api/bots/{}", base_url, fake_id); let response = client.get(&url).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::NOT_FOUND || resp.status() == StatusCode::UNAUTHORIZED || resp.status() == StatusCode::FORBIDDEN, "Nonexistent bot should return 404, 401, or 403" ); } Err(e) => { eprintln!("Bot test failed: {}", e); } } } #[tokio::test] async fn test_send_message_missing_body() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/chat/send", base_url); let response = client .post(&url) .header("Content-Type", "application/json") .body("{}") .send() .await; match response { Ok(resp) => { assert!( resp.status().is_client_error(), "Missing body should return client error" ); } Err(e) => { eprintln!("Message test failed: {}", e); } } } #[tokio::test] async fn test_send_message_invalid_bot() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/chat/send", base_url); let response = client .post(&url) .json(&json!({ "bot_id": Uuid::new_v4().to_string(), "message": "Hello", "session_id": Uuid::new_v4().to_string() })) .send() .await; match response { Ok(resp) => { assert!( resp.status().is_client_error(), "Invalid bot should return client error" ); } Err(e) => { eprintln!("Message test failed: {}", e); } } } #[tokio::test] async fn test_whatsapp_webhook_verification() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!( "{}/webhook/whatsapp?hub.mode=subscribe&hub.verify_token=test&hub.challenge=test123", base_url ); let response = client.get(&url).send().await; match response { Ok(resp) => { let status = resp.status(); assert!( status == StatusCode::OK || status == StatusCode::FORBIDDEN || status == StatusCode::NOT_FOUND, "Webhook verification should return 200, 403, or 404" ); } Err(e) => { eprintln!("Webhook test failed: {}", e); } } } #[tokio::test] async fn test_whatsapp_webhook_invalid_payload() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/webhook/whatsapp", base_url); let response = client .post(&url) .json(&json!({"invalid": "payload"})) .send() .await; match response { Ok(resp) => { assert!( resp.status().is_client_error() || resp.status().is_success(), "Invalid webhook payload should be handled" ); } Err(e) => { eprintln!("Webhook test failed: {}", e); } } } #[tokio::test] async fn test_json_content_type() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client .post(&url) .header("Content-Type", "text/plain") .body("not json") .send() .await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE || resp.status() == StatusCode::BAD_REQUEST, "Wrong content type should return 415 or 400" ); } Err(e) => { eprintln!("Content type test failed: {}", e); } } } #[tokio::test] async fn test_404_response() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/nonexistent/path/here", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { assert_eq!( resp.status(), StatusCode::NOT_FOUND, "Unknown path should return 404" ); } Err(e) => { eprintln!("404 test failed: {}", e); } } } #[tokio::test] async fn test_method_not_allowed() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/health", base_url); let response = client.delete(&url).send().await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::METHOD_NOT_ALLOWED || resp.status() == StatusCode::NOT_FOUND, "Wrong method should return 405 or 404" ); } Err(e) => { eprintln!("Method test failed: {}", e); } } } #[tokio::test] async fn test_cors_preflight() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/bots", base_url); let response = client .request(reqwest::Method::OPTIONS, &url) .header("Origin", "http://localhost:3000") .header("Access-Control-Request-Method", "GET") .send() .await; match response { Ok(resp) => { let status = resp.status(); assert!( status == StatusCode::OK || status == StatusCode::NO_CONTENT || status == StatusCode::NOT_FOUND, "CORS preflight should return 200, 204, or 404" ); } Err(e) => { eprintln!("CORS test failed: {}", e); } } } #[tokio::test] async fn test_rate_limiting_headers() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/health", base_url); let response = client.get(&url).send().await; match response { Ok(resp) => { let headers = resp.headers(); if headers.contains_key("x-ratelimit-limit") { assert!( headers.contains_key("x-ratelimit-remaining"), "Rate limit headers should include remaining" ); } } Err(e) => { eprintln!("Rate limit test failed: {}", e); } } } #[tokio::test] async fn test_malformed_json() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client .post(&url) .header("Content-Type", "application/json") .body("{malformed json") .send() .await; match response { Ok(resp) => { assert!( resp.status() == StatusCode::BAD_REQUEST || resp.status() == StatusCode::UNPROCESSABLE_ENTITY, "Malformed JSON should return 400 or 422" ); } Err(e) => { eprintln!("Malformed JSON test failed: {}", e); } } } #[tokio::test] async fn test_empty_body_where_required() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client .post(&url) .header("Content-Type", "application/json") .send() .await; match response { Ok(resp) => { assert!( resp.status().is_client_error(), "Empty body should return client error" ); } Err(e) => { eprintln!("Empty body test failed: {}", e); } } } #[tokio::test] async fn test_error_response_format() { let server = get_test_server().await; skip_if_no_server!(server); let (_ctx, base_url) = server.unwrap(); let client = test_client(); let url = format!("{}/api/auth/login", base_url); let response = client.post(&url).json(&json!({})).send().await; match response { Ok(resp) => { if resp.status().is_client_error() { let content_type = resp .headers() .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); if content_type.contains("application/json") { let body: Result = resp.json().await; if let Ok(json) = body { assert!( json.get("error").is_some() || json.get("message").is_some(), "Error response should have error or message field" ); } } } } Err(e) => { eprintln!("Error format test failed: {}", e); } } } #[tokio::test] async fn test_with_mock_llm() { let ctx = match TestHarness::quick().await { Ok(ctx) => ctx, Err(e) => { eprintln!("Skipping: {}", e); return; } }; if let Some(mock_llm) = ctx.mock_llm() { mock_llm.expect_completion("hello", "Hi there!").await; mock_llm.set_default_response("Default response").await; let call_count = mock_llm.call_count().await; assert_eq!(call_count, 0); } } #[tokio::test] async fn test_mock_llm_assertions() { let ctx = match TestHarness::quick().await { Ok(ctx) => ctx, Err(e) => { eprintln!("Skipping: {}", e); return; } }; if let Some(mock_llm) = ctx.mock_llm() { mock_llm.assert_not_called().await; mock_llm.set_default_response("Test response").await; let client = reqwest::Client::new(); let _ = client .post(&format!("{}/v1/chat/completions", mock_llm.url())) .json(&serde_json::json!({ "model": "gpt-4", "messages": [{"role": "user", "content": "test"}] })) .send() .await; mock_llm.assert_called().await; } } #[tokio::test] async fn test_mock_llm_error_simulation() { let ctx = match TestHarness::quick().await { Ok(ctx) => ctx, Err(e) => { eprintln!("Skipping: {}", e); return; } }; if let Some(mock_llm) = ctx.mock_llm() { mock_llm.next_call_fails(500, "Internal error").await; let client = reqwest::Client::new(); let response = client .post(&format!("{}/v1/chat/completions", mock_llm.url())) .json(&serde_json::json!({ "model": "gpt-4", "messages": [{"role": "user", "content": "test"}] })) .send() .await; if let Ok(resp) = response { assert_eq!(resp.status().as_u16(), 500); } } }