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!(
202            "Request completed: {} {} - Status: {} - Duration: {:?}",
203            method,
204            uri,
205            response.status(),
206            duration
207        );
208
209        response
210    }
211
212    /// Root handler - returns API information
213    async fn root_handler() -> Json<Value> {
214        Json(json!({
215            "name": "MockForge Mock Server",
216            "version": "1.0.0",
217            "description": "Mock server powered by MockForge",
218            "endpoints": {
219                "/health": "Health check endpoint",
220                "/openapi.json": "OpenAPI specification",
221                "/mock-data": "Generated mock data"
222            }
223        }))
224    }
225
226    /// Health check handler
227    async fn health_handler() -> Json<Value> {
228        Json(json!({
229            "status": "healthy",
230            "timestamp": chrono::Utc::now().to_rfc3339(),
231            "service": "mockforge-mock-server"
232        }))
233    }
234
235    /// OpenAPI specification handler
236    async fn openapi_handler(
237        axum::extract::State(state): axum::extract::State<MockServerState>,
238    ) -> Json<Value> {
239        // Apply response delay if configured
240        if let Some(delay) = state.config.response_delays.get("GET /openapi.json") {
241            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
242        }
243
244        Json(serde_json::to_value(&state.mock_data.spec_info).unwrap_or(json!({})))
245    }
246
247    /// Mock data handler - returns all generated mock data
248    async fn mock_data_handler(
249        axum::extract::State(state): axum::extract::State<MockServerState>,
250    ) -> Json<Value> {
251        // Apply response delay if configured
252        if let Some(delay) = state.config.response_delays.get("GET /mock-data") {
253            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
254        }
255
256        // Use handlers map to get response if available, otherwise return all mock data
257        let endpoint_key = "GET /mock-data";
258        if let Some(response) = state.handlers.get(endpoint_key) {
259            Json(response.body.clone())
260        } else {
261            Json(json!({
262                "schemas": state.mock_data.schemas,
263                "responses": state.mock_data.responses,
264                "warnings": state.mock_data.warnings
265            }))
266        }
267    }
268
269    /// Generic endpoint handler that serves mock data based on the request
270    ///
271    /// This handler can be used for catch-all routes to serve mock data
272    /// based on the request method and path.
273    async fn generic_handler(
274        axum::extract::State(state): axum::extract::State<MockServerState>,
275        method: axum::http::Method,
276        path: axum::extract::Path<String>,
277        query: Query<HashMap<String, String>>,
278        _headers: HeaderMap,
279    ) -> std::result::Result<Json<Value>, StatusCode> {
280        let endpoint_key = format!("{} /{}", method.as_str().to_uppercase(), path.as_str());
281
282        // Log request if enabled
283        if state.config.log_requests {
284            info!("Handling request: {} with query: {:?}", endpoint_key, query);
285        }
286
287        // Apply response delay if configured
288        if let Some(delay) = state.config.response_delays.get(&endpoint_key) {
289            tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
290        }
291
292        // Find matching handler
293        if let Some(response) = state.handlers.get(&endpoint_key) {
294            Ok(Json(response.body.clone()))
295        } else {
296            // Try to find a similar endpoint (for path parameters)
297            let similar_endpoint = state
298                .handlers
299                .keys()
300                .find(|key| Self::endpoints_match(key, &endpoint_key))
301                .cloned();
302
303            if let Some(endpoint) = similar_endpoint {
304                if let Some(response) = state.handlers.get(&endpoint) {
305                    Ok(Json(response.body.clone()))
306                } else {
307                    Err(StatusCode::NOT_FOUND)
308                }
309            } else {
310                // Return a generic mock response
311                let generic_response = json!({
312                    "message": "Mock response",
313                    "endpoint": endpoint_key,
314                    "timestamp": chrono::Utc::now().to_rfc3339(),
315                    "data": {}
316                });
317                Ok(Json(generic_response))
318            }
319        }
320    }
321
322    /// Check if two endpoints match (handles path parameters)
323    ///
324    /// This is integrated into the mock server for matching endpoints with path parameters.
325    pub fn endpoints_match(pattern: &str, request: &str) -> bool {
326        // Simple pattern matching - in a real implementation,
327        // you'd want more sophisticated path parameter matching
328        let pattern_parts: Vec<&str> = pattern.split(' ').collect();
329        let request_parts: Vec<&str> = request.split(' ').collect();
330
331        if pattern_parts.len() != request_parts.len() {
332            return false;
333        }
334
335        for (pattern_part, request_part) in pattern_parts.iter().zip(request_parts.iter()) {
336            if pattern_part != request_part && !pattern_part.contains(":") {
337                return false;
338            }
339        }
340
341        true
342    }
343}
344
345/// State shared across all handlers
346#[derive(Debug, Clone)]
347struct MockServerState {
348    mock_data: Arc<MockDataResult>,
349    /// Server configuration (integrated - used for CORS, delays, logging)
350    config: Arc<MockServerConfig>,
351    /// Route handlers map (integrated - used for endpoint matching and responses)
352    handlers: Arc<HashMap<String, MockResponse>>,
353}
354
355/// Builder for creating mock servers
356#[derive(Debug)]
357pub struct MockServerBuilder {
358    config: MockServerConfig,
359}
360
361impl MockServerBuilder {
362    /// Create a new mock server builder
363    pub fn new(openapi_spec: Value) -> Self {
364        Self {
365            config: MockServerConfig::new(openapi_spec),
366        }
367    }
368
369    /// Set the port
370    pub fn port(mut self, port: u16) -> Self {
371        self.config = self.config.port(port);
372        self
373    }
374
375    /// Set the host
376    pub fn host(mut self, host: String) -> Self {
377        self.config = self.config.host(host);
378        self
379    }
380
381    /// Set the generator configuration
382    pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
383        self.config = self.config.generator_config(config);
384        self
385    }
386
387    /// Enable or disable CORS
388    pub fn enable_cors(mut self, enabled: bool) -> Self {
389        self.config = self.config.enable_cors(enabled);
390        self
391    }
392
393    /// Add a response delay for a specific endpoint
394    pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
395        self.config = self.config.response_delay(endpoint, delay_ms);
396        self
397    }
398
399    /// Enable or disable request logging
400    pub fn log_requests(mut self, enabled: bool) -> Self {
401        self.config = self.config.log_requests(enabled);
402        self
403    }
404
405    /// Build the mock server
406    pub fn build(self) -> Result<MockServer> {
407        MockServer::new(self.config)
408    }
409}
410
411/// Quick function to start a mock server
412pub async fn start_mock_server(openapi_spec: Value, port: u16) -> Result<()> {
413    let server = MockServerBuilder::new(openapi_spec).port(port).build()?;
414
415    server.start().await
416}
417
418/// Quick function to start a mock server with custom configuration
419pub async fn start_mock_server_with_config(config: MockServerConfig) -> Result<()> {
420    let server = MockServer::new(config)?;
421    server.start().await
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use serde_json::json;
428
429    #[test]
430    fn test_mock_server_config_default() {
431        let config = MockServerConfig::default();
432
433        assert_eq!(config.port, 3000);
434        assert_eq!(config.host, "127.0.0.1");
435        assert!(config.enable_cors);
436        assert!(config.log_requests);
437        assert!(config.response_delays.is_empty());
438    }
439
440    #[test]
441    fn test_mock_server_config_new() {
442        let spec = json!({
443            "openapi": "3.0.0",
444            "info": {
445                "title": "Test API",
446                "version": "1.0.0"
447            }
448        });
449
450        let config = MockServerConfig::new(spec);
451
452        assert_eq!(config.port, 3000);
453        assert_eq!(config.host, "127.0.0.1");
454        assert!(config.enable_cors);
455    }
456
457    #[test]
458    fn test_mock_server_config_builder_methods() {
459        let spec = json!({
460            "openapi": "3.0.0",
461            "info": {
462                "title": "Test API",
463                "version": "1.0.0"
464            }
465        });
466
467        let config = MockServerConfig::new(spec)
468            .port(8080)
469            .host("0.0.0.0".to_string())
470            .enable_cors(false)
471            .response_delay("/api/users".to_string(), 100)
472            .log_requests(false);
473
474        assert_eq!(config.port, 8080);
475        assert_eq!(config.host, "0.0.0.0");
476        assert!(!config.enable_cors);
477        assert!(!config.log_requests);
478        assert!(config.response_delays.contains_key("/api/users"));
479        assert_eq!(config.response_delays.get("/api/users"), Some(&100));
480    }
481
482    #[test]
483    fn test_mock_server_builder() {
484        let spec = json!({
485            "openapi": "3.0.0",
486            "info": {
487                "title": "Test API",
488                "version": "1.0.0"
489            }
490        });
491
492        let builder = MockServerBuilder::new(spec)
493            .port(8080)
494            .host("0.0.0.0".to_string())
495            .enable_cors(false);
496
497        assert_eq!(builder.config.port, 8080);
498        assert_eq!(builder.config.host, "0.0.0.0");
499        assert!(!builder.config.enable_cors);
500    }
501
502    #[test]
503    fn test_endpoints_match_exact() {
504        assert!(MockServer::endpoints_match("GET /api/users", "GET /api/users"));
505        assert!(!MockServer::endpoints_match("GET /api/users", "POST /api/users"));
506        assert!(!MockServer::endpoints_match("GET /api/users", "GET /api/products"));
507    }
508
509    #[test]
510    fn test_endpoints_match_with_params() {
511        // This is a simplified test - real path parameter matching would be more complex
512        assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/123"));
513        assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/abc"));
514    }
515
516    #[tokio::test]
517    async fn test_mock_server_creation() {
518        let spec = json!({
519            "openapi": "3.0.0",
520            "info": {
521                "title": "Test API",
522                "version": "1.0.0"
523            },
524            "paths": {
525                "/api/users": {
526                    "get": {
527                        "responses": {
528                            "200": {
529                                "description": "List of users",
530                                "content": {
531                                    "application/json": {
532                                        "schema": {
533                                            "type": "object",
534                                            "properties": {
535                                                "users": {
536                                                    "type": "array",
537                                                    "items": {
538                                                        "type": "object",
539                                                        "properties": {
540                                                            "id": {"type": "string"},
541                                                            "name": {"type": "string"},
542                                                            "email": {"type": "string"}
543                                                        }
544                                                    }
545                                                }
546                                            }
547                                        }
548                                    }
549                                }
550                            }
551                        }
552                    }
553                }
554            }
555        });
556
557        let config = MockServerConfig::new(spec);
558        let server = MockServer::new(config);
559
560        assert!(server.is_ok());
561    }
562}