701 lines
18 KiB
Rust
701 lines
18 KiB
Rust
|
|
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<String> {
|
||
|
|
std::env::var("BOTSERVER_URL").ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn get_test_server() -> Option<(Option<TestContext>, 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<serde_json::Value, _> = 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|