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