Skip to main content

mockforge_sdk/
server.rs

1//! Mock server implementation
2#![allow(clippy::missing_errors_doc, clippy::too_many_lines)]
3
4use crate::builder::MockServerBuilder;
5use crate::stub::ResponseStub;
6use crate::{Error, Result};
7use axum::Router;
8use mockforge_core::config::{RouteConfig, RouteResponseConfig};
9use mockforge_core::{Config, ServerConfig};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::net::SocketAddr;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15use tokio::task::JoinHandle;
16
17/// A stored stub configuration for runtime matching
18#[derive(Debug, Clone)]
19struct StoredStub {
20    method: String,
21    path: String,
22    status: u16,
23    headers: HashMap<String, String>,
24    body: Value,
25}
26
27/// Shared stub store for runtime stub management
28type StubStore = Arc<RwLock<Vec<StoredStub>>>;
29
30/// A mock server that can be embedded in tests
31///
32/// The mock server supports dynamically adding stubs at runtime after the server
33/// has started. Stubs added via `stub_response()` or `add_stub()` will be served
34/// by a fallback handler that matches requests against the stub store.
35pub struct MockServer {
36    port: u16,
37    address: SocketAddr,
38    config: ServerConfig,
39    server_handle: Option<JoinHandle<()>>,
40    shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
41    routes: Vec<RouteConfig>,
42    /// Shared stub store for runtime updates
43    stub_store: StubStore,
44}
45
46impl MockServer {
47    /// Create a new mock server builder
48    #[must_use]
49    #[allow(clippy::new_ret_no_self)]
50    pub const fn new() -> MockServerBuilder {
51        MockServerBuilder::new()
52    }
53
54    /// Create a mock server from configuration
55    pub(crate) fn from_config(server_config: ServerConfig, _core_config: Config) -> Result<Self> {
56        let port = server_config.http.port;
57        let host = server_config.http.host.clone();
58
59        let address: SocketAddr = format!("{host}:{port}")
60            .parse()
61            .map_err(|e| Error::InvalidConfig(format!("Invalid address: {e}")))?;
62
63        Ok(Self {
64            port,
65            address,
66            config: server_config,
67            server_handle: None,
68            shutdown_tx: None,
69            routes: Vec::new(),
70            stub_store: Arc::new(RwLock::new(Vec::new())),
71        })
72    }
73
74    /// Start the mock server
75    ///
76    /// # Panics
77    ///
78    /// Panics if the internal axum server task encounters an error.
79    pub async fn start(&mut self) -> Result<()> {
80        if self.server_handle.is_some() {
81            return Err(Error::ServerAlreadyStarted(self.port));
82        }
83
84        // Build the router with the shared stub store
85        let router = self.build_simple_router(self.stub_store.clone());
86
87        // Create shutdown channel
88        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
89        self.shutdown_tx = Some(shutdown_tx);
90
91        // Bind the listener BEFORE spawning so we can get the actual address
92        // This is important for port 0 (auto-assign) to work correctly
93        let listener = tokio::net::TcpListener::bind(self.address)
94            .await
95            .map_err(|e| Error::General(format!("Failed to bind to {}: {e}", self.address)))?;
96
97        // Get the actual bound address (important when using port 0)
98        let actual_address = listener
99            .local_addr()
100            .map_err(|e| Error::General(format!("Failed to get local address: {e}")))?;
101
102        // Update our address and port with the actual bound values
103        self.address = actual_address;
104        self.port = actual_address.port();
105
106        tracing::info!("MockForge SDK server listening on {}", actual_address);
107
108        // Spawn the server with the already-bound listener
109        let server_handle = tokio::spawn(async move {
110            axum::serve(listener, router)
111                .with_graceful_shutdown(async move {
112                    let _ = shutdown_rx.await;
113                })
114                .await
115                .expect("Server error");
116        });
117
118        self.server_handle = Some(server_handle);
119
120        // Wait for the server to be ready by polling health
121        self.wait_for_ready().await?;
122
123        Ok(())
124    }
125
126    /// Wait for the server to be ready
127    async fn wait_for_ready(&self) -> Result<()> {
128        let max_attempts = 50;
129        let delay = tokio::time::Duration::from_millis(100);
130
131        for attempt in 0..max_attempts {
132            // Try to connect to the server
133            let client = reqwest::Client::builder()
134                .timeout(tokio::time::Duration::from_millis(100))
135                .build()
136                .map_err(|e| Error::General(format!("Failed to create HTTP client: {e}")))?;
137
138            match client.get(format!("{}/health", self.url())).send().await {
139                Ok(response) if response.status().is_success() => return Ok(()),
140                _ => {
141                    if attempt < max_attempts - 1 {
142                        tokio::time::sleep(delay).await;
143                    }
144                }
145            }
146        }
147
148        let timeout_ms = u32::try_from(delay.as_millis()).unwrap_or(u32::MAX);
149        Err(Error::General(format!(
150            "Server failed to become ready within {}ms",
151            max_attempts * timeout_ms
152        )))
153    }
154
155    /// Build a simple router from stored routes
156    fn build_simple_router(&self, stub_store: StubStore) -> Router {
157        use axum::extract::{Path, Request, State};
158        use axum::http::StatusCode;
159        use axum::routing::{delete, get, post, put};
160        use axum::{response::IntoResponse, Json};
161
162        // Shared state for admin API (separate from stub store)
163        type MockStore = Arc<RwLock<HashMap<String, Value>>>;
164        let mock_store: MockStore = Arc::new(RwLock::new(HashMap::new()));
165
166        // Admin API handlers
167        let store_for_list = mock_store.clone();
168        let list_mocks = move || {
169            let store = store_for_list.clone();
170            async move {
171                let items: Vec<Value> = store.read().await.values().cloned().collect();
172                let total = items.len();
173                Json(serde_json::json!({
174                    "mocks": items,
175                    "total": total,
176                    "enabled": total  // All mocks are enabled by default
177                }))
178            }
179        };
180
181        let store_for_create = mock_store.clone();
182        let create_mock = move |Json(mut mock): Json<Value>| {
183            let store = store_for_create.clone();
184            async move {
185                let id = mock
186                    .get("id")
187                    .and_then(|v| v.as_str())
188                    .filter(|s| !s.is_empty())
189                    .map_or_else(|| uuid::Uuid::new_v4().to_string(), String::from);
190                mock["id"] = serde_json::json!(id);
191                store.write().await.insert(id, mock.clone());
192                (StatusCode::CREATED, Json(mock))
193            }
194        };
195
196        let store_for_get = mock_store.clone();
197        let get_mock = move |Path(id): Path<String>| {
198            let store = store_for_get.clone();
199            async move {
200                store.read().await.get(&id).map_or_else(
201                    || StatusCode::NOT_FOUND.into_response(),
202                    |mock| (StatusCode::OK, Json(mock.clone())).into_response(),
203                )
204            }
205        };
206
207        let store_for_update = mock_store.clone();
208        let update_mock = move |Path(id): Path<String>, Json(mut mock): Json<Value>| {
209            let store = store_for_update.clone();
210            async move {
211                mock["id"] = serde_json::json!(id.clone());
212                store.write().await.insert(id, mock.clone());
213                Json(mock)
214            }
215        };
216
217        let store_for_delete = mock_store.clone();
218        let delete_mock = move |Path(id): Path<String>| {
219            let store = store_for_delete.clone();
220            async move {
221                store.write().await.remove(&id);
222                StatusCode::NO_CONTENT
223            }
224        };
225
226        let get_stats = move || {
227            let store = mock_store.clone();
228            async move {
229                let count = store.read().await.len();
230                Json(serde_json::json!({
231                    "uptime_seconds": 1,  // Minimum uptime for tests
232                    "total_requests": 0,
233                    "active_mocks": count,
234                    "enabled_mocks": count,
235                    "registered_routes": count
236                }))
237            }
238        };
239
240        // Fallback handler that matches against dynamically added stubs
241        let fallback_handler = move |request: Request| {
242            let stub_store = stub_store.clone();
243            async move {
244                let method = request.method().to_string();
245                let path = request.uri().path().to_string();
246
247                // Search for a matching stub, cloning out before dropping the lock
248                let matching_stub = stub_store
249                    .read()
250                    .await
251                    .iter()
252                    .find(|s| s.method.eq_ignore_ascii_case(&method) && s.path == path)
253                    .cloned();
254
255                if let Some(stub) = matching_stub {
256                    let mut response = Json(stub.body).into_response();
257                    *response.status_mut() =
258                        StatusCode::from_u16(stub.status).unwrap_or(StatusCode::OK);
259
260                    for (key, value) in &stub.headers {
261                        if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes())
262                        {
263                            if let Ok(header_value) = axum::http::HeaderValue::from_str(value) {
264                                response.headers_mut().insert(header_name, header_value);
265                            }
266                        }
267                    }
268
269                    return response;
270                }
271
272                // No matching stub found
273                StatusCode::NOT_FOUND.into_response()
274            }
275        };
276
277        // Start with health and admin API endpoints
278        let mut router = Router::new()
279            .route("/health", get(|| async { (StatusCode::OK, "OK") }))
280            .route("/api/mocks", get(list_mocks).post(create_mock))
281            .route("/api/mocks/{id}", get(get_mock).put(update_mock).delete(delete_mock))
282            .route("/api/stats", get(get_stats));
283
284        // Add pre-defined routes (added before server start)
285        for route_config in &self.routes {
286            let status = route_config.response.status;
287            let body = route_config.response.body.clone();
288            let headers = route_config.response.headers.clone();
289
290            let handler = move || {
291                let body = body.clone();
292                let headers = headers.clone();
293                async move {
294                    let mut response = Json(body).into_response();
295                    *response.status_mut() = StatusCode::from_u16(status).unwrap();
296
297                    for (key, value) in headers {
298                        if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes())
299                        {
300                            if let Ok(header_value) = axum::http::HeaderValue::from_str(&value) {
301                                response.headers_mut().insert(header_name, header_value);
302                            }
303                        }
304                    }
305
306                    response
307                }
308            };
309
310            let path = &route_config.path;
311
312            router = match route_config.method.to_uppercase().as_str() {
313                "GET" => router.route(path, get(handler)),
314                "POST" => router.route(path, post(handler)),
315                "PUT" => router.route(path, put(handler)),
316                "DELETE" => router.route(path, delete(handler)),
317                _ => router,
318            };
319        }
320
321        // Add fallback for dynamically added stubs
322        router.fallback(fallback_handler)
323    }
324
325    /// Stop the mock server
326    pub async fn stop(mut self) -> Result<()> {
327        if let Some(shutdown_tx) = self.shutdown_tx.take() {
328            let _ = shutdown_tx.send(());
329        }
330
331        if let Some(handle) = self.server_handle.take() {
332            let _ = handle.await;
333        }
334
335        Ok(())
336    }
337
338    /// Stub a response for a given method and path
339    pub async fn stub_response(
340        &mut self,
341        method: impl Into<String>,
342        path: impl Into<String>,
343        body: Value,
344    ) -> Result<()> {
345        let stub = ResponseStub::new(method, path, body);
346        self.add_stub(stub).await
347    }
348
349    /// Add a response stub
350    ///
351    /// Stubs can be added before or after the server starts.
352    /// Stubs added after start are served via a fallback handler.
353    pub async fn add_stub(&mut self, stub: ResponseStub) -> Result<()> {
354        // Add to the shared stub store (works at runtime)
355        let stored_stub = StoredStub {
356            method: stub.method.clone(),
357            path: stub.path.clone(),
358            status: stub.status,
359            headers: stub.headers.clone(),
360            body: stub.body.clone(),
361        };
362        self.stub_store.write().await.push(stored_stub);
363
364        // Also add to routes for pre-start configuration
365        let route_config = RouteConfig {
366            path: stub.path.clone(),
367            method: stub.method,
368            request: None,
369            response: RouteResponseConfig {
370                status: stub.status,
371                headers: stub.headers,
372                body: Some(stub.body),
373            },
374            fault_injection: None,
375            latency: None,
376        };
377
378        self.routes.push(route_config);
379
380        Ok(())
381    }
382
383    /// Remove all stubs
384    pub async fn clear_stubs(&mut self) -> Result<()> {
385        self.routes.clear();
386        self.stub_store.write().await.clear();
387        Ok(())
388    }
389
390    /// Get the server port
391    #[must_use]
392    pub const fn port(&self) -> u16 {
393        self.port
394    }
395
396    /// Get the server base URL
397    ///
398    /// Note: If the server is bound to `0.0.0.0` (all interfaces),
399    /// this returns `127.0.0.1` as the host for client connections.
400    #[must_use]
401    pub fn url(&self) -> String {
402        // 0.0.0.0 means "bind to all interfaces" but isn't a valid connection target
403        // Convert to localhost for client connections
404        if self.address.ip().is_unspecified() {
405            format!("http://127.0.0.1:{}", self.address.port())
406        } else {
407            format!("http://{}", self.address)
408        }
409    }
410
411    /// Check if the server is running
412    #[must_use]
413    pub const fn is_running(&self) -> bool {
414        self.server_handle.is_some()
415    }
416}
417
418impl Default for MockServer {
419    fn default() -> Self {
420        Self {
421            port: 0,
422            address: "127.0.0.1:0".parse().unwrap(),
423            config: ServerConfig::default(),
424            server_handle: None,
425            shutdown_tx: None,
426            routes: Vec::new(),
427            stub_store: Arc::new(RwLock::new(Vec::new())),
428        }
429    }
430}
431
432impl std::fmt::Debug for MockServer {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_struct("MockServer")
435            .field("port", &self.port)
436            .field("address", &self.address)
437            .field("is_running", &self.server_handle.is_some())
438            .field("routes_count", &self.routes.len())
439            .finish_non_exhaustive()
440    }
441}
442
443// Implement Drop to ensure server is stopped
444impl Drop for MockServer {
445    fn drop(&mut self) {
446        if let Some(shutdown_tx) = self.shutdown_tx.take() {
447            let _ = shutdown_tx.send(());
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use serde_json::json;
456    use std::mem::{size_of, size_of_val};
457
458    #[test]
459    fn test_mock_server_new() {
460        let builder = MockServer::new();
461        // Should return a MockServerBuilder
462        assert_eq!(size_of_val(&builder), size_of::<MockServerBuilder>());
463    }
464
465    #[test]
466    fn test_mock_server_default() {
467        let server = MockServer::default();
468        assert_eq!(server.port, 0);
469        assert!(!server.is_running());
470        assert!(server.routes.is_empty());
471    }
472
473    #[test]
474    fn test_mock_server_port() {
475        let server = MockServer::default();
476        assert_eq!(server.port(), 0);
477    }
478
479    #[test]
480    fn test_mock_server_url() {
481        let mut server = MockServer::default();
482        server.port = 8080;
483        server.address = "127.0.0.1:8080".parse().unwrap();
484        assert_eq!(server.url(), "http://127.0.0.1:8080");
485    }
486
487    #[test]
488    fn test_mock_server_is_running_false() {
489        let server = MockServer::default();
490        assert!(!server.is_running());
491    }
492
493    #[test]
494    fn test_from_config_valid() {
495        let server_config = ServerConfig::default();
496        let core_config = Config::default();
497
498        let result = MockServer::from_config(server_config, core_config);
499        assert!(result.is_ok());
500
501        let server = result.unwrap();
502        assert!(!server.is_running());
503        assert!(server.routes.is_empty());
504    }
505
506    #[test]
507    fn test_from_config_invalid_address() {
508        let mut server_config = ServerConfig::default();
509        server_config.http.host = "invalid host with spaces".to_string();
510        let core_config = Config::default();
511
512        let result = MockServer::from_config(server_config, core_config);
513        assert!(result.is_err());
514        match result {
515            Err(Error::InvalidConfig(msg)) => {
516                assert!(msg.contains("Invalid address"));
517            }
518            _ => panic!("Expected InvalidConfig error"),
519        }
520    }
521
522    #[tokio::test]
523    async fn test_add_stub() {
524        let mut server = MockServer::default();
525        let stub = ResponseStub::new("GET", "/api/test", json!({"test": true}));
526
527        let result = server.add_stub(stub.clone()).await;
528        assert!(result.is_ok());
529        assert_eq!(server.routes.len(), 1);
530
531        let route = &server.routes[0];
532        assert_eq!(route.path, "/api/test");
533        assert_eq!(route.method, "GET");
534        assert_eq!(route.response.status, 200);
535    }
536
537    #[tokio::test]
538    async fn test_add_stub_with_custom_status() {
539        let mut server = MockServer::default();
540        let stub = ResponseStub::new("POST", "/api/create", json!({"created": true})).status(201);
541
542        let result = server.add_stub(stub).await;
543        assert!(result.is_ok());
544        assert_eq!(server.routes.len(), 1);
545
546        let route = &server.routes[0];
547        assert_eq!(route.response.status, 201);
548    }
549
550    #[tokio::test]
551    async fn test_add_stub_with_headers() {
552        let mut server = MockServer::default();
553        let stub = ResponseStub::new("GET", "/api/test", json!({}))
554            .header("Content-Type", "application/json")
555            .header("X-Custom", "value");
556
557        let result = server.add_stub(stub).await;
558        assert!(result.is_ok());
559
560        let route = &server.routes[0];
561        assert_eq!(
562            route.response.headers.get("Content-Type"),
563            Some(&"application/json".to_string())
564        );
565        assert_eq!(route.response.headers.get("X-Custom"), Some(&"value".to_string()));
566    }
567
568    #[tokio::test]
569    async fn test_stub_response() {
570        let mut server = MockServer::default();
571
572        let result = server.stub_response("GET", "/api/users", json!({"users": []})).await;
573        assert!(result.is_ok());
574        assert_eq!(server.routes.len(), 1);
575
576        let route = &server.routes[0];
577        assert_eq!(route.path, "/api/users");
578        assert_eq!(route.method, "GET");
579    }
580
581    #[tokio::test]
582    async fn test_clear_stubs() {
583        let mut server = MockServer::default();
584
585        server.stub_response("GET", "/api/test1", json!({})).await.unwrap();
586        server.stub_response("POST", "/api/test2", json!({})).await.unwrap();
587        assert_eq!(server.routes.len(), 2);
588
589        let result = server.clear_stubs().await;
590        assert!(result.is_ok());
591        assert_eq!(server.routes.len(), 0);
592    }
593
594    #[tokio::test]
595    async fn test_multiple_stubs() {
596        let mut server = MockServer::default();
597
598        server.stub_response("GET", "/api/users", json!({"users": []})).await.unwrap();
599        server
600            .stub_response("POST", "/api/users", json!({"created": true}))
601            .await
602            .unwrap();
603        server
604            .stub_response("DELETE", "/api/users/1", json!({"deleted": true}))
605            .await
606            .unwrap();
607
608        assert_eq!(server.routes.len(), 3);
609
610        assert_eq!(server.routes[0].method, "GET");
611        assert_eq!(server.routes[1].method, "POST");
612        assert_eq!(server.routes[2].method, "DELETE");
613    }
614
615    #[test]
616    fn test_build_simple_router_empty() {
617        let server = MockServer::default();
618        let router = server.build_simple_router(server.stub_store.clone());
619        // Router should be created without panicking
620        assert_eq!(size_of_val(&router), size_of::<Router>());
621    }
622
623    #[tokio::test]
624    async fn test_build_simple_router_with_routes() {
625        let mut server = MockServer::default();
626        server.stub_response("GET", "/test", json!({"test": true})).await.unwrap();
627        server.stub_response("POST", "/create", json!({"created": true})).await.unwrap();
628
629        let router = server.build_simple_router(server.stub_store.clone());
630        // Router should be built with the routes
631        assert_eq!(size_of_val(&router), size_of::<Router>());
632    }
633
634    #[tokio::test]
635    async fn test_start_server_already_started() {
636        // Use port 0 for OS assignment - the server now properly updates
637        // self.address after binding
638        let mut server = MockServer::default();
639        server.port = 0;
640        server.address = "127.0.0.1:0".parse().unwrap();
641
642        // Start the server
643        let result = server.start().await;
644        assert!(result.is_ok(), "Failed to start server: {:?}", result.err());
645        assert!(server.is_running());
646
647        // Verify the port was updated from 0 to an actual port
648        assert_ne!(server.port, 0, "Port should have been updated from 0");
649
650        // Try to start again
651        let result2 = server.start().await;
652        assert!(result2.is_err());
653        match result2 {
654            Err(Error::ServerAlreadyStarted(_)) => (),
655            _ => panic!("Expected ServerAlreadyStarted error"),
656        }
657
658        // Clean up
659        let _ = Box::pin(server.stop()).await;
660    }
661
662    #[test]
663    fn test_server_debug_format() {
664        let server = MockServer::default();
665        let debug_str = format!("{server:?}");
666        assert!(debug_str.contains("MockServer"));
667    }
668
669    #[tokio::test]
670    async fn test_route_config_conversion() {
671        let mut server = MockServer::default();
672        let stub = ResponseStub::new("PUT", "/api/update", json!({"updated": true}))
673            .status(200)
674            .header("X-Version", "1.0");
675
676        server.add_stub(stub).await.unwrap();
677
678        let route = &server.routes[0];
679        assert_eq!(route.path, "/api/update");
680        assert_eq!(route.method, "PUT");
681        assert_eq!(route.response.status, 200);
682        assert_eq!(route.response.headers.get("X-Version"), Some(&"1.0".to_string()));
683        assert!(route.response.body.is_some());
684        assert_eq!(route.response.body, Some(json!({"updated": true})));
685    }
686
687    #[tokio::test]
688    async fn test_server_with_different_methods() {
689        let mut server = MockServer::default();
690
691        server.stub_response("GET", "/test", json!({})).await.unwrap();
692        server.stub_response("POST", "/test", json!({})).await.unwrap();
693        server.stub_response("PUT", "/test", json!({})).await.unwrap();
694        server.stub_response("DELETE", "/test", json!({})).await.unwrap();
695        server.stub_response("PATCH", "/test", json!({})).await.unwrap();
696
697        assert_eq!(server.routes.len(), 5);
698
699        // Verify all methods are different
700        let methods: Vec<_> = server.routes.iter().map(|r| r.method.as_str()).collect();
701        assert!(methods.contains(&"GET"));
702        assert!(methods.contains(&"POST"));
703        assert!(methods.contains(&"PUT"));
704        assert!(methods.contains(&"DELETE"));
705        assert!(methods.contains(&"PATCH"));
706    }
707
708    #[tokio::test]
709    async fn test_server_url_format() {
710        let mut server = MockServer::default();
711        server.port = 3000;
712        server.address = "127.0.0.1:3000".parse().unwrap();
713
714        let url = server.url();
715        assert_eq!(url, "http://127.0.0.1:3000");
716        assert!(url.starts_with("http://"));
717    }
718
719    #[tokio::test]
720    async fn test_server_with_ipv6_address() {
721        let mut server = MockServer::default();
722        server.port = 8080;
723        server.address = "[::1]:8080".parse().unwrap();
724
725        let url = server.url();
726        assert_eq!(url, "http://[::1]:8080");
727    }
728}