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