obscura-server 0.9.1

A server for relaying secure messages using the Signal Protocol
Documentation
use crate::api::schemas::common::ErrorResponse;
use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("Authentication failed")]
    AuthError,
    #[error("Not found")]
    NotFound,
    #[error("Invalid request: {0}")]
    BadRequest(String),
    #[error("Conflict: {0}")]
    Conflict(String),
    #[error("Forbidden: {0}")]
    Forbidden(String),
    #[error("Precondition failed")]
    PreconditionFailed,
    #[error("Request timeout")]
    Timeout,
    #[error("Length required")]
    LengthRequired,
    #[error("Payload too large")]
    PayloadTooLarge,
    #[error("Internal server error")]
    Internal,
    #[error("Internal error: {0}")]
    InternalMsg(String),
}

pub type Result<T> = std::result::Result<T, AppError>;

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            Self::AuthError => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
            Self::NotFound => (StatusCode::NOT_FOUND, "Not found".to_string()),
            Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
            Self::Conflict(msg) => (StatusCode::CONFLICT, msg),
            Self::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
            Self::PreconditionFailed => (StatusCode::PRECONDITION_FAILED, "Precondition failed".to_string()),
            Self::Timeout => (StatusCode::REQUEST_TIMEOUT, "Request timeout".to_string()),
            Self::LengthRequired => (StatusCode::LENGTH_REQUIRED, "Length required".to_string()),
            Self::PayloadTooLarge => (StatusCode::PAYLOAD_TOO_LARGE, "Payload too large".to_string()),
            Self::Database(_) | Self::Internal | Self::InternalMsg(_) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
            }
        };

        let body = Json(ErrorResponse { error: message });

        (status, body).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::response::IntoResponse;
    use http_body_util::BodyExt;

    fn status_of(err: AppError) -> StatusCode {
        err.into_response().status()
    }

    #[test]
    fn test_error_status_codes() {
        assert_eq!(status_of(AppError::AuthError), StatusCode::UNAUTHORIZED);
        assert_eq!(status_of(AppError::NotFound), StatusCode::NOT_FOUND);
        assert_eq!(status_of(AppError::BadRequest("bad".into())), StatusCode::BAD_REQUEST);
        assert_eq!(status_of(AppError::Conflict("dup".into())), StatusCode::CONFLICT);
        assert_eq!(status_of(AppError::Forbidden("no".into())), StatusCode::FORBIDDEN);
        assert_eq!(status_of(AppError::PreconditionFailed), StatusCode::PRECONDITION_FAILED);
        assert_eq!(status_of(AppError::Timeout), StatusCode::REQUEST_TIMEOUT);
        assert_eq!(status_of(AppError::LengthRequired), StatusCode::LENGTH_REQUIRED);
        assert_eq!(status_of(AppError::PayloadTooLarge), StatusCode::PAYLOAD_TOO_LARGE);
        assert_eq!(status_of(AppError::Internal), StatusCode::INTERNAL_SERVER_ERROR);
        assert_eq!(status_of(AppError::InternalMsg("oops".into())), StatusCode::INTERNAL_SERVER_ERROR);
    }

    #[tokio::test]
    async fn test_error_response_body_format() {
        let response = AppError::NotFound.into_response();
        let body = response.into_body().collect().await.expect("body").to_bytes();
        let json: serde_json::Value = serde_json::from_slice(&body).expect("valid JSON");
        assert_eq!(json["error"], "Not found");
    }
}