clawspec_core/test_client/
error.rs

1//! Error types for the test client framework.
2//!
3//! This module provides comprehensive error handling for test operations,
4//! server lifecycle management, and OpenAPI generation.
5
6use std::time::Duration;
7
8use crate::ApiClientError;
9
10/// Error types for test client operations.
11///
12/// `TestAppError` represents all possible errors that can occur during
13/// test client operations, from server startup to OpenAPI generation.
14/// It provides detailed error information to help diagnose test failures.
15///
16/// # Error Categories
17///
18/// - **I/O Errors**: File operations, network binding issues
19/// - **Client Errors**: ApiClient configuration and request errors  
20/// - **Serialization Errors**: JSON/YAML parsing and generation errors
21/// - **Server Health Errors**: Server startup and health check failures
22///
23/// All variants implement standard error traits and provide detailed context for debugging.
24#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
25pub enum TestAppError {
26    /// I/O operation failed.
27    ///
28    /// This includes file operations (creating directories, writing files)
29    /// and network operations (binding to ports, socket operations).
30    #[display("I/O error: {_0}")]
31    IoError(tokio::io::Error),
32
33    /// ApiClient configuration or operation failed.
34    ///
35    /// This wraps errors from the underlying ApiClient, including
36    /// configuration errors, request failures, and response parsing issues.
37    #[display("API client error: {_0}")]
38    ClientError(ApiClientError),
39
40    /// JSON serialization or deserialization failed.
41    ///
42    /// This occurs when generating OpenAPI specifications in JSON format
43    /// or when processing JSON request/response data.
44    #[display("JSON error: {_0}")]
45    JsonError(serde_json::Error),
46
47    /// YAML serialization failed.
48    ///
49    /// This occurs specifically when generating OpenAPI specifications
50    /// in YAML format. Contains the detailed error message.
51    #[display("YAML serialization error: {error}")]
52    YamlError {
53        /// Detailed error message from the YAML serializer.
54        error: String,
55    },
56
57    /// Server failed to become healthy within the timeout period.
58    ///
59    /// This indicates that the test server either failed to start properly
60    /// or did not respond to health checks within the configured timeout.
61    ///
62    /// # Troubleshooting
63    ///
64    /// - Check server logs for startup errors
65    /// - Verify health check implementation
66    /// - Increase timeout if server startup is slow
67    /// - Ensure no port conflicts with other services
68    #[from(ignore)]
69    #[display("Server failed to become healthy within {timeout:?}")]
70    UnhealthyServer {
71        /// The timeout duration that was exceeded.
72        timeout: Duration,
73    },
74
75    /// Server operation failed.
76    ///
77    /// This wraps errors from the TestServer implementation, including
78    /// launch failures and health check errors.
79    #[display("Server error: {_0}")]
80    ServerError(Box<dyn std::error::Error + Send + Sync + 'static>),
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::time::Duration;
87
88    #[test]
89    fn test_test_app_error_display() {
90        let yaml_error = TestAppError::YamlError {
91            error: "Invalid YAML format".to_string(),
92        };
93        assert_eq!(
94            format!("{yaml_error}"),
95            "YAML serialization error: Invalid YAML format"
96        );
97
98        let unhealthy_error = TestAppError::UnhealthyServer {
99            timeout: Duration::from_secs(5),
100        };
101        assert_eq!(
102            format!("{unhealthy_error}"),
103            "Server failed to become healthy within 5s"
104        );
105    }
106
107    #[test]
108    fn test_test_app_error_debug() {
109        let yaml_error = TestAppError::YamlError {
110            error: "Invalid YAML format".to_string(),
111        };
112        let debug_str = format!("{yaml_error:?}");
113        assert!(debug_str.contains("YamlError"));
114        assert!(debug_str.contains("Invalid YAML format"));
115
116        let unhealthy_error = TestAppError::UnhealthyServer {
117            timeout: Duration::from_secs(10),
118        };
119        let debug_str = format!("{unhealthy_error:?}");
120        assert!(debug_str.contains("UnhealthyServer"));
121        assert!(debug_str.contains("10s"));
122    }
123
124    #[test]
125    fn test_test_app_error_from_io_error() {
126        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
127        let tokio_error = io_error;
128        let test_error: TestAppError = tokio_error.into();
129
130        match test_error {
131            TestAppError::IoError(_) => {} // Expected
132            other => panic!("Expected IoError, got: {other:?}"),
133        }
134    }
135
136    #[test]
137    fn test_test_app_error_from_json_error() {
138        let json_str = r#"{"invalid": json"#;
139        let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
140        let test_error: TestAppError = json_error.into();
141
142        match test_error {
143            TestAppError::JsonError(_) => {} // Expected
144            other => panic!("Expected JsonError, got: {other:?}"),
145        }
146    }
147
148    #[test]
149    fn test_test_app_error_from_api_client_error() {
150        // Create an ApiClient error by building with invalid URL parts
151        use crate::ApiClient;
152
153        // Create a malformed URL that will cause ApiClientError
154        let build_result = ApiClient::builder()
155            .with_scheme(http::uri::Scheme::HTTPS)
156            .with_host("invalid\0host") // Invalid host with null byte
157            .build();
158
159        match build_result {
160            Err(api_error) => {
161                let test_error: TestAppError = api_error.into();
162                match test_error {
163                    TestAppError::ClientError(_) => {} // Expected
164                    other => panic!("Expected ClientError, got: {other:?}"),
165                }
166            }
167            Ok(_) => {
168                // If the above doesn't work, try a different approach
169                // Some invalid base paths might not cause errors at build time
170                // Let's just test the conversion directly
171                use crate::ApiClientError;
172                let api_error = ApiClientError::InvalidBasePath {
173                    error: "invalid".to_string(),
174                };
175                let test_error: TestAppError = api_error.into();
176                match test_error {
177                    TestAppError::ClientError(_) => {} // Expected
178                    other => panic!("Expected ClientError, got: {other:?}"),
179                }
180            }
181        }
182    }
183
184    #[test]
185    fn test_yaml_error_creation() {
186        let error_msg = "YAML serialization failed";
187        let yaml_error = TestAppError::YamlError {
188            error: error_msg.to_string(),
189        };
190
191        match yaml_error {
192            TestAppError::YamlError { error } => {
193                assert_eq!(error, error_msg);
194            }
195            other => panic!("Expected YamlError, got: {other:?}"),
196        }
197    }
198
199    #[test]
200    fn test_unhealthy_server_error_creation() {
201        let timeout = Duration::from_millis(2500);
202        let unhealthy_error = TestAppError::UnhealthyServer { timeout };
203
204        match unhealthy_error {
205            TestAppError::UnhealthyServer {
206                timeout: actual_timeout,
207            } => {
208                assert_eq!(actual_timeout, timeout);
209            }
210            other => panic!("Expected UnhealthyServer, got: {other:?}"),
211        }
212    }
213
214    #[test]
215    fn test_server_error_creation() {
216        let server_error = std::io::Error::new(std::io::ErrorKind::AddrInUse, "Port in use");
217        let test_error = TestAppError::ServerError(Box::new(server_error));
218
219        match test_error {
220            TestAppError::ServerError(_) => {} // Expected
221            other => panic!("Expected ServerError, got: {other:?}"),
222        }
223    }
224
225    #[test]
226    fn test_error_trait_bounds() {
227        // Verify that TestAppError implements the required traits
228        fn assert_error_traits<T>(_: T)
229        where
230            T: std::error::Error + std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
231        {
232        }
233
234        let yaml_error = TestAppError::YamlError {
235            error: "test".to_string(),
236        };
237        assert_error_traits(yaml_error);
238    }
239
240    #[test]
241    fn test_error_source_chain() {
242        // Test that errors maintain their source chain
243        let original_io_error =
244            std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
245        let tokio_error = original_io_error;
246        let test_error: TestAppError = tokio_error.into();
247
248        // Check that we can access the source error
249        match test_error {
250            TestAppError::IoError(ref io_err) => {
251                assert_eq!(io_err.kind(), tokio::io::ErrorKind::PermissionDenied);
252            }
253            other => panic!("Expected IoError, got: {other:?}"),
254        }
255    }
256}