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 config: ServerConfig,
39 server_handle: Option<JoinHandle<()>>,
40 shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
41 routes: Vec<RouteConfig>,
42 stub_store: StubStore,
44}
45
46impl MockServer {
47 #[must_use]
49 #[allow(clippy::new_ret_no_self)]
50 pub const fn new() -> MockServerBuilder {
51 MockServerBuilder::new()
52 }
53
54 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 pub async fn start(&mut self) -> Result<()> {
80 if self.server_handle.is_some() {
81 return Err(Error::ServerAlreadyStarted(self.port));
82 }
83
84 let router = self.build_simple_router(self.stub_store.clone());
86
87 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
89 self.shutdown_tx = Some(shutdown_tx);
90
91 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 let actual_address = listener
99 .local_addr()
100 .map_err(|e| Error::General(format!("Failed to get local address: {e}")))?;
101
102 self.address = actual_address;
104 self.port = actual_address.port();
105
106 tracing::info!("MockForge SDK server listening on {}", actual_address);
107
108 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 self.wait_for_ready().await?;
122
123 Ok(())
124 }
125
126 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 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 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 type MockStore = Arc<RwLock<HashMap<String, Value>>>;
164 let mock_store: MockStore = Arc::new(RwLock::new(HashMap::new()));
165
166 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 }))
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, "total_requests": 0,
233 "active_mocks": count,
234 "enabled_mocks": count,
235 "registered_routes": count
236 }))
237 }
238 };
239
240 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 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 StatusCode::NOT_FOUND.into_response()
274 }
275 };
276
277 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 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 router.fallback(fallback_handler)
323 }
324
325 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 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 pub async fn add_stub(&mut self, stub: ResponseStub) -> Result<()> {
354 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 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 pub async fn clear_stubs(&mut self) -> Result<()> {
385 self.routes.clear();
386 self.stub_store.write().await.clear();
387 Ok(())
388 }
389
390 #[must_use]
392 pub const fn port(&self) -> u16 {
393 self.port
394 }
395
396 #[must_use]
401 pub fn url(&self) -> String {
402 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 #[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
443impl 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 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 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 assert_eq!(size_of_val(&router), size_of::<Router>());
632 }
633
634 #[tokio::test]
635 async fn test_start_server_already_started() {
636 let mut server = MockServer::default();
639 server.port = 0;
640 server.address = "127.0.0.1:0".parse().unwrap();
641
642 let result = server.start().await;
644 assert!(result.is_ok(), "Failed to start server: {:?}", result.err());
645 assert!(server.is_running());
646
647 assert_ne!(server.port, 0, "Port should have been updated from 0");
649
650 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 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 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}