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