mockforge_http/
lib.rs

1//! # MockForge HTTP
2//!
3//! HTTP/REST API mocking library for MockForge.
4//!
5//! This crate provides HTTP-specific functionality for creating mock REST APIs,
6//! including OpenAPI integration, request validation, AI-powered response generation,
7//! and management endpoints.
8//!
9//! ## Overview
10//!
11//! MockForge HTTP enables you to:
12//!
13//! - **Serve OpenAPI specs**: Automatically generate mock endpoints from OpenAPI/Swagger
14//! - **Validate requests**: Enforce schema validation with configurable modes
15//! - **AI-powered responses**: Generate intelligent responses using LLMs
16//! - **Management API**: Real-time monitoring, configuration, and control
17//! - **Request logging**: Comprehensive HTTP request/response logging
18//! - **Metrics collection**: Track performance and usage statistics
19//! - **Server-Sent Events**: Stream logs and metrics to clients
20//!
21//! ## Quick Start
22//!
23//! ### Basic HTTP Server from OpenAPI
24//!
25//! ```rust,no_run
26//! use axum::Router;
27//! use mockforge_core::openapi_routes::ValidationMode;
28//! use mockforge_core::ValidationOptions;
29//! use mockforge_http::build_router;
30//!
31//! #[tokio::main]
32//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
33//!     // Build router from OpenAPI specification
34//!     let router = build_router(
35//!         Some("./api-spec.json".to_string()),
36//!         Some(ValidationOptions {
37//!             request_mode: ValidationMode::Enforce,
38//!             ..ValidationOptions::default()
39//!         }),
40//!         None,
41//!     ).await;
42//!
43//!     // Start the server
44//!     let addr: std::net::SocketAddr = "0.0.0.0:3000".parse()?;
45//!     let listener = tokio::net::TcpListener::bind(addr).await?;
46//!     axum::serve(listener, router).await?;
47//!
48//!     Ok(())
49//! }
50//! ```
51//!
52//! ### With Management API
53//!
54//! Enable real-time monitoring and configuration:
55//!
56//! ```rust,no_run
57//! use mockforge_http::{management_router, ManagementState};
58//!
59//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
60//! let state = ManagementState::new(None, None, 3000);
61//!
62//! // Build management router
63//! let mgmt_router = management_router(state);
64//!
65//! // Mount under your main router
66//! let app = axum::Router::new()
67//!     .nest("/__mockforge", mgmt_router);
68//! # Ok(())
69//! # }
70//! ```
71//!
72//! ### AI-Powered Responses
73//!
74//! Generate intelligent responses based on request context:
75//!
76//! ```rust,no_run
77//! use mockforge_data::intelligent_mock::{IntelligentMockConfig, ResponseMode};
78//! use mockforge_http::{process_response_with_ai, AiResponseConfig};
79//! use serde_json::json;
80//!
81//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
82//! let ai_config = AiResponseConfig {
83//!     intelligent: Some(
84//!         IntelligentMockConfig::new(ResponseMode::Intelligent)
85//!             .with_prompt("Generate realistic user data".to_string()),
86//!     ),
87//!     drift: None,
88//! };
89//!
90//! let response = process_response_with_ai(
91//!     Some(json!({"name": "Alice"})),
92//!     ai_config
93//!         .intelligent
94//!         .clone()
95//!         .map(serde_json::to_value)
96//!         .transpose()?,
97//!     ai_config
98//!         .drift
99//!         .clone()
100//!         .map(serde_json::to_value)
101//!         .transpose()?,
102//! )
103//! .await?;
104//! # Ok(())
105//! # }
106//! ```
107//!
108//! ## Key Features
109//!
110//! ### OpenAPI Integration
111//! - Automatic endpoint generation from specs
112//! - Request/response validation
113//! - Schema-based mock data generation
114//!
115//! ### Management & Monitoring
116//! - [`management`]: REST API for server control and monitoring
117//! - [`management_ws`]: WebSocket API for real-time updates
118//! - [`sse`]: Server-Sent Events for log streaming
119//! - [`request_logging`]: Comprehensive request/response logging
120//! - [`metrics_middleware`]: Performance metrics collection
121//!
122//! ### Advanced Features
123//! - [`ai_handler`]: AI-powered response generation
124//! - [`auth`]: Authentication and authorization
125//! - [`chain_handlers`]: Multi-step request workflows
126//! - [`latency_profiles`]: Configurable latency simulation
127//! - [`replay_listing`]: Fixture management
128//!
129//! ## Middleware
130//!
131//! MockForge HTTP includes several middleware layers:
132//!
133//! - **Request Tracing**: [`http_tracing_middleware`] - Distributed tracing integration
134//! - **Metrics Collection**: [`metrics_middleware`] - Prometheus-compatible metrics
135//! - **Operation Metadata**: [`op_middleware`] - OpenAPI operation tracking
136//!
137//! ## Management API Endpoints
138//!
139//! When using the management router, these endpoints are available:
140//!
141//! - `GET /health` - Health check
142//! - `GET /stats` - Server statistics
143//! - `GET /logs` - Request logs (SSE stream)
144//! - `GET /metrics` - Performance metrics
145//! - `GET /fixtures` - List available fixtures
146//! - `POST /config/*` - Update configuration
147//!
148//! ## Examples
149//!
150//! See the [examples directory](https://github.com/SaaSy-Solutions/mockforge/tree/main/examples)
151//! for complete working examples.
152//!
153//! ## Related Crates
154//!
155//! - [`mockforge-core`](https://docs.rs/mockforge-core): Core mocking functionality
156//! - [`mockforge-data`](https://docs.rs/mockforge-data): Synthetic data generation
157//! - [`mockforge-plugin-core`](https://docs.rs/mockforge-plugin-core): Plugin development
158//!
159//! ## Documentation
160//!
161//! - [MockForge Book](https://docs.mockforge.dev/)
162//! - [HTTP Mocking Guide](https://docs.mockforge.dev/user-guide/http-mocking.html)
163//! - [API Reference](https://docs.rs/mockforge-http)
164
165pub mod ai_handler;
166pub mod auth;
167pub mod chain_handlers;
168pub mod coverage;
169/// Kubernetes-native health check endpoints (liveness, readiness, startup probes)
170pub mod health;
171pub mod http_tracing_middleware;
172/// Latency profile configuration for HTTP request simulation
173pub mod latency_profiles;
174/// Management API for server control and monitoring
175pub mod management;
176/// WebSocket-based management API for real-time updates
177pub mod management_ws;
178pub mod metrics_middleware;
179pub mod middleware;
180pub mod op_middleware;
181/// Browser/Mobile Proxy Server
182pub mod proxy_server;
183/// Quick mock generation utilities
184pub mod quick_mock;
185/// RAG-powered AI response generation
186pub mod rag_ai_generator;
187/// Replay listing and fixture management
188pub mod replay_listing;
189pub mod request_logging;
190/// Specification import API for OpenAPI and AsyncAPI
191pub mod spec_import;
192/// Server-Sent Events for streaming logs and metrics
193pub mod sse;
194/// TLS/HTTPS support
195pub mod tls;
196/// Token response utilities
197pub mod token_response;
198/// UI Builder API for low-code mock endpoint creation
199pub mod ui_builder;
200
201// Re-export AI handler utilities
202pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
203// Re-export health check utilities
204pub use health::{HealthManager, ServiceStatus};
205
206// Re-export management API utilities
207pub use management::{
208    management_router, management_router_with_ui_builder, ManagementState, MockConfig,
209    ServerConfig, ServerStats,
210};
211
212// Re-export UI Builder utilities
213pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
214
215// Re-export management WebSocket utilities
216pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
217
218// Re-export metrics middleware
219pub use metrics_middleware::collect_http_metrics;
220
221// Re-export tracing middleware
222pub use http_tracing_middleware::http_tracing_middleware;
223
224// Re-export coverage utilities
225pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
226
227use axum::middleware::from_fn_with_state;
228use axum::{extract::State, response::Json, Router};
229use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
230use mockforge_core::latency::LatencyInjector;
231use mockforge_core::openapi::OpenApiSpec;
232use mockforge_core::openapi_routes::OpenApiRouteRegistry;
233use mockforge_core::openapi_routes::ValidationOptions;
234
235use mockforge_core::LatencyProfile;
236#[cfg(feature = "data-faker")]
237use mockforge_data::provider::register_core_faker_provider;
238use std::collections::HashMap;
239use std::ffi::OsStr;
240use std::path::Path;
241use tokio::fs;
242use tokio::sync::RwLock;
243use tracing::*;
244
245/// Route info for storing in state
246#[derive(Clone)]
247pub struct RouteInfo {
248    /// HTTP method (GET, POST, PUT, etc.)
249    pub method: String,
250    /// API path pattern (e.g., "/api/users/{id}")
251    pub path: String,
252    /// OpenAPI operation ID if available
253    pub operation_id: Option<String>,
254    /// Operation summary from OpenAPI spec
255    pub summary: Option<String>,
256    /// Operation description from OpenAPI spec
257    pub description: Option<String>,
258    /// List of parameter names for this route
259    pub parameters: Vec<String>,
260}
261
262/// Shared state for tracking OpenAPI routes
263#[derive(Clone)]
264pub struct HttpServerState {
265    /// List of registered routes from OpenAPI spec
266    pub routes: Vec<RouteInfo>,
267    /// Optional global rate limiter for request throttling
268    pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
269}
270
271impl Default for HttpServerState {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl HttpServerState {
278    /// Create a new empty HTTP server state
279    pub fn new() -> Self {
280        Self {
281            routes: Vec::new(),
282            rate_limiter: None,
283        }
284    }
285
286    /// Create HTTP server state with pre-configured routes
287    pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
288        Self {
289            routes,
290            rate_limiter: None,
291        }
292    }
293
294    /// Add a rate limiter to the HTTP server state
295    pub fn with_rate_limiter(
296        mut self,
297        rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
298    ) -> Self {
299        self.rate_limiter = Some(rate_limiter);
300        self
301    }
302}
303
304/// Handler to return OpenAPI routes information
305async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
306    let route_info: Vec<serde_json::Value> = state
307        .routes
308        .iter()
309        .map(|route| {
310            serde_json::json!({
311                "method": route.method,
312                "path": route.path,
313                "operation_id": route.operation_id,
314                "summary": route.summary,
315                "description": route.description,
316                "parameters": route.parameters
317            })
318        })
319        .collect();
320
321    Json(serde_json::json!({
322        "routes": route_info,
323        "total": state.routes.len()
324    }))
325}
326
327/// Build the base HTTP router, optionally from an OpenAPI spec.
328pub async fn build_router(
329    spec_path: Option<String>,
330    options: Option<ValidationOptions>,
331    failure_config: Option<FailureConfig>,
332) -> Router {
333    build_router_with_multi_tenant(spec_path, options, failure_config, None, None, None, None, None)
334        .await
335}
336
337/// Build the base HTTP router with multi-tenant workspace support
338#[allow(clippy::too_many_arguments)]
339pub async fn build_router_with_multi_tenant(
340    spec_path: Option<String>,
341    options: Option<ValidationOptions>,
342    failure_config: Option<FailureConfig>,
343    multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
344    _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
345    _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
346    ai_generator: Option<
347        std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
348    >,
349    smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
350) -> Router {
351    use std::time::Instant;
352
353    let startup_start = Instant::now();
354
355    // Set up the basic router
356    let mut app = Router::new();
357
358    // Initialize rate limiter with default configuration
359    // Can be customized via environment variables or config
360    let rate_limit_config = crate::middleware::RateLimitConfig {
361        requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
362            .ok()
363            .and_then(|v| v.parse().ok())
364            .unwrap_or(1000),
365        burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
366            .ok()
367            .and_then(|v| v.parse().ok())
368            .unwrap_or(2000),
369        per_ip: true,
370        per_endpoint: false,
371    };
372    let rate_limiter =
373        std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
374
375    let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
376
377    // Clone spec_path for later use
378    let spec_path_for_mgmt = spec_path.clone();
379
380    // If an OpenAPI spec is provided, integrate it
381    if let Some(spec_path) = spec_path {
382        tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
383
384        // Measure OpenAPI spec loading
385        let spec_load_start = Instant::now();
386        match OpenApiSpec::from_file(&spec_path).await {
387            Ok(openapi) => {
388                let spec_load_duration = spec_load_start.elapsed();
389                info!(
390                    "Successfully loaded OpenAPI spec from {} (took {:?})",
391                    spec_path, spec_load_duration
392                );
393
394                // Measure route registry creation
395                tracing::debug!("Creating OpenAPI route registry...");
396                let registry_start = Instant::now();
397                let registry = if let Some(opts) = options {
398                    tracing::debug!("Using custom validation options");
399                    OpenApiRouteRegistry::new_with_options(openapi, opts)
400                } else {
401                    tracing::debug!("Using environment-based options");
402                    OpenApiRouteRegistry::new_with_env(openapi)
403                };
404                let registry_duration = registry_start.elapsed();
405                info!(
406                    "Created OpenAPI route registry with {} routes (took {:?})",
407                    registry.routes().len(),
408                    registry_duration
409                );
410
411                // Measure route extraction
412                let extract_start = Instant::now();
413                let route_info: Vec<RouteInfo> = registry
414                    .routes()
415                    .iter()
416                    .map(|route| RouteInfo {
417                        method: route.method.clone(),
418                        path: route.path.clone(),
419                        operation_id: route.operation.operation_id.clone(),
420                        summary: route.operation.summary.clone(),
421                        description: route.operation.description.clone(),
422                        parameters: route.parameters.clone(),
423                    })
424                    .collect();
425                state.routes = route_info;
426                let extract_duration = extract_start.elapsed();
427                debug!("Extracted route information (took {:?})", extract_duration);
428
429                // Measure overrides loading
430                let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
431                    tracing::debug!("Loading overrides from environment variable");
432                    let overrides_start = Instant::now();
433                    match mockforge_core::Overrides::load_from_globs(&[]).await {
434                        Ok(overrides) => {
435                            let overrides_duration = overrides_start.elapsed();
436                            info!(
437                                "Loaded {} override rules (took {:?})",
438                                overrides.rules().len(),
439                                overrides_duration
440                            );
441                            Some(overrides)
442                        }
443                        Err(e) => {
444                            tracing::warn!("Failed to load overrides: {}", e);
445                            None
446                        }
447                    }
448                } else {
449                    None
450                };
451
452                // Measure router building
453                let router_build_start = Instant::now();
454                let overrides_enabled = overrides.is_some();
455                let openapi_router = if let Some(ai_generator) = &ai_generator {
456                    tracing::debug!("Building router with AI generator support");
457                    registry.build_router_with_ai(Some(ai_generator.clone()))
458                } else if let Some(failure_config) = &failure_config {
459                    tracing::debug!("Building router with failure injection and overrides");
460                    let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
461                    registry.build_router_with_injectors_and_overrides(
462                        LatencyInjector::default(),
463                        Some(failure_injector),
464                        overrides,
465                        overrides_enabled,
466                    )
467                } else {
468                    tracing::debug!("Building router with overrides");
469                    registry.build_router_with_injectors_and_overrides(
470                        LatencyInjector::default(),
471                        None,
472                        overrides,
473                        overrides_enabled,
474                    )
475                };
476                let router_build_duration = router_build_start.elapsed();
477                debug!("Built OpenAPI router (took {:?})", router_build_duration);
478
479                tracing::debug!("Merging OpenAPI router with main router");
480                app = app.merge(openapi_router);
481                tracing::debug!("Router built successfully");
482            }
483            Err(e) => {
484                warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
485            }
486        }
487    }
488
489    // Add basic health check endpoint
490    app = app.route(
491        "/health",
492        axum::routing::get(|| async {
493            use mockforge_core::server_utils::health::HealthStatus;
494            {
495                // HealthStatus should always serialize, but handle errors gracefully
496                match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
497                    Ok(value) => axum::Json(value),
498                    Err(e) => {
499                        // Log error but return a simple healthy response
500                        tracing::error!("Failed to serialize health status: {}", e);
501                        axum::Json(serde_json::json!({
502                            "status": "healthy",
503                            "service": "mockforge-http",
504                            "uptime_seconds": 0
505                        }))
506                    }
507                }
508            }
509        }),
510    )
511    // Add SSE endpoints
512    .merge(sse::sse_router());
513
514    // Clone state for routes_router since we'll use it for middleware too
515    let state_for_routes = state.clone();
516
517    // Create a router with state for the routes and coverage endpoints
518    let routes_router = Router::new()
519        .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
520        .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
521        .with_state(state_for_routes);
522
523    // Merge the routes router with the main app
524    app = app.merge(routes_router);
525
526    // Add static coverage UI
527    // Determine the path to the coverage.html file
528    let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
529        .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
530
531    // Check if the file exists before serving it
532    if std::path::Path::new(&coverage_html_path).exists() {
533        app = app.nest_service(
534            "/__mockforge/coverage.html",
535            tower_http::services::ServeFile::new(&coverage_html_path),
536        );
537        debug!("Serving coverage UI from: {}", coverage_html_path);
538    } else {
539        debug!(
540            "Coverage UI file not found at: {}. Skipping static file serving.",
541            coverage_html_path
542        );
543    }
544
545    // Add management API endpoints
546    let management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); // Port will be updated when we know the actual port
547    #[cfg(feature = "smtp")]
548    let management_state = {
549        if let Some(smtp_reg) = smtp_registry {
550            match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
551                Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
552                Err(e) => {
553                    error!(
554                        "Invalid SMTP registry type passed to HTTP management state: {:?}",
555                        e.type_id()
556                    );
557                    management_state
558                }
559            }
560        } else {
561            management_state
562        }
563    };
564    #[cfg(not(feature = "smtp"))]
565    let management_state = management_state;
566    #[cfg(not(feature = "smtp"))]
567    let _ = smtp_registry;
568    app = app.nest("/__mockforge/api", management_router(management_state));
569
570    // Add management WebSocket endpoint
571    let ws_state = WsManagementState::new();
572    app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
573
574    // Add request logging middleware to capture all requests
575    app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
576
577    // Add rate limiting middleware (before logging to rate limit early)
578    app = app.layer(from_fn_with_state(state, crate::middleware::rate_limit_middleware));
579
580    // Add workspace routing middleware if multi-tenant is enabled
581    if let Some(mt_config) = multi_tenant_config {
582        if mt_config.enabled {
583            use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
584            use std::sync::Arc;
585
586            info!(
587                "Multi-tenant mode enabled with {} routing strategy",
588                match mt_config.routing_strategy {
589                    mockforge_core::RoutingStrategy::Path => "path-based",
590                    mockforge_core::RoutingStrategy::Port => "port-based",
591                    mockforge_core::RoutingStrategy::Both => "hybrid",
592                }
593            );
594
595            // Create the multi-tenant workspace registry
596            let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
597
598            // Register the default workspace before wrapping in Arc
599            let default_workspace =
600                mockforge_core::Workspace::new(mt_config.default_workspace.clone());
601            if let Err(e) =
602                registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
603            {
604                warn!("Failed to register default workspace: {}", e);
605            } else {
606                info!("Registered default workspace: '{}'", mt_config.default_workspace);
607            }
608
609            // Auto-discover and register workspaces if configured
610            if mt_config.auto_discover {
611                if let Some(config_dir) = &mt_config.config_directory {
612                    let config_path = Path::new(config_dir);
613                    if config_path.exists() && config_path.is_dir() {
614                        match fs::read_dir(config_path).await {
615                            Ok(mut entries) => {
616                                while let Ok(Some(entry)) = entries.next_entry().await {
617                                    let path = entry.path();
618                                    if path.extension() == Some(OsStr::new("yaml")) {
619                                        match fs::read_to_string(&path).await {
620                                            Ok(content) => {
621                                                match serde_yaml::from_str::<
622                                                    mockforge_core::Workspace,
623                                                >(
624                                                    &content
625                                                ) {
626                                                    Ok(workspace) => {
627                                                        if let Err(e) = registry.register_workspace(
628                                                            workspace.id.clone(),
629                                                            workspace,
630                                                        ) {
631                                                            warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
632                                                        } else {
633                                                            info!("Auto-registered workspace from {:?}", path);
634                                                        }
635                                                    }
636                                                    Err(e) => {
637                                                        warn!("Failed to parse workspace from {:?}: {}", path, e);
638                                                    }
639                                                }
640                                            }
641                                            Err(e) => {
642                                                warn!(
643                                                    "Failed to read workspace file {:?}: {}",
644                                                    path, e
645                                                );
646                                            }
647                                        }
648                                    }
649                                }
650                            }
651                            Err(e) => {
652                                warn!("Failed to read config directory {:?}: {}", config_path, e);
653                            }
654                        }
655                    } else {
656                        warn!(
657                            "Config directory {:?} does not exist or is not a directory",
658                            config_path
659                        );
660                    }
661                }
662            }
663
664            // Wrap registry in Arc for shared access
665            let registry = Arc::new(registry);
666
667            // Create workspace router and wrap the app with workspace middleware
668            let _workspace_router = WorkspaceRouter::new(registry);
669
670            // Note: The actual middleware integration would need to be implemented
671            // in the WorkspaceRouter to work with Axum's middleware system
672            info!("Workspace routing middleware initialized for HTTP server");
673        }
674    }
675
676    let total_startup_duration = startup_start.elapsed();
677    info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
678
679    app
680}
681
682/// Build the base HTTP router with authentication and latency support
683pub async fn build_router_with_auth_and_latency(
684    _spec_path: Option<String>,
685    _options: Option<()>,
686    _auth_config: Option<mockforge_core::config::AuthConfig>,
687    _latency_injector: Option<LatencyInjector>,
688) -> Router {
689    // For now, just use the basic router. Full auth and latency support can be added later.
690    build_router(None, None, None).await
691}
692
693/// Build the base HTTP router with latency injection support
694pub async fn build_router_with_latency(
695    _spec_path: Option<String>,
696    _options: Option<ValidationOptions>,
697    _latency_injector: Option<LatencyInjector>,
698) -> Router {
699    // For now, fall back to basic router since injectors are complex to implement
700    build_router(None, None, None).await
701}
702
703/// Build the base HTTP router with authentication support
704pub async fn build_router_with_auth(
705    spec_path: Option<String>,
706    options: Option<ValidationOptions>,
707    auth_config: Option<mockforge_core::config::AuthConfig>,
708) -> Router {
709    use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
710    use std::sync::Arc;
711
712    // If richer faker is available, register provider once (idempotent)
713    #[cfg(feature = "data-faker")]
714    {
715        register_core_faker_provider();
716    }
717
718    // Set up authentication state
719    let spec = if let Some(spec_path) = &spec_path {
720        match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
721            Ok(spec) => Some(Arc::new(spec)),
722            Err(e) => {
723                warn!("Failed to load OpenAPI spec for auth: {}", e);
724                None
725            }
726        }
727    } else {
728        None
729    };
730
731    // Create OAuth2 client if configured
732    let oauth2_client = if let Some(auth_config) = &auth_config {
733        if let Some(oauth2_config) = &auth_config.oauth2 {
734            match create_oauth2_client(oauth2_config) {
735                Ok(client) => Some(client),
736                Err(e) => {
737                    warn!("Failed to create OAuth2 client: {}", e);
738                    None
739                }
740            }
741        } else {
742            None
743        }
744    } else {
745        None
746    };
747
748    let auth_state = AuthState {
749        config: auth_config.unwrap_or_default(),
750        spec,
751        oauth2_client,
752        introspection_cache: Arc::new(RwLock::new(HashMap::new())),
753    };
754
755    // Set up the basic router with auth state
756    let mut app = Router::new().with_state(auth_state.clone());
757
758    // If an OpenAPI spec is provided, integrate it
759    if let Some(spec_path) = spec_path {
760        match OpenApiSpec::from_file(&spec_path).await {
761            Ok(openapi) => {
762                info!("Loaded OpenAPI spec from {}", spec_path);
763                let registry = if let Some(opts) = options {
764                    OpenApiRouteRegistry::new_with_options(openapi, opts)
765                } else {
766                    OpenApiRouteRegistry::new_with_env(openapi)
767                };
768
769                app = registry.build_router();
770            }
771            Err(e) => {
772                warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
773            }
774        }
775    }
776
777    // Add basic health check endpoint
778    app = app.route(
779        "/health",
780        axum::routing::get(|| async {
781            use mockforge_core::server_utils::health::HealthStatus;
782            {
783                // HealthStatus should always serialize, but handle errors gracefully
784                match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
785                    Ok(value) => axum::Json(value),
786                    Err(e) => {
787                        // Log error but return a simple healthy response
788                        tracing::error!("Failed to serialize health status: {}", e);
789                        axum::Json(serde_json::json!({
790                            "status": "healthy",
791                            "service": "mockforge-http",
792                            "uptime_seconds": 0
793                        }))
794                    }
795                }
796            }
797        }),
798    )
799    // Add SSE endpoints
800    .merge(sse::sse_router())
801    // Add authentication middleware (before logging)
802    .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
803    // Add request logging middleware
804    .layer(axum::middleware::from_fn(request_logging::log_http_requests));
805
806    app
807}
808
809/// Serve a provided router on the given port.
810pub async fn serve_router(
811    port: u16,
812    app: Router,
813) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
814    serve_router_with_tls(port, app, None).await
815}
816
817/// Serve a provided router on the given port with optional TLS support.
818pub async fn serve_router_with_tls(
819    port: u16,
820    app: Router,
821    tls_config: Option<mockforge_core::config::HttpTlsConfig>,
822) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
823    use std::net::SocketAddr;
824
825    let addr = mockforge_core::wildcard_socket_addr(port);
826
827    if let Some(ref tls) = tls_config {
828        if tls.enabled {
829            info!("HTTPS listening on {}", addr);
830            return serve_with_tls(addr, app, tls).await;
831        }
832    }
833
834    info!("HTTP listening on {}", addr);
835
836    let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
837        format!(
838            "Failed to bind HTTP server to port {}: {}\n\
839             Hint: The port may already be in use. Try using a different port with --http-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
840            port, e, port, port
841        )
842    })?;
843
844    axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
845    Ok(())
846}
847
848/// Serve router with TLS/HTTPS support
849///
850/// Note: This is a simplified implementation. For production use, consider using
851/// a reverse proxy (nginx) for TLS termination, or use axum-server crate.
852/// This implementation validates TLS configuration but recommends using a reverse proxy.
853async fn serve_with_tls(
854    addr: std::net::SocketAddr,
855    _app: Router,
856    tls_config: &mockforge_core::config::HttpTlsConfig,
857) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
858    // Validate TLS configuration by attempting to load certificates
859    let _acceptor = tls::load_tls_acceptor(tls_config)?;
860
861    // For now, return an informative error suggesting reverse proxy usage
862    // Full TLS implementation with axum requires axum-server or similar
863    Err(format!(
864        "TLS/HTTPS support is configured but requires a reverse proxy (nginx) for production use.\n\
865         Certificate validation passed: {} and {}\n\
866         For native TLS support, please use a reverse proxy or wait for axum-server integration.\n\
867         You can configure nginx with TLS termination pointing to the HTTP server on port {}.",
868        tls_config.cert_file,
869        tls_config.key_file,
870        addr.port()
871    )
872    .into())
873}
874
875/// Backwards-compatible start that builds + serves the base router.
876pub async fn start(
877    port: u16,
878    spec_path: Option<String>,
879    options: Option<ValidationOptions>,
880) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
881    start_with_latency(port, spec_path, options, None).await
882}
883
884/// Start HTTP server with authentication and latency support
885pub async fn start_with_auth_and_latency(
886    port: u16,
887    spec_path: Option<String>,
888    options: Option<ValidationOptions>,
889    auth_config: Option<mockforge_core::config::AuthConfig>,
890    latency_profile: Option<LatencyProfile>,
891) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
892    start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
893        .await
894}
895
896/// Start HTTP server with authentication and injectors support
897pub async fn start_with_auth_and_injectors(
898    port: u16,
899    spec_path: Option<String>,
900    options: Option<ValidationOptions>,
901    auth_config: Option<mockforge_core::config::AuthConfig>,
902    _latency_profile: Option<LatencyProfile>,
903    _failure_injector: Option<mockforge_core::FailureInjector>,
904) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
905    // For now, ignore latency and failure injectors and just use auth
906    let app = build_router_with_auth(spec_path, options, auth_config).await;
907    serve_router(port, app).await
908}
909
910/// Start HTTP server with latency injection support
911pub async fn start_with_latency(
912    port: u16,
913    spec_path: Option<String>,
914    options: Option<ValidationOptions>,
915    latency_profile: Option<LatencyProfile>,
916) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
917    let latency_injector =
918        latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
919
920    let app = build_router_with_latency(spec_path, options, latency_injector).await;
921    serve_router(port, app).await
922}
923
924/// Build the base HTTP router with chaining support
925pub async fn build_router_with_chains(
926    spec_path: Option<String>,
927    options: Option<ValidationOptions>,
928    circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
929) -> Router {
930    build_router_with_chains_and_multi_tenant(
931        spec_path,
932        options,
933        circling_config,
934        None,
935        None,
936        None,
937        None,
938        None,
939        None,
940        None,
941        false,
942        None, // health_manager
943    )
944    .await
945}
946
947/// Build the base HTTP router with chaining and multi-tenant support
948#[allow(clippy::too_many_arguments)]
949pub async fn build_router_with_chains_and_multi_tenant(
950    spec_path: Option<String>,
951    options: Option<ValidationOptions>,
952    _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
953    multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
954    _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
955    _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
956    _ai_generator: Option<
957        std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
958    >,
959    smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
960    mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
961    traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
962    traffic_shaping_enabled: bool,
963    health_manager: Option<std::sync::Arc<health::HealthManager>>,
964) -> Router {
965    use crate::latency_profiles::LatencyProfiles;
966    use crate::op_middleware::Shared;
967    use mockforge_core::Overrides;
968
969    let _shared = Shared {
970        profiles: LatencyProfiles::default(),
971        overrides: Overrides::default(),
972        failure_injector: None,
973        traffic_shaper,
974        overrides_enabled: false,
975        traffic_shaping_enabled,
976    };
977
978    // Start with basic router
979    let mut app = Router::new();
980    let mut include_default_health = true;
981
982    // If an OpenAPI spec is provided, integrate it
983    if let Some(ref spec) = spec_path {
984        match OpenApiSpec::from_file(&spec).await {
985            Ok(openapi) => {
986                info!("Loaded OpenAPI spec from {}", spec);
987                let registry = if let Some(opts) = options {
988                    OpenApiRouteRegistry::new_with_options(openapi, opts)
989                } else {
990                    OpenApiRouteRegistry::new_with_env(openapi)
991                };
992                if registry
993                    .routes()
994                    .iter()
995                    .any(|route| route.method == "GET" && route.path == "/health")
996                {
997                    include_default_health = false;
998                }
999                let spec_router = registry.build_router();
1000                app = app.merge(spec_router);
1001            }
1002            Err(e) => {
1003                warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1004            }
1005        }
1006    }
1007
1008    // Add health check endpoints
1009    if let Some(health) = health_manager {
1010        // Use comprehensive health check router with all probe endpoints
1011        app = app.merge(health::health_router(health));
1012        info!(
1013            "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1014        );
1015    } else if include_default_health {
1016        // Fallback to basic health endpoint for backwards compatibility
1017        app = app.route(
1018            "/health",
1019            axum::routing::get(|| async {
1020                use mockforge_core::server_utils::health::HealthStatus;
1021                {
1022                    // HealthStatus should always serialize, but handle errors gracefully
1023                    match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1024                        Ok(value) => axum::Json(value),
1025                        Err(e) => {
1026                            // Log error but return a simple healthy response
1027                            tracing::error!("Failed to serialize health status: {}", e);
1028                            axum::Json(serde_json::json!({
1029                                "status": "healthy",
1030                                "service": "mockforge-http",
1031                                "uptime_seconds": 0
1032                            }))
1033                        }
1034                    }
1035                }
1036            }),
1037        );
1038    }
1039
1040    app = app.merge(sse::sse_router());
1041
1042    // Add management API endpoints
1043    let management_state = ManagementState::new(None, spec_path, 3000); // Port will be updated when we know the actual port
1044    #[cfg(feature = "smtp")]
1045    let management_state = {
1046        if let Some(smtp_reg) = smtp_registry {
1047            match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1048                Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1049                Err(e) => {
1050                    error!(
1051                        "Invalid SMTP registry type passed to HTTP management state: {:?}",
1052                        e.type_id()
1053                    );
1054                    management_state
1055                }
1056            }
1057        } else {
1058            management_state
1059        }
1060    };
1061    #[cfg(not(feature = "smtp"))]
1062    let management_state = {
1063        let _ = smtp_registry;
1064        management_state
1065    };
1066    #[cfg(feature = "mqtt")]
1067    let management_state = {
1068        if let Some(broker) = mqtt_broker {
1069            match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1070                Ok(broker) => management_state.with_mqtt_broker(broker),
1071                Err(e) => {
1072                    error!(
1073                        "Invalid MQTT broker passed to HTTP management state: {:?}",
1074                        e.type_id()
1075                    );
1076                    management_state
1077                }
1078            }
1079        } else {
1080            management_state
1081        }
1082    };
1083    #[cfg(not(feature = "mqtt"))]
1084    let management_state = {
1085        let _ = mqtt_broker;
1086        management_state
1087    };
1088    app = app.nest("/__mockforge/api", management_router(management_state));
1089
1090    // Add workspace routing middleware if multi-tenant is enabled
1091    if let Some(mt_config) = multi_tenant_config {
1092        if mt_config.enabled {
1093            use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1094            use std::sync::Arc;
1095
1096            info!(
1097                "Multi-tenant mode enabled with {} routing strategy",
1098                match mt_config.routing_strategy {
1099                    mockforge_core::RoutingStrategy::Path => "path-based",
1100                    mockforge_core::RoutingStrategy::Port => "port-based",
1101                    mockforge_core::RoutingStrategy::Both => "hybrid",
1102                }
1103            );
1104
1105            // Create the multi-tenant workspace registry
1106            let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1107
1108            // Register the default workspace before wrapping in Arc
1109            let default_workspace =
1110                mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1111            if let Err(e) =
1112                registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1113            {
1114                warn!("Failed to register default workspace: {}", e);
1115            } else {
1116                info!("Registered default workspace: '{}'", mt_config.default_workspace);
1117            }
1118
1119            // Wrap registry in Arc for shared access
1120            let registry = Arc::new(registry);
1121
1122            // Create workspace router
1123            let _workspace_router = WorkspaceRouter::new(registry);
1124            info!("Workspace routing middleware initialized for HTTP server");
1125        }
1126    }
1127
1128    app
1129}
1130
1131// Note: start_with_traffic_shaping function removed due to compilation issues
1132// Use build_router_with_traffic_shaping_and_multi_tenant directly instead
1133
1134#[test]
1135fn test_route_info_clone() {
1136    let route = RouteInfo {
1137        method: "POST".to_string(),
1138        path: "/users".to_string(),
1139        operation_id: Some("createUser".to_string()),
1140        summary: None,
1141        description: None,
1142        parameters: vec![],
1143    };
1144
1145    let cloned = route.clone();
1146    assert_eq!(route.method, cloned.method);
1147    assert_eq!(route.path, cloned.path);
1148    assert_eq!(route.operation_id, cloned.operation_id);
1149}
1150
1151#[test]
1152fn test_http_server_state_new() {
1153    let state = HttpServerState::new();
1154    assert_eq!(state.routes.len(), 0);
1155}
1156
1157#[test]
1158fn test_http_server_state_with_routes() {
1159    let routes = vec![
1160        RouteInfo {
1161            method: "GET".to_string(),
1162            path: "/users".to_string(),
1163            operation_id: Some("getUsers".to_string()),
1164            summary: None,
1165            description: None,
1166            parameters: vec![],
1167        },
1168        RouteInfo {
1169            method: "POST".to_string(),
1170            path: "/users".to_string(),
1171            operation_id: Some("createUser".to_string()),
1172            summary: None,
1173            description: None,
1174            parameters: vec![],
1175        },
1176    ];
1177
1178    let state = HttpServerState::with_routes(routes.clone());
1179    assert_eq!(state.routes.len(), 2);
1180    assert_eq!(state.routes[0].method, "GET");
1181    assert_eq!(state.routes[1].method, "POST");
1182}
1183
1184#[test]
1185fn test_http_server_state_clone() {
1186    let routes = vec![RouteInfo {
1187        method: "GET".to_string(),
1188        path: "/test".to_string(),
1189        operation_id: None,
1190        summary: None,
1191        description: None,
1192        parameters: vec![],
1193    }];
1194
1195    let state = HttpServerState::with_routes(routes);
1196    let cloned = state.clone();
1197
1198    assert_eq!(state.routes.len(), cloned.routes.len());
1199    assert_eq!(state.routes[0].method, cloned.routes[0].method);
1200}
1201
1202#[tokio::test]
1203async fn test_build_router_without_openapi() {
1204    let _router = build_router(None, None, None).await;
1205    // Should succeed without OpenAPI spec
1206}
1207
1208#[tokio::test]
1209async fn test_build_router_with_nonexistent_spec() {
1210    let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
1211    // Should succeed but log a warning
1212}
1213
1214#[tokio::test]
1215async fn test_build_router_with_auth_and_latency() {
1216    let _router = build_router_with_auth_and_latency(None, None, None, None).await;
1217    // Should succeed without parameters
1218}
1219
1220#[tokio::test]
1221async fn test_build_router_with_latency() {
1222    let _router = build_router_with_latency(None, None, None).await;
1223    // Should succeed without parameters
1224}
1225
1226#[tokio::test]
1227async fn test_build_router_with_auth() {
1228    let _router = build_router_with_auth(None, None, None).await;
1229    // Should succeed without parameters
1230}
1231
1232#[tokio::test]
1233async fn test_build_router_with_chains() {
1234    let _router = build_router_with_chains(None, None, None).await;
1235    // Should succeed without parameters
1236}
1237
1238#[test]
1239fn test_route_info_with_all_fields() {
1240    let route = RouteInfo {
1241        method: "PUT".to_string(),
1242        path: "/users/{id}".to_string(),
1243        operation_id: Some("updateUser".to_string()),
1244        summary: Some("Update user".to_string()),
1245        description: Some("Updates an existing user".to_string()),
1246        parameters: vec!["id".to_string(), "body".to_string()],
1247    };
1248
1249    assert!(route.operation_id.is_some());
1250    assert!(route.summary.is_some());
1251    assert!(route.description.is_some());
1252    assert_eq!(route.parameters.len(), 2);
1253}
1254
1255#[test]
1256fn test_route_info_with_minimal_fields() {
1257    let route = RouteInfo {
1258        method: "DELETE".to_string(),
1259        path: "/users/{id}".to_string(),
1260        operation_id: None,
1261        summary: None,
1262        description: None,
1263        parameters: vec![],
1264    };
1265
1266    assert!(route.operation_id.is_none());
1267    assert!(route.summary.is_none());
1268    assert!(route.description.is_none());
1269    assert_eq!(route.parameters.len(), 0);
1270}
1271
1272#[test]
1273fn test_http_server_state_empty_routes() {
1274    let state = HttpServerState::with_routes(vec![]);
1275    assert_eq!(state.routes.len(), 0);
1276}
1277
1278#[test]
1279fn test_http_server_state_multiple_routes() {
1280    let routes = vec![
1281        RouteInfo {
1282            method: "GET".to_string(),
1283            path: "/users".to_string(),
1284            operation_id: Some("listUsers".to_string()),
1285            summary: Some("List all users".to_string()),
1286            description: None,
1287            parameters: vec![],
1288        },
1289        RouteInfo {
1290            method: "GET".to_string(),
1291            path: "/users/{id}".to_string(),
1292            operation_id: Some("getUser".to_string()),
1293            summary: Some("Get a user".to_string()),
1294            description: None,
1295            parameters: vec!["id".to_string()],
1296        },
1297        RouteInfo {
1298            method: "POST".to_string(),
1299            path: "/users".to_string(),
1300            operation_id: Some("createUser".to_string()),
1301            summary: Some("Create a user".to_string()),
1302            description: None,
1303            parameters: vec!["body".to_string()],
1304        },
1305    ];
1306
1307    let state = HttpServerState::with_routes(routes);
1308    assert_eq!(state.routes.len(), 3);
1309
1310    // Verify different HTTP methods
1311    let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
1312    assert!(methods.contains(&"GET"));
1313    assert!(methods.contains(&"POST"));
1314}
1315
1316#[test]
1317fn test_http_server_state_with_rate_limiter() {
1318    use std::sync::Arc;
1319
1320    let config = crate::middleware::RateLimitConfig::default();
1321    let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
1322
1323    let state = HttpServerState::new().with_rate_limiter(rate_limiter);
1324
1325    assert!(state.rate_limiter.is_some());
1326    assert_eq!(state.routes.len(), 0);
1327}
1328
1329#[tokio::test]
1330async fn test_build_router_includes_rate_limiter() {
1331    let _router = build_router(None, None, None).await;
1332    // Router should be created successfully with rate limiter initialized
1333}