use crate::mock_generator::{MockDataGenerator, MockDataResult, MockGeneratorConfig, MockResponse};
use crate::{Error, Result};
use axum::{
http::{HeaderMap, StatusCode},
response::Json,
routing::get,
Router,
};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tracing::info;
#[derive(Debug, Clone)]
pub struct MockServerConfig {
pub port: u16,
pub host: String,
pub openapi_spec: Value,
pub generator_config: MockGeneratorConfig,
pub enable_cors: bool,
pub response_delays: HashMap<String, u64>,
pub log_requests: bool,
}
impl Default for MockServerConfig {
fn default() -> Self {
Self {
port: 3000,
host: "127.0.0.1".to_string(),
openapi_spec: json!({}),
generator_config: MockGeneratorConfig::default(),
enable_cors: true,
response_delays: HashMap::new(),
log_requests: true,
}
}
}
impl MockServerConfig {
pub fn new(openapi_spec: Value) -> Self {
Self {
openapi_spec,
..Default::default()
}
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn host(mut self, host: String) -> Self {
self.host = host;
self
}
pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
self.generator_config = config;
self
}
pub fn enable_cors(mut self, enabled: bool) -> Self {
self.enable_cors = enabled;
self
}
pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
self.response_delays.insert(endpoint, delay_ms);
self
}
pub fn log_requests(mut self, enabled: bool) -> Self {
self.log_requests = enabled;
self
}
}
#[derive(Debug)]
pub struct MockServer {
config: MockServerConfig,
mock_data: Arc<MockDataResult>,
handlers: HashMap<String, MockResponse>,
}
impl MockServer {
pub fn new(config: MockServerConfig) -> Result<Self> {
info!("Creating mock server with OpenAPI specification");
let mut generator = MockDataGenerator::with_config(config.generator_config.clone());
let mock_data = generator.generate_from_openapi_spec(&config.openapi_spec)?;
let mut handlers = HashMap::new();
for (endpoint, response) in &mock_data.responses {
handlers.insert(endpoint.clone(), response.clone());
}
Ok(Self {
config,
mock_data: Arc::new(mock_data),
handlers,
})
}
pub async fn start(self) -> Result<()> {
let config = self.config.clone();
let app = self.create_router();
let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
info!("Starting mock server on {}", addr);
let listener = TcpListener::bind(addr)
.await
.map_err(|e| Error::generic(format!("Failed to bind to {}: {}", addr, e)))?;
axum::serve(listener, app)
.await
.map_err(|e| Error::generic(format!("Server error: {}", e)))?;
Ok(())
}
fn create_router(self) -> Router {
let mock_data = Arc::clone(&self.mock_data);
let config = Arc::new(self.config);
let handlers = Arc::new(self.handlers);
let mut router = Router::new()
.route("/", get(Self::root_handler))
.route("/health", get(Self::health_handler))
.route("/openapi.json", get(Self::openapi_handler))
.route("/mock-data", get(Self::mock_data_handler))
.fallback(Self::generic_handler)
.with_state(MockServerState {
mock_data,
config: config.clone(),
handlers: handlers.clone(),
});
if config.enable_cors {
use tower_http::cors::CorsLayer;
router = router.layer(CorsLayer::permissive());
info!("CORS middleware enabled for mock server");
}
if config.log_requests {
router = router.layer(axum::middleware::from_fn(Self::request_logging_middleware));
}
router
}
async fn request_logging_middleware(
request: axum::http::Request<axum::body::Body>,
next: axum::middleware::Next,
) -> axum::response::Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = std::time::Instant::now();
info!("Incoming request: {} {}", method, uri);
let response = next.run(request).await;
let duration = start.elapsed();
info!(
"Request completed: {} {} - Status: {} - Duration: {:?}",
method,
uri,
response.status(),
duration
);
response
}
async fn root_handler() -> Json<Value> {
Json(json!({
"name": "MockForge Mock Server",
"version": "1.0.0",
"description": "Mock server powered by MockForge",
"endpoints": {
"/health": "Health check endpoint",
"/openapi.json": "OpenAPI specification",
"/mock-data": "Generated mock data"
}
}))
}
async fn health_handler() -> Json<Value> {
Json(json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
"service": "mockforge-mock-server"
}))
}
async fn openapi_handler(
axum::extract::State(state): axum::extract::State<MockServerState>,
) -> Json<Value> {
if let Some(delay) = state.config.response_delays.get("GET /openapi.json") {
tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
}
Json(serde_json::to_value(&state.mock_data.spec_info).unwrap_or(json!({})))
}
async fn mock_data_handler(
axum::extract::State(state): axum::extract::State<MockServerState>,
) -> Json<Value> {
if let Some(delay) = state.config.response_delays.get("GET /mock-data") {
tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
}
let endpoint_key = "GET /mock-data";
if let Some(response) = state.handlers.get(endpoint_key) {
Json(response.body.clone())
} else {
Json(json!({
"schemas": state.mock_data.schemas,
"responses": state.mock_data.responses,
"warnings": state.mock_data.warnings
}))
}
}
async fn generic_handler(
axum::extract::State(state): axum::extract::State<MockServerState>,
method: axum::http::Method,
uri: axum::http::Uri,
_headers: HeaderMap,
) -> std::result::Result<Json<Value>, StatusCode> {
let path = uri.path();
let endpoint_key = format!("{} {}", method.as_str().to_uppercase(), path);
if state.config.log_requests {
info!("Handling request: {}", endpoint_key);
}
if let Some(delay) = state.config.response_delays.get(&endpoint_key) {
tokio::time::sleep(tokio::time::Duration::from_millis(*delay)).await;
}
if let Some(response) = state.handlers.get(&endpoint_key) {
Ok(Json(response.body.clone()))
} else {
let similar_endpoint = state
.handlers
.keys()
.find(|key| Self::endpoints_match(key, &endpoint_key))
.cloned();
if let Some(endpoint) = similar_endpoint {
if let Some(response) = state.handlers.get(&endpoint) {
Ok(Json(response.body.clone()))
} else {
Err(StatusCode::NOT_FOUND)
}
} else {
let generic_response = json!({
"message": "Mock response",
"endpoint": endpoint_key,
"timestamp": chrono::Utc::now().to_rfc3339(),
"data": {}
});
Ok(Json(generic_response))
}
}
}
pub fn endpoints_match(pattern: &str, request: &str) -> bool {
let pattern_parts: Vec<&str> = pattern.split(' ').collect();
let request_parts: Vec<&str> = request.split(' ').collect();
if pattern_parts.len() != request_parts.len() {
return false;
}
for (pattern_part, request_part) in pattern_parts.iter().zip(request_parts.iter()) {
if pattern_part != request_part && !pattern_part.contains(":") {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
struct MockServerState {
mock_data: Arc<MockDataResult>,
config: Arc<MockServerConfig>,
handlers: Arc<HashMap<String, MockResponse>>,
}
#[derive(Debug)]
pub struct MockServerBuilder {
config: MockServerConfig,
}
impl MockServerBuilder {
pub fn new(openapi_spec: Value) -> Self {
Self {
config: MockServerConfig::new(openapi_spec),
}
}
pub fn port(mut self, port: u16) -> Self {
self.config = self.config.port(port);
self
}
pub fn host(mut self, host: String) -> Self {
self.config = self.config.host(host);
self
}
pub fn generator_config(mut self, config: MockGeneratorConfig) -> Self {
self.config = self.config.generator_config(config);
self
}
pub fn enable_cors(mut self, enabled: bool) -> Self {
self.config = self.config.enable_cors(enabled);
self
}
pub fn response_delay(mut self, endpoint: String, delay_ms: u64) -> Self {
self.config = self.config.response_delay(endpoint, delay_ms);
self
}
pub fn log_requests(mut self, enabled: bool) -> Self {
self.config = self.config.log_requests(enabled);
self
}
pub fn build(self) -> Result<MockServer> {
MockServer::new(self.config)
}
}
pub async fn start_mock_server(openapi_spec: Value, port: u16) -> Result<()> {
let server = MockServerBuilder::new(openapi_spec).port(port).build()?;
server.start().await
}
pub async fn start_mock_server_with_config(config: MockServerConfig) -> Result<()> {
let server = MockServer::new(config)?;
server.start().await
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_mock_server_config_default() {
let config = MockServerConfig::default();
assert_eq!(config.port, 3000);
assert_eq!(config.host, "127.0.0.1");
assert!(config.enable_cors);
assert!(config.log_requests);
assert!(config.response_delays.is_empty());
}
#[test]
fn test_mock_server_config_new() {
let spec = json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
}
});
let config = MockServerConfig::new(spec);
assert_eq!(config.port, 3000);
assert_eq!(config.host, "127.0.0.1");
assert!(config.enable_cors);
}
#[test]
fn test_mock_server_config_builder_methods() {
let spec = json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
}
});
let config = MockServerConfig::new(spec)
.port(8080)
.host("0.0.0.0".to_string())
.enable_cors(false)
.response_delay("/api/users".to_string(), 100)
.log_requests(false);
assert_eq!(config.port, 8080);
assert_eq!(config.host, "0.0.0.0");
assert!(!config.enable_cors);
assert!(!config.log_requests);
assert!(config.response_delays.contains_key("/api/users"));
assert_eq!(config.response_delays.get("/api/users"), Some(&100));
}
#[test]
fn test_mock_server_builder() {
let spec = json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
}
});
let builder = MockServerBuilder::new(spec)
.port(8080)
.host("0.0.0.0".to_string())
.enable_cors(false);
assert_eq!(builder.config.port, 8080);
assert_eq!(builder.config.host, "0.0.0.0");
assert!(!builder.config.enable_cors);
}
#[test]
fn test_endpoints_match_exact() {
assert!(MockServer::endpoints_match("GET /api/users", "GET /api/users"));
assert!(!MockServer::endpoints_match("GET /api/users", "POST /api/users"));
assert!(!MockServer::endpoints_match("GET /api/users", "GET /api/products"));
}
#[test]
fn test_endpoints_match_with_params() {
assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/123"));
assert!(MockServer::endpoints_match("GET /api/users/:id", "GET /api/users/abc"));
}
#[tokio::test]
async fn test_mock_server_creation() {
let spec = json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/api/users": {
"get": {
"responses": {
"200": {
"description": "List of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"}
}
}
}
}
}
}
}
}
}
}
}
}
});
let config = MockServerConfig::new(spec);
let server = MockServer::new(config);
assert!(server.is_ok());
}
}