mockforge_sdk/
server.rs

1//! Mock server implementation
2
3use crate::builder::MockServerBuilder;
4use crate::stub::ResponseStub;
5use crate::{Error, Result};
6use axum::Router;
7use mockforge_core::config::{RouteConfig, RouteResponseConfig};
8use mockforge_core::{Config, ServerConfig};
9use serde_json::Value;
10use std::net::SocketAddr;
11use tokio::task::JoinHandle;
12
13/// A mock server that can be embedded in tests
14#[derive(Debug)]
15pub struct MockServer {
16    port: u16,
17    address: SocketAddr,
18    config: ServerConfig,
19    server_handle: Option<JoinHandle<()>>,
20    shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
21    routes: Vec<RouteConfig>,
22}
23
24impl MockServer {
25    /// Create a new mock server builder
26    #[must_use]
27    pub const fn new() -> MockServerBuilder {
28        MockServerBuilder::new()
29    }
30
31    /// Create a mock server from configuration
32    pub(crate) async fn from_config(
33        server_config: ServerConfig,
34        _core_config: Config,
35    ) -> Result<Self> {
36        let port = server_config.http.port;
37        let host = server_config.http.host.clone();
38
39        let address: SocketAddr = format!("{host}:{port}")
40            .parse()
41            .map_err(|e| Error::InvalidConfig(format!("Invalid address: {e}")))?;
42
43        Ok(Self {
44            port,
45            address,
46            config: server_config,
47            server_handle: None,
48            shutdown_tx: None,
49            routes: Vec::new(),
50        })
51    }
52
53    /// Start the mock server
54    pub async fn start(&mut self) -> Result<()> {
55        if self.server_handle.is_some() {
56            return Err(Error::ServerAlreadyStarted(self.port));
57        }
58
59        // Build the router from routes
60        let router = self.build_simple_router();
61
62        // Create shutdown channel
63        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
64        self.shutdown_tx = Some(shutdown_tx);
65
66        let address = self.address;
67
68        // Spawn the server
69        let server_handle = tokio::spawn(async move {
70            let listener = match tokio::net::TcpListener::bind(address).await {
71                Ok(l) => l,
72                Err(e) => {
73                    tracing::error!("Failed to bind to {}: {}", address, e);
74                    return;
75                }
76            };
77
78            tracing::info!("MockForge SDK server listening on {}", address);
79
80            axum::serve(listener, router)
81                .with_graceful_shutdown(async move {
82                    let _ = shutdown_rx.await;
83                })
84                .await
85                .expect("Server error");
86        });
87
88        self.server_handle = Some(server_handle);
89
90        // Wait for the server to be ready by polling health
91        self.wait_for_ready().await?;
92
93        Ok(())
94    }
95
96    /// Wait for the server to be ready
97    async fn wait_for_ready(&self) -> Result<()> {
98        let max_attempts = 50;
99        let delay = tokio::time::Duration::from_millis(100);
100
101        for attempt in 0..max_attempts {
102            // Try to connect to the server
103            let client = reqwest::Client::builder()
104                .timeout(tokio::time::Duration::from_millis(100))
105                .build()
106                .map_err(|e| Error::General(format!("Failed to create HTTP client: {e}")))?;
107
108            match client.get(format!("{}/health", self.url())).send().await {
109                Ok(response) if response.status().is_success() => return Ok(()),
110                _ => {
111                    if attempt < max_attempts - 1 {
112                        tokio::time::sleep(delay).await;
113                    }
114                }
115            }
116        }
117
118        Err(Error::General(format!(
119            "Server failed to become ready within {}ms",
120            max_attempts * delay.as_millis() as u32
121        )))
122    }
123
124    /// Build a simple router from stored routes
125    fn build_simple_router(&self) -> Router {
126        use axum::http::StatusCode;
127        use axum::routing::{delete, get, post, put};
128        use axum::{response::IntoResponse, Json};
129
130        let mut router = Router::new();
131
132        for route_config in &self.routes {
133            let status = route_config.response.status;
134            let body = route_config.response.body.clone();
135            let headers = route_config.response.headers.clone();
136
137            let handler = move || {
138                let body = body.clone();
139                let headers = headers.clone();
140                async move {
141                    let mut response = Json(body).into_response();
142                    *response.status_mut() = StatusCode::from_u16(status).unwrap();
143
144                    for (key, value) in headers {
145                        if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes())
146                        {
147                            if let Ok(header_value) = axum::http::HeaderValue::from_str(&value) {
148                                response.headers_mut().insert(header_name, header_value);
149                            }
150                        }
151                    }
152
153                    response
154                }
155            };
156
157            let path = &route_config.path;
158
159            router = match route_config.method.to_uppercase().as_str() {
160                "GET" => router.route(path, get(handler)),
161                "POST" => router.route(path, post(handler)),
162                "PUT" => router.route(path, put(handler)),
163                "DELETE" => router.route(path, delete(handler)),
164                _ => router,
165            };
166        }
167
168        router
169    }
170
171    /// Stop the mock server
172    pub async fn stop(mut self) -> Result<()> {
173        if let Some(shutdown_tx) = self.shutdown_tx.take() {
174            let _ = shutdown_tx.send(());
175        }
176
177        if let Some(handle) = self.server_handle.take() {
178            let _ = handle.await;
179        }
180
181        Ok(())
182    }
183
184    /// Stub a response for a given method and path
185    pub async fn stub_response(
186        &mut self,
187        method: impl Into<String>,
188        path: impl Into<String>,
189        body: Value,
190    ) -> Result<()> {
191        let stub = ResponseStub::new(method, path, body);
192        self.add_stub(stub).await
193    }
194
195    /// Add a response stub
196    pub async fn add_stub(&mut self, stub: ResponseStub) -> Result<()> {
197        let route_config = RouteConfig {
198            path: stub.path.clone(),
199            method: stub.method,
200            request: None,
201            response: RouteResponseConfig {
202                status: stub.status,
203                headers: stub.headers,
204                body: Some(stub.body),
205            },
206            fault_injection: None,
207            latency: None,
208        };
209
210        self.routes.push(route_config);
211
212        Ok(())
213    }
214
215    /// Remove all stubs
216    pub async fn clear_stubs(&mut self) -> Result<()> {
217        self.routes.clear();
218        Ok(())
219    }
220
221    /// Get the server port
222    #[must_use]
223    pub const fn port(&self) -> u16 {
224        self.port
225    }
226
227    /// Get the server base URL
228    #[must_use]
229    pub fn url(&self) -> String {
230        format!("http://{}", self.address)
231    }
232
233    /// Check if the server is running
234    #[must_use]
235    pub const fn is_running(&self) -> bool {
236        self.server_handle.is_some()
237    }
238}
239
240impl Default for MockServer {
241    fn default() -> Self {
242        Self {
243            port: 0,
244            address: "127.0.0.1:0".parse().unwrap(),
245            config: ServerConfig::default(),
246            server_handle: None,
247            shutdown_tx: None,
248            routes: Vec::new(),
249        }
250    }
251}
252
253// Implement Drop to ensure server is stopped
254impl Drop for MockServer {
255    fn drop(&mut self) {
256        if let Some(shutdown_tx) = self.shutdown_tx.take() {
257            let _ = shutdown_tx.send(());
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use serde_json::json;
266
267    #[test]
268    fn test_mock_server_new() {
269        let builder = MockServer::new();
270        // Should return a MockServerBuilder
271        assert_eq!(std::mem::size_of_val(&builder), std::mem::size_of::<MockServerBuilder>());
272    }
273
274    #[test]
275    fn test_mock_server_default() {
276        let server = MockServer::default();
277        assert_eq!(server.port, 0);
278        assert!(!server.is_running());
279        assert!(server.routes.is_empty());
280    }
281
282    #[test]
283    fn test_mock_server_port() {
284        let server = MockServer::default();
285        assert_eq!(server.port(), 0);
286    }
287
288    #[test]
289    fn test_mock_server_url() {
290        let mut server = MockServer::default();
291        server.port = 8080;
292        server.address = "127.0.0.1:8080".parse().unwrap();
293        assert_eq!(server.url(), "http://127.0.0.1:8080");
294    }
295
296    #[test]
297    fn test_mock_server_is_running_false() {
298        let server = MockServer::default();
299        assert!(!server.is_running());
300    }
301
302    #[tokio::test]
303    async fn test_from_config_valid() {
304        let server_config = ServerConfig::default();
305        let core_config = Config::default();
306
307        let result = MockServer::from_config(server_config, core_config).await;
308        assert!(result.is_ok());
309
310        let server = result.unwrap();
311        assert!(!server.is_running());
312        assert!(server.routes.is_empty());
313    }
314
315    #[tokio::test]
316    async fn test_from_config_invalid_address() {
317        let mut server_config = ServerConfig::default();
318        server_config.http.host = "invalid host with spaces".to_string();
319        let core_config = Config::default();
320
321        let result = MockServer::from_config(server_config, core_config).await;
322        assert!(result.is_err());
323        match result {
324            Err(Error::InvalidConfig(msg)) => {
325                assert!(msg.contains("Invalid address"));
326            }
327            _ => panic!("Expected InvalidConfig error"),
328        }
329    }
330
331    #[tokio::test]
332    async fn test_add_stub() {
333        let mut server = MockServer::default();
334        let stub = ResponseStub::new("GET", "/api/test", json!({"test": true}));
335
336        let result = server.add_stub(stub.clone()).await;
337        assert!(result.is_ok());
338        assert_eq!(server.routes.len(), 1);
339
340        let route = &server.routes[0];
341        assert_eq!(route.path, "/api/test");
342        assert_eq!(route.method, "GET");
343        assert_eq!(route.response.status, 200);
344    }
345
346    #[tokio::test]
347    async fn test_add_stub_with_custom_status() {
348        let mut server = MockServer::default();
349        let stub = ResponseStub::new("POST", "/api/create", json!({"created": true})).status(201);
350
351        let result = server.add_stub(stub).await;
352        assert!(result.is_ok());
353        assert_eq!(server.routes.len(), 1);
354
355        let route = &server.routes[0];
356        assert_eq!(route.response.status, 201);
357    }
358
359    #[tokio::test]
360    async fn test_add_stub_with_headers() {
361        let mut server = MockServer::default();
362        let stub = ResponseStub::new("GET", "/api/test", json!({}))
363            .header("Content-Type", "application/json")
364            .header("X-Custom", "value");
365
366        let result = server.add_stub(stub).await;
367        assert!(result.is_ok());
368
369        let route = &server.routes[0];
370        assert_eq!(
371            route.response.headers.get("Content-Type"),
372            Some(&"application/json".to_string())
373        );
374        assert_eq!(route.response.headers.get("X-Custom"), Some(&"value".to_string()));
375    }
376
377    #[tokio::test]
378    async fn test_stub_response() {
379        let mut server = MockServer::default();
380
381        let result = server.stub_response("GET", "/api/users", json!({"users": []})).await;
382        assert!(result.is_ok());
383        assert_eq!(server.routes.len(), 1);
384
385        let route = &server.routes[0];
386        assert_eq!(route.path, "/api/users");
387        assert_eq!(route.method, "GET");
388    }
389
390    #[tokio::test]
391    async fn test_clear_stubs() {
392        let mut server = MockServer::default();
393
394        server.stub_response("GET", "/api/test1", json!({})).await.unwrap();
395        server.stub_response("POST", "/api/test2", json!({})).await.unwrap();
396        assert_eq!(server.routes.len(), 2);
397
398        let result = server.clear_stubs().await;
399        assert!(result.is_ok());
400        assert_eq!(server.routes.len(), 0);
401    }
402
403    #[tokio::test]
404    async fn test_multiple_stubs() {
405        let mut server = MockServer::default();
406
407        server.stub_response("GET", "/api/users", json!({"users": []})).await.unwrap();
408        server
409            .stub_response("POST", "/api/users", json!({"created": true}))
410            .await
411            .unwrap();
412        server
413            .stub_response("DELETE", "/api/users/1", json!({"deleted": true}))
414            .await
415            .unwrap();
416
417        assert_eq!(server.routes.len(), 3);
418
419        assert_eq!(server.routes[0].method, "GET");
420        assert_eq!(server.routes[1].method, "POST");
421        assert_eq!(server.routes[2].method, "DELETE");
422    }
423
424    #[test]
425    fn test_build_simple_router_empty() {
426        let server = MockServer::default();
427        let router = server.build_simple_router();
428        // Router should be created without panicking
429        assert_eq!(std::mem::size_of_val(&router), std::mem::size_of::<Router>());
430    }
431
432    #[tokio::test]
433    async fn test_build_simple_router_with_routes() {
434        let mut server = MockServer::default();
435        server.stub_response("GET", "/test", json!({"test": true})).await.unwrap();
436        server.stub_response("POST", "/create", json!({"created": true})).await.unwrap();
437
438        let router = server.build_simple_router();
439        // Router should be built with the routes
440        assert_eq!(std::mem::size_of_val(&router), std::mem::size_of::<Router>());
441    }
442
443    #[tokio::test]
444    async fn test_start_server_already_started() {
445        let mut server = MockServer::default();
446        server.port = 0; // Use port 0 for OS assignment
447        server.address = "127.0.0.1:0".parse().unwrap();
448
449        // Start the server
450        let result = server.start().await;
451        assert!(result.is_ok());
452        assert!(server.is_running());
453
454        // Try to start again
455        let result2 = server.start().await;
456        assert!(result2.is_err());
457        match result2 {
458            Err(Error::ServerAlreadyStarted(_)) => (),
459            _ => panic!("Expected ServerAlreadyStarted error"),
460        }
461
462        // Clean up
463        let _ = server.stop().await;
464    }
465
466    #[test]
467    fn test_server_debug_format() {
468        let server = MockServer::default();
469        let debug_str = format!("{server:?}");
470        assert!(debug_str.contains("MockServer"));
471    }
472
473    #[tokio::test]
474    async fn test_route_config_conversion() {
475        let mut server = MockServer::default();
476        let stub = ResponseStub::new("PUT", "/api/update", json!({"updated": true}))
477            .status(200)
478            .header("X-Version", "1.0");
479
480        server.add_stub(stub).await.unwrap();
481
482        let route = &server.routes[0];
483        assert_eq!(route.path, "/api/update");
484        assert_eq!(route.method, "PUT");
485        assert_eq!(route.response.status, 200);
486        assert_eq!(route.response.headers.get("X-Version"), Some(&"1.0".to_string()));
487        assert!(route.response.body.is_some());
488        assert_eq!(route.response.body, Some(json!({"updated": true})));
489    }
490
491    #[tokio::test]
492    async fn test_server_with_different_methods() {
493        let mut server = MockServer::default();
494
495        server.stub_response("GET", "/test", json!({})).await.unwrap();
496        server.stub_response("POST", "/test", json!({})).await.unwrap();
497        server.stub_response("PUT", "/test", json!({})).await.unwrap();
498        server.stub_response("DELETE", "/test", json!({})).await.unwrap();
499        server.stub_response("PATCH", "/test", json!({})).await.unwrap();
500
501        assert_eq!(server.routes.len(), 5);
502
503        // Verify all methods are different
504        let methods: Vec<_> = server.routes.iter().map(|r| r.method.as_str()).collect();
505        assert!(methods.contains(&"GET"));
506        assert!(methods.contains(&"POST"));
507        assert!(methods.contains(&"PUT"));
508        assert!(methods.contains(&"DELETE"));
509        assert!(methods.contains(&"PATCH"));
510    }
511
512    #[tokio::test]
513    async fn test_server_url_format() {
514        let mut server = MockServer::default();
515        server.port = 3000;
516        server.address = "127.0.0.1:3000".parse().unwrap();
517
518        let url = server.url();
519        assert_eq!(url, "http://127.0.0.1:3000");
520        assert!(url.starts_with("http://"));
521    }
522
523    #[tokio::test]
524    async fn test_server_with_ipv6_address() {
525        let mut server = MockServer::default();
526        server.port = 8080;
527        server.address = "[::1]:8080".parse().unwrap();
528
529        let url = server.url();
530        assert_eq!(url, "http://[::1]:8080");
531    }
532}