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