1#![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#[derive(Debug, Clone)]
19struct StoredStub {
20 method: String,
21 path: String,
22 status: u16,
23 headers: HashMap<String, String>,
24 body: Value,
25}
26
27type StubStore = Arc<RwLock<Vec<StoredStub>>>;
29
30pub struct MockServer {
36 port: u16,
37 address: SocketAddr,
38 #[allow(dead_code)]
39 config: ServerConfig,
40 server_handle: Option<JoinHandle<()>>,
41 shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
42 routes: Vec<RouteConfig>,
43 stub_store: StubStore,
45}
46
47impl MockServer {
48 #[must_use]
50 #[allow(clippy::new_ret_no_self)]
51 pub const fn new() -> MockServerBuilder {
52 MockServerBuilder::new()
53 }
54
55 pub(crate) fn from_config(server_config: ServerConfig, _core_config: Config) -> 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 pub async fn start(&mut self) -> Result<()> {
81 if self.server_handle.is_some() {
82 return Err(Error::ServerAlreadyStarted(self.port));
83 }
84
85 let router = self.build_simple_router(self.stub_store.clone());
87
88 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
90 self.shutdown_tx = Some(shutdown_tx);
91
92 let listener = tokio::net::TcpListener::bind(self.address)
95 .await
96 .map_err(|e| Error::General(format!("Failed to bind to {}: {e}", self.address)))?;
97
98 let actual_address = listener
100 .local_addr()
101 .map_err(|e| Error::General(format!("Failed to get local address: {e}")))?;
102
103 self.address = actual_address;
105 self.port = actual_address.port();
106
107 tracing::info!("MockForge SDK server listening on {}", actual_address);
108
109 let server_handle = tokio::spawn(async move {
111 axum::serve(listener, router)
112 .with_graceful_shutdown(async move {
113 let _ = shutdown_rx.await;
114 })
115 .await
116 .expect("Server error");
117 });
118
119 self.server_handle = Some(server_handle);
120
121 self.wait_for_ready().await?;
123
124 Ok(())
125 }
126
127 async fn wait_for_ready(&self) -> Result<()> {
129 let max_attempts = 50;
130 let delay = tokio::time::Duration::from_millis(100);
131
132 for attempt in 0..max_attempts {
133 let client = reqwest::Client::builder()
135 .timeout(tokio::time::Duration::from_millis(100))
136 .build()
137 .map_err(|e| Error::General(format!("Failed to create HTTP client: {e}")))?;
138
139 match client.get(format!("{}/health", self.url())).send().await {
140 Ok(response) if response.status().is_success() => return Ok(()),
141 _ => {
142 if attempt < max_attempts - 1 {
143 tokio::time::sleep(delay).await;
144 }
145 }
146 }
147 }
148
149 let timeout_ms = u32::try_from(delay.as_millis()).unwrap_or(u32::MAX);
150 Err(Error::General(format!(
151 "Server failed to become ready within {}ms",
152 max_attempts * timeout_ms
153 )))
154 }
155
156 fn build_simple_router(&self, stub_store: StubStore) -> Router {
158 use axum::extract::{Path, Request};
159 use axum::http::StatusCode;
160 use axum::routing::{delete, get, post, put};
161 use axum::{response::IntoResponse, Json};
162
163 type MockStore = Arc<RwLock<HashMap<String, Value>>>;
165 let mock_store: MockStore = Arc::new(RwLock::new(HashMap::new()));
166
167 let store_for_list = mock_store.clone();
169 let list_mocks = move || {
170 let store = store_for_list.clone();
171 async move {
172 let items: Vec<Value> = store.read().await.values().cloned().collect();
173 let total = items.len();
174 Json(serde_json::json!({
175 "mocks": items,
176 "total": total,
177 "enabled": total }))
179 }
180 };
181
182 let store_for_create = mock_store.clone();
183 let create_mock = move |Json(mut mock): Json<Value>| {
184 let store = store_for_create.clone();
185 async move {
186 let id = mock
187 .get("id")
188 .and_then(|v| v.as_str())
189 .filter(|s| !s.is_empty())
190 .map_or_else(|| uuid::Uuid::new_v4().to_string(), String::from);
191 mock["id"] = serde_json::json!(id);
192 store.write().await.insert(id, mock.clone());
193 (StatusCode::CREATED, Json(mock))
194 }
195 };
196
197 let store_for_get = mock_store.clone();
198 let get_mock = move |Path(id): Path<String>| {
199 let store = store_for_get.clone();
200 async move {
201 store.read().await.get(&id).map_or_else(
202 || StatusCode::NOT_FOUND.into_response(),
203 |mock| (StatusCode::OK, Json(mock.clone())).into_response(),
204 )
205 }
206 };
207
208 let store_for_update = mock_store.clone();
209 let update_mock = move |Path(id): Path<String>, Json(mut mock): Json<Value>| {
210 let store = store_for_update.clone();
211 async move {
212 mock["id"] = serde_json::json!(id.clone());
213 store.write().await.insert(id, mock.clone());
214 Json(mock)
215 }
216 };
217
218 let store_for_delete = mock_store.clone();
219 let delete_mock = move |Path(id): Path<String>| {
220 let store = store_for_delete.clone();
221 async move {
222 store.write().await.remove(&id);
223 StatusCode::NO_CONTENT
224 }
225 };
226
227 let get_stats = move || {
228 let store = mock_store.clone();
229 async move {
230 let count = store.read().await.len();
231 Json(serde_json::json!({
232 "uptime_seconds": 1, "total_requests": 0,
234 "active_mocks": count,
235 "enabled_mocks": count,
236 "registered_routes": count
237 }))
238 }
239 };
240
241 let fallback_handler = move |request: Request| {
243 let stub_store = stub_store.clone();
244 async move {
245 let method = request.method().to_string();
246 let path = request.uri().path().to_string();
247
248 let matching_stub = stub_store
250 .read()
251 .await
252 .iter()
253 .find(|s| s.method.eq_ignore_ascii_case(&method) && s.path == path)
254 .cloned();
255
256 if let Some(stub) = matching_stub {
257 let mut response = Json(stub.body).into_response();
258 *response.status_mut() =
259 StatusCode::from_u16(stub.status).unwrap_or(StatusCode::OK);
260
261 for (key, value) in &stub.headers {
262 if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes())
263 {
264 if let Ok(header_value) = axum::http::HeaderValue::from_str(value) {
265 response.headers_mut().insert(header_name, header_value);
266 }
267 }
268 }
269
270 return response;
271 }
272
273 StatusCode::NOT_FOUND.into_response()
275 }
276 };
277
278 let mut router = Router::new()
280 .route("/health", get(|| async { (StatusCode::OK, "OK") }))
281 .route("/api/mocks", get(list_mocks).post(create_mock))
282 .route("/api/mocks/{id}", get(get_mock).put(update_mock).delete(delete_mock))
283 .route("/api/stats", get(get_stats));
284
285 for route_config in &self.routes {
287 let status = route_config.response.status;
288 let body = route_config.response.body.clone();
289 let headers = route_config.response.headers.clone();
290
291 let handler = move || {
292 let body = body.clone();
293 let headers = headers.clone();
294 async move {
295 let mut response = Json(body).into_response();
296 *response.status_mut() = StatusCode::from_u16(status)
297 .expect("status code validated at stub registration");
298
299 for (key, value) in headers {
300 if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes())
301 {
302 if let Ok(header_value) = axum::http::HeaderValue::from_str(&value) {
303 response.headers_mut().insert(header_name, header_value);
304 }
305 }
306 }
307
308 response
309 }
310 };
311
312 let path = &route_config.path;
313
314 router = match route_config.method.to_uppercase().as_str() {
315 "GET" => router.route(path, get(handler)),
316 "POST" => router.route(path, post(handler)),
317 "PUT" => router.route(path, put(handler)),
318 "DELETE" => router.route(path, delete(handler)),
319 _ => router,
320 };
321 }
322
323 router.fallback(fallback_handler)
325 }
326
327 pub async fn stop(mut self) -> Result<()> {
329 if let Some(shutdown_tx) = self.shutdown_tx.take() {
330 let _ = shutdown_tx.send(());
331 }
332
333 if let Some(handle) = self.server_handle.take() {
334 let _ = handle.await;
335 }
336
337 Ok(())
338 }
339
340 pub async fn stub_response(
342 &mut self,
343 method: impl Into<String>,
344 path: impl Into<String>,
345 body: Value,
346 ) -> Result<()> {
347 let stub = ResponseStub::new(method, path, body);
348 self.add_stub(stub).await
349 }
350
351 pub async fn add_stub(&mut self, stub: ResponseStub) -> Result<()> {
356 let stored_stub = StoredStub {
358 method: stub.method.clone(),
359 path: stub.path.clone(),
360 status: stub.status,
361 headers: stub.headers.clone(),
362 body: stub.body.clone(),
363 };
364 self.stub_store.write().await.push(stored_stub);
365
366 let route_config = RouteConfig {
368 path: stub.path.clone(),
369 method: stub.method,
370 request: None,
371 response: RouteResponseConfig {
372 status: stub.status,
373 headers: stub.headers,
374 body: Some(stub.body),
375 },
376 fault_injection: None,
377 latency: None,
378 };
379
380 self.routes.push(route_config);
381
382 Ok(())
383 }
384
385 pub async fn clear_stubs(&mut self) -> Result<()> {
387 self.routes.clear();
388 self.stub_store.write().await.clear();
389 Ok(())
390 }
391
392 #[must_use]
394 pub const fn port(&self) -> u16 {
395 self.port
396 }
397
398 #[must_use]
403 pub fn url(&self) -> String {
404 if self.address.ip().is_unspecified() {
407 format!("http://127.0.0.1:{}", self.address.port())
408 } else {
409 format!("http://{}", self.address)
410 }
411 }
412
413 #[must_use]
415 pub const fn is_running(&self) -> bool {
416 self.server_handle.is_some()
417 }
418}
419
420impl Default for MockServer {
421 fn default() -> Self {
422 Self {
423 port: 0,
424 address: "127.0.0.1:0"
425 .parse()
426 .expect("hardcoded loopback literal parses as SocketAddr"),
427 config: ServerConfig::default(),
428 server_handle: None,
429 shutdown_tx: None,
430 routes: Vec::new(),
431 stub_store: Arc::new(RwLock::new(Vec::new())),
432 }
433 }
434}
435
436impl std::fmt::Debug for MockServer {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 f.debug_struct("MockServer")
439 .field("port", &self.port)
440 .field("address", &self.address)
441 .field("is_running", &self.server_handle.is_some())
442 .field("routes_count", &self.routes.len())
443 .finish_non_exhaustive()
444 }
445}
446
447impl Drop for MockServer {
449 fn drop(&mut self) {
450 if let Some(shutdown_tx) = self.shutdown_tx.take() {
451 let _ = shutdown_tx.send(());
452 }
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use serde_json::json;
460 use std::mem::{size_of, size_of_val};
461
462 #[test]
463 fn test_mock_server_new() {
464 let builder = MockServer::new();
465 assert_eq!(size_of_val(&builder), size_of::<MockServerBuilder>());
467 }
468
469 #[test]
470 fn test_mock_server_default() {
471 let server = MockServer::default();
472 assert_eq!(server.port, 0);
473 assert!(!server.is_running());
474 assert!(server.routes.is_empty());
475 }
476
477 #[test]
478 fn test_mock_server_port() {
479 let server = MockServer::default();
480 assert_eq!(server.port(), 0);
481 }
482
483 #[test]
484 fn test_mock_server_url() {
485 let mut server = MockServer::default();
486 server.port = 8080;
487 server.address = "127.0.0.1:8080".parse().unwrap();
488 assert_eq!(server.url(), "http://127.0.0.1:8080");
489 }
490
491 #[test]
492 fn test_mock_server_is_running_false() {
493 let server = MockServer::default();
494 assert!(!server.is_running());
495 }
496
497 #[test]
498 fn test_from_config_valid() {
499 let server_config = ServerConfig::default();
500 let core_config = Config::default();
501
502 let result = MockServer::from_config(server_config, core_config);
503 assert!(result.is_ok());
504
505 let server = result.unwrap();
506 assert!(!server.is_running());
507 assert!(server.routes.is_empty());
508 }
509
510 #[test]
511 fn test_from_config_invalid_address() {
512 let mut server_config = ServerConfig::default();
513 server_config.http.host = "invalid host with spaces".to_string();
514 let core_config = Config::default();
515
516 let result = MockServer::from_config(server_config, core_config);
517 assert!(result.is_err());
518 match result {
519 Err(Error::InvalidConfig(msg)) => {
520 assert!(msg.contains("Invalid address"));
521 }
522 _ => panic!("Expected InvalidConfig error"),
523 }
524 }
525
526 #[tokio::test]
527 async fn test_add_stub() {
528 let mut server = MockServer::default();
529 let stub = ResponseStub::new("GET", "/api/test", json!({"test": true}));
530
531 let result = server.add_stub(stub.clone()).await;
532 assert!(result.is_ok());
533 assert_eq!(server.routes.len(), 1);
534
535 let route = &server.routes[0];
536 assert_eq!(route.path, "/api/test");
537 assert_eq!(route.method, "GET");
538 assert_eq!(route.response.status, 200);
539 }
540
541 #[tokio::test]
542 async fn test_add_stub_with_custom_status() {
543 let mut server = MockServer::default();
544 let stub = ResponseStub::new("POST", "/api/create", json!({"created": true})).status(201);
545
546 let result = server.add_stub(stub).await;
547 assert!(result.is_ok());
548 assert_eq!(server.routes.len(), 1);
549
550 let route = &server.routes[0];
551 assert_eq!(route.response.status, 201);
552 }
553
554 #[tokio::test]
555 async fn test_add_stub_with_headers() {
556 let mut server = MockServer::default();
557 let stub = ResponseStub::new("GET", "/api/test", json!({}))
558 .header("Content-Type", "application/json")
559 .header("X-Custom", "value");
560
561 let result = server.add_stub(stub).await;
562 assert!(result.is_ok());
563
564 let route = &server.routes[0];
565 assert_eq!(
566 route.response.headers.get("Content-Type"),
567 Some(&"application/json".to_string())
568 );
569 assert_eq!(route.response.headers.get("X-Custom"), Some(&"value".to_string()));
570 }
571
572 #[tokio::test]
573 async fn test_stub_response() {
574 let mut server = MockServer::default();
575
576 let result = server.stub_response("GET", "/api/users", json!({"users": []})).await;
577 assert!(result.is_ok());
578 assert_eq!(server.routes.len(), 1);
579
580 let route = &server.routes[0];
581 assert_eq!(route.path, "/api/users");
582 assert_eq!(route.method, "GET");
583 }
584
585 #[tokio::test]
586 async fn test_clear_stubs() {
587 let mut server = MockServer::default();
588
589 server.stub_response("GET", "/api/test1", json!({})).await.unwrap();
590 server.stub_response("POST", "/api/test2", json!({})).await.unwrap();
591 assert_eq!(server.routes.len(), 2);
592
593 let result = server.clear_stubs().await;
594 assert!(result.is_ok());
595 assert_eq!(server.routes.len(), 0);
596 }
597
598 #[tokio::test]
599 async fn test_multiple_stubs() {
600 let mut server = MockServer::default();
601
602 server.stub_response("GET", "/api/users", json!({"users": []})).await.unwrap();
603 server
604 .stub_response("POST", "/api/users", json!({"created": true}))
605 .await
606 .unwrap();
607 server
608 .stub_response("DELETE", "/api/users/1", json!({"deleted": true}))
609 .await
610 .unwrap();
611
612 assert_eq!(server.routes.len(), 3);
613
614 assert_eq!(server.routes[0].method, "GET");
615 assert_eq!(server.routes[1].method, "POST");
616 assert_eq!(server.routes[2].method, "DELETE");
617 }
618
619 #[test]
620 fn test_build_simple_router_empty() {
621 let server = MockServer::default();
622 let router = server.build_simple_router(server.stub_store.clone());
623 assert_eq!(size_of_val(&router), size_of::<Router>());
625 }
626
627 #[tokio::test]
628 async fn test_build_simple_router_with_routes() {
629 let mut server = MockServer::default();
630 server.stub_response("GET", "/test", json!({"test": true})).await.unwrap();
631 server.stub_response("POST", "/create", json!({"created": true})).await.unwrap();
632
633 let router = server.build_simple_router(server.stub_store.clone());
634 assert_eq!(size_of_val(&router), size_of::<Router>());
636 }
637
638 #[tokio::test]
639 async fn test_start_server_already_started() {
640 let mut server = MockServer::default();
643 server.port = 0;
644 server.address = "127.0.0.1:0".parse().unwrap();
645
646 let result = server.start().await;
648 assert!(result.is_ok(), "Failed to start server: {:?}", result.err());
649 assert!(server.is_running());
650
651 assert_ne!(server.port, 0, "Port should have been updated from 0");
653
654 let result2 = server.start().await;
656 assert!(result2.is_err());
657 match result2 {
658 Err(Error::ServerAlreadyStarted(_)) => (),
659 _ => panic!("Expected ServerAlreadyStarted error"),
660 }
661
662 let _ = Box::pin(server.stop()).await;
664 }
665
666 #[test]
667 fn test_server_debug_format() {
668 let server = MockServer::default();
669 let debug_str = format!("{server:?}");
670 assert!(debug_str.contains("MockServer"));
671 }
672
673 #[tokio::test]
674 async fn test_route_config_conversion() {
675 let mut server = MockServer::default();
676 let stub = ResponseStub::new("PUT", "/api/update", json!({"updated": true}))
677 .status(200)
678 .header("X-Version", "1.0");
679
680 server.add_stub(stub).await.unwrap();
681
682 let route = &server.routes[0];
683 assert_eq!(route.path, "/api/update");
684 assert_eq!(route.method, "PUT");
685 assert_eq!(route.response.status, 200);
686 assert_eq!(route.response.headers.get("X-Version"), Some(&"1.0".to_string()));
687 assert!(route.response.body.is_some());
688 assert_eq!(route.response.body, Some(json!({"updated": true})));
689 }
690
691 #[tokio::test]
692 async fn test_server_with_different_methods() {
693 let mut server = MockServer::default();
694
695 server.stub_response("GET", "/test", json!({})).await.unwrap();
696 server.stub_response("POST", "/test", json!({})).await.unwrap();
697 server.stub_response("PUT", "/test", json!({})).await.unwrap();
698 server.stub_response("DELETE", "/test", json!({})).await.unwrap();
699 server.stub_response("PATCH", "/test", json!({})).await.unwrap();
700
701 assert_eq!(server.routes.len(), 5);
702
703 let methods: Vec<_> = server.routes.iter().map(|r| r.method.as_str()).collect();
705 assert!(methods.contains(&"GET"));
706 assert!(methods.contains(&"POST"));
707 assert!(methods.contains(&"PUT"));
708 assert!(methods.contains(&"DELETE"));
709 assert!(methods.contains(&"PATCH"));
710 }
711
712 #[tokio::test]
713 async fn test_server_url_format() {
714 let mut server = MockServer::default();
715 server.port = 3000;
716 server.address = "127.0.0.1:3000".parse().unwrap();
717
718 let url = server.url();
719 assert_eq!(url, "http://127.0.0.1:3000");
720 assert!(url.starts_with("http://"));
721 }
722
723 #[tokio::test]
724 async fn test_server_with_ipv6_address() {
725 let mut server = MockServer::default();
726 server.port = 8080;
727 server.address = "[::1]:8080".parse().unwrap();
728
729 let url = server.url();
730 assert_eq!(url, "http://[::1]:8080");
731 }
732}