mockforge_sdk/
error.rs

1//! Error types for the `MockForge` SDK
2
3use thiserror::Error;
4
5/// SDK Result type
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// SDK Error types
9#[derive(Error, Debug)]
10pub enum Error {
11    /// Server already started
12    #[error("Mock server is already running on port {0}. Call stop() before starting again.")]
13    ServerAlreadyStarted(u16),
14
15    /// Server not started
16    #[error("Mock server has not been started yet. Call start() first.")]
17    ServerNotStarted,
18
19    /// Port already in use
20    #[error("Port {0} is already in use. Try using a different port or enable auto_port().")]
21    PortInUse(u16),
22
23    /// Port discovery failed
24    #[error("Port discovery failed: {0}\nTip: Try expanding the port range using port_range(start, end).")]
25    PortDiscoveryFailed(String),
26
27    /// Invalid configuration
28    #[error("Invalid configuration: {0}\nCheck your configuration file or builder settings.")]
29    InvalidConfig(String),
30
31    /// Invalid stub
32    #[error("Invalid stub: {0}\nEnsure method, path, and response body are properly set.")]
33    InvalidStub(String),
34
35    /// Stub not found
36    #[error("Stub not found for {method} {path}. Available stubs: {available}")]
37    StubNotFound {
38        /// HTTP method that was requested
39        method: String,
40        /// Path that was requested
41        path: String,
42        /// Comma-separated list of available stubs
43        available: String,
44    },
45
46    /// HTTP error
47    #[error("HTTP error: {0}\nThis may indicate a network or protocol issue.")]
48    Http(#[from] axum::http::Error),
49
50    /// IO error
51    #[error("IO error: {0}\nCheck file permissions and network connectivity.")]
52    Io(#[from] std::io::Error),
53
54    /// JSON serialization error
55    #[error("JSON serialization error: {0}\nEnsure your request/response body is valid JSON.")]
56    Json(#[from] serde_json::Error),
57
58    /// `MockForge` core error
59    #[error("MockForge core error: {0}")]
60    Core(#[from] mockforge_core::Error),
61
62    /// Server startup timeout
63    #[error("Server failed to start within {timeout_secs} seconds.\nCheck logs for details or increase timeout.")]
64    StartupTimeout {
65        /// Number of seconds waited before timeout
66        timeout_secs: u64,
67    },
68
69    /// Server shutdown timeout
70    #[error("Server failed to stop within {timeout_secs} seconds.\nSome connections may still be active.")]
71    ShutdownTimeout {
72        /// Number of seconds waited before timeout
73        timeout_secs: u64,
74    },
75
76    /// Admin API error
77    #[error("Admin API error ({operation}): {message}\nEndpoint: {endpoint}")]
78    AdminApiError {
79        /// The operation that failed (e.g., "`create_mock`", "`list_mocks`")
80        operation: String,
81        /// The error message from the server or client
82        message: String,
83        /// The API endpoint that was called
84        endpoint: String,
85    },
86
87    /// General error
88    #[error("{0}")]
89    General(String),
90}
91
92impl Error {
93    /// Create an admin API error with context
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use mockforge_sdk::Error;
99    ///
100    /// let err = Error::admin_api_error(
101    ///     "create_mock",
102    ///     "Invalid JSON",
103    ///     "/api/mocks"
104    /// );
105    /// ```
106    pub fn admin_api_error(
107        operation: impl Into<String>,
108        message: impl Into<String>,
109        endpoint: impl Into<String>,
110    ) -> Self {
111        Self::AdminApiError {
112            operation: operation.into(),
113            message: message.into(),
114            endpoint: endpoint.into(),
115        }
116    }
117
118    /// Create a stub not found error with available stubs
119    ///
120    /// # Examples
121    ///
122    /// ```rust
123    /// use mockforge_sdk::Error;
124    ///
125    /// let err = Error::stub_not_found(
126    ///     "GET",
127    ///     "/api/missing",
128    ///     vec!["GET /api/users".to_string()]
129    /// );
130    /// ```
131    pub fn stub_not_found(
132        method: impl Into<String>,
133        path: impl Into<String>,
134        available: Vec<String>,
135    ) -> Self {
136        Self::StubNotFound {
137            method: method.into(),
138            path: path.into(),
139            available: if available.is_empty() {
140                "none".to_string()
141            } else {
142                available.join(", ")
143            },
144        }
145    }
146
147    /// Format error for logging (single line, no ANSI colors)
148    ///
149    /// Useful for structured logging where multi-line messages aren't desired.
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use mockforge_sdk::Error;
155    ///
156    /// let err = Error::ServerNotStarted;
157    /// let log_msg = err.to_log_string();
158    /// // Use in logging: log::error!("{}", log_msg);
159    /// ```
160    #[must_use]
161    pub fn to_log_string(&self) -> String {
162        format!("{self}").replace('\n', " | ")
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_server_already_started_error() {
172        let err = Error::ServerAlreadyStarted(3000);
173        let msg = format!("{err}");
174        assert!(msg.contains("3000"));
175        assert!(msg.contains("already running"));
176        assert!(msg.contains("stop()"));
177    }
178
179    #[test]
180    fn test_server_not_started_error() {
181        let err = Error::ServerNotStarted;
182        let msg = format!("{err}");
183        assert!(msg.contains("not been started"));
184        assert!(msg.contains("start()"));
185    }
186
187    #[test]
188    fn test_port_in_use_error() {
189        let err = Error::PortInUse(8080);
190        let msg = format!("{err}");
191        assert!(msg.contains("8080"));
192        assert!(msg.contains("already in use"));
193        assert!(msg.contains("auto_port()"));
194    }
195
196    #[test]
197    fn test_port_discovery_failed_error() {
198        let err = Error::PortDiscoveryFailed("No ports available in range".to_string());
199        let msg = format!("{err}");
200        assert!(msg.contains("Port discovery failed"));
201        assert!(msg.contains("No ports available"));
202        assert!(msg.contains("port_range"));
203    }
204
205    #[test]
206    fn test_invalid_config_error() {
207        let err = Error::InvalidConfig("Invalid host address".to_string());
208        let msg = format!("{err}");
209        assert!(msg.contains("Invalid configuration"));
210        assert!(msg.contains("Invalid host address"));
211        assert!(msg.contains("configuration file"));
212    }
213
214    #[test]
215    fn test_invalid_stub_error() {
216        let err = Error::InvalidStub("Missing response body".to_string());
217        let msg = format!("{err}");
218        assert!(msg.contains("Invalid stub"));
219        assert!(msg.contains("Missing response body"));
220        assert!(msg.contains("properly set"));
221    }
222
223    #[test]
224    fn test_stub_not_found_error_with_available() {
225        let err = Error::stub_not_found(
226            "GET",
227            "/api/missing",
228            vec!["GET /api/users".to_string(), "POST /api/orders".to_string()],
229        );
230        let msg = format!("{err}");
231        assert!(msg.contains("GET"));
232        assert!(msg.contains("/api/missing"));
233        assert!(msg.contains("GET /api/users"));
234        assert!(msg.contains("POST /api/orders"));
235    }
236
237    #[test]
238    fn test_stub_not_found_error_no_available() {
239        let err = Error::stub_not_found("DELETE", "/api/users/1", vec![]);
240        let msg = format!("{err}");
241        assert!(msg.contains("DELETE"));
242        assert!(msg.contains("/api/users/1"));
243        assert!(msg.contains("none"));
244    }
245
246    #[test]
247    fn test_startup_timeout_error() {
248        let err = Error::StartupTimeout { timeout_secs: 30 };
249        let msg = format!("{err}");
250        assert!(msg.contains("30 seconds"));
251        assert!(msg.contains("failed to start"));
252    }
253
254    #[test]
255    fn test_shutdown_timeout_error() {
256        let err = Error::ShutdownTimeout { timeout_secs: 10 };
257        let msg = format!("{err}");
258        assert!(msg.contains("10 seconds"));
259        assert!(msg.contains("failed to stop"));
260        assert!(msg.contains("connections"));
261    }
262
263    #[test]
264    fn test_admin_api_error() {
265        let err = Error::admin_api_error("create_mock", "Invalid JSON payload", "/api/mocks");
266        let msg = format!("{err}");
267        assert!(msg.contains("create_mock"));
268        assert!(msg.contains("Invalid JSON payload"));
269        assert!(msg.contains("/api/mocks"));
270    }
271
272    #[test]
273    fn test_general_error() {
274        let err = Error::General("Something went wrong".to_string());
275        let msg = format!("{err}");
276        assert_eq!(msg, "Something went wrong");
277    }
278
279    #[test]
280    fn test_to_log_string_single_line() {
281        let err = Error::General("Simple error".to_string());
282        let log_str = err.to_log_string();
283        assert_eq!(log_str, "Simple error");
284        assert!(!log_str.contains('\n'));
285    }
286
287    #[test]
288    fn test_to_log_string_multiline() {
289        let err = Error::InvalidConfig("Line 1\nLine 2\nLine 3".to_string());
290        let log_str = err.to_log_string();
291        assert!(!log_str.contains('\n'));
292        assert!(log_str.contains(" | "));
293        assert!(log_str.contains("Line 1"));
294        assert!(log_str.contains("Line 2"));
295        assert!(log_str.contains("Line 3"));
296    }
297
298    #[test]
299    fn test_http_error_conversion() {
300        // Create an HTTP error using an invalid header value
301        use axum::http::header::InvalidHeaderValue;
302        let http_err: axum::http::Error =
303            axum::http::header::HeaderValue::from_bytes(&[0x80]).unwrap_err().into();
304        let err = Error::from(http_err);
305        let msg = format!("{err}");
306        assert!(msg.contains("HTTP error"));
307    }
308
309    #[test]
310    fn test_io_error_conversion() {
311        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
312        let err = Error::from(io_err);
313        let msg = format!("{err}");
314        assert!(msg.contains("IO error"));
315        assert!(msg.contains("file not found"));
316    }
317
318    #[test]
319    fn test_json_error_conversion() {
320        let json_str = "{invalid json";
321        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
322        let err = Error::from(json_err);
323        let msg = format!("{err}");
324        assert!(msg.contains("JSON serialization error"));
325    }
326
327    #[test]
328    fn test_error_debug_format() {
329        let err = Error::ServerNotStarted;
330        let debug_str = format!("{err:?}");
331        assert!(debug_str.contains("ServerNotStarted"));
332    }
333
334    #[test]
335    fn test_stub_not_found_with_single_available() {
336        let err = Error::stub_not_found("POST", "/api/create", vec!["GET /api/list".to_string()]);
337        let msg = format!("{err}");
338        assert!(msg.contains("GET /api/list"));
339        assert!(!msg.contains(", ")); // No comma for single item
340    }
341
342    #[test]
343    fn test_result_type_ok() {
344        let result: Result<i32> = Ok(42);
345        assert!(result.is_ok());
346        assert_eq!(result.unwrap(), 42);
347    }
348
349    #[test]
350    fn test_result_type_err() {
351        let result: Result<i32> = Err(Error::ServerNotStarted);
352        assert!(result.is_err());
353    }
354}