mockforge_data/
mock_server.rs

1//! Mock Server Mode Implementation
2//!
3//! This module provides MSW-style mock server capabilities that can serve
4//! generated mock data based on OpenAPI specifications.
5
6use crate::mock_generator::{MockDataGenerator, MockDataResult, MockGeneratorConfig, MockResponse};
7use crate::{Error, Result};
8use axum::{
9    extract::Query,
10    http::{HeaderMap, StatusCode},
11    response::Json,
12    routing::get,
13    Router,
14};
15use serde_json::{json, Value};
16use std::collections::HashMap;
17use std::net::SocketAddr;
18use std::sync::Arc;
19use tokio::net::TcpListener;
20use tracing::info;
21
22/// Configuration for the mock server
23#[derive(Debug, Clone)]
24pub struct MockServerConfig {
25    /// Port to run the server on
26    pub port: u16,
27    /// Host to bind to
28    pub host: String,
29    /// OpenAPI specification
30    pub openapi_spec: Value,
31    /// Mock data generator configuration
32    pub generator_config: MockGeneratorConfig,
33    /// Whether to enable CORS
34    pub enable_cors: bool,
35    /// Custom response delays (in milliseconds)
36    pub response_delays: HashMap<String, u64>,
37    /// Whether to log all requests
38    pub log_requests: bool,
39}
40
41impl Default for MockServerConfig {
42    fn default() -> Self {
43        Self {
44            port: 3000,
45            host: "127.0.0.1".to_string(),
46            openapi_spec: json!({}),
47            generator_config: MockGeneratorConfig::default(),
48            enable_cors: true,
49            response_delays: HashMap::new(),
50            log_requests: true,
51        }
52    }
53}
54
55impl MockServerConfig {
56    /// Create a new mock server configuration
57    pub fn new(openapi_spec: Value) -> Self {
58        Self {
59            openapi_spec,
60            ..Default::default()
61        }
62    }
63
64    /// Set the port
65    pub fn port(mut self, port: u16) -> Self {
66        self.port = port;
67        self
68    }
69
70    /// Set the host
71    pub fn host(mut self, host: String) -> Self {
72        self.host = host;
73        self
74    }
75
76    /// Set the generator configuration
77    pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
78        self.generator_config = config;
79        self
80    }
81
82    /// Enable or disable CORS
83    pub fn enable_cors(mut self, enabled: bool) -> Self {
84        self.enable_cors = enabled;
85        self
86    }
87
88    /// Add a response delay for a specific endpoint
89    pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
90        self.response_delays.insert(endpoint, delay_ms);
91        self
92    }
93
94    /// Enable or disable request logging
95    pub fn log_requests(mut self, enabled: bool) -> Self {
96        self.log_requests = enabled;
97        self
98    }
99}
100
101/// Mock server that serves generated data based on OpenAPI specifications
102#[derive(Debug)]
103pub struct MockServer {
104    /// Server configuration
105    config: MockServerConfig,
106    /// Generated mock data
107    mock_data: Arc<MockDataResult>,
108    /// Route handlers
109    handlers: HashMap<String, MockResponse>,
110}
111
112impl MockServer {
113    /// Create a new mock server
114    pub fn new(config: MockServerConfig) -> Result<Self> {
115        info!("Creating mock server with OpenAPI specification");
116
117        // Generate mock data from the OpenAPI spec
118        let mut generator = MockDataGenerator::with_config(config.generator_config.clone());
119        let mock_data = generator.generate_from_openapi_spec(&config.openapi_spec)?;
120
121        // Create handlers map from generated responses
122        let mut handlers = HashMap::new();
123        for (endpoint, response) in &mock_data.responses {
124            handlers.insert(endpoint.clone(), response.clone());
125        }
126
127        Ok(Self {
128            config,
129            mock_data: Arc::new(mock_data),
130            handlers,
131        })
132    }
133
134    /// Start the mock server
135    pub async fn start(self) -> Result<()> {
136        let config = self.config.clone();
137        let app = self.create_router();
138        let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
139
140        info!("Starting mock server on {}", addr);
141
142        let listener = TcpListener::bind(addr)
143            .await
144            .map_err(|e| Error::generic(format!("Failed to bind to {}: {}", addr, e)))?;
145
146        axum::serve(listener, app)
147            .await
148            .map_err(|e| Error::generic(format!("Server error: {}", e)))?;
149
150        Ok(())
151    }
152
153    /// Create the Axum router with all endpoints
154    fn create_router(self) -> Router {
155        let mock_data = Arc::clone(&self.mock_data);
156        let config = Arc::new(self.config);
157        let handlers = Arc::new(self.handlers);
158
159        let mut router = Router::new()
160            // Add all OpenAPI endpoints dynamically
161            .route("/", get(Self::root_handler))
162            .route("/health", get(Self::health_handler))
163            .route("/openapi.json", get(Self::openapi_handler))
164            .route("/mock-data", get(Self::mock_data_handler))
165            // Dynamic routes will be added based on OpenAPI spec
166            .with_state(MockServerState {
167                mock_data,
168                config: config.clone(),
169                handlers: handlers.clone(),
170            });
171
172        // Integrate CORS middleware if enabled
173        if config.enable_cors {
174            use tower_http::cors::CorsLayer;
175            router = router.layer(CorsLayer::permissive());
176            info!("CORS middleware enabled for mock server");
177        }
178
179        // Integrate request logging middleware if enabled
180        if config.log_requests {
181            router = router.layer(axum::middleware::from_fn(Self::request_logging_middleware));
182        }
183
184        router
185    }
186
187    /// Request logging middleware
188    async fn request_logging_middleware(
189        request: axum::http::Request<axum::body::Body>,
190        next: axum::middleware::Next,
191    ) -> axum::response::Response {
192        let method = request.method().clone();
193        let uri = request.uri().clone();
194        let start = std::time::Instant::now();
195
196        info!("Incoming request: {} {}", method, uri);
197
198        let response = next.run(request).await;
199
200        let duration = start.elapsed();
201        info!("Request completed: {} {} - Status: {} - Duration: {:?}", method, uri, response.status(), duration);
202
203        response
204    }
205
206    /// Root handler - returns API information
207    async fn root_handler() -> Json<Value> {
208        Json(json!({
209            "name": "MockForge Mock Server",
210            "version": "1.0.0",
211            "description": "Mock server powered by MockForge",
212            "endpoints": {
213                "/health": "Health check endpoint",
214                "/openapi.json": "OpenAPI specification",
215                "/mock-data": "Generated mock data"
216            }
217        }))
218    }
219
220    /// Health check handler
221    async fn health_handler() -> Json<Value> {
222        Json(json!({
223            "status": "healthy",
224            "timestamp": chrono::Utc::now().to_rfc3339(),
225            "service": "mockforge-mock-server"
226        }))
227    }
228
229    /// OpenAPI specification handler
230    async fn openapi_handler(
231        axum::extract::State(state): axum::extract::State<MockServerState>,
232    ) -> Json<Value> {
233        // Apply response delay if configured
234        if let Some(delay) = state.config.response_delays.get("GET /openapi.json") {
235            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
236        }
237
238        Json(serde_json::to_value(&state.mock_data.spec_info).unwrap_or(json!({})))
239    }
240
241    /// Mock data handler - returns all generated mock data
242    async fn mock_data_handler(
243        axum::extract::State(state): axum::extract::State<MockServerState>,
244    ) -> Json<Value> {
245        // Apply response delay if configured
246        if let Some(delay) = state.config.response_delays.get("GET /mock-data") {
247            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
248        }
249
250        // Use handlers map to get response if available, otherwise return all mock data
251        let endpoint_key = "GET /mock-data";
252        if let Some(response) = state.handlers.get(endpoint_key) {
253            Json(response.body.clone())
254        } else {
255            Json(json!({
256                "schemas": state.mock_data.schemas,
257                "responses": state.mock_data.responses,
258                "warnings": state.mock_data.warnings
259            }))
260        }
261    }
262
263    /// Generic endpoint handler that serves mock data based on the request
264    ///
265    /// This handler can be used for catch-all routes to serve mock data
266    /// based on the request method and path.
267    async fn generic_handler(
268        axum::extract::State(state): axum::extract::State<MockServerState>,
269        method: axum::http::Method,
270        path: axum::extract::Path<String>,
271        query: Query<HashMap<String, String>>,
272        _headers: HeaderMap,
273    ) -> std::result::Result<Json<Value>, StatusCode> {
274        let endpoint_key = format!("{} /{}", method.as_str().to_uppercase(), path.as_str());
275
276        // Log request if enabled
277        if state.config.log_requests {
278            info!("Handling request: {} with query: {:?}", endpoint_key, query);
279        }
280
281        // Apply response delay if configured
282        if let Some(delay) = state.config.response_delays.get(&endpoint_key) {
283            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
284        }
285
286        // Find matching handler
287        if let Some(response) = state.handlers.get(&endpoint_key) {
288            Ok(Json(response.body.clone()))
289        } else {
290            // Try to find a similar endpoint (for path parameters)
291            let similar_endpoint = state
292                .handlers
293                .keys()
294                .find(|key| Self::endpoints_match(key, &endpoint_key))
295                .cloned();
296
297            if let Some(endpoint) = similar_endpoint {
298                if let Some(response) = state.handlers.get(&endpoint) {
299                    Ok(Json(response.body.clone()))
300                } else {
301                    Err(StatusCode::NOT_FOUND)
302                }
303            } else {
304                // Return a generic mock response
305                let generic_response = json!({
306                    "message": "Mock response",
307                    "endpoint": endpoint_key,
308                    "timestamp": chrono::Utc::now().to_rfc3339(),
309                    "data": {}
310                });
311                Ok(Json(generic_response))
312            }
313        }
314    }
315
316    /// Check if two endpoints match (handles path parameters)
317    ///
318    /// This is integrated into the mock server for matching endpoints with path parameters.
319    pub fn endpoints_match(pattern: &str, request: &str) -> bool {
320        // Simple pattern matching - in a real implementation,
321        // you'd want more sophisticated path parameter matching
322        let pattern_parts: Vec<&str> = pattern.split(' ').collect();
323        let request_parts: Vec<&str> = request.split(' ').collect();
324
325        if pattern_parts.len() != request_parts.len() {
326            return false;
327        }
328
329        for (pattern_part, request_part) in pattern_parts.iter().zip(request_parts.iter()) {
330            if pattern_part != request_part && !pattern_part.contains(":") {
331                return false;
332            }
333        }
334
335        true
336    }
337}
338
339/// State shared across all handlers
340#[derive(Debug, Clone)]
341struct MockServerState {
342    mock_data: Arc<MockDataResult>,
343    /// Server configuration (integrated - used for CORS, delays, logging)
344    config: Arc<MockServerConfig>,
345    /// Route handlers map (integrated - used for endpoint matching and responses)
346    handlers: Arc<HashMap<String, MockResponse>>,
347}
348
349/// Builder for creating mock servers
350#[derive(Debug)]
351pub struct MockServerBuilder {
352    config: MockServerConfig,
353}
354
355impl MockServerBuilder {
356    /// Create a new mock server builder
357    pub fn new(openapi_spec: Value) -> Self {
358        Self {
359            config: MockServerConfig::new(openapi_spec),
360        }
361    }
362
363    /// Set the port
364    pub fn port(mut self, port: u16) -> Self {
365        self.config = self.config.port(port);
366        self
367    }
368
369    /// Set the host
370    pub fn host(mut self, host: String) -> Self {
371        self.config = self.config.host(host);
372        self
373    }
374
375    /// Set the generator configuration
376    pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
377        self.config = self.config.generator_config(config);
378        self
379    }
380
381    /// Enable or disable CORS
382    pub fn enable_cors(mut self, enabled: bool) -> Self {
383        self.config = self.config.enable_cors(enabled);
384        self
385    }
386
387    /// Add a response delay for a specific endpoint
388    pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
389        self.config = self.config.response_delay(endpoint, delay_ms);
390        self
391    }
392
393    /// Enable or disable request logging
394    pub fn log_requests(mut self, enabled: bool) -> Self {
395        self.config = self.config.log_requests(enabled);
396        self
397    }
398
399    /// Build the mock server
400    pub fn build(self) -> Result<MockServer> {
401        MockServer::new(self.config)
402    }
403}
404
405/// Quick function to start a mock server
406pub async fn start_mock_server(openapi_spec: Value, port: u16) -> Result<()> {
407    let server = MockServerBuilder::new(openapi_spec).port(port).build()?;
408
409    server.start().await
410}
411
412/// Quick function to start a mock server with custom configuration
413pub async fn start_mock_server_with_config(config: MockServerConfig) -> Result<()> {
414    let server = MockServer::new(config)?;
415    server.start().await
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use serde_json::json;
422
423    #[test]
424    fn test_mock_server_config_default() {
425        let config = MockServerConfig::default();
426
427        assert_eq!(config.port, 3000);
428        assert_eq!(config.host, "127.0.0.1");
429        assert!(config.enable_cors);
430        assert!(config.log_requests);
431        assert!(config.response_delays.is_empty());
432    }
433
434    #[test]
435    fn test_mock_server_config_new() {
436        let spec = json!({
437            "openapi": "3.0.0",
438            "info": {
439                "title": "Test API",
440                "version": "1.0.0"
441            }
442        });
443
444        let config = MockServerConfig::new(spec);
445
446        assert_eq!(config.port, 3000);
447        assert_eq!(config.host, "127.0.0.1");
448        assert!(config.enable_cors);
449    }
450
451    #[test]
452    fn test_mock_server_config_builder_methods() {
453        let spec = json!({
454            "openapi": "3.0.0",
455            "info": {
456                "title": "Test API",
457                "version": "1.0.0"
458            }
459        });
460
461        let config = MockServerConfig::new(spec)
462            .port(8080)
463            .host("0.0.0.0".to_string())
464            .enable_cors(false)
465            .response_delay("/api/users".to_string(), 100)
466            .log_requests(false);
467
468        assert_eq!(config.port, 8080);
469        assert_eq!(config.host, "0.0.0.0");
470        assert!(!config.enable_cors);
471        assert!(!config.log_requests);
472        assert!(config.response_delays.contains_key("/api/users"));
473        assert_eq!(config.response_delays.get("/api/users"), Some(&100));
474    }
475
476    #[test]
477    fn test_mock_server_builder() {
478        let spec = json!({
479            "openapi": "3.0.0",
480            "info": {
481                "title": "Test API",
482                "version": "1.0.0"
483            }
484        });
485
486        let builder = MockServerBuilder::new(spec)
487            .port(8080)
488            .host("0.0.0.0".to_string())
489            .enable_cors(false);
490
491        assert_eq!(builder.config.port, 8080);
492        assert_eq!(builder.config.host, "0.0.0.0");
493        assert!(!builder.config.enable_cors);
494    }
495
496    #[test]
497    fn test_endpoints_match_exact() {
498        assert!(MockServer::endpoints_match("GET /api/users", "GET /api/users"));
499        assert!(!MockServer::endpoints_match("GET /api/users", "POST /api/users"));
500        assert!(!MockServer::endpoints_match("GET /api/users", "GET /api/products"));
501    }
502
503    #[test]
504    fn test_endpoints_match_with_params() {
505        // This is a simplified test - real path parameter matching would be more complex
506        assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/123"));
507        assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/abc"));
508    }
509
510    #[tokio::test]
511    async fn test_mock_server_creation() {
512        let spec = json!({
513            "openapi": "3.0.0",
514            "info": {
515                "title": "Test API",
516                "version": "1.0.0"
517            },
518            "paths": {
519                "/api/users": {
520                    "get": {
521                        "responses": {
522                            "200": {
523                                "description": "List of users",
524                                "content": {
525                                    "application/json": {
526                                        "schema": {
527                                            "type": "object",
528                                            "properties": {
529                                                "users": {
530                                                    "type": "array",
531                                                    "items": {
532                                                        "type": "object",
533                                                        "properties": {
534                                                            "id": {"type": "string"},
535                                                            "name": {"type": "string"},
536                                                            "email": {"type": "string"}
537                                                        }
538                                                    }
539                                                }
540                                            }
541                                        }
542                                    }
543                                }
544                            }
545                        }
546                    }
547                }
548            }
549        });
550
551        let config = MockServerConfig::new(spec);
552        let server = MockServer::new(config);
553
554        assert!(server.is_ok());
555    }
556}