use insta::assert_json_snapshot;
use rmcp_openapi::error::{ToolCallError, ToolCallValidationError, ValidationError};
use rmcp_openapi::{HttpClient, Server, ToolGenerator};
use serde_json::json;
use std::env;
use url::Url;
mod common;
use common::mock_server::MockPetstoreServer;
use mockito::Mock;
fn should_use_live_api() -> bool {
env::var("RMCP_TEST_LIVE_API").unwrap_or_default() == "true"
}
const LIVE_API_BASE_URL: &str = "https://petstore.swagger.io/v2";
#[actix_web::test]
async fn test_http_404_not_found_error() -> anyhow::Result<()> {
let non_existent_pet_id = 999999u64;
if should_use_live_api() {
let server = create_server_with_base_url(Url::parse(LIVE_API_BASE_URL)?)?;
let client = HttpClient::new().with_base_url(Url::parse(LIVE_API_BASE_URL)?)?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": non_existent_pet_id
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert_eq!(response.status_code, 404);
assert!(!response.is_success);
assert!(response.status_text.contains("Not Found"));
} else {
let mut mock_server = MockPetstoreServer::new_with_port(9001).await;
let _mock = mock_server.mock_get_pet_by_id_not_found(non_existent_pet_id);
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": non_existent_pet_id
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert_eq!(response.status_code, 404);
assert!(!response.is_success);
assert!(response.status_text.contains("Not Found"));
let error_data = response.json()?;
assert_eq!(error_data["message"], "Pet not found");
}
Ok(())
}
#[actix_web::test]
async fn test_http_400_bad_request_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("addPet")
.expect("addPet tool should be registered");
let invalid_pet_data = json!({
"status": "invalid"
});
let arguments = json!({
"request_body": invalid_pet_data
});
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
#[actix_web::test]
async fn test_http_500_server_error() -> anyhow::Result<()> {
let mut mock_server = MockPetstoreServer::new_with_port(9003).await;
let _mock = mock_server.mock_server_error("/pet/123");
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": 123
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert_eq!(response.status_code, 500);
assert!(!response.is_success);
assert!(response.status_text.contains("Internal Server Error"));
let error_data = response.json()?;
assert_eq!(error_data["message"], "Internal Server Error");
assert_eq!(error_data["details"], "Something went wrong on the server");
Ok(())
}
#[actix_web::test]
async fn test_network_connection_error() -> anyhow::Result<()> {
let server =
create_server_with_base_url(Url::parse("http://invalid-host-that-does-not-exist.com")?)?;
let client = HttpClient::new()
.with_base_url(Url::parse("http://invalid-host-that-does-not-exist.com")?)?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": 123
});
let result = client.execute_tool_call(tool_metadata, &arguments).await;
assert!(result.is_err());
let error_message = result.unwrap_err().to_string();
assert!(
error_message.contains("Connection failed")
|| error_message.contains("connection")
|| error_message.contains("network")
);
Ok(())
}
#[actix_web::test]
async fn test_missing_required_parameter_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let client = HttpClient::new().with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
});
let result = client.execute_tool_call(tool_metadata, &arguments).await;
assert!(result.is_err());
let error = result.unwrap_err();
let error_message = error.to_string();
match error {
ToolCallError::Validation(ToolCallValidationError::InvalidParameters { violations }) => {
assert!(!violations.is_empty());
let has_missing_petid = violations.iter().any(|e| match e {
ValidationError::MissingRequiredParameter { parameter, .. } => parameter == "petId",
_ => false,
});
assert!(
has_missing_petid,
"Expected missing required parameter error for petId"
);
}
_ => panic!("Expected ValidationErrors variant, got: {error_message}"),
}
Ok(())
}
#[actix_web::test]
async fn test_type_validation_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let client = HttpClient::new().with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": "not_a_number"
});
let result = client.execute_tool_call(tool_metadata, &arguments).await;
assert!(result.is_err());
let error = result.unwrap_err();
if let Ok(error_json) = serde_json::to_value(&error) {
assert_json_snapshot!(error_json);
} else {
let error_message = error.to_string();
assert!(error_message.contains("\"not_a_number\" is not of type \"integer\""));
}
Ok(())
}
#[actix_web::test]
async fn test_array_type_validation_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("findPetsByStatus")
.expect("findPetsByStatus tool should be registered");
let arguments = json!({
"status": "available" });
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
#[actix_web::test]
async fn test_enum_validation_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("findPetsByStatus")
.expect("findPetsByStatus tool should be registered");
let arguments = json!({
"status": ["invalid_status"] });
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
#[actix_web::test]
async fn test_enum_validation_parameter_passing() -> anyhow::Result<()> {
if should_use_live_api() {
let server = create_server_with_base_url(Url::parse(LIVE_API_BASE_URL)?)?;
let client = HttpClient::new().with_base_url(Url::parse(LIVE_API_BASE_URL)?)?;
let tool_metadata = server
.get_tool_metadata("findPetsByStatus")
.expect("findPetsByStatus tool should be registered");
let arguments = json!({
"status": ["available"]
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(response.is_success);
assert!(response.request_url.contains("/pet/findByStatus"));
} else {
let mut mock_server = MockPetstoreServer::new_with_port(9005).await;
let _mock = mock_server.mock_find_pets_by_status("available");
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("findPetsByStatus")
.expect("findPetsByStatus tool should be registered");
let arguments = json!({
"status": ["available"]
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(response.is_success);
assert!(response.request_url.contains("/pet/findByStatus"));
}
Ok(())
}
#[actix_web::test]
async fn test_non_json_response_handling() -> anyhow::Result<()> {
let mut mock_server = MockPetstoreServer::new_with_port(9006).await;
let _mock = mock_server
.server
.mock("GET", "/pet/123")
.with_status(200)
.with_header("content-type", "text/html")
.with_body("<html><body>This is HTML, not JSON</body></html>")
.create();
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": 123
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(response.is_success);
assert_eq!(response.status_code, 200);
assert!(response.body.contains("<html>"));
let json_result = response.json();
assert!(json_result.is_err());
Ok(())
}
#[actix_web::test]
async fn test_malformed_json_response_handling() -> anyhow::Result<()> {
let mut mock_server = MockPetstoreServer::new_with_port(9007).await;
let _mock = mock_server
.server
.mock("GET", "/pet/123")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id": 123, "name": "doggie", "invalid": json}"#) .create();
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": 123
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(response.is_success);
assert_eq!(response.status_code, 200);
let json_result = response.json();
assert!(json_result.is_err());
let error_message = json_result.unwrap_err().to_string();
assert!(error_message.contains("JSON") || error_message.contains("parse"));
Ok(())
}
#[actix_web::test]
async fn test_empty_response_handling() -> anyhow::Result<()> {
let mut mock_server = MockPetstoreServer::new_with_port(9008).await;
let _mock = mock_server
.server
.mock("DELETE", "/pet/123")
.with_status(204)
.with_header("content-length", "0")
.create();
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let _mock = mock_server
.server
.mock("GET", "/pet/123")
.with_status(204)
.with_header("content-length", "0")
.create();
let arguments = json!({
"petId": 123
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(response.is_success);
assert_eq!(response.status_code, 204);
assert!(response.body.is_empty());
if !response.body.is_empty() {
let json_result = response.json();
if json_result.is_err() {
let error_message = json_result.unwrap_err().to_string();
assert!(error_message.contains("JSON") || error_message.contains("parse"));
}
}
Ok(())
}
#[actix_web::test]
async fn test_large_response_handling() -> anyhow::Result<()> {
let mut mock_server = MockPetstoreServer::new_with_port(9009).await;
let large_description = "A".repeat(50000); let large_pet = json!({
"id": 123,
"name": "large_pet",
"status": "available",
"photoUrls": ["https://example.com/photo1.jpg"],
"description": large_description
});
let large_response = serde_json::to_string(&large_pet)?;
let _mock = mock_server
.server
.mock("GET", "/pet/123")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(large_response)
.create();
let server = create_server_with_base_url(mock_server.base_url())?;
let client = HttpClient::new().with_base_url(mock_server.base_url())?;
let tool_metadata = server
.get_tool_metadata("getPetById")
.expect("getPetById tool should be registered");
let arguments = json!({
"petId": 123
});
let response = client.execute_tool_call(tool_metadata, &arguments).await?;
assert!(
response.is_success,
"Response failed with status: {} - {}",
response.status_code, response.status_text
);
assert_eq!(response.status_code, 200);
assert!(response.body.len() > 10000);
let pet_data = response.json()?;
assert_eq!(pet_data["id"], 123);
assert_eq!(pet_data["name"], "large_pet");
assert!(pet_data["description"].as_str().unwrap().len() > 40000);
Ok(())
}
#[actix_web::test]
async fn test_null_value_for_required_parameter() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("addPet")
.expect("addPet tool should be registered");
let arguments = json!({
"request_body": {
"name": null, "photoUrls": ["https://example.com/photo.jpg"]
}
});
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
let error_message = error.to_string();
assert!(error_message.contains("required"));
assert!(error_message.contains("must not be null"));
Ok(())
}
#[actix_web::test]
async fn test_null_value_for_optional_parameter() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("addPet")
.expect("addPet tool should be registered");
let arguments = json!({
"request_body": {
"name": "doggie",
"photoUrls": ["https://example.com/photo.jpg"],
"status": null }
});
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
let error_message = error.to_string();
assert!(error_message.contains("optional") && error_message.contains("must not be null"));
Ok(())
}
#[actix_web::test]
async fn test_integer_for_string_validation_error() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_metadata = server
.get_tool_metadata("addPet")
.expect("addPet tool should be registered");
let arguments = json!({
"request_body": {
"name": 12345, "photoUrls": ["https://example.com/photo.jpg"],
"status": "available"
}
});
let result = ToolGenerator::extract_parameters(tool_metadata, &arguments);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
ToolCallValidationError::InvalidParameters { .. }
));
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
#[actix_web::test]
async fn test_tool_not_found_with_suggestions() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_names = server.get_tool_names();
let mut suggestions = Vec::new();
for known_tool in &tool_names {
let distance = strsim::jaro("getPetByID", known_tool);
if distance > 0.7 {
suggestions.push((distance, known_tool.clone()));
}
}
suggestions.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
let suggestions: Vec<String> = suggestions
.into_iter()
.take(3)
.map(|(_, name)| name.to_string())
.collect();
let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
tool_name: "getPetByID".to_string(),
suggestions,
});
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
#[actix_web::test]
async fn test_tool_not_found_multiple_suggestions() -> anyhow::Result<()> {
let server = create_server_with_base_url(Url::parse("http://example.com")?)?;
let tool_names = server.get_tool_names();
let mut suggestions = Vec::new();
for known_tool in &tool_names {
let distance = strsim::jaro("findPet", known_tool);
if distance > 0.7 {
suggestions.push((distance, known_tool.clone()));
}
}
suggestions.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
let suggestions: Vec<String> = suggestions
.into_iter()
.take(3)
.map(|(_, name)| name.to_string())
.collect();
let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
tool_name: "findPet".to_string(),
suggestions,
});
let error_json = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(error_json);
Ok(())
}
fn create_server_with_base_url(base_url: Url) -> anyhow::Result<Server> {
let spec_content = include_str!("assets/petstore-openapi-norefs.json");
let json_value: serde_json::Value = serde_json::from_str(spec_content)?;
let mut server = Server::builder()
.openapi_spec(json_value)
.base_url(base_url)
.build();
server.load_openapi_spec()?;
Ok(server)
}
impl MockPetstoreServer {
pub fn mock_get_pet_by_id_not_found(&mut self, pet_id: u64) -> Mock {
self.server
.mock("GET", format!("/pet/{pet_id}").as_str())
.with_status(404)
.with_header("content-type", "application/json")
.with_body(json!({"message": "Pet not found"}).to_string())
.create()
}
pub fn mock_server_error(&mut self, path: &str) -> Mock {
self.server
.mock("GET", path)
.with_status(500)
.with_header("content-type", "application/json")
.with_body(
json!({
"message": "Internal Server Error",
"details": "Something went wrong on the server"
})
.to_string(),
)
.create()
}
pub fn mock_find_pets_by_status(&mut self, status: &str) -> Mock {
let pets_response = json!([
{
"id": 1,
"name": "doggie",
"category": {
"id": 1,
"name": "Dogs"
},
"photoUrls": ["https://example.com/photo1.jpg"],
"tags": [
{
"id": 1,
"name": "tag1"
}
],
"status": status
},
{
"id": 2,
"name": "kitty",
"category": {
"id": 2,
"name": "Cats"
},
"photoUrls": ["https://example.com/photo2.jpg"],
"tags": [
{
"id": 2,
"name": "tag2"
}
],
"status": status
}
]);
self.server
.mock("GET", "/pet/findByStatus")
.match_query(mockito::Matcher::AnyOf(vec![
mockito::Matcher::UrlEncoded("status".to_string(), status.to_string()),
mockito::Matcher::Regex(r"status=.+".to_string()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(pets_response.to_string())
.create()
}
}