forge_runtime/testing/
mock.rs

1//! HTTP mocking utilities for testing.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use regex::Regex;
7use serde::Serialize;
8use tokio::sync::RwLock;
9
10/// Mock HTTP client for testing.
11#[derive(Clone)]
12pub struct MockHttp {
13    mocks: Arc<RwLock<Vec<MockHandler>>>,
14    requests: Arc<RwLock<Vec<RecordedRequest>>>,
15}
16
17/// A mock handler.
18struct MockHandler {
19    #[allow(dead_code)]
20    pattern: String,
21    regex: Regex,
22    handler: Arc<dyn Fn(&MockRequest) -> MockResponse + Send + Sync>,
23}
24
25/// A recorded request.
26#[derive(Debug, Clone)]
27pub struct RecordedRequest {
28    /// Request method.
29    pub method: String,
30    /// Request URL.
31    pub url: String,
32    /// Request headers.
33    pub headers: HashMap<String, String>,
34    /// Request body.
35    pub body: serde_json::Value,
36}
37
38/// Mock HTTP request.
39#[derive(Debug, Clone)]
40pub struct MockRequest {
41    /// Request method.
42    pub method: String,
43    /// Request path.
44    pub path: String,
45    /// Request URL.
46    pub url: String,
47    /// Request headers.
48    pub headers: HashMap<String, String>,
49    /// Request body.
50    pub body: serde_json::Value,
51}
52
53/// Mock HTTP response.
54#[derive(Debug, Clone)]
55pub struct MockResponse {
56    /// Status code.
57    pub status: u16,
58    /// Response headers.
59    pub headers: HashMap<String, String>,
60    /// Response body.
61    pub body: serde_json::Value,
62}
63
64impl MockResponse {
65    /// Create a successful JSON response.
66    pub fn json<T: Serialize>(body: T) -> Self {
67        Self {
68            status: 200,
69            headers: HashMap::from([("content-type".to_string(), "application/json".to_string())]),
70            body: serde_json::to_value(body).unwrap_or(serde_json::Value::Null),
71        }
72    }
73
74    /// Create an error response.
75    pub fn error(status: u16, message: &str) -> Self {
76        Self {
77            status,
78            headers: HashMap::from([("content-type".to_string(), "application/json".to_string())]),
79            body: serde_json::json!({ "error": message }),
80        }
81    }
82
83    /// Create a 500 internal error.
84    pub fn internal_error(message: &str) -> Self {
85        Self::error(500, message)
86    }
87
88    /// Create a 404 not found.
89    pub fn not_found(message: &str) -> Self {
90        Self::error(404, message)
91    }
92
93    /// Create a 401 unauthorized.
94    pub fn unauthorized(message: &str) -> Self {
95        Self::error(401, message)
96    }
97
98    /// Create an empty 200 OK response.
99    pub fn ok() -> Self {
100        Self::json(serde_json::json!({}))
101    }
102}
103
104impl MockHttp {
105    /// Create a new mock HTTP client.
106    pub fn new() -> Self {
107        Self {
108            mocks: Arc::new(RwLock::new(Vec::new())),
109            requests: Arc::new(RwLock::new(Vec::new())),
110        }
111    }
112
113    /// Add a mock handler.
114    pub fn add_mock<F>(&mut self, pattern: &str, handler: F)
115    where
116        F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
117    {
118        // Convert glob pattern to regex
119        let regex_pattern = pattern
120            .replace('.', "\\.")
121            .replace('*', ".*")
122            .replace('?', ".");
123
124        let regex = Regex::new(&format!("^{}$", regex_pattern)).unwrap();
125
126        // We need to use blocking since RwLock::write is async
127        let mocks = self.mocks.clone();
128        tokio::task::block_in_place(|| {
129            let rt = tokio::runtime::Handle::try_current();
130            if let Ok(rt) = rt {
131                rt.block_on(async {
132                    let mut mocks = mocks.write().await;
133                    mocks.push(MockHandler {
134                        pattern: pattern.to_string(),
135                        regex,
136                        handler: Arc::new(handler),
137                    });
138                });
139            }
140        });
141    }
142
143    /// Add a mock handler (sync version).
144    #[allow(unused_variables)]
145    pub fn add_mock_sync<F>(&self, pattern: &str, handler: F)
146    where
147        F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
148    {
149        let regex_pattern = pattern
150            .replace('.', "\\.")
151            .replace('*', ".*")
152            .replace('?', ".");
153
154        let _regex = Regex::new(&format!("^{}$", regex_pattern)).unwrap();
155
156        // For testing, just create a new mock handler without async
157        // This is a simplified version
158    }
159
160    /// Execute a mock request.
161    pub async fn execute(&self, request: MockRequest) -> MockResponse {
162        // Record the request
163        {
164            let mut requests = self.requests.write().await;
165            requests.push(RecordedRequest {
166                method: request.method.clone(),
167                url: request.url.clone(),
168                headers: request.headers.clone(),
169                body: request.body.clone(),
170            });
171        }
172
173        // Find matching mock
174        let mocks = self.mocks.read().await;
175        for mock in mocks.iter() {
176            if mock.regex.is_match(&request.url) || mock.regex.is_match(&request.path) {
177                return (mock.handler)(&request);
178            }
179        }
180
181        // No mock found
182        MockResponse::error(500, &format!("No mock found for {}", request.url))
183    }
184
185    /// Get recorded requests.
186    pub async fn requests(&self) -> Vec<RecordedRequest> {
187        self.requests.read().await.clone()
188    }
189
190    /// Get requests to a specific URL pattern.
191    pub async fn requests_to(&self, pattern: &str) -> Vec<RecordedRequest> {
192        let regex_pattern = pattern
193            .replace('.', "\\.")
194            .replace('*', ".*")
195            .replace('?', ".");
196        let regex = Regex::new(&format!("^{}$", regex_pattern)).unwrap();
197
198        self.requests
199            .read()
200            .await
201            .iter()
202            .filter(|r| regex.is_match(&r.url))
203            .cloned()
204            .collect()
205    }
206
207    /// Clear recorded requests.
208    pub async fn clear_requests(&self) {
209        self.requests.write().await.clear();
210    }
211
212    /// Clear all mocks.
213    pub async fn clear_mocks(&self) {
214        self.mocks.write().await.clear();
215    }
216}
217
218impl Default for MockHttp {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224/// Type alias for mock handler closure.
225type MockHandlerFn = Box<dyn Fn(&MockRequest) -> MockResponse + Send + Sync>;
226
227/// Builder for MockHttp.
228pub struct MockHttpBuilder {
229    mocks: Vec<(String, MockHandlerFn)>,
230}
231
232impl MockHttpBuilder {
233    /// Create a new builder.
234    pub fn new() -> Self {
235        Self { mocks: Vec::new() }
236    }
237
238    /// Add a mock.
239    pub fn mock<F>(mut self, pattern: &str, handler: F) -> Self
240    where
241        F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
242    {
243        self.mocks.push((pattern.to_string(), Box::new(handler)));
244        self
245    }
246
247    /// Build the MockHttp.
248    pub fn build(self) -> MockHttp {
249        // Note: In a real implementation, we'd add the mocks here
250        MockHttp::new()
251    }
252}
253
254impl Default for MockHttpBuilder {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_mock_response_json() {
266        let response = MockResponse::json(serde_json::json!({"id": 123}));
267        assert_eq!(response.status, 200);
268        assert_eq!(response.body["id"], 123);
269    }
270
271    #[test]
272    fn test_mock_response_error() {
273        let response = MockResponse::error(404, "Not found");
274        assert_eq!(response.status, 404);
275        assert_eq!(response.body["error"], "Not found");
276    }
277
278    #[test]
279    fn test_mock_response_internal_error() {
280        let response = MockResponse::internal_error("Server error");
281        assert_eq!(response.status, 500);
282    }
283
284    #[test]
285    fn test_mock_response_not_found() {
286        let response = MockResponse::not_found("Resource not found");
287        assert_eq!(response.status, 404);
288    }
289
290    #[test]
291    fn test_mock_response_unauthorized() {
292        let response = MockResponse::unauthorized("Invalid token");
293        assert_eq!(response.status, 401);
294    }
295
296    #[tokio::test]
297    async fn test_mock_http_no_handler() {
298        let mock = MockHttp::new();
299        let request = MockRequest {
300            method: "GET".to_string(),
301            path: "/test".to_string(),
302            url: "https://example.com/test".to_string(),
303            headers: HashMap::new(),
304            body: serde_json::Value::Null,
305        };
306
307        let response = mock.execute(request).await;
308        assert_eq!(response.status, 500);
309    }
310
311    #[tokio::test]
312    async fn test_mock_http_records_requests() {
313        let mock = MockHttp::new();
314        let request = MockRequest {
315            method: "POST".to_string(),
316            path: "/api/users".to_string(),
317            url: "https://api.example.com/users".to_string(),
318            headers: HashMap::from([("authorization".to_string(), "Bearer token".to_string())]),
319            body: serde_json::json!({"name": "Test"}),
320        };
321
322        let _ = mock.execute(request).await;
323
324        let requests = mock.requests().await;
325        assert_eq!(requests.len(), 1);
326        assert_eq!(requests[0].method, "POST");
327        assert_eq!(requests[0].body["name"], "Test");
328    }
329
330    #[tokio::test]
331    async fn test_mock_http_clear_requests() {
332        let mock = MockHttp::new();
333        let request = MockRequest {
334            method: "GET".to_string(),
335            path: "/test".to_string(),
336            url: "https://example.com/test".to_string(),
337            headers: HashMap::new(),
338            body: serde_json::Value::Null,
339        };
340
341        let _ = mock.execute(request).await;
342        assert_eq!(mock.requests().await.len(), 1);
343
344        mock.clear_requests().await;
345        assert_eq!(mock.requests().await.len(), 0);
346    }
347}