Skip to main content

usenet_dl/api/
error_response.rs

1//! HTTP error response handling for the API
2//!
3//! This module provides conversions from domain errors to HTTP responses
4//! with appropriate status codes and JSON error bodies.
5
6use crate::error::{ApiError, Error, ToHttpStatus};
7use axum::{
8    Json,
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12
13/// Implement IntoResponse for Error to automatically convert errors to HTTP responses
14impl IntoResponse for Error {
15    fn into_response(self) -> Response {
16        let status_code =
17            StatusCode::from_u16(self.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
18
19        let api_error: ApiError = self.into();
20
21        (status_code, Json(api_error)).into_response()
22    }
23}
24
25/// Implement IntoResponse for ApiError for explicit error responses
26impl IntoResponse for ApiError {
27    fn into_response(self) -> Response {
28        // Default to 500 if we're directly converting an ApiError
29        // (usually errors go through Error::into_response which has the status code)
30        (StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response()
31    }
32}
33
34// unwrap/expect are acceptable in tests for concise failure-on-error assertions
35#[allow(clippy::unwrap_used, clippy::expect_used)]
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::error::{DatabaseError, DownloadError, PostProcessError};
40    use std::path::PathBuf;
41
42    #[test]
43    fn test_error_to_http_status_not_found() {
44        let error = Error::NotFound("test".to_string());
45        assert_eq!(error.status_code(), 404);
46        assert_eq!(error.error_code(), "not_found");
47    }
48
49    #[test]
50    fn test_error_to_http_status_download_not_found() {
51        let error = Error::Download(DownloadError::NotFound { id: 123 });
52        assert_eq!(error.status_code(), 404);
53        assert_eq!(error.error_code(), "download_not_found");
54    }
55
56    #[test]
57    fn test_error_to_http_status_conflict() {
58        let error = Error::Download(DownloadError::AlreadyInState {
59            id: 123,
60            state: "paused".to_string(),
61        });
62        assert_eq!(error.status_code(), 409);
63        assert_eq!(error.error_code(), "already_in_state");
64    }
65
66    #[test]
67    fn test_error_to_http_status_unprocessable() {
68        let error = Error::InvalidNzb("bad nzb".to_string());
69        assert_eq!(error.status_code(), 422);
70        assert_eq!(error.error_code(), "invalid_nzb");
71    }
72
73    #[test]
74    fn test_error_to_http_status_service_unavailable() {
75        let error = Error::ShuttingDown;
76        assert_eq!(error.status_code(), 503);
77        assert_eq!(error.error_code(), "shutting_down");
78    }
79
80    #[test]
81    fn test_error_to_http_status_internal_server() {
82        let error = Error::Database(DatabaseError::QueryFailed("query failed".to_string()));
83        assert_eq!(error.status_code(), 500);
84        assert_eq!(error.error_code(), "database_error");
85    }
86
87    #[test]
88    fn test_error_to_api_error_with_details() {
89        let error = Error::Download(DownloadError::NotFound { id: 123 });
90        let api_error: ApiError = error.into();
91
92        assert_eq!(api_error.error.code, "download_not_found");
93        assert!(api_error.error.message.contains("123"));
94        assert!(api_error.error.details.is_some());
95
96        let details = api_error.error.details.unwrap();
97        assert_eq!(details["download_id"], 123);
98    }
99
100    #[test]
101    fn test_error_to_api_error_insufficient_space() {
102        let error = Error::InsufficientSpace {
103            required: 1000,
104            available: 500,
105        };
106        let api_error: ApiError = error.into();
107
108        assert_eq!(api_error.error.code, "insufficient_space");
109        assert!(api_error.error.message.contains("1000"));
110        assert!(api_error.error.message.contains("500"));
111
112        let details = api_error.error.details.unwrap();
113        assert_eq!(details["required_bytes"], 1000);
114        assert_eq!(details["available_bytes"], 500);
115    }
116
117    #[test]
118    fn test_error_to_api_error_post_process() {
119        let error = Error::PostProcess(PostProcessError::WrongPassword {
120            archive: PathBuf::from("/path/to/archive.rar"),
121        });
122        let api_error: ApiError = error.into();
123
124        assert_eq!(api_error.error.code, "wrong_password");
125        assert!(api_error.error.details.is_some());
126
127        let details = api_error.error.details.unwrap();
128        assert!(details["archive"].as_str().unwrap().contains("archive.rar"));
129    }
130
131    #[tokio::test]
132    async fn test_error_into_response() {
133        let error = Error::NotFound("test resource".to_string());
134        let response = error.into_response();
135
136        assert_eq!(response.status(), StatusCode::NOT_FOUND);
137
138        // Extract and verify the JSON body
139        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
140            .await
141            .unwrap();
142        let api_error: ApiError = serde_json::from_slice(&body).unwrap();
143
144        assert_eq!(api_error.error.code, "not_found");
145        assert!(api_error.error.message.contains("test resource"));
146    }
147
148    #[tokio::test]
149    async fn test_download_error_into_response() {
150        let error = Error::Download(DownloadError::AlreadyInState {
151            id: 456,
152            state: "downloading".to_string(),
153        });
154        let response = error.into_response();
155
156        assert_eq!(response.status(), StatusCode::CONFLICT);
157
158        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
159            .await
160            .unwrap();
161        let api_error: ApiError = serde_json::from_slice(&body).unwrap();
162
163        assert_eq!(api_error.error.code, "already_in_state");
164        assert_eq!(
165            api_error.error.details.as_ref().unwrap()["download_id"],
166            456
167        );
168        assert_eq!(
169            api_error.error.details.as_ref().unwrap()["state"],
170            "downloading"
171        );
172    }
173}