1use 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#[derive(Debug, Clone)]
18struct StoredStub {
19 method: String,
20 path: String,
21 status: u16,
22 headers: HashMap<String, String>,
23 body: Value,
24}
25
26type StubStore = Arc<RwLock<Vec<StoredStub>>>;
28
29pub 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 stub_store: StubStore,
43}
44
45impl MockServer {
46 #[must_use]
48 pub const fn new() -> MockServerBuilder {
49 MockServerBuilder::new()
50 }
51
52 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 pub async fn start(&mut self) -> Result<()> {
77 if self.server_handle.is_some() {
78 return Err(Error::ServerAlreadyStarted(self.port));
79 }
80
81 let router = self.build_simple_router(self.stub_store.clone());
83
84 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
86 self.shutdown_tx = Some(shutdown_tx);
87
88 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 let actual_address = listener
96 .local_addr()
97 .map_err(|e| Error::General(format!("Failed to get local address: {}", e)))?;
98
99 self.address = actual_address;
101 self.port = actual_address.port();
102
103 tracing::info!("MockForge SDK server listening on {}", actual_address);
104
105 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 self.wait_for_ready().await?;
119
120 Ok(())
121 }
122
123 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 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 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 type MockStore = Arc<RwLock<HashMap<String, Value>>>;
160 let mock_store: MockStore = Arc::new(RwLock::new(HashMap::new()));
161
162 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 }))
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, "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 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 StatusCode::NOT_FOUND.into_response()
271 }
272 };
273
274 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 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 router.fallback(fallback_handler)
320 }
321
322 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 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 pub async fn add_stub(&mut self, stub: ResponseStub) -> Result<()> {
351 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 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 pub async fn clear_stubs(&mut self) -> Result<()> {
382 self.routes.clear();
383 self.stub_store.write().await.clear();
384 Ok(())
385 }
386
387 #[must_use]
389 pub const fn port(&self) -> u16 {
390 self.port
391 }
392
393 #[must_use]
398 pub fn url(&self) -> String {
399 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 #[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
440impl 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 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 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 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 let mut server = MockServer::default();
635 server.port = 0;
636 server.address = "127.0.0.1:0".parse().unwrap();
637
638 let result = server.start().await;
640 assert!(result.is_ok(), "Failed to start server: {:?}", result.err());
641 assert!(server.is_running());
642
643 assert_ne!(server.port, 0, "Port should have been updated from 0");
645
646 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 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 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}