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