Skip to main content

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    http::{HeaderMap, StatusCode},
10    response::Json,
11    routing::get,
12    Router,
13};
14use serde_json::{json, Value};
15use std::collections::HashMap;
16use std::net::SocketAddr;
17use std::sync::Arc;
18use tokio::net::TcpListener;
19use tracing::info;
20
21/// Configuration for the mock server
22#[derive(Debug, Clone)]
23pub struct MockServerConfig {
24    /// Port to run the server on
25    pub port: u16,
26    /// Host to bind to
27    pub host: String,
28    /// OpenAPI specification
29    pub openapi_spec: Value,
30    /// Mock data generator configuration
31    pub generator_config: MockGeneratorConfig,
32    /// Whether to enable CORS
33    pub enable_cors: bool,
34    /// Custom response delays (in milliseconds)
35    pub response_delays: HashMap<String, u64>,
36    /// Whether to log all requests
37    pub log_requests: bool,
38}
39
40impl Default for MockServerConfig {
41    fn default() -> Self {
42        Self {
43            port: 3000,
44            host: "127.0.0.1".to_string(),
45            openapi_spec: json!({}),
46            generator_config: MockGeneratorConfig::default(),
47            enable_cors: true,
48            response_delays: HashMap::new(),
49            log_requests: true,
50        }
51    }
52}
53
54impl MockServerConfig {
55    /// Create a new mock server configuration
56    pub fn new(openapi_spec: Value) -> Self {
57        Self {
58            openapi_spec,
59            ..Default::default()
60        }
61    }
62
63    /// Set the port
64    pub fn port(mut self, port: u16) -> Self {
65        self.port = port;
66        self
67    }
68
69    /// Set the host
70    pub fn host(mut self, host: String) -> Self {
71        self.host = host;
72        self
73    }
74
75    /// Set the generator configuration
76    pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
77        self.generator_config = config;
78        self
79    }
80
81    /// Enable or disable CORS
82    pub fn enable_cors(mut self, enabled: bool) -> Self {
83        self.enable_cors = enabled;
84        self
85    }
86
87    /// Add a response delay for a specific endpoint
88    pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
89        self.response_delays.insert(endpoint, delay_ms);
90        self
91    }
92
93    /// Enable or disable request logging
94    pub fn log_requests(mut self, enabled: bool) -> Self {
95        self.log_requests = enabled;
96        self
97    }
98}
99
100/// Mock server that serves generated data based on OpenAPI specifications
101#[derive(Debug)]
102pub struct MockServer {
103    /// Server configuration
104    config: MockServerConfig,
105    /// Generated mock data
106    mock_data: Arc<MockDataResult>,
107    /// Route handlers
108    handlers: HashMap<String, MockResponse>,
109}
110
111impl MockServer {
112    /// Create a new mock server
113    pub fn new(config: MockServerConfig) -> Result<Self> {
114        info!("Creating mock server with OpenAPI specification");
115
116        // Generate mock data from the OpenAPI spec
117        let mut generator = MockDataGenerator::with_config(config.generator_config.clone());
118        let mock_data = generator.generate_from_openapi_spec(&config.openapi_spec)?;
119
120        // Create handlers map from generated responses
121        let mut handlers = HashMap::new();
122        for (endpoint, response) in &mock_data.responses {
123            handlers.insert(endpoint.clone(), response.clone());
124        }
125
126        Ok(Self {
127            config,
128            mock_data: Arc::new(mock_data),
129            handlers,
130        })
131    }
132
133    /// Start the mock server
134    pub async fn start(self) -> Result<()> {
135        let config = self.config.clone();
136        let app = self.create_router();
137        let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
138
139        info!("Starting mock server on {}", addr);
140
141        let listener = TcpListener::bind(addr)
142            .await
143            .map_err(|e| Error::generic(format!("Failed to bind to {}: {}", addr, e)))?;
144
145        axum::serve(listener, app)
146            .await
147            .map_err(|e| Error::generic(format!("Server error: {}", e)))?;
148
149        Ok(())
150    }
151
152    /// Create the Axum router with all endpoints
153    fn create_router(self) -> Router {
154        let mock_data = Arc::clone(&self.mock_data);
155        let config = Arc::new(self.config);
156        let handlers = Arc::new(self.handlers);
157
158        let mut router = Router::new()
159            // Add all OpenAPI endpoints dynamically
160            .route("/", get(Self::root_handler))
161            .route("/health", get(Self::health_handler))
162            .route("/openapi.json", get(Self::openapi_handler))
163            .route("/mock-data", get(Self::mock_data_handler))
164            // Catch-all fallback for dynamic mock endpoints
165            .fallback(Self::generic_handler)
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    /// Catch-all fallback route that serves 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        uri: axum::http::Uri,
277        _headers: HeaderMap,
278    ) -> std::result::Result<Json<Value>, StatusCode> {
279        let path = uri.path();
280        let endpoint_key = format!("{} {}", method.as_str().to_uppercase(), path);
281
282        // Log request if enabled
283        if state.config.log_requests {
284            info!("Handling request: {}", endpoint_key);
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}