use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Serialize; #[derive(Serialize)] struct ErrorBody { error: String, } #[derive(Debug)] pub enum AppError { Internal, NotFound, BadRequest(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match self { AppError::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()), AppError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()), AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), }; (status, Json(ErrorBody { error: message })).into_response() } } impl From for AppError { fn from(e: sqlx::Error) -> Self { tracing::error!("{e}"); AppError::Internal } } #[cfg(test)] mod tests { use super::*; async fn body_json(error: AppError) -> serde_json::Value { let response = error.into_response(); let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); serde_json::from_slice(&bytes).unwrap() } #[tokio::test] async fn internal_maps_to_500_with_error_body() { let response = AppError::Internal.into_response(); assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); let body = body_json(AppError::Internal).await; assert_eq!(body["error"], "internal server error"); } #[tokio::test] async fn not_found_maps_to_404_with_error_body() { let response = AppError::NotFound.into_response(); assert_eq!(response.status(), StatusCode::NOT_FOUND); let body = body_json(AppError::NotFound).await; assert_eq!(body["error"], "not found"); } #[tokio::test] async fn bad_request_maps_to_400_and_carries_its_message() { let response = AppError::BadRequest("nope".to_string()).into_response(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); let body = body_json(AppError::BadRequest("nope".to_string())).await; assert_eq!(body["error"], "nope"); } #[tokio::test] async fn sqlx_errors_map_to_internal() { let sqlx_err = sqlx::Error::RowNotFound; let app_err: AppError = sqlx_err.into(); assert!(matches!(app_err, AppError::Internal)); } }