#[cfg(any(feature = "http", feature = "mcp"))]
mod error_handling_tests {
use sdforge::core::error::{ApiError, ErrorCategory, ErrorContext};
use sdforge::core::response::{ServiceError, ServiceResponse};
use serde_json::json;
use tower::ServiceExt;
#[test]
fn test_api_error_not_found() {
let error = ApiError::NotFound {
resource: "user".to_string(),
resource_id: Some("12345".to_string()),
};
assert!(error.to_string().contains("Resource not found"));
assert!(error.to_string().contains("user"));
assert_eq!(error.category(), ErrorCategory::ClientError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(sanitized.contains("user"));
}
#[test]
fn test_api_error_validation() {
let error = ApiError::ValidationError {
field: "email".to_string(),
constraint: "must be valid email format".to_string(),
};
assert!(error.to_string().contains("Validation failed"));
assert!(error.to_string().contains("email"));
assert_eq!(error.category(), ErrorCategory::ValidationError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(sanitized.contains("email"));
}
#[test]
fn test_api_error_unauthorized() {
let error = ApiError::AuthenticationFailed {
reason: "Invalid or expired token".to_string(),
};
assert!(error.to_string().contains("Authentication failed"));
assert!(error.to_string().contains("Invalid or expired token"));
assert_eq!(error.category(), ErrorCategory::AuthError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(sanitized.contains("Authentication failed"));
}
#[test]
fn test_api_error_rate_limit() {
let error = ApiError::RateLimitExceeded {
limit: 100,
window_seconds: 60,
};
assert!(error.to_string().contains("Rate limit exceeded"));
assert_eq!(error.category(), ErrorCategory::RateLimitError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(sanitized.contains("Rate limit"));
}
#[test]
fn test_api_error_internal() {
let error = ApiError::Internal {
message: "Database connection failed: host=localhost port=5432".to_string(),
error_id: "ERR001".to_string(),
source: None,
context: None,
};
assert!(error.to_string().contains("Internal server error"));
assert_eq!(error.category(), ErrorCategory::ServerError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(!sanitized.contains("localhost"));
assert!(!sanitized.contains("5432"));
assert!(sanitized.contains("internal error"));
}
#[test]
fn test_api_error_bad_request() {
let error = ApiError::InvalidInput {
message: "Invalid JSON payload".to_string(),
field: Some("body".to_string()),
value: Some(json!("{broken")),
};
assert!(error.to_string().contains("Invalid input"));
assert!(error.to_string().contains("Invalid JSON payload"));
assert_eq!(error.category(), ErrorCategory::ClientError);
assert!(error.source().is_none());
let sanitized = error.sanitized_message();
assert!(sanitized.contains("Invalid input"));
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_error_to_http_status_mapping() {
use axum::{body::Body, http::Request, http::StatusCode, routing::get, Router};
async fn not_found_handler() -> Result<&'static str, ApiError> {
Err(ApiError::not_found("user", Some("123".into())))
}
let router = Router::new().route("/not_found", get(not_found_handler));
let response = router
.oneshot(
Request::builder()
.uri("/not_found")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
async fn invalid_input_handler() -> Result<&'static str, ApiError> {
Err(ApiError::invalid_input(
"bad data",
Some("field".into()),
None,
))
}
let router = Router::new().route("/invalid_input", get(invalid_input_handler));
let response = router
.oneshot(
Request::builder()
.uri("/invalid_input")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
async fn auth_failed_handler() -> Result<&'static str, ApiError> {
Err(ApiError::authentication_failed("Invalid token"))
}
let router = Router::new().route("/auth_failed", get(auth_failed_handler));
let response = router
.oneshot(
Request::builder()
.uri("/auth_failed")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
async fn access_denied_handler() -> Result<&'static str, ApiError> {
Err(ApiError::access_denied("admin", Some("user1".into())))
}
let router = Router::new().route("/access_denied", get(access_denied_handler));
let response = router
.oneshot(
Request::builder()
.uri("/access_denied")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
async fn rate_limit_handler() -> Result<&'static str, ApiError> {
Err(ApiError::rate_limit_exceeded(100, 60))
}
let router = Router::new().route("/rate_limit", get(rate_limit_handler));
let response = router
.oneshot(
Request::builder()
.uri("/rate_limit")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
async fn internal_handler() -> Result<&'static str, ApiError> {
Err(ApiError::internal_error("Something went wrong", "ERR123"))
}
let router = Router::new().route("/internal", get(internal_handler));
let response = router
.oneshot(
Request::builder()
.uri("/internal")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
async fn unavailable_handler() -> Result<&'static str, ApiError> {
Err(ApiError::service_unavailable("database", Some(30)))
}
let router = Router::new().route("/unavailable", get(unavailable_handler));
let response = router
.oneshot(
Request::builder()
.uri("/unavailable")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
async fn validation_handler() -> Result<&'static str, ApiError> {
Err(ApiError::validation("email", "must be valid"))
}
let router = Router::new().route("/validation", get(validation_handler));
let response = router
.oneshot(
Request::builder()
.uri("/validation")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[test]
fn test_error_to_mcp_response_conversion() {
let not_found = ApiError::not_found("user", Some("123".into()));
let mcp_json = not_found.to_mcp_json();
assert!(mcp_json.contains("\"success\":false"));
assert!(mcp_json.contains("\"code\":\"NOT_FOUND\""));
assert!(mcp_json.contains("Resource not found"));
let auth_failed = ApiError::authentication_failed("Invalid token");
let mcp_json = auth_failed.to_mcp_json();
assert!(mcp_json.contains("AUTHENTICATION_FAILED"));
let rate_limit = ApiError::rate_limit_exceeded(100, 60);
let mcp_json = rate_limit.to_mcp_json();
assert!(mcp_json.contains("RATE_LIMIT_EXCEEDED"));
let internal = ApiError::internal_error("Server error", "ERR001");
let mcp_json = internal.to_mcp_json();
assert!(mcp_json.contains("INTERNAL_ERROR"));
let validation = ApiError::validation("email", "required");
let mcp_json = validation.to_mcp_json();
assert!(mcp_json.contains("VALIDATION_ERROR"));
assert!(mcp_json.contains("email"));
}
#[test]
fn test_error_chain_preservation() {
#[derive(Debug)]
struct DatabaseError(&'static str);
impl std::fmt::Display for DatabaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for DatabaseError {}
unsafe impl Send for DatabaseError {}
unsafe impl Sync for DatabaseError {}
let source_error = DatabaseError("connection refused");
let api_error =
ApiError::internal_with_source("Database operation failed", "DB001", source_error);
let source = api_error.source();
assert!(source.is_some());
let source_msg = source.unwrap().to_string();
assert!(source_msg.contains("connection refused"));
}
#[test]
fn test_error_context_enrichment() {
let context = ErrorContext::new()
.with_extra("user_id".to_string(), "user_123".to_string())
.with_extra("operation".to_string(), "create_user".to_string())
.with_extra("request_path".to_string(), "/api/users".to_string());
let error = ApiError::internal_with_context("Failed to create user", "CREATE001", context);
match error {
ApiError::Internal {
message,
error_id,
context: ctx,
..
} => {
assert_eq!(message, "Failed to create user");
assert_eq!(error_id, "CREATE001");
assert!(ctx.is_some());
let ctx = ctx.unwrap();
assert_eq!(ctx.extra.get("user_id"), Some(&"user_123".to_string()));
assert_eq!(ctx.extra.get("operation"), Some(&"create_user".to_string()));
assert_eq!(
ctx.extra.get("request_path"),
Some(&"/api/users".to_string())
);
}
_ => panic!("Expected Internal error"),
}
}
#[test]
fn test_error_logging_format() {
let error = ApiError::NotFound {
resource: "order".to_string(),
resource_id: Some("ORD-12345".to_string()),
};
let json = serde_json::to_string(&error).expect("Should serialize to JSON");
assert!(json.contains("\"type\":\"NotFound\""));
assert!(json.contains("\"resource\":\"order\""));
assert!(json.contains("\"resource_id\":\"ORD-12345\""));
let deserialized: ApiError = serde_json::from_str(&json).expect("Should deserialize");
match deserialized {
ApiError::NotFound {
resource,
resource_id,
} => {
assert_eq!(resource, "order");
assert_eq!(resource_id, Some("ORD-12345".to_string()));
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_error_context_capture() {
let context = ErrorContext::current();
assert!(context.file.is_some());
let file = context.file.as_ref().unwrap();
assert!(file.contains("error_handling_tests") || file.ends_with(".rs"));
assert!(context.line.is_some());
assert!(context.line.unwrap() > 0);
assert!(context.function.is_some());
assert!(context.extra.is_empty());
let json = serde_json::to_string(&context).expect("Should serialize context");
assert!(json.contains("file") || json.contains("line") || json.contains("function"));
}
#[test]
fn test_error_correlation_id() {
let request_id = "req_abc123def456";
let context = ErrorContext::new()
.with_extra("request_id".to_string(), request_id.to_string())
.with_extra("endpoint".to_string(), "/api/v1/users".to_string())
.with_extra("method".to_string(), "POST".to_string());
let error =
ApiError::internal_with_context("Request processing failed", "PROC001", context);
match error {
ApiError::Internal {
message,
error_id,
context: ctx,
..
} => {
assert_eq!(message, "Request processing failed");
assert_eq!(error_id, "PROC001");
assert!(ctx.is_some());
let ctx = ctx.unwrap();
assert_eq!(
ctx.extra.get("request_id"),
Some(&"req_abc123def456".to_string())
);
assert_eq!(
ctx.extra.get("endpoint"),
Some(&"/api/v1/users".to_string())
);
}
_ => panic!("Expected Internal error"),
}
}
#[test]
fn test_error_constructors() {
let not_found = ApiError::not_found("resource", None);
assert!(matches!(not_found, ApiError::NotFound { .. }));
let invalid = ApiError::invalid_input("bad input", Some("field".into()), None);
assert!(matches!(invalid, ApiError::InvalidInput { .. }));
let auth = ApiError::authentication_failed("token expired");
assert!(matches!(auth, ApiError::AuthenticationFailed { .. }));
let denied = ApiError::access_denied("write", Some("user1".into()));
assert!(matches!(denied, ApiError::AccessDenied { .. }));
let rate = ApiError::rate_limit_exceeded(50, 30);
assert!(matches!(rate, ApiError::RateLimitExceeded { .. }));
let internal = ApiError::internal_error("error message", "E001");
assert!(matches!(internal, ApiError::Internal { .. }));
let unavailable = ApiError::service_unavailable("service", Some(60));
assert!(matches!(unavailable, ApiError::ServiceUnavailable { .. }));
let validation = ApiError::validation("field", "constraint");
assert!(matches!(validation, ApiError::ValidationError { .. }));
}
#[test]
fn test_api_error_to_service_error_conversion() {
let api_error = ApiError::not_found("order", Some("123".into()));
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "NOT_FOUND");
assert_eq!(service_error.http_status(), 404);
assert!(service_error.details().is_some());
let api_error = ApiError::invalid_input("Invalid email", Some("email".into()), None);
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "INVALID_INPUT");
assert_eq!(service_error.http_status(), 400);
let api_error = ApiError::authentication_failed("Token expired");
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "AUTHENTICATION_FAILED");
assert_eq!(service_error.http_status(), 401);
let api_error = ApiError::access_denied("admin", Some("user1".into()));
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "ACCESS_DENIED");
assert_eq!(service_error.http_status(), 403);
let api_error = ApiError::rate_limit_exceeded(100, 60);
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "RATE_LIMIT_EXCEEDED");
assert_eq!(service_error.http_status(), 429);
let api_error = ApiError::internal_error("Internal error", "ERR001");
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "INTERNAL_ERROR");
assert_eq!(service_error.http_status(), 500);
let api_error = ApiError::validation("email", "required");
let service_error: ServiceError = api_error.into();
assert_eq!(service_error.code(), "VALIDATION_ERROR");
assert_eq!(service_error.http_status(), 422);
}
#[test]
fn test_service_response_with_error() {
let service_error = ServiceError::with_details(
"VALIDATION_ERROR",
"Invalid input data",
json!({"field": "email", "reason": "missing @ symbol"}),
422,
);
let response: ServiceResponse<String> = ServiceResponse::error(service_error);
assert!(!response.is_success());
assert!(response.data().is_none());
assert!(response.error_ref().is_some());
let error = response.error_ref().unwrap();
assert_eq!(error.code(), "VALIDATION_ERROR");
assert_eq!(error.message(), "Invalid input data");
assert_eq!(error.http_status(), 422);
assert!(error.details().is_some());
}
#[test]
fn test_error_with_all_fields() {
let context = ErrorContext::new()
.with_extra("key1".to_string(), "value1".to_string())
.with_extra("key2".to_string(), "value2".to_string());
#[derive(Debug)]
struct SourceError(&'static str);
impl std::fmt::Display for SourceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SourceError {}
unsafe impl Send for SourceError {}
unsafe impl Sync for SourceError {}
let error = ApiError::Internal {
message: "Detailed error message".to_string(),
error_id: "ERR_ALL_FIELDS".to_string(),
source: Some(Box::new(SourceError("original error"))),
context: Some(context),
};
assert!(error.source().is_some());
assert_eq!(error.category(), ErrorCategory::ServerError);
let json = serde_json::to_string(&error).expect("Should serialize");
assert!(json.contains("ERR_ALL_FIELDS"));
assert!(json.contains("key1"));
assert!(json.contains("value1"));
}
#[test]
fn test_error_serialization_roundtrip() {
let original = ApiError::ValidationError {
field: "username".to_string(),
constraint: "must be alphanumeric".to_string(),
};
let json = serde_json::to_string(&original).expect("Should serialize");
let deserialized: ApiError = serde_json::from_str(&json).expect("Should deserialize");
match deserialized {
ApiError::ValidationError { field, constraint } => {
assert_eq!(field, "username");
assert_eq!(constraint, "must be alphanumeric");
}
_ => panic!("Expected ValidationError"),
}
}
#[test]
fn test_error_context_empty_extra() {
let context = ErrorContext::new();
assert!(context.extra.is_empty());
assert!(context.file.is_none());
assert!(context.line.is_none());
assert!(context.function.is_none());
let json = serde_json::to_string(&context).expect("Should serialize");
let deserialized: ErrorContext = serde_json::from_str(&json).expect("Should deserialize");
assert!(deserialized.extra.is_empty());
}
#[test]
fn test_error_category_all_variants() {
assert_eq!(
ApiError::not_found("x", None).category(),
ErrorCategory::ClientError
);
assert_eq!(
ApiError::invalid_input("x", None, None).category(),
ErrorCategory::ClientError
);
assert_eq!(
ApiError::authentication_failed("x").category(),
ErrorCategory::AuthError
);
assert_eq!(
ApiError::access_denied("x", None).category(),
ErrorCategory::AuthError
);
assert_eq!(
ApiError::internal_error("x", "x").category(),
ErrorCategory::ServerError
);
assert_eq!(
ApiError::service_unavailable("x", None).category(),
ErrorCategory::ServerError
);
assert_eq!(
ApiError::rate_limit_exceeded(1, 1).category(),
ErrorCategory::RateLimitError
);
assert_eq!(
ApiError::validation("x", "x").category(),
ErrorCategory::ValidationError
);
}
#[test]
fn test_sanitized_message_all_types() {
let msg = ApiError::not_found("user", None).sanitized_message();
assert!(msg.contains("user"));
let msg = ApiError::invalid_input("bad data", None, None).sanitized_message();
assert!(msg.contains("bad data"));
let msg = ApiError::authentication_failed("token expired").sanitized_message();
assert!(msg.contains("Authentication failed"));
let msg = ApiError::access_denied("admin", None).sanitized_message();
assert!(msg.contains("Access denied"));
let msg = ApiError::rate_limit_exceeded(1, 1).sanitized_message();
assert!(msg.contains("Rate limit"));
let msg = ApiError::internal_error("secret info", "ERR").sanitized_message();
assert!(msg.contains("internal error"));
assert!(!msg.contains("secret info"));
let msg = ApiError::service_unavailable("database", None).sanitized_message();
assert!(msg.contains("unavailable"));
assert!(!msg.contains("database"));
let msg = ApiError::validation("field", "constraint").sanitized_message();
assert!(msg.contains("Validation failed"));
}
}