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