use road_runner_common::error::{AppError, ErrorCode};
use road_runner_common::response::ApiErrorDetail;
#[test]
fn error_code_wire_strings_are_stable() {
assert_eq!(ErrorCode::Validation.as_str(), "VALIDATION_ERROR");
assert_eq!(ErrorCode::Unauthorized.as_str(), "UNAUTHORIZED");
assert_eq!(ErrorCode::Forbidden.as_str(), "FORBIDDEN");
assert_eq!(ErrorCode::NotFound.as_str(), "NOT_FOUND");
assert_eq!(ErrorCode::Conflict.as_str(), "CONFLICT");
assert_eq!(ErrorCode::RateLimited.as_str(), "RATE_LIMITED");
assert_eq!(ErrorCode::Internal.as_str(), "INTERNAL_ERROR");
assert_eq!(ErrorCode::External.as_str(), "EXTERNAL_ERROR");
assert_eq!(ErrorCode::ServiceUnavailable.as_str(), "SERVICE_UNAVAILABLE");
}
#[test]
fn app_error_code_and_public_message_cover_all_variants() {
let cases = vec![
(AppError::validation("invalid", vec![]), ErrorCode::Validation, "invalid"),
(AppError::Unauthorized { message: "login".to_string() }, ErrorCode::Unauthorized, "login"),
(AppError::Forbidden { message: "no".to_string() }, ErrorCode::Forbidden, "no"),
(AppError::NotFound { message: "missing".to_string() }, ErrorCode::NotFound, "missing"),
(AppError::Conflict { message: "duplicate".to_string() }, ErrorCode::Conflict, "duplicate"),
(AppError::RateLimited { message: "slow".to_string() }, ErrorCode::RateLimited, "slow"),
(AppError::internal("secret", None), ErrorCode::Internal, "Internal server error"),
(AppError::external("upstream detail", None), ErrorCode::External, "Upstream service error"),
(
AppError::ServiceUnavailable { message: "down".to_string() },
ErrorCode::ServiceUnavailable,
"down",
),
];
for (error, code, public_message) in cases {
assert_eq!(error.code().as_str(), code.as_str());
assert_eq!(error.public_message(), public_message);
}
}
#[test]
fn validation_constructor_preserves_details_in_api_response() {
let error = AppError::validation(
"invalid request",
vec![ApiErrorDetail::new("FIELD", "bad").with_field("name")],
);
let response = error.to_api_response("rid-validation");
assert!(!response.success);
assert_eq!(response.meta.request_id, "rid-validation");
assert_eq!(response.errors.len(), 1);
assert_eq!(response.errors[0].code, "FIELD");
assert_eq!(response.errors[0].field.as_deref(), Some("name"));
}
#[test]
fn validation_without_details_uses_error_code_and_message() {
let error = AppError::validation("missing body", vec![]);
let response = error.to_api_response("rid-empty-validation");
assert_eq!(response.errors.len(), 1);
assert_eq!(response.errors[0].code, "VALIDATION_ERROR");
assert_eq!(response.errors[0].message, "missing body");
}
#[test]
fn non_validation_api_response_contains_single_public_error() {
let error = AppError::internal("database password leaked here", None);
let response = error.to_api_response("rid-internal");
assert!(!response.success);
assert_eq!(response.errors.len(), 1);
assert_eq!(response.errors[0].code, "INTERNAL_ERROR");
assert_eq!(response.errors[0].message, "Internal server error");
}
#[cfg(feature = "web")]
mod web {
use actix_web::http::StatusCode;
use actix_web::ResponseError;
use road_runner_common::error::AppError;
#[test]
fn http_status_maps_all_error_variants() {
let cases = vec![
(AppError::validation("invalid", vec![]), StatusCode::BAD_REQUEST),
(AppError::Unauthorized { message: "login".to_string() }, StatusCode::UNAUTHORIZED),
(AppError::Forbidden { message: "no".to_string() }, StatusCode::FORBIDDEN),
(AppError::NotFound { message: "missing".to_string() }, StatusCode::NOT_FOUND),
(AppError::Conflict { message: "duplicate".to_string() }, StatusCode::CONFLICT),
(AppError::RateLimited { message: "slow".to_string() }, StatusCode::TOO_MANY_REQUESTS),
(AppError::internal("secret", None), StatusCode::INTERNAL_SERVER_ERROR),
(AppError::external("upstream", None), StatusCode::BAD_GATEWAY),
(
AppError::ServiceUnavailable { message: "down".to_string() },
StatusCode::SERVICE_UNAVAILABLE,
),
];
for (error, status) in cases {
assert_eq!(error.http_status(), status);
assert_eq!(error.status_code(), status);
}
}
#[test]
fn error_response_uses_status_and_json_content_type() {
let error = AppError::Forbidden { message: "blocked".to_string() };
let response = error.error_response();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(
response.headers().get("content-type").unwrap().to_str().unwrap(),
"application/json"
);
}
}