664 lines
22 KiB
Rust
664 lines
22 KiB
Rust
use axum::http::StatusCode;
|
|
use axum::response::{IntoResponse, Response};
|
|
use axum::Json;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
/// Gestion centralisée des erreurs du stream server
|
|
///
|
|
/// Hiérarchie d'erreurs pour un debugging efficace et une gestion robuste
|
|
use std::fmt;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AppError {
|
|
// Configuration et initialisation
|
|
ConfigError {
|
|
message: String,
|
|
},
|
|
InitializationError {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs réseau et connexion
|
|
NetworkError {
|
|
message: String,
|
|
},
|
|
ConnectionClosed,
|
|
ConnectionTimeout,
|
|
|
|
// Erreurs audio et streaming
|
|
AudioError {
|
|
message: String,
|
|
},
|
|
StreamingError {
|
|
message: String,
|
|
},
|
|
EncodingError {
|
|
message: String,
|
|
},
|
|
DecodingError {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs de codec
|
|
UnsupportedCodec {
|
|
codec: String,
|
|
},
|
|
InvalidSampleRate {
|
|
rate: u32,
|
|
supported: Vec<u32>,
|
|
},
|
|
InvalidChannelCount {
|
|
channels: u8,
|
|
},
|
|
InvalidBitrate {
|
|
codec: String,
|
|
bitrate: u32,
|
|
min: u32,
|
|
max: u32,
|
|
},
|
|
ParameterMismatch {
|
|
expected: String,
|
|
received: String,
|
|
},
|
|
|
|
// Erreurs de fichier et stockage
|
|
FileError {
|
|
message: String,
|
|
},
|
|
NotFound {
|
|
resource: String,
|
|
},
|
|
NotFoundWithStreamId {
|
|
resource: String,
|
|
stream_id: String,
|
|
},
|
|
StorageError {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs de sérialisation
|
|
SerializationError,
|
|
InvalidData {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs de ressources
|
|
ResourceExhausted {
|
|
resource: String,
|
|
},
|
|
LimitExceeded {
|
|
limit: String,
|
|
current: u32,
|
|
},
|
|
ListenerLimitExceeded {
|
|
limit: u32,
|
|
stream_id: String,
|
|
},
|
|
NotEnoughData,
|
|
InsufficientData {
|
|
message: String,
|
|
},
|
|
BufferOverflow,
|
|
BufferNotFound {
|
|
buffer_id: String,
|
|
stream_id: Option<String>,
|
|
},
|
|
BufferFull {
|
|
buffer_id: String,
|
|
stream_id: Option<String>,
|
|
},
|
|
|
|
// Erreurs d'autorisation
|
|
Unauthorized,
|
|
Forbidden,
|
|
SignatureError {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs de thread et concurrence
|
|
AlreadyRunning,
|
|
AlreadyProcessing {
|
|
resource: String,
|
|
},
|
|
ThreadError {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs de synchronisation
|
|
NoSyncPoint {
|
|
message: String,
|
|
},
|
|
TimeSync {
|
|
message: String,
|
|
},
|
|
|
|
// Erreurs génériques
|
|
InternalError {
|
|
message: String,
|
|
},
|
|
Internal {
|
|
message: String,
|
|
}, // Alias pour compatibilité
|
|
ExternalServiceError {
|
|
service: String,
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
impl fmt::Display for AppError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
AppError::ConfigError { message } => write!(f, "Configuration error: {}", message),
|
|
AppError::InitializationError { message } => {
|
|
write!(f, "Initialization error: {}", message)
|
|
}
|
|
AppError::NetworkError { message } => write!(f, "Network error: {}", message),
|
|
AppError::ConnectionClosed => write!(f, "Connection closed"),
|
|
AppError::ConnectionTimeout => write!(f, "Connection timeout"),
|
|
AppError::AudioError { message } => write!(f, "Audio error: {}", message),
|
|
AppError::StreamingError { message } => write!(f, "Streaming error: {}", message),
|
|
AppError::EncodingError { message } => write!(f, "Encoding error: {}", message),
|
|
AppError::DecodingError { message } => write!(f, "Decoding error: {}", message),
|
|
AppError::UnsupportedCodec { codec } => write!(f, "Unsupported codec: {}", codec),
|
|
AppError::InvalidSampleRate { rate, supported } => write!(
|
|
f,
|
|
"Invalid sample rate: {} (supported: {:?})",
|
|
rate, supported
|
|
),
|
|
AppError::InvalidChannelCount { channels } => {
|
|
write!(f, "Invalid channel count: {}", channels)
|
|
}
|
|
AppError::InvalidBitrate {
|
|
codec,
|
|
bitrate,
|
|
min,
|
|
max,
|
|
} => write!(
|
|
f,
|
|
"Invalid bitrate for {}: {} (supported: {}-{})",
|
|
codec, bitrate, min, max
|
|
),
|
|
AppError::ParameterMismatch { expected, received } => write!(
|
|
f,
|
|
"Parameter mismatch: expected {}, received {}",
|
|
expected, received
|
|
),
|
|
AppError::FileError { message } => write!(f, "File error: {}", message),
|
|
AppError::NotFound { resource } => write!(f, "Not found: {}", resource),
|
|
AppError::NotFoundWithStreamId {
|
|
resource,
|
|
stream_id,
|
|
} => write!(f, "Not found: {} (stream_id: {})", resource, stream_id),
|
|
AppError::StorageError { message } => write!(f, "Storage error: {}", message),
|
|
AppError::SerializationError => write!(f, "Serialization error"),
|
|
AppError::InvalidData { message } => write!(f, "Invalid data: {}", message),
|
|
AppError::ResourceExhausted { resource } => {
|
|
write!(f, "Resource exhausted: {}", resource)
|
|
}
|
|
AppError::LimitExceeded { limit, current } => {
|
|
write!(f, "Limit exceeded: {} (current: {})", limit, current)
|
|
}
|
|
AppError::ListenerLimitExceeded { limit, stream_id } => write!(
|
|
f,
|
|
"Listener limit exceeded: {} (stream_id: {})",
|
|
limit, stream_id
|
|
),
|
|
AppError::NotEnoughData => write!(f, "Not enough data"),
|
|
AppError::InsufficientData { message } => write!(f, "Insufficient data: {}", message),
|
|
AppError::BufferOverflow => write!(f, "Buffer overflow"),
|
|
AppError::BufferNotFound {
|
|
buffer_id,
|
|
stream_id,
|
|
} => {
|
|
if let Some(sid) = stream_id {
|
|
write!(f, "Buffer not found: {} (stream_id: {})", buffer_id, sid)
|
|
} else {
|
|
write!(f, "Buffer not found: {}", buffer_id)
|
|
}
|
|
}
|
|
AppError::BufferFull {
|
|
buffer_id,
|
|
stream_id,
|
|
} => {
|
|
if let Some(sid) = stream_id {
|
|
write!(f, "Buffer full: {} (stream_id: {})", buffer_id, sid)
|
|
} else {
|
|
write!(f, "Buffer full: {}", buffer_id)
|
|
}
|
|
}
|
|
AppError::Unauthorized => write!(f, "Unauthorized access"),
|
|
AppError::Forbidden => write!(f, "Forbidden access"),
|
|
AppError::SignatureError { message } => write!(f, "Signature error: {}", message),
|
|
AppError::AlreadyRunning => write!(f, "Process already running"),
|
|
AppError::AlreadyProcessing { resource } => {
|
|
write!(f, "Already processing: {}", resource)
|
|
}
|
|
AppError::ThreadError { message } => write!(f, "Thread error: {}", message),
|
|
AppError::NoSyncPoint { message } => write!(f, "No sync point: {}", message),
|
|
AppError::TimeSync { message } => write!(f, "Time sync error: {}", message),
|
|
AppError::InternalError { message } => write!(f, "Internal error: {}", message),
|
|
AppError::Internal { message } => write!(f, "Internal error: {}", message),
|
|
AppError::ExternalServiceError { service, message } => {
|
|
write!(f, "External service error: {} - {}", service, message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for AppError {}
|
|
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> Response {
|
|
let (status, error_message) = match &self {
|
|
AppError::ConfigError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::InitializationError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::NetworkError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::ConnectionClosed => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Connection closed".to_string(),
|
|
),
|
|
AppError::ConnectionTimeout => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Connection timeout".to_string(),
|
|
),
|
|
AppError::AudioError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::StreamingError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::EncodingError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::DecodingError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::UnsupportedCodec { codec } => (
|
|
StatusCode::BAD_REQUEST,
|
|
format!("Unsupported codec: {}", codec),
|
|
),
|
|
AppError::InvalidSampleRate { rate, supported } => (
|
|
StatusCode::BAD_REQUEST,
|
|
format!("Invalid sample rate: {} (supported: {:?})", rate, supported),
|
|
),
|
|
AppError::InvalidChannelCount { channels } => (
|
|
StatusCode::BAD_REQUEST,
|
|
format!("Invalid channel count: {}", channels),
|
|
),
|
|
AppError::InvalidBitrate {
|
|
codec,
|
|
bitrate,
|
|
min,
|
|
max,
|
|
} => (
|
|
StatusCode::BAD_REQUEST,
|
|
format!(
|
|
"Invalid bitrate for {}: {} (supported: {}-{})",
|
|
codec, bitrate, min, max
|
|
),
|
|
),
|
|
AppError::ParameterMismatch { expected, received } => (
|
|
StatusCode::BAD_REQUEST,
|
|
format!(
|
|
"Parameter mismatch: expected {}, received {}",
|
|
expected, received
|
|
),
|
|
),
|
|
AppError::FileError { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()),
|
|
AppError::NotFound { resource } => {
|
|
(StatusCode::NOT_FOUND, format!("Not found: {}", resource))
|
|
}
|
|
AppError::StorageError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::SerializationError => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Serialization error".to_string(),
|
|
),
|
|
AppError::InvalidData { message } => (StatusCode::BAD_REQUEST, message.clone()),
|
|
AppError::ResourceExhausted { resource } => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("Resource exhausted: {}", resource),
|
|
),
|
|
AppError::LimitExceeded { limit, current } => (
|
|
StatusCode::TOO_MANY_REQUESTS,
|
|
format!("Limit exceeded: {} (current: {})", limit, current),
|
|
),
|
|
AppError::ListenerLimitExceeded { limit, stream_id } => (
|
|
StatusCode::TOO_MANY_REQUESTS,
|
|
format!(
|
|
"Listener limit exceeded: {} (stream_id: {})",
|
|
limit, stream_id
|
|
),
|
|
),
|
|
AppError::NotEnoughData => (StatusCode::BAD_REQUEST, "Not enough data".to_string()),
|
|
AppError::InsufficientData { message } => (StatusCode::BAD_REQUEST, message.clone()),
|
|
AppError::BufferOverflow => (StatusCode::BAD_REQUEST, "Buffer overflow".to_string()),
|
|
AppError::BufferNotFound {
|
|
buffer_id,
|
|
stream_id,
|
|
} => {
|
|
let msg = if let Some(sid) = stream_id {
|
|
format!("Buffer not found: {} (stream_id: {})", buffer_id, sid)
|
|
} else {
|
|
format!("Buffer not found: {}", buffer_id)
|
|
};
|
|
(StatusCode::NOT_FOUND, msg)
|
|
}
|
|
AppError::BufferFull {
|
|
buffer_id,
|
|
stream_id,
|
|
} => {
|
|
let msg = if let Some(sid) = stream_id {
|
|
format!("Buffer full: {} (stream_id: {})", buffer_id, sid)
|
|
} else {
|
|
format!("Buffer full: {}", buffer_id)
|
|
};
|
|
(StatusCode::BAD_REQUEST, msg)
|
|
}
|
|
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized access".to_string()),
|
|
AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden access".to_string()),
|
|
AppError::SignatureError { message } => (StatusCode::UNAUTHORIZED, message.clone()),
|
|
AppError::AlreadyRunning => {
|
|
(StatusCode::CONFLICT, "Process already running".to_string())
|
|
}
|
|
AppError::AlreadyProcessing { resource } => (
|
|
StatusCode::CONFLICT,
|
|
format!("Already processing: {}", resource),
|
|
),
|
|
AppError::ThreadError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::NoSyncPoint { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::TimeSync { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()),
|
|
AppError::InternalError { message } => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, message.clone())
|
|
}
|
|
AppError::Internal { message } => (StatusCode::INTERNAL_SERVER_ERROR, message.clone()),
|
|
AppError::ExternalServiceError { service, message } => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("External service error: {} - {}", service, message),
|
|
),
|
|
AppError::NotFoundWithStreamId {
|
|
resource,
|
|
stream_id,
|
|
} => (
|
|
StatusCode::NOT_FOUND,
|
|
format!("Not found: {} (stream_id: {})", resource, stream_id),
|
|
),
|
|
};
|
|
|
|
let body = Json(json!({
|
|
"error": error_message.clone(),
|
|
"status": status.as_u16(),
|
|
}));
|
|
|
|
(status, body).into_response()
|
|
}
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, AppError>;
|
|
|
|
/// Types d'erreurs spécifiques au streaming
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum StreamError {
|
|
FileNotFound,
|
|
InvalidRange,
|
|
StreamError(String),
|
|
}
|
|
|
|
impl fmt::Display for StreamError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
StreamError::FileNotFound => write!(f, "File not found"),
|
|
StreamError::InvalidRange => write!(f, "Invalid range"),
|
|
StreamError::StreamError(msg) => write!(f, "Stream error: {}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for StreamError {}
|
|
|
|
/// Conversion de StreamError vers AppError
|
|
impl From<StreamError> for AppError {
|
|
fn from(err: StreamError) -> Self {
|
|
match err {
|
|
StreamError::FileNotFound => AppError::NotFound {
|
|
resource: "stream_file".to_string(),
|
|
},
|
|
StreamError::InvalidRange => AppError::InvalidData {
|
|
message: "Invalid range request".to_string(),
|
|
},
|
|
StreamError::StreamError(msg) => AppError::StreamingError { message: msg },
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conversion de AppError vers StreamError (quand applicable)
|
|
impl From<AppError> for StreamError {
|
|
fn from(err: AppError) -> Self {
|
|
match err {
|
|
AppError::NotFound { resource } => StreamError::FileNotFound,
|
|
AppError::InvalidData { message }
|
|
if message.contains("range") || message.contains("Range") =>
|
|
{
|
|
StreamError::InvalidRange
|
|
}
|
|
AppError::StreamingError { message } => StreamError::StreamError(message),
|
|
_ => StreamError::StreamError(format!("{}", err)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conversion depuis std::io::Error
|
|
impl From<std::io::Error> for StreamError {
|
|
fn from(err: std::io::Error) -> Self {
|
|
match err.kind() {
|
|
std::io::ErrorKind::NotFound => StreamError::FileNotFound,
|
|
std::io::ErrorKind::InvalidInput => StreamError::InvalidRange,
|
|
_ => StreamError::StreamError(format!("IO error: {}", err)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conversion depuis std::io::Error vers AppError
|
|
impl From<std::io::Error> for AppError {
|
|
fn from(err: std::io::Error) -> Self {
|
|
match err.kind() {
|
|
std::io::ErrorKind::NotFound => AppError::NotFound {
|
|
resource: "file".to_string(),
|
|
},
|
|
std::io::ErrorKind::InvalidInput => AppError::InvalidData {
|
|
message: format!("Invalid input: {}", err),
|
|
},
|
|
std::io::ErrorKind::PermissionDenied => AppError::Forbidden,
|
|
_ => AppError::FileError {
|
|
message: format!("IO error: {}", err),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conversion depuis sqlx::Error
|
|
impl From<sqlx::Error> for AppError {
|
|
fn from(err: sqlx::Error) -> Self {
|
|
match err {
|
|
sqlx::Error::Database(db_err) => AppError::ExternalServiceError {
|
|
service: "database".to_string(),
|
|
message: db_err.message().to_string(),
|
|
},
|
|
sqlx::Error::RowNotFound => AppError::NotFound {
|
|
resource: "database_row".to_string(),
|
|
},
|
|
sqlx::Error::PoolClosed => AppError::ConnectionClosed,
|
|
sqlx::Error::PoolTimedOut => AppError::ConnectionTimeout,
|
|
_ => AppError::ExternalServiceError {
|
|
service: "database".to_string(),
|
|
message: format!("Database error: {}", err),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conversion depuis serde_json::Error
|
|
impl From<serde_json::Error> for AppError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
AppError::SerializationError
|
|
}
|
|
}
|
|
|
|
/// Conversion depuis tokio::time::error::Elapsed
|
|
impl From<tokio::time::error::Elapsed> for AppError {
|
|
fn from(_: tokio::time::error::Elapsed) -> Self {
|
|
AppError::ConnectionTimeout
|
|
}
|
|
}
|
|
|
|
/// Implémentation de IntoResponse pour StreamError
|
|
impl IntoResponse for StreamError {
|
|
fn into_response(self) -> Response {
|
|
let app_error: AppError = self.into();
|
|
app_error.into_response()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_stream_error_display() {
|
|
let err1 = StreamError::FileNotFound;
|
|
assert_eq!(format!("{}", err1), "File not found");
|
|
|
|
let err2 = StreamError::InvalidRange;
|
|
assert_eq!(format!("{}", err2), "Invalid range");
|
|
|
|
let err3 = StreamError::StreamError("Test error".to_string());
|
|
assert_eq!(format!("{}", err3), "Stream error: Test error");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stream_error_to_app_error() {
|
|
let stream_err = StreamError::FileNotFound;
|
|
let app_err: AppError = stream_err.into();
|
|
|
|
match app_err {
|
|
AppError::NotFound { resource } => {
|
|
assert_eq!(resource, "stream_file");
|
|
}
|
|
_ => panic!("Expected NotFound variant"),
|
|
}
|
|
|
|
let stream_err2 = StreamError::InvalidRange;
|
|
let app_err2: AppError = stream_err2.into();
|
|
|
|
match app_err2 {
|
|
AppError::InvalidData { message } => {
|
|
assert!(message.contains("range"));
|
|
}
|
|
_ => panic!("Expected InvalidData variant"),
|
|
}
|
|
|
|
let stream_err3 = StreamError::StreamError("Test message".to_string());
|
|
let app_err3: AppError = stream_err3.into();
|
|
|
|
match app_err3 {
|
|
AppError::StreamingError { message } => {
|
|
assert_eq!(message, "Test message");
|
|
}
|
|
_ => panic!("Expected StreamingError variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_app_error_to_stream_error() {
|
|
let app_err = AppError::NotFound {
|
|
resource: "test".to_string(),
|
|
};
|
|
let stream_err: StreamError = app_err.into();
|
|
|
|
match stream_err {
|
|
StreamError::FileNotFound => {}
|
|
_ => panic!("Expected FileNotFound variant"),
|
|
}
|
|
|
|
let app_err2 = AppError::InvalidData {
|
|
message: "Invalid range request".to_string(),
|
|
};
|
|
let stream_err2: StreamError = app_err2.into();
|
|
|
|
match stream_err2 {
|
|
StreamError::InvalidRange => {}
|
|
_ => panic!("Expected InvalidRange variant"),
|
|
}
|
|
|
|
let app_err3 = AppError::StreamingError {
|
|
message: "Stream failed".to_string(),
|
|
};
|
|
let stream_err3: StreamError = app_err3.into();
|
|
|
|
match stream_err3 {
|
|
StreamError::StreamError(msg) => {
|
|
assert_eq!(msg, "Stream failed");
|
|
}
|
|
_ => panic!("Expected StreamError variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_io_error_conversions() {
|
|
let io_err = std::io::Error::from(std::io::ErrorKind::NotFound);
|
|
let stream_err: StreamError = io_err.into();
|
|
|
|
match stream_err {
|
|
StreamError::FileNotFound => {}
|
|
_ => panic!("Expected FileNotFound variant"),
|
|
}
|
|
|
|
let io_err2 = std::io::Error::from(std::io::ErrorKind::InvalidInput);
|
|
let stream_err2: StreamError = io_err2.into();
|
|
|
|
match stream_err2 {
|
|
StreamError::InvalidRange => {}
|
|
_ => panic!("Expected InvalidRange variant"),
|
|
}
|
|
|
|
let io_err3 = std::io::Error::from(std::io::ErrorKind::NotFound);
|
|
let app_err: AppError = io_err3.into();
|
|
|
|
match app_err {
|
|
AppError::NotFound { resource } => {
|
|
assert_eq!(resource, "file");
|
|
}
|
|
_ => panic!("Expected NotFound variant"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_stream_error_into_response() {
|
|
let stream_err = StreamError::FileNotFound;
|
|
let response = stream_err.into_response();
|
|
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[test]
|
|
fn test_serde_json_error_conversion() {
|
|
let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
|
let app_err: AppError = json_err.into();
|
|
|
|
match app_err {
|
|
AppError::SerializationError => {}
|
|
_ => panic!("Expected SerializationError variant"),
|
|
}
|
|
}
|
|
}
|