1pub mod ai_handler;
166pub mod auth;
167pub mod chain_handlers;
168pub mod coverage;
169pub mod health;
171pub mod http_tracing_middleware;
172pub mod latency_profiles;
174pub mod management;
176pub mod management_ws;
178pub mod metrics_middleware;
179pub mod middleware;
180pub mod op_middleware;
181pub mod proxy_server;
183pub mod quick_mock;
185pub mod rag_ai_generator;
187pub mod replay_listing;
189pub mod request_logging;
190pub mod spec_import;
192pub mod sse;
194pub mod tls;
196pub mod token_response;
198pub mod ui_builder;
200
201pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
203pub use health::{HealthManager, ServiceStatus};
205
206pub use management::{
208 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
209 ServerConfig, ServerStats,
210};
211
212pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
214
215pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
217
218pub use metrics_middleware::collect_http_metrics;
220
221pub use http_tracing_middleware::http_tracing_middleware;
223
224pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
226
227use axum::middleware::from_fn_with_state;
228use axum::{extract::State, response::Json, Router};
229use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
230use mockforge_core::latency::LatencyInjector;
231use mockforge_core::openapi::OpenApiSpec;
232use mockforge_core::openapi_routes::OpenApiRouteRegistry;
233use mockforge_core::openapi_routes::ValidationOptions;
234
235use mockforge_core::LatencyProfile;
236#[cfg(feature = "data-faker")]
237use mockforge_data::provider::register_core_faker_provider;
238use std::collections::HashMap;
239use std::ffi::OsStr;
240use std::path::Path;
241use tokio::fs;
242use tokio::sync::RwLock;
243use tracing::*;
244
245#[derive(Clone)]
247pub struct RouteInfo {
248 pub method: String,
250 pub path: String,
252 pub operation_id: Option<String>,
254 pub summary: Option<String>,
256 pub description: Option<String>,
258 pub parameters: Vec<String>,
260}
261
262#[derive(Clone)]
264pub struct HttpServerState {
265 pub routes: Vec<RouteInfo>,
267 pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
269}
270
271impl Default for HttpServerState {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl HttpServerState {
278 pub fn new() -> Self {
280 Self {
281 routes: Vec::new(),
282 rate_limiter: None,
283 }
284 }
285
286 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
288 Self {
289 routes,
290 rate_limiter: None,
291 }
292 }
293
294 pub fn with_rate_limiter(
296 mut self,
297 rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
298 ) -> Self {
299 self.rate_limiter = Some(rate_limiter);
300 self
301 }
302}
303
304async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
306 let route_info: Vec<serde_json::Value> = state
307 .routes
308 .iter()
309 .map(|route| {
310 serde_json::json!({
311 "method": route.method,
312 "path": route.path,
313 "operation_id": route.operation_id,
314 "summary": route.summary,
315 "description": route.description,
316 "parameters": route.parameters
317 })
318 })
319 .collect();
320
321 Json(serde_json::json!({
322 "routes": route_info,
323 "total": state.routes.len()
324 }))
325}
326
327pub async fn build_router(
329 spec_path: Option<String>,
330 options: Option<ValidationOptions>,
331 failure_config: Option<FailureConfig>,
332) -> Router {
333 build_router_with_multi_tenant(spec_path, options, failure_config, None, None, None, None, None)
334 .await
335}
336
337#[allow(clippy::too_many_arguments)]
339pub async fn build_router_with_multi_tenant(
340 spec_path: Option<String>,
341 options: Option<ValidationOptions>,
342 failure_config: Option<FailureConfig>,
343 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
344 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
345 _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
346 ai_generator: Option<
347 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
348 >,
349 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
350) -> Router {
351 use std::time::Instant;
352
353 let startup_start = Instant::now();
354
355 let mut app = Router::new();
357
358 let rate_limit_config = crate::middleware::RateLimitConfig {
361 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
362 .ok()
363 .and_then(|v| v.parse().ok())
364 .unwrap_or(1000),
365 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
366 .ok()
367 .and_then(|v| v.parse().ok())
368 .unwrap_or(2000),
369 per_ip: true,
370 per_endpoint: false,
371 };
372 let rate_limiter =
373 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
374
375 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
376
377 let spec_path_for_mgmt = spec_path.clone();
379
380 if let Some(spec_path) = spec_path {
382 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
383
384 let spec_load_start = Instant::now();
386 match OpenApiSpec::from_file(&spec_path).await {
387 Ok(openapi) => {
388 let spec_load_duration = spec_load_start.elapsed();
389 info!(
390 "Successfully loaded OpenAPI spec from {} (took {:?})",
391 spec_path, spec_load_duration
392 );
393
394 tracing::debug!("Creating OpenAPI route registry...");
396 let registry_start = Instant::now();
397 let registry = if let Some(opts) = options {
398 tracing::debug!("Using custom validation options");
399 OpenApiRouteRegistry::new_with_options(openapi, opts)
400 } else {
401 tracing::debug!("Using environment-based options");
402 OpenApiRouteRegistry::new_with_env(openapi)
403 };
404 let registry_duration = registry_start.elapsed();
405 info!(
406 "Created OpenAPI route registry with {} routes (took {:?})",
407 registry.routes().len(),
408 registry_duration
409 );
410
411 let extract_start = Instant::now();
413 let route_info: Vec<RouteInfo> = registry
414 .routes()
415 .iter()
416 .map(|route| RouteInfo {
417 method: route.method.clone(),
418 path: route.path.clone(),
419 operation_id: route.operation.operation_id.clone(),
420 summary: route.operation.summary.clone(),
421 description: route.operation.description.clone(),
422 parameters: route.parameters.clone(),
423 })
424 .collect();
425 state.routes = route_info;
426 let extract_duration = extract_start.elapsed();
427 debug!("Extracted route information (took {:?})", extract_duration);
428
429 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
431 tracing::debug!("Loading overrides from environment variable");
432 let overrides_start = Instant::now();
433 match mockforge_core::Overrides::load_from_globs(&[]).await {
434 Ok(overrides) => {
435 let overrides_duration = overrides_start.elapsed();
436 info!(
437 "Loaded {} override rules (took {:?})",
438 overrides.rules().len(),
439 overrides_duration
440 );
441 Some(overrides)
442 }
443 Err(e) => {
444 tracing::warn!("Failed to load overrides: {}", e);
445 None
446 }
447 }
448 } else {
449 None
450 };
451
452 let router_build_start = Instant::now();
454 let overrides_enabled = overrides.is_some();
455 let openapi_router = if let Some(ai_generator) = &ai_generator {
456 tracing::debug!("Building router with AI generator support");
457 registry.build_router_with_ai(Some(ai_generator.clone()))
458 } else if let Some(failure_config) = &failure_config {
459 tracing::debug!("Building router with failure injection and overrides");
460 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
461 registry.build_router_with_injectors_and_overrides(
462 LatencyInjector::default(),
463 Some(failure_injector),
464 overrides,
465 overrides_enabled,
466 )
467 } else {
468 tracing::debug!("Building router with overrides");
469 registry.build_router_with_injectors_and_overrides(
470 LatencyInjector::default(),
471 None,
472 overrides,
473 overrides_enabled,
474 )
475 };
476 let router_build_duration = router_build_start.elapsed();
477 debug!("Built OpenAPI router (took {:?})", router_build_duration);
478
479 tracing::debug!("Merging OpenAPI router with main router");
480 app = app.merge(openapi_router);
481 tracing::debug!("Router built successfully");
482 }
483 Err(e) => {
484 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
485 }
486 }
487 }
488
489 app = app.route(
491 "/health",
492 axum::routing::get(|| async {
493 use mockforge_core::server_utils::health::HealthStatus;
494 {
495 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
497 Ok(value) => axum::Json(value),
498 Err(e) => {
499 tracing::error!("Failed to serialize health status: {}", e);
501 axum::Json(serde_json::json!({
502 "status": "healthy",
503 "service": "mockforge-http",
504 "uptime_seconds": 0
505 }))
506 }
507 }
508 }
509 }),
510 )
511 .merge(sse::sse_router());
513
514 let state_for_routes = state.clone();
516
517 let routes_router = Router::new()
519 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
520 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
521 .with_state(state_for_routes);
522
523 app = app.merge(routes_router);
525
526 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
529 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
530
531 if std::path::Path::new(&coverage_html_path).exists() {
533 app = app.nest_service(
534 "/__mockforge/coverage.html",
535 tower_http::services::ServeFile::new(&coverage_html_path),
536 );
537 debug!("Serving coverage UI from: {}", coverage_html_path);
538 } else {
539 debug!(
540 "Coverage UI file not found at: {}. Skipping static file serving.",
541 coverage_html_path
542 );
543 }
544
545 let management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); #[cfg(feature = "smtp")]
548 let management_state = {
549 if let Some(smtp_reg) = smtp_registry {
550 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
551 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
552 Err(e) => {
553 error!(
554 "Invalid SMTP registry type passed to HTTP management state: {:?}",
555 e.type_id()
556 );
557 management_state
558 }
559 }
560 } else {
561 management_state
562 }
563 };
564 #[cfg(not(feature = "smtp"))]
565 let management_state = management_state;
566 #[cfg(not(feature = "smtp"))]
567 let _ = smtp_registry;
568 app = app.nest("/__mockforge/api", management_router(management_state));
569
570 let ws_state = WsManagementState::new();
572 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
573
574 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
576
577 app = app.layer(from_fn_with_state(state, crate::middleware::rate_limit_middleware));
579
580 if let Some(mt_config) = multi_tenant_config {
582 if mt_config.enabled {
583 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
584 use std::sync::Arc;
585
586 info!(
587 "Multi-tenant mode enabled with {} routing strategy",
588 match mt_config.routing_strategy {
589 mockforge_core::RoutingStrategy::Path => "path-based",
590 mockforge_core::RoutingStrategy::Port => "port-based",
591 mockforge_core::RoutingStrategy::Both => "hybrid",
592 }
593 );
594
595 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
597
598 let default_workspace =
600 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
601 if let Err(e) =
602 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
603 {
604 warn!("Failed to register default workspace: {}", e);
605 } else {
606 info!("Registered default workspace: '{}'", mt_config.default_workspace);
607 }
608
609 if mt_config.auto_discover {
611 if let Some(config_dir) = &mt_config.config_directory {
612 let config_path = Path::new(config_dir);
613 if config_path.exists() && config_path.is_dir() {
614 match fs::read_dir(config_path).await {
615 Ok(mut entries) => {
616 while let Ok(Some(entry)) = entries.next_entry().await {
617 let path = entry.path();
618 if path.extension() == Some(OsStr::new("yaml")) {
619 match fs::read_to_string(&path).await {
620 Ok(content) => {
621 match serde_yaml::from_str::<
622 mockforge_core::Workspace,
623 >(
624 &content
625 ) {
626 Ok(workspace) => {
627 if let Err(e) = registry.register_workspace(
628 workspace.id.clone(),
629 workspace,
630 ) {
631 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
632 } else {
633 info!("Auto-registered workspace from {:?}", path);
634 }
635 }
636 Err(e) => {
637 warn!("Failed to parse workspace from {:?}: {}", path, e);
638 }
639 }
640 }
641 Err(e) => {
642 warn!(
643 "Failed to read workspace file {:?}: {}",
644 path, e
645 );
646 }
647 }
648 }
649 }
650 }
651 Err(e) => {
652 warn!("Failed to read config directory {:?}: {}", config_path, e);
653 }
654 }
655 } else {
656 warn!(
657 "Config directory {:?} does not exist or is not a directory",
658 config_path
659 );
660 }
661 }
662 }
663
664 let registry = Arc::new(registry);
666
667 let _workspace_router = WorkspaceRouter::new(registry);
669
670 info!("Workspace routing middleware initialized for HTTP server");
673 }
674 }
675
676 let total_startup_duration = startup_start.elapsed();
677 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
678
679 app
680}
681
682pub async fn build_router_with_auth_and_latency(
684 _spec_path: Option<String>,
685 _options: Option<()>,
686 _auth_config: Option<mockforge_core::config::AuthConfig>,
687 _latency_injector: Option<LatencyInjector>,
688) -> Router {
689 build_router(None, None, None).await
691}
692
693pub async fn build_router_with_latency(
695 _spec_path: Option<String>,
696 _options: Option<ValidationOptions>,
697 _latency_injector: Option<LatencyInjector>,
698) -> Router {
699 build_router(None, None, None).await
701}
702
703pub async fn build_router_with_auth(
705 spec_path: Option<String>,
706 options: Option<ValidationOptions>,
707 auth_config: Option<mockforge_core::config::AuthConfig>,
708) -> Router {
709 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
710 use std::sync::Arc;
711
712 #[cfg(feature = "data-faker")]
714 {
715 register_core_faker_provider();
716 }
717
718 let spec = if let Some(spec_path) = &spec_path {
720 match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
721 Ok(spec) => Some(Arc::new(spec)),
722 Err(e) => {
723 warn!("Failed to load OpenAPI spec for auth: {}", e);
724 None
725 }
726 }
727 } else {
728 None
729 };
730
731 let oauth2_client = if let Some(auth_config) = &auth_config {
733 if let Some(oauth2_config) = &auth_config.oauth2 {
734 match create_oauth2_client(oauth2_config) {
735 Ok(client) => Some(client),
736 Err(e) => {
737 warn!("Failed to create OAuth2 client: {}", e);
738 None
739 }
740 }
741 } else {
742 None
743 }
744 } else {
745 None
746 };
747
748 let auth_state = AuthState {
749 config: auth_config.unwrap_or_default(),
750 spec,
751 oauth2_client,
752 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
753 };
754
755 let mut app = Router::new().with_state(auth_state.clone());
757
758 if let Some(spec_path) = spec_path {
760 match OpenApiSpec::from_file(&spec_path).await {
761 Ok(openapi) => {
762 info!("Loaded OpenAPI spec from {}", spec_path);
763 let registry = if let Some(opts) = options {
764 OpenApiRouteRegistry::new_with_options(openapi, opts)
765 } else {
766 OpenApiRouteRegistry::new_with_env(openapi)
767 };
768
769 app = registry.build_router();
770 }
771 Err(e) => {
772 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
773 }
774 }
775 }
776
777 app = app.route(
779 "/health",
780 axum::routing::get(|| async {
781 use mockforge_core::server_utils::health::HealthStatus;
782 {
783 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
785 Ok(value) => axum::Json(value),
786 Err(e) => {
787 tracing::error!("Failed to serialize health status: {}", e);
789 axum::Json(serde_json::json!({
790 "status": "healthy",
791 "service": "mockforge-http",
792 "uptime_seconds": 0
793 }))
794 }
795 }
796 }
797 }),
798 )
799 .merge(sse::sse_router())
801 .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
803 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
805
806 app
807}
808
809pub async fn serve_router(
811 port: u16,
812 app: Router,
813) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
814 serve_router_with_tls(port, app, None).await
815}
816
817pub async fn serve_router_with_tls(
819 port: u16,
820 app: Router,
821 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
822) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
823 use std::net::SocketAddr;
824
825 let addr = mockforge_core::wildcard_socket_addr(port);
826
827 if let Some(ref tls) = tls_config {
828 if tls.enabled {
829 info!("HTTPS listening on {}", addr);
830 return serve_with_tls(addr, app, tls).await;
831 }
832 }
833
834 info!("HTTP listening on {}", addr);
835
836 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
837 format!(
838 "Failed to bind HTTP server to port {}: {}\n\
839 Hint: The port may already be in use. Try using a different port with --http-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
840 port, e, port, port
841 )
842 })?;
843
844 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
845 Ok(())
846}
847
848async fn serve_with_tls(
854 addr: std::net::SocketAddr,
855 _app: Router,
856 tls_config: &mockforge_core::config::HttpTlsConfig,
857) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
858 let _acceptor = tls::load_tls_acceptor(tls_config)?;
860
861 Err(format!(
864 "TLS/HTTPS support is configured but requires a reverse proxy (nginx) for production use.\n\
865 Certificate validation passed: {} and {}\n\
866 For native TLS support, please use a reverse proxy or wait for axum-server integration.\n\
867 You can configure nginx with TLS termination pointing to the HTTP server on port {}.",
868 tls_config.cert_file,
869 tls_config.key_file,
870 addr.port()
871 )
872 .into())
873}
874
875pub async fn start(
877 port: u16,
878 spec_path: Option<String>,
879 options: Option<ValidationOptions>,
880) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
881 start_with_latency(port, spec_path, options, None).await
882}
883
884pub async fn start_with_auth_and_latency(
886 port: u16,
887 spec_path: Option<String>,
888 options: Option<ValidationOptions>,
889 auth_config: Option<mockforge_core::config::AuthConfig>,
890 latency_profile: Option<LatencyProfile>,
891) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
892 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
893 .await
894}
895
896pub async fn start_with_auth_and_injectors(
898 port: u16,
899 spec_path: Option<String>,
900 options: Option<ValidationOptions>,
901 auth_config: Option<mockforge_core::config::AuthConfig>,
902 _latency_profile: Option<LatencyProfile>,
903 _failure_injector: Option<mockforge_core::FailureInjector>,
904) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
905 let app = build_router_with_auth(spec_path, options, auth_config).await;
907 serve_router(port, app).await
908}
909
910pub async fn start_with_latency(
912 port: u16,
913 spec_path: Option<String>,
914 options: Option<ValidationOptions>,
915 latency_profile: Option<LatencyProfile>,
916) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
917 let latency_injector =
918 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
919
920 let app = build_router_with_latency(spec_path, options, latency_injector).await;
921 serve_router(port, app).await
922}
923
924pub async fn build_router_with_chains(
926 spec_path: Option<String>,
927 options: Option<ValidationOptions>,
928 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
929) -> Router {
930 build_router_with_chains_and_multi_tenant(
931 spec_path,
932 options,
933 circling_config,
934 None,
935 None,
936 None,
937 None,
938 None,
939 None,
940 None,
941 false,
942 None, )
944 .await
945}
946
947#[allow(clippy::too_many_arguments)]
949pub async fn build_router_with_chains_and_multi_tenant(
950 spec_path: Option<String>,
951 options: Option<ValidationOptions>,
952 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
953 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
954 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
955 _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
956 _ai_generator: Option<
957 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
958 >,
959 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
960 mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
961 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
962 traffic_shaping_enabled: bool,
963 health_manager: Option<std::sync::Arc<health::HealthManager>>,
964) -> Router {
965 use crate::latency_profiles::LatencyProfiles;
966 use crate::op_middleware::Shared;
967 use mockforge_core::Overrides;
968
969 let _shared = Shared {
970 profiles: LatencyProfiles::default(),
971 overrides: Overrides::default(),
972 failure_injector: None,
973 traffic_shaper,
974 overrides_enabled: false,
975 traffic_shaping_enabled,
976 };
977
978 let mut app = Router::new();
980 let mut include_default_health = true;
981
982 if let Some(ref spec) = spec_path {
984 match OpenApiSpec::from_file(&spec).await {
985 Ok(openapi) => {
986 info!("Loaded OpenAPI spec from {}", spec);
987 let registry = if let Some(opts) = options {
988 OpenApiRouteRegistry::new_with_options(openapi, opts)
989 } else {
990 OpenApiRouteRegistry::new_with_env(openapi)
991 };
992 if registry
993 .routes()
994 .iter()
995 .any(|route| route.method == "GET" && route.path == "/health")
996 {
997 include_default_health = false;
998 }
999 let spec_router = registry.build_router();
1000 app = app.merge(spec_router);
1001 }
1002 Err(e) => {
1003 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1004 }
1005 }
1006 }
1007
1008 if let Some(health) = health_manager {
1010 app = app.merge(health::health_router(health));
1012 info!(
1013 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1014 );
1015 } else if include_default_health {
1016 app = app.route(
1018 "/health",
1019 axum::routing::get(|| async {
1020 use mockforge_core::server_utils::health::HealthStatus;
1021 {
1022 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1024 Ok(value) => axum::Json(value),
1025 Err(e) => {
1026 tracing::error!("Failed to serialize health status: {}", e);
1028 axum::Json(serde_json::json!({
1029 "status": "healthy",
1030 "service": "mockforge-http",
1031 "uptime_seconds": 0
1032 }))
1033 }
1034 }
1035 }
1036 }),
1037 );
1038 }
1039
1040 app = app.merge(sse::sse_router());
1041
1042 let management_state = ManagementState::new(None, spec_path, 3000); #[cfg(feature = "smtp")]
1045 let management_state = {
1046 if let Some(smtp_reg) = smtp_registry {
1047 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1048 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1049 Err(e) => {
1050 error!(
1051 "Invalid SMTP registry type passed to HTTP management state: {:?}",
1052 e.type_id()
1053 );
1054 management_state
1055 }
1056 }
1057 } else {
1058 management_state
1059 }
1060 };
1061 #[cfg(not(feature = "smtp"))]
1062 let management_state = {
1063 let _ = smtp_registry;
1064 management_state
1065 };
1066 #[cfg(feature = "mqtt")]
1067 let management_state = {
1068 if let Some(broker) = mqtt_broker {
1069 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1070 Ok(broker) => management_state.with_mqtt_broker(broker),
1071 Err(e) => {
1072 error!(
1073 "Invalid MQTT broker passed to HTTP management state: {:?}",
1074 e.type_id()
1075 );
1076 management_state
1077 }
1078 }
1079 } else {
1080 management_state
1081 }
1082 };
1083 #[cfg(not(feature = "mqtt"))]
1084 let management_state = {
1085 let _ = mqtt_broker;
1086 management_state
1087 };
1088 app = app.nest("/__mockforge/api", management_router(management_state));
1089
1090 if let Some(mt_config) = multi_tenant_config {
1092 if mt_config.enabled {
1093 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1094 use std::sync::Arc;
1095
1096 info!(
1097 "Multi-tenant mode enabled with {} routing strategy",
1098 match mt_config.routing_strategy {
1099 mockforge_core::RoutingStrategy::Path => "path-based",
1100 mockforge_core::RoutingStrategy::Port => "port-based",
1101 mockforge_core::RoutingStrategy::Both => "hybrid",
1102 }
1103 );
1104
1105 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1107
1108 let default_workspace =
1110 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1111 if let Err(e) =
1112 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1113 {
1114 warn!("Failed to register default workspace: {}", e);
1115 } else {
1116 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1117 }
1118
1119 let registry = Arc::new(registry);
1121
1122 let _workspace_router = WorkspaceRouter::new(registry);
1124 info!("Workspace routing middleware initialized for HTTP server");
1125 }
1126 }
1127
1128 app
1129}
1130
1131#[test]
1135fn test_route_info_clone() {
1136 let route = RouteInfo {
1137 method: "POST".to_string(),
1138 path: "/users".to_string(),
1139 operation_id: Some("createUser".to_string()),
1140 summary: None,
1141 description: None,
1142 parameters: vec![],
1143 };
1144
1145 let cloned = route.clone();
1146 assert_eq!(route.method, cloned.method);
1147 assert_eq!(route.path, cloned.path);
1148 assert_eq!(route.operation_id, cloned.operation_id);
1149}
1150
1151#[test]
1152fn test_http_server_state_new() {
1153 let state = HttpServerState::new();
1154 assert_eq!(state.routes.len(), 0);
1155}
1156
1157#[test]
1158fn test_http_server_state_with_routes() {
1159 let routes = vec![
1160 RouteInfo {
1161 method: "GET".to_string(),
1162 path: "/users".to_string(),
1163 operation_id: Some("getUsers".to_string()),
1164 summary: None,
1165 description: None,
1166 parameters: vec![],
1167 },
1168 RouteInfo {
1169 method: "POST".to_string(),
1170 path: "/users".to_string(),
1171 operation_id: Some("createUser".to_string()),
1172 summary: None,
1173 description: None,
1174 parameters: vec![],
1175 },
1176 ];
1177
1178 let state = HttpServerState::with_routes(routes.clone());
1179 assert_eq!(state.routes.len(), 2);
1180 assert_eq!(state.routes[0].method, "GET");
1181 assert_eq!(state.routes[1].method, "POST");
1182}
1183
1184#[test]
1185fn test_http_server_state_clone() {
1186 let routes = vec![RouteInfo {
1187 method: "GET".to_string(),
1188 path: "/test".to_string(),
1189 operation_id: None,
1190 summary: None,
1191 description: None,
1192 parameters: vec![],
1193 }];
1194
1195 let state = HttpServerState::with_routes(routes);
1196 let cloned = state.clone();
1197
1198 assert_eq!(state.routes.len(), cloned.routes.len());
1199 assert_eq!(state.routes[0].method, cloned.routes[0].method);
1200}
1201
1202#[tokio::test]
1203async fn test_build_router_without_openapi() {
1204 let _router = build_router(None, None, None).await;
1205 }
1207
1208#[tokio::test]
1209async fn test_build_router_with_nonexistent_spec() {
1210 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
1211 }
1213
1214#[tokio::test]
1215async fn test_build_router_with_auth_and_latency() {
1216 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
1217 }
1219
1220#[tokio::test]
1221async fn test_build_router_with_latency() {
1222 let _router = build_router_with_latency(None, None, None).await;
1223 }
1225
1226#[tokio::test]
1227async fn test_build_router_with_auth() {
1228 let _router = build_router_with_auth(None, None, None).await;
1229 }
1231
1232#[tokio::test]
1233async fn test_build_router_with_chains() {
1234 let _router = build_router_with_chains(None, None, None).await;
1235 }
1237
1238#[test]
1239fn test_route_info_with_all_fields() {
1240 let route = RouteInfo {
1241 method: "PUT".to_string(),
1242 path: "/users/{id}".to_string(),
1243 operation_id: Some("updateUser".to_string()),
1244 summary: Some("Update user".to_string()),
1245 description: Some("Updates an existing user".to_string()),
1246 parameters: vec!["id".to_string(), "body".to_string()],
1247 };
1248
1249 assert!(route.operation_id.is_some());
1250 assert!(route.summary.is_some());
1251 assert!(route.description.is_some());
1252 assert_eq!(route.parameters.len(), 2);
1253}
1254
1255#[test]
1256fn test_route_info_with_minimal_fields() {
1257 let route = RouteInfo {
1258 method: "DELETE".to_string(),
1259 path: "/users/{id}".to_string(),
1260 operation_id: None,
1261 summary: None,
1262 description: None,
1263 parameters: vec![],
1264 };
1265
1266 assert!(route.operation_id.is_none());
1267 assert!(route.summary.is_none());
1268 assert!(route.description.is_none());
1269 assert_eq!(route.parameters.len(), 0);
1270}
1271
1272#[test]
1273fn test_http_server_state_empty_routes() {
1274 let state = HttpServerState::with_routes(vec![]);
1275 assert_eq!(state.routes.len(), 0);
1276}
1277
1278#[test]
1279fn test_http_server_state_multiple_routes() {
1280 let routes = vec![
1281 RouteInfo {
1282 method: "GET".to_string(),
1283 path: "/users".to_string(),
1284 operation_id: Some("listUsers".to_string()),
1285 summary: Some("List all users".to_string()),
1286 description: None,
1287 parameters: vec![],
1288 },
1289 RouteInfo {
1290 method: "GET".to_string(),
1291 path: "/users/{id}".to_string(),
1292 operation_id: Some("getUser".to_string()),
1293 summary: Some("Get a user".to_string()),
1294 description: None,
1295 parameters: vec!["id".to_string()],
1296 },
1297 RouteInfo {
1298 method: "POST".to_string(),
1299 path: "/users".to_string(),
1300 operation_id: Some("createUser".to_string()),
1301 summary: Some("Create a user".to_string()),
1302 description: None,
1303 parameters: vec!["body".to_string()],
1304 },
1305 ];
1306
1307 let state = HttpServerState::with_routes(routes);
1308 assert_eq!(state.routes.len(), 3);
1309
1310 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
1312 assert!(methods.contains(&"GET"));
1313 assert!(methods.contains(&"POST"));
1314}
1315
1316#[test]
1317fn test_http_server_state_with_rate_limiter() {
1318 use std::sync::Arc;
1319
1320 let config = crate::middleware::RateLimitConfig::default();
1321 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
1322
1323 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
1324
1325 assert!(state.rate_limiter.is_some());
1326 assert_eq!(state.routes.len(), 0);
1327}
1328
1329#[tokio::test]
1330async fn test_build_router_includes_rate_limiter() {
1331 let _router = build_router(None, None, None).await;
1332 }