1pub mod ai_handler;
201pub mod auth;
202pub mod chain_handlers;
203pub mod consistency;
205pub mod contract_diff_api;
207pub mod contract_diff_middleware;
209pub mod counting_listener;
212pub mod coverage;
213#[cfg(feature = "database")]
219pub mod database {
220 pub use mockforge_intelligence::database::*;
221}
222pub mod file_generator;
224pub mod file_server;
226pub mod fixtures_api;
228pub mod health;
230pub mod http_tracing_middleware;
231pub mod latency_profiles;
233
234pub mod llm_mock;
236
237pub mod management;
239pub mod management_ws;
241pub mod mcp_mock;
243pub mod metrics_middleware;
244pub mod middleware;
245pub mod mockai_api;
247pub mod network_profile_runtime;
249pub mod op_middleware;
250pub mod protocol_server;
252pub mod proxy_server;
254pub mod quick_mock;
256pub mod rag_ai_generator;
258pub mod reality_proxy;
260pub mod replay_listing;
262pub mod request_logging;
263pub mod route_chaos_runtime;
265#[cfg(feature = "scenario-engine")]
267pub mod scenarios_runtime;
268pub mod spec_import;
270pub mod sse;
272pub mod state_machine_api;
274pub mod time_travel_api;
276pub mod tls;
278pub mod token_response;
280pub mod ui_builder;
282pub mod verification;
284
285pub mod handlers;
287
288pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
290pub use health::{HealthManager, ServiceStatus};
292
293pub use management::{
295 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
296 ServerConfig, ServerStats,
297};
298
299pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
301
302pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
304
305pub use verification::verification_router;
307
308pub use metrics_middleware::collect_http_metrics;
310
311pub use http_tracing_middleware::http_tracing_middleware;
313
314pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
316
317async fn load_persona_from_config() -> Option<Arc<Persona>> {
320 use mockforge_core::config::load_config;
321
322 let config_paths = [
324 "config.yaml",
325 "mockforge.yaml",
326 "tools/mockforge/config.yaml",
327 "../tools/mockforge/config.yaml",
328 ];
329
330 for path in &config_paths {
331 if let Ok(config) = load_config(path).await {
332 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
335 {
336 tracing::info!(
337 "Loaded active persona '{}' from config file: {}",
338 persona.name,
339 path
340 );
341 return Some(Arc::new(persona.clone()));
342 } else {
343 tracing::debug!(
344 "No active persona found in config file: {} (personas count: {})",
345 path,
346 config.mockai.intelligent_behavior.personas.personas.len()
347 );
348 }
349 } else {
350 tracing::debug!("Could not load config from: {}", path);
351 }
352 }
353
354 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
355 None
356}
357
358use axum::body::Body;
359use axum::extract::State;
360use axum::http::Request;
361use axum::middleware::from_fn_with_state;
362use axum::response::Json;
363use axum::Router;
364use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
365use mockforge_core::intelligent_behavior::config::Persona;
366use mockforge_foundation::latency::LatencyInjector;
367use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
368use mockforge_openapi::openapi_routes::ValidationOptions;
369use mockforge_openapi::OpenApiSpec;
370use std::sync::Arc;
371use tower_http::cors::{Any, CorsLayer};
372
373#[cfg(feature = "data-faker")]
374use mockforge_data::provider::register_core_faker_provider;
375use mockforge_foundation::latency::LatencyProfile;
376use std::collections::HashMap;
377use std::ffi::OsStr;
378use std::path::Path;
379use tokio::fs;
380use tokio::sync::RwLock;
381use tracing::*;
382
383#[derive(Clone)]
385pub struct RouteInfo {
386 pub method: String,
388 pub path: String,
390 pub operation_id: Option<String>,
392 pub summary: Option<String>,
394 pub description: Option<String>,
396 pub parameters: Vec<String>,
398}
399
400#[derive(Clone)]
402pub struct HttpServerState {
403 pub routes: Vec<RouteInfo>,
405 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
407 pub production_headers: Option<Arc<HashMap<String, String>>>,
409}
410
411impl Default for HttpServerState {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417impl HttpServerState {
418 pub fn new() -> Self {
420 Self {
421 routes: Vec::new(),
422 rate_limiter: None,
423 production_headers: None,
424 }
425 }
426
427 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
429 Self {
430 routes,
431 rate_limiter: None,
432 production_headers: None,
433 }
434 }
435
436 pub fn with_rate_limiter(
438 mut self,
439 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
440 ) -> Self {
441 self.rate_limiter = Some(rate_limiter);
442 self
443 }
444
445 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
447 self.production_headers = Some(headers);
448 self
449 }
450}
451
452async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
454 let route_info: Vec<serde_json::Value> = state
455 .routes
456 .iter()
457 .map(|route| {
458 serde_json::json!({
459 "method": route.method,
460 "path": route.path,
461 "operation_id": route.operation_id,
462 "summary": route.summary,
463 "description": route.description,
464 "parameters": route.parameters
465 })
466 })
467 .collect();
468
469 Json(serde_json::json!({
470 "routes": route_info,
471 "total": state.routes.len()
472 }))
473}
474
475async fn get_docs_handler() -> axum::response::Html<&'static str> {
477 axum::response::Html(include_str!("../static/docs.html"))
478}
479
480pub async fn build_router(
482 spec_path: Option<String>,
483 options: Option<ValidationOptions>,
484 failure_config: Option<FailureConfig>,
485) -> Router {
486 build_router_with_multi_tenant(
487 spec_path,
488 options,
489 failure_config,
490 None,
491 None,
492 None,
493 None,
494 None,
495 None,
496 None,
497 )
498 .await
499}
500
501fn apply_cors_middleware(
503 app: Router,
504 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
505) -> Router {
506 use http::Method;
507 use tower_http::cors::AllowOrigin;
508
509 if let Some(config) = cors_config {
510 if !config.enabled {
511 return app;
512 }
513
514 let mut cors_layer = CorsLayer::new();
515 let is_wildcard_origin;
516
517 if config.allowed_origins.contains(&"*".to_string()) {
519 cors_layer = cors_layer.allow_origin(Any);
520 is_wildcard_origin = true;
521 } else if !config.allowed_origins.is_empty() {
522 let origins: Vec<_> = config
524 .allowed_origins
525 .iter()
526 .filter_map(|origin| {
527 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
528 })
529 .collect();
530
531 if origins.is_empty() {
532 warn!("No valid CORS origins configured, using permissive CORS");
534 cors_layer = cors_layer.allow_origin(Any);
535 is_wildcard_origin = true;
536 } else {
537 if origins.len() == 1 {
540 cors_layer = cors_layer.allow_origin(origins[0].clone());
541 is_wildcard_origin = false;
542 } else {
543 warn!(
545 "Multiple CORS origins configured, using permissive CORS. \
546 Consider using '*' for all origins."
547 );
548 cors_layer = cors_layer.allow_origin(Any);
549 is_wildcard_origin = true;
550 }
551 }
552 } else {
553 cors_layer = cors_layer.allow_origin(Any);
555 is_wildcard_origin = true;
556 }
557
558 if !config.allowed_methods.is_empty() {
560 let methods: Vec<Method> =
561 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
562 if !methods.is_empty() {
563 cors_layer = cors_layer.allow_methods(methods);
564 }
565 } else {
566 cors_layer = cors_layer.allow_methods([
568 Method::GET,
569 Method::POST,
570 Method::PUT,
571 Method::DELETE,
572 Method::PATCH,
573 Method::OPTIONS,
574 ]);
575 }
576
577 if !config.allowed_headers.is_empty() {
579 let headers: Vec<_> = config
580 .allowed_headers
581 .iter()
582 .filter_map(|h| h.parse::<http::HeaderName>().ok())
583 .collect();
584 if !headers.is_empty() {
585 cors_layer = cors_layer.allow_headers(headers);
586 }
587 } else {
588 cors_layer =
590 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
591 }
592
593 let should_allow_credentials = if is_wildcard_origin {
597 false
599 } else {
600 config.allow_credentials
602 };
603
604 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
605
606 info!(
607 "CORS middleware enabled with configured settings (credentials: {})",
608 should_allow_credentials
609 );
610 app.layer(cors_layer)
611 } else {
612 debug!("No CORS config provided, using permissive CORS for development");
616 app.layer(CorsLayer::permissive().allow_credentials(false))
619 }
620}
621
622#[allow(clippy::too_many_arguments)]
624#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
626 spec_path: Option<String>,
627 options: Option<ValidationOptions>,
628 failure_config: Option<FailureConfig>,
629 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
630 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
631 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
632 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
633 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
634 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
635 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
636) -> Router {
637 use std::time::Instant;
638
639 let startup_start = Instant::now();
640
641 let mut app = Router::new();
643
644 let mut rate_limit_config = middleware::RateLimitConfig {
647 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
648 .ok()
649 .and_then(|v| v.parse().ok())
650 .unwrap_or(1000),
651 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
652 .ok()
653 .and_then(|v| v.parse().ok())
654 .unwrap_or(2000),
655 per_ip: true,
656 per_endpoint: false,
657 };
658
659 let mut final_cors_config = cors_config;
661 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
662 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
664
665 if let Some(deploy_config) = &deceptive_deploy_config {
666 if deploy_config.enabled {
667 info!("Deceptive deploy mode enabled - applying production-like configuration");
668
669 if let Some(prod_cors) = &deploy_config.cors {
671 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
672 enabled: true,
673 allowed_origins: prod_cors.allowed_origins.clone(),
674 allowed_methods: prod_cors.allowed_methods.clone(),
675 allowed_headers: prod_cors.allowed_headers.clone(),
676 allow_credentials: prod_cors.allow_credentials,
677 });
678 info!("Applied production-like CORS configuration");
679 }
680
681 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
683 rate_limit_config = middleware::RateLimitConfig {
684 requests_per_minute: prod_rate_limit.requests_per_minute,
685 burst: prod_rate_limit.burst,
686 per_ip: prod_rate_limit.per_ip,
687 per_endpoint: false,
688 };
689 info!(
690 "Applied production-like rate limiting: {} req/min, burst: {}",
691 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
692 );
693 }
694
695 if !deploy_config.headers.is_empty() {
697 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
698 production_headers = Some(std::sync::Arc::new(headers_map));
699 info!("Configured {} production headers", deploy_config.headers.len());
700 }
701
702 if let Some(prod_oauth) = &deploy_config.oauth {
704 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
705 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
706 oauth2: Some(oauth2_config),
707 ..Default::default()
708 });
709 info!("Applied production-like OAuth configuration for deceptive deploy");
710 }
711 }
712 }
713
714 let rate_limit_disabled = middleware::is_rate_limit_disabled();
715 let rate_limiter =
716 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
717
718 let mut state = HttpServerState::new();
719 if rate_limit_disabled {
720 info!(
721 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
722 );
723 } else {
724 state = state.with_rate_limiter(rate_limiter.clone());
725 }
726
727 if let Some(headers) = production_headers.clone() {
729 state = state.with_production_headers(headers);
730 }
731
732 let spec_path_for_mgmt = spec_path.clone();
734
735 if let Some(spec_path) = spec_path {
737 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
738
739 let spec_load_start = Instant::now();
741 match OpenApiSpec::from_file(&spec_path).await {
742 Ok(openapi) => {
743 let spec_load_duration = spec_load_start.elapsed();
744 info!(
745 "Successfully loaded OpenAPI spec from {} (took {:?})",
746 spec_path, spec_load_duration
747 );
748
749 tracing::debug!("Creating OpenAPI route registry...");
751 let registry_start = Instant::now();
752
753 let persona = load_persona_from_config().await;
755
756 let registry = if let Some(opts) = options {
757 tracing::debug!("Using custom validation options");
758 if let Some(ref persona) = persona {
759 tracing::info!("Using persona '{}' for route generation", persona.name);
760 }
761 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
762 } else {
763 tracing::debug!("Using environment-based options");
764 if let Some(ref persona) = persona {
765 tracing::info!("Using persona '{}' for route generation", persona.name);
766 }
767 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
768 };
769 let registry_duration = registry_start.elapsed();
770 info!(
771 "Created OpenAPI route registry with {} routes (took {:?})",
772 registry.routes().len(),
773 registry_duration
774 );
775
776 let extract_start = Instant::now();
778 let route_info: Vec<RouteInfo> = registry
779 .routes()
780 .iter()
781 .map(|route| RouteInfo {
782 method: route.method.clone(),
783 path: route.path.clone(),
784 operation_id: route.operation.operation_id.clone(),
785 summary: route.operation.summary.clone(),
786 description: route.operation.description.clone(),
787 parameters: route.parameters.clone(),
788 })
789 .collect();
790 state.routes = route_info;
791 let extract_duration = extract_start.elapsed();
792 debug!("Extracted route information (took {:?})", extract_duration);
793
794 let registry = registry.with_overrides(Arc::new(
795 mockforge_core::SharedOverrides::load(Vec::new()).await,
796 ));
797 let router_build_start = Instant::now();
798 let response_rewriter: Option<
799 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
800 > = Some(std::sync::Arc::new(
801 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(),
802 ));
803 let openapi_router = if let Some(mockai_instance) = &mockai {
804 tracing::debug!("Building router with MockAI support");
805 registry.build_router_with_mockai(Some(mockai_instance.clone()))
806 } else if let Some(ai_generator) = &ai_generator {
807 tracing::debug!("Building router with AI generator support");
808 registry.build_router_with_ai(Some(ai_generator.clone()))
809 } else if let Some(failure_config) = &failure_config {
810 tracing::debug!("Building router with failure injection and overrides");
811 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
812 registry.build_router_with_injectors_and_rewriter(
813 LatencyInjector::default(),
814 Some(failure_injector),
815 response_rewriter,
816 )
817 } else {
818 tracing::debug!("Building router with overrides");
819 registry.build_router_with_injectors_and_rewriter(
820 LatencyInjector::default(),
821 None,
822 response_rewriter,
823 )
824 };
825 let router_build_duration = router_build_start.elapsed();
826 debug!("Built OpenAPI router (took {:?})", router_build_duration);
827
828 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
836 .ok()
837 .and_then(|v| v.parse::<usize>().ok())
838 .unwrap_or(50);
839 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
840 let openapi_router =
841 openapi_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
842 tracing::info!(
843 body_limit_mb = body_limit_mb,
844 "Merging OpenAPI router with main router"
845 );
846 app = app.merge(openapi_router);
847 tracing::debug!("Router built successfully");
848 }
849 Err(e) => {
850 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
851 }
852 }
853 }
854
855 app = app.route(
857 "/health",
858 axum::routing::get(|| async {
859 use mockforge_core::server_utils::health::HealthStatus;
860 {
861 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
863 Ok(value) => Json(value),
864 Err(e) => {
865 tracing::error!("Failed to serialize health status: {}", e);
867 Json(serde_json::json!({
868 "status": "healthy",
869 "service": "mockforge-http",
870 "uptime_seconds": 0
871 }))
872 }
873 }
874 }
875 }),
876 )
877 .merge(sse::sse_router())
879 .merge(file_server::file_serving_router());
881
882 let state_for_routes = state.clone();
884
885 let routes_router = Router::new()
887 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
888 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
889 .with_state(state_for_routes);
890
891 app = app.merge(routes_router);
893
894 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
896
897 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
900 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
901
902 if Path::new(&coverage_html_path).exists() {
904 app = app.nest_service(
905 "/__mockforge/coverage.html",
906 tower_http::services::ServeFile::new(&coverage_html_path),
907 );
908 debug!("Serving coverage UI from: {}", coverage_html_path);
909 } else {
910 debug!(
911 "Coverage UI file not found at: {}. Skipping static file serving.",
912 coverage_html_path
913 );
914 }
915
916 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
919 match OpenApiSpec::from_file(sp).await {
920 Ok(s) => Some(Arc::new(s)),
921 Err(e) => {
922 debug!("Failed to load OpenAPI spec for management API: {}", e);
923 None
924 }
925 }
926 } else {
927 None
928 };
929 let mgmt_port = std::env::var("PORT")
930 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
931 .ok()
932 .and_then(|p| p.parse().ok())
933 .unwrap_or(3000);
934 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
935
936 use std::sync::Arc;
938 let ws_state = WsManagementState::new();
939 let ws_broadcast = Arc::new(ws_state.tx.clone());
940 let management_state = management_state.with_ws_broadcast(ws_broadcast);
941
942 #[cfg(feature = "smtp")]
946 let management_state = {
947 if let Some(smtp_reg) = smtp_registry {
948 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
949 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
950 Err(e) => {
951 error!(
952 "Invalid SMTP registry type passed to HTTP management state: {:?}",
953 e.type_id()
954 );
955 management_state
956 }
957 }
958 } else {
959 management_state
960 }
961 };
962 #[cfg(not(feature = "smtp"))]
963 let management_state = management_state;
964 #[cfg(not(feature = "smtp"))]
965 let _ = smtp_registry;
966 let management_state_for_fallback = management_state.clone();
967 app = app.nest("/__mockforge/api", management_router(management_state));
968 app = app.fallback_service(
973 axum::routing::any(management::dynamic_mock_fallback)
974 .with_state(management_state_for_fallback),
975 );
976
977 app = app.merge(verification_router());
979
980 use crate::auth::oidc::oidc_router;
982 app = app.merge(oidc_router());
983
984 {
986 use mockforge_core::security::get_global_access_review_service;
987 if let Some(service) = get_global_access_review_service().await {
988 use crate::handlers::access_review::{access_review_router, AccessReviewState};
989 let review_state = AccessReviewState { service };
990 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
991 debug!("Access review API mounted at /api/v1/security/access-reviews");
992 }
993 }
994
995 {
997 use mockforge_core::security::get_global_privileged_access_manager;
998 if let Some(manager) = get_global_privileged_access_manager().await {
999 use crate::handlers::privileged_access::{
1000 privileged_access_router, PrivilegedAccessState,
1001 };
1002 let privileged_state = PrivilegedAccessState { manager };
1003 app = app.nest(
1004 "/api/v1/security/privileged-access",
1005 privileged_access_router(privileged_state),
1006 );
1007 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1008 }
1009 }
1010
1011 {
1013 use mockforge_core::security::get_global_change_management_engine;
1014 if let Some(engine) = get_global_change_management_engine().await {
1015 use crate::handlers::change_management::{
1016 change_management_router, ChangeManagementState,
1017 };
1018 let change_state = ChangeManagementState { engine };
1019 app = app.nest("/api/v1/change-management", change_management_router(change_state));
1020 debug!("Change management API mounted at /api/v1/change-management");
1021 }
1022 }
1023
1024 {
1026 use mockforge_core::security::get_global_risk_assessment_engine;
1027 if let Some(engine) = get_global_risk_assessment_engine().await {
1028 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1029 let risk_state = RiskAssessmentState { engine };
1030 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1031 debug!("Risk assessment API mounted at /api/v1/security/risks");
1032 }
1033 }
1034
1035 {
1037 use crate::auth::token_lifecycle::TokenLifecycleManager;
1038 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1039 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1040 let lifecycle_state = TokenLifecycleState {
1041 manager: lifecycle_manager,
1042 };
1043 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1044 debug!("Token lifecycle API mounted at /api/v1/auth");
1045 }
1046
1047 {
1049 use crate::auth::oidc::load_oidc_state;
1050 use crate::auth::token_lifecycle::TokenLifecycleManager;
1051 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1052 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1054 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1055 let oauth2_state = OAuth2ServerState {
1056 oidc_state,
1057 lifecycle_manager,
1058 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1059 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1060 };
1061 app = app.merge(oauth2_server_router(oauth2_state));
1062 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1063 }
1064
1065 {
1067 use crate::auth::oidc::load_oidc_state;
1068 use crate::auth::risk_engine::RiskEngine;
1069 use crate::auth::token_lifecycle::TokenLifecycleManager;
1070 use crate::handlers::consent::{consent_router, ConsentState};
1071 use crate::handlers::oauth2_server::OAuth2ServerState;
1072 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1074 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1075 let oauth2_state = OAuth2ServerState {
1076 oidc_state: oidc_state.clone(),
1077 lifecycle_manager: lifecycle_manager.clone(),
1078 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1079 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1080 };
1081 let risk_engine = Arc::new(RiskEngine::default());
1082 let consent_state = ConsentState {
1083 oauth2_state,
1084 risk_engine,
1085 };
1086 app = app.merge(consent_router(consent_state));
1087 debug!("Consent screen endpoints mounted at /consent");
1088 }
1089
1090 {
1092 use crate::auth::risk_engine::RiskEngine;
1093 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1094 let risk_engine = Arc::new(RiskEngine::default());
1095 let risk_state = RiskSimulationState { risk_engine };
1096 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1097 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1098 }
1099
1100 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1102
1103 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1105
1106 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1108
1109 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1112
1113 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1115
1116 if state.production_headers.is_some() {
1118 app =
1119 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1120 }
1121
1122 if middleware::is_keepalive_hint_enabled() {
1128 info!(
1129 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
1130 );
1131 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
1132 }
1133
1134 if middleware::is_conn_log_enabled() {
1139 info!(
1140 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
1141 );
1142 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
1143 }
1144
1145 if let Some(auth_config) = deceptive_deploy_auth_config {
1147 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1148 use std::collections::HashMap;
1149 use std::sync::Arc;
1150 use tokio::sync::RwLock;
1151
1152 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1154 match create_oauth2_client(oauth2_config) {
1155 Ok(client) => Some(client),
1156 Err(e) => {
1157 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1158 None
1159 }
1160 }
1161 } else {
1162 None
1163 };
1164
1165 let auth_state = AuthState {
1167 config: auth_config,
1168 spec: None, oauth2_client,
1170 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1171 };
1172
1173 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1175 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1176 }
1177
1178 app = apply_cors_middleware(app, final_cors_config);
1180
1181 if let Some(mt_config) = multi_tenant_config {
1183 if mt_config.enabled {
1184 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1185 use std::sync::Arc;
1186
1187 info!(
1188 "Multi-tenant mode enabled with {} routing strategy",
1189 match mt_config.routing_strategy {
1190 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1191 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1192 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1193 }
1194 );
1195
1196 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1198
1199 let default_workspace =
1201 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1202 if let Err(e) =
1203 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1204 {
1205 warn!("Failed to register default workspace: {}", e);
1206 } else {
1207 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1208 }
1209
1210 if mt_config.auto_discover {
1212 if let Some(config_dir) = &mt_config.config_directory {
1213 let config_path = Path::new(config_dir);
1214 if config_path.exists() && config_path.is_dir() {
1215 match fs::read_dir(config_path).await {
1216 Ok(mut entries) => {
1217 while let Ok(Some(entry)) = entries.next_entry().await {
1218 let path = entry.path();
1219 if path.extension() == Some(OsStr::new("yaml")) {
1220 match fs::read_to_string(&path).await {
1221 Ok(content) => {
1222 match serde_yaml::from_str::<
1223 mockforge_core::Workspace,
1224 >(
1225 &content
1226 ) {
1227 Ok(workspace) => {
1228 if let Err(e) = registry.register_workspace(
1229 workspace.id.clone(),
1230 workspace,
1231 ) {
1232 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1233 } else {
1234 info!("Auto-registered workspace from {:?}", path);
1235 }
1236 }
1237 Err(e) => {
1238 warn!("Failed to parse workspace from {:?}: {}", path, e);
1239 }
1240 }
1241 }
1242 Err(e) => {
1243 warn!(
1244 "Failed to read workspace file {:?}: {}",
1245 path, e
1246 );
1247 }
1248 }
1249 }
1250 }
1251 }
1252 Err(e) => {
1253 warn!("Failed to read config directory {:?}: {}", config_path, e);
1254 }
1255 }
1256 } else {
1257 warn!(
1258 "Config directory {:?} does not exist or is not a directory",
1259 config_path
1260 );
1261 }
1262 }
1263 }
1264
1265 let registry = Arc::new(registry);
1267
1268 let _workspace_router = WorkspaceRouter::new(registry);
1270
1271 info!("Workspace routing middleware initialized for HTTP server");
1274 }
1275 }
1276
1277 let total_startup_duration = startup_start.elapsed();
1278 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1279
1280 app
1281}
1282
1283pub async fn build_router_with_auth_and_latency(
1285 spec_path: Option<String>,
1286 _options: Option<()>,
1287 auth_config: Option<mockforge_core::config::AuthConfig>,
1288 latency_injector: Option<LatencyInjector>,
1289) -> Router {
1290 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1292
1293 if let Some(injector) = latency_injector {
1295 let injector = Arc::new(injector);
1296 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1297 let injector = injector.clone();
1298 async move {
1299 let _ = injector.inject_latency(&[]).await;
1300 next.run(req).await
1301 }
1302 }));
1303 }
1304
1305 app
1306}
1307
1308pub async fn build_router_with_latency(
1310 spec_path: Option<String>,
1311 options: Option<ValidationOptions>,
1312 latency_injector: Option<LatencyInjector>,
1313) -> Router {
1314 if let Some(spec) = &spec_path {
1315 match OpenApiSpec::from_file(spec).await {
1316 Ok(openapi) => {
1317 let registry = if let Some(opts) = options {
1318 OpenApiRouteRegistry::new_with_options(openapi, opts)
1319 } else {
1320 OpenApiRouteRegistry::new_with_env(openapi)
1321 };
1322
1323 if let Some(injector) = latency_injector {
1324 return registry.build_router_with_latency(injector);
1325 } else {
1326 return registry.build_router();
1327 }
1328 }
1329 Err(e) => {
1330 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1331 }
1332 }
1333 }
1334
1335 build_router(None, None, None).await
1336}
1337
1338pub async fn build_router_with_auth(
1340 spec_path: Option<String>,
1341 options: Option<ValidationOptions>,
1342 auth_config: Option<mockforge_core::config::AuthConfig>,
1343) -> Router {
1344 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1345 use std::sync::Arc;
1346
1347 #[cfg(feature = "data-faker")]
1349 {
1350 register_core_faker_provider();
1351 }
1352
1353 let spec = if let Some(spec_path) = &spec_path {
1355 match OpenApiSpec::from_file(&spec_path).await {
1356 Ok(spec) => Some(Arc::new(spec)),
1357 Err(e) => {
1358 warn!("Failed to load OpenAPI spec for auth: {}", e);
1359 None
1360 }
1361 }
1362 } else {
1363 None
1364 };
1365
1366 let oauth2_client = if let Some(auth_config) = &auth_config {
1368 if let Some(oauth2_config) = &auth_config.oauth2 {
1369 match create_oauth2_client(oauth2_config) {
1370 Ok(client) => Some(client),
1371 Err(e) => {
1372 warn!("Failed to create OAuth2 client: {}", e);
1373 None
1374 }
1375 }
1376 } else {
1377 None
1378 }
1379 } else {
1380 None
1381 };
1382
1383 let auth_state = AuthState {
1384 config: auth_config.unwrap_or_default(),
1385 spec,
1386 oauth2_client,
1387 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1388 };
1389
1390 let mut app = Router::new().with_state(auth_state.clone());
1392
1393 if let Some(spec_path) = spec_path {
1395 match OpenApiSpec::from_file(&spec_path).await {
1396 Ok(openapi) => {
1397 info!("Loaded OpenAPI spec from {}", spec_path);
1398 let registry = if let Some(opts) = options {
1399 OpenApiRouteRegistry::new_with_options(openapi, opts)
1400 } else {
1401 OpenApiRouteRegistry::new_with_env(openapi)
1402 };
1403
1404 app = registry.build_router();
1405 }
1406 Err(e) => {
1407 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1408 }
1409 }
1410 }
1411
1412 app = app.route(
1414 "/health",
1415 axum::routing::get(|| async {
1416 use mockforge_core::server_utils::health::HealthStatus;
1417 {
1418 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1420 Ok(value) => Json(value),
1421 Err(e) => {
1422 tracing::error!("Failed to serialize health status: {}", e);
1424 Json(serde_json::json!({
1425 "status": "healthy",
1426 "service": "mockforge-http",
1427 "uptime_seconds": 0
1428 }))
1429 }
1430 }
1431 }
1432 }),
1433 )
1434 .merge(sse::sse_router())
1436 .merge(file_server::file_serving_router())
1438 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1440 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1442
1443 app
1444}
1445
1446pub async fn serve_router(
1448 port: u16,
1449 app: Router,
1450) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1451 serve_router_with_tls(port, app, None).await
1452}
1453
1454pub async fn serve_router_with_tls(
1456 port: u16,
1457 app: Router,
1458 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1459) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1460 serve_router_with_tls_notify(port, app, tls_config, None).await
1461}
1462
1463pub async fn serve_router_with_tls_notify(
1471 port: u16,
1472 app: Router,
1473 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1474 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1475) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1476 serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1477}
1478
1479pub async fn serve_router_with_tls_notify_chaos(
1488 port: u16,
1489 app: Router,
1490 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1491 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1492 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1493) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1494 use std::net::SocketAddr;
1495
1496 let addr = mockforge_core::wildcard_socket_addr(port);
1497
1498 if let Some(ref tls) = tls_config {
1499 if tls.enabled {
1500 info!("HTTPS listening on {}", addr);
1501 if let Some(tx) = bound_port_tx {
1502 let _ = tx.send(port);
1503 }
1504 return serve_with_tls(addr, app, tls).await;
1505 }
1506 }
1507
1508 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1509 format!(
1510 "Failed to bind HTTP server to port {}: {}\n\
1511 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 {}",
1512 port, e, port, port
1513 )
1514 })?;
1515
1516 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1517 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1518 if let Some(tx) = bound_port_tx {
1519 let _ = tx.send(actual_port);
1520 }
1521
1522 let odata_app = tower::ServiceBuilder::new()
1526 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1527 .service(app);
1528 if let Some(cfg) = chaos_config {
1529 info!("HTTP listener wrapped with chaos TCP listener (RST/FIN injection enabled)");
1530 let chaos_listener = mockforge_chaos::ChaosTcpListener::new(listener, cfg);
1531 let app_with_addr_compat = tower::ServiceBuilder::new()
1534 .layer(axum::middleware::from_fn(copy_chaos_addr_to_socketaddr))
1535 .service(odata_app);
1536 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1537 mockforge_chaos::ChaosClientAddr,
1538 >(app_with_addr_compat);
1539 let counted = counting_listener::CountingMakeService::new(make_svc);
1541 axum::serve(chaos_listener, counted).await?;
1542 } else {
1543 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1544 SocketAddr,
1545 >(odata_app);
1546 let counted = counting_listener::CountingMakeService::new(make_svc);
1549 axum::serve(listener, counted).await?;
1550 }
1551 Ok(())
1552}
1553
1554async fn copy_chaos_addr_to_socketaddr(
1558 mut req: Request<Body>,
1559 next: axum::middleware::Next,
1560) -> axum::response::Response {
1561 use axum::extract::ConnectInfo;
1562 if let Some(ConnectInfo(chaos_addr)) =
1563 req.extensions().get::<ConnectInfo<mockforge_chaos::ChaosClientAddr>>().copied()
1564 {
1565 let sock: std::net::SocketAddr = *chaos_addr;
1566 req.extensions_mut().insert(ConnectInfo(sock));
1567 }
1568 next.run(req).await
1569}
1570
1571async fn serve_with_tls(
1576 addr: std::net::SocketAddr,
1577 app: Router,
1578 tls_config: &mockforge_core::config::HttpTlsConfig,
1579) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1580 use axum_server::tls_rustls::RustlsConfig;
1581 use std::net::SocketAddr;
1582
1583 tls::init_crypto_provider();
1585
1586 info!("Loading TLS configuration for HTTPS server");
1587
1588 let server_config = tls::load_tls_server_config(tls_config)?;
1590
1591 let rustls_config = RustlsConfig::from_config(server_config);
1594
1595 info!("Starting HTTPS server on {}", addr);
1596
1597 let odata_app = tower::ServiceBuilder::new()
1601 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1602 .service(app);
1603 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1604 SocketAddr,
1605 >(odata_app);
1606 let counted = counting_listener::CountingMakeService::new(make_svc);
1609
1610 axum_server::bind_rustls(addr, rustls_config)
1612 .serve(counted)
1613 .await
1614 .map_err(|e| format!("HTTPS server error: {}", e).into())
1615}
1616
1617pub async fn start(
1619 port: u16,
1620 spec_path: Option<String>,
1621 options: Option<ValidationOptions>,
1622) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1623 start_with_latency(port, spec_path, options, None).await
1624}
1625
1626pub async fn start_with_auth_and_latency(
1628 port: u16,
1629 spec_path: Option<String>,
1630 options: Option<ValidationOptions>,
1631 auth_config: Option<mockforge_core::config::AuthConfig>,
1632 latency_profile: Option<LatencyProfile>,
1633) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1634 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1635 .await
1636}
1637
1638pub async fn start_with_auth_and_injectors(
1640 port: u16,
1641 spec_path: Option<String>,
1642 options: Option<ValidationOptions>,
1643 auth_config: Option<mockforge_core::config::AuthConfig>,
1644 _latency_profile: Option<LatencyProfile>,
1645 _failure_injector: Option<FailureInjector>,
1646) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1647 let app = build_router_with_auth(spec_path, options, auth_config).await;
1649 serve_router(port, app).await
1650}
1651
1652pub async fn start_with_latency(
1654 port: u16,
1655 spec_path: Option<String>,
1656 options: Option<ValidationOptions>,
1657 latency_profile: Option<LatencyProfile>,
1658) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1659 let latency_injector =
1660 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1661
1662 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1663 serve_router(port, app).await
1664}
1665
1666pub async fn build_router_with_chains(
1668 spec_path: Option<String>,
1669 options: Option<ValidationOptions>,
1670 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1671) -> Router {
1672 build_router_with_chains_and_multi_tenant(
1673 spec_path,
1674 options,
1675 circling_config,
1676 None,
1677 None,
1678 None,
1679 None,
1680 None, None, None, None, None, false,
1686 None, None, None, None, mockforge_core::SharedOverrides::load(Vec::new()).await,
1691 )
1692 .await
1693}
1694
1695async fn apply_route_chaos(
1703 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1704 method: &http::Method,
1705 uri: &http::Uri,
1706) -> Option<axum::response::Response> {
1707 use axum::http::StatusCode;
1708 use axum::response::IntoResponse;
1709
1710 if let Some(injector) = injector {
1711 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1713 let mut response = Json(serde_json::json!({
1715 "error": fault_response.error_message,
1716 "fault_type": fault_response.fault_type,
1717 }))
1718 .into_response();
1719 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1720 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1721 return Some(response);
1722 }
1723
1724 if let Err(e) = injector.inject_latency(method, uri).await {
1726 tracing::warn!("Failed to inject latency: {}", e);
1727 }
1728 }
1729
1730 None }
1732
1733#[allow(clippy::too_many_arguments)]
1735#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1737 spec_path: Option<String>,
1738 options: Option<ValidationOptions>,
1739 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1740 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1741 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1742 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1743 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1744 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1745 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1746 amqp_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1747 kafka_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1748 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1749 traffic_shaping_enabled: bool,
1750 health_manager: Option<Arc<HealthManager>>,
1751 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1752 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1753 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1754 overrides: mockforge_core::SharedOverrides,
1755) -> Router {
1756 use crate::latency_profiles::LatencyProfiles;
1757 use crate::op_middleware::Shared;
1758
1759 let template_expand =
1761 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1762 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1763 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1764 .unwrap_or(false)
1765 });
1766
1767 let _shared = Shared {
1768 profiles: LatencyProfiles::default(),
1769 failure_injector: None,
1770 traffic_shaper,
1771 traffic_shaping_enabled,
1772 };
1773
1774 let mut app = Router::new();
1776 let mut include_default_health = true;
1777 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1778
1779 if let Some(ref spec) = spec_path {
1781 match OpenApiSpec::from_file(&spec).await {
1782 Ok(openapi) => {
1783 info!("Loaded OpenAPI spec from {}", spec);
1784
1785 let persona = load_persona_from_config().await;
1787
1788 let mut registry = if let Some(opts) = options {
1789 tracing::debug!("Using custom validation options");
1790 if let Some(ref persona) = persona {
1791 tracing::info!("Using persona '{}' for route generation", persona.name);
1792 }
1793 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1794 } else {
1795 tracing::debug!("Using environment-based options");
1796 if let Some(ref persona) = persona {
1797 tracing::info!("Using persona '{}' for route generation", persona.name);
1798 }
1799 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1800 };
1801
1802 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1804 .unwrap_or_else(|_| "/app/fixtures".to_string());
1805 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1806 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1807 .unwrap_or(true); if custom_fixtures_enabled {
1810 use mockforge_openapi::CustomFixtureLoader;
1811 use std::path::PathBuf;
1812 use std::sync::Arc;
1813
1814 let fixtures_path = PathBuf::from(&fixtures_dir);
1815 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1816
1817 if let Err(e) = custom_loader.load_fixtures().await {
1818 tracing::warn!("Failed to load custom fixtures: {}", e);
1819 } else {
1820 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1821 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1822 }
1823 }
1824 registry = registry.with_overrides(Arc::new(overrides.clone()));
1825
1826 if registry
1827 .routes()
1828 .iter()
1829 .any(|route| route.method == "GET" && route.path == "/health")
1830 {
1831 include_default_health = false;
1832 }
1833 captured_routes = registry
1835 .routes()
1836 .iter()
1837 .map(|r| RouteInfo {
1838 method: r.method.clone(),
1839 path: r.path.clone(),
1840 operation_id: r.operation.operation_id.clone(),
1841 summary: r.operation.summary.clone(),
1842 description: r.operation.description.clone(),
1843 parameters: r.parameters.clone(),
1844 })
1845 .collect();
1846
1847 {
1850 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1851 captured_routes
1852 .iter()
1853 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1854 method: r.method.clone(),
1855 path: r.path.clone(),
1856 operation_id: r.operation_id.clone(),
1857 summary: r.summary.clone(),
1858 description: r.description.clone(),
1859 parameters: r.parameters.clone(),
1860 })
1861 .collect();
1862 mockforge_core::request_logger::set_global_routes(global_routes);
1863 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1864 }
1865
1866 let spec_router = if let Some(ref mockai_instance) = mockai {
1868 tracing::debug!("Building router with MockAI support");
1869 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1870 } else {
1871 registry.build_router()
1872 };
1873 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
1881 .ok()
1882 .and_then(|v| v.parse::<usize>().ok())
1883 .unwrap_or(50);
1884 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
1885 let spec_router =
1886 spec_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
1887 tracing::info!(
1888 body_limit_mb = body_limit_mb,
1889 "Merging OpenAPI router with main router"
1890 );
1891 app = app.merge(spec_router);
1892 }
1893 Err(e) => {
1894 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1895 }
1896 }
1897 }
1898
1899 let route_chaos_injector: Option<
1903 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1904 > = if let Some(ref route_configs) = route_configs {
1905 if !route_configs.is_empty() {
1906 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1909 route_configs.to_vec();
1910 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1911 Ok(injector) => {
1912 info!(
1913 "Initialized advanced routing features for {} route(s)",
1914 route_configs.len()
1915 );
1916 Some(std::sync::Arc::new(injector)
1919 as std::sync::Arc<
1920 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1921 >)
1922 }
1923 Err(e) => {
1924 warn!(
1925 "Failed to initialize advanced routing features: {}. Using basic routing.",
1926 e
1927 );
1928 None
1929 }
1930 }
1931 } else {
1932 None
1933 }
1934 } else {
1935 None
1936 };
1937
1938 if let Some(route_configs) = route_configs {
1939 use axum::http::StatusCode;
1940 use axum::response::IntoResponse;
1941
1942 if !route_configs.is_empty() {
1943 info!("Registering {} custom route(s) from config", route_configs.len());
1944 }
1945
1946 let injector = route_chaos_injector.clone();
1947 for route_config in route_configs {
1948 let status = route_config.response.status;
1949 let body = route_config.response.body.clone();
1950 let headers = route_config.response.headers.clone();
1951 let path = route_config.path.clone();
1952 let method = route_config.method.clone();
1953
1954 let expected_method = method.to_uppercase();
1959 let injector_clone = injector.clone();
1963 app = app.route(
1964 &path,
1965 #[allow(clippy::non_send_fields_in_send_ty)]
1966 axum::routing::any(move |req: Request<Body>| {
1967 let body = body.clone();
1968 let headers = headers.clone();
1969 let expand = template_expand;
1970 let expected = expected_method.clone();
1971 let status_code = status;
1972 let injector_for_chaos = injector_clone.clone();
1974
1975 async move {
1976 if req.method().as_str() != expected.as_str() {
1978 return axum::response::Response::builder()
1980 .status(StatusCode::METHOD_NOT_ALLOWED)
1981 .header("Allow", &expected)
1982 .body(Body::empty())
1983 .unwrap()
1984 .into_response();
1985 }
1986
1987 if let Some(fault_response) = apply_route_chaos(
1991 injector_for_chaos.as_deref(),
1992 req.method(),
1993 req.uri(),
1994 )
1995 .await
1996 {
1997 return fault_response;
1998 }
1999
2000 let mut body_value = body.unwrap_or(serde_json::json!({}));
2002
2003 if expand {
2007 use mockforge_template_expansion::RequestContext;
2008 use serde_json::Value;
2009 use std::collections::HashMap;
2010
2011 let method = req.method().to_string();
2013 let path = req.uri().path().to_string();
2014
2015 let query_params: HashMap<String, Value> = req
2017 .uri()
2018 .query()
2019 .map(|q| {
2020 url::form_urlencoded::parse(q.as_bytes())
2021 .into_owned()
2022 .map(|(k, v)| (k, Value::String(v)))
2023 .collect()
2024 })
2025 .unwrap_or_default();
2026
2027 let headers: HashMap<String, Value> = req
2029 .headers()
2030 .iter()
2031 .map(|(k, v)| {
2032 (
2033 k.to_string(),
2034 Value::String(v.to_str().unwrap_or_default().to_string()),
2035 )
2036 })
2037 .collect();
2038
2039 let context = RequestContext {
2043 method,
2044 path,
2045 query_params,
2046 headers,
2047 body: None, path_params: HashMap::new(),
2049 multipart_fields: HashMap::new(),
2050 multipart_files: HashMap::new(),
2051 };
2052
2053 let body_value_clone = body_value.clone();
2057 let context_clone = context.clone();
2058 body_value = match tokio::task::spawn_blocking(move || {
2059 mockforge_template_expansion::expand_templates_in_json(
2060 body_value_clone,
2061 &context_clone,
2062 )
2063 })
2064 .await
2065 {
2066 Ok(result) => result,
2067 Err(_) => body_value, };
2069 }
2070
2071 let mut response = Json(body_value).into_response();
2072
2073 *response.status_mut() =
2075 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2076
2077 for (key, value) in headers {
2079 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2080 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2081 response.headers_mut().insert(header_name, header_value);
2082 }
2083 }
2084 }
2085
2086 response
2087 }
2088 }),
2089 );
2090
2091 debug!("Registered route: {} {}", method, path);
2092 }
2093 }
2094
2095 if let Some(health) = health_manager {
2097 app = app.merge(health::health_router(health));
2099 info!(
2100 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
2101 );
2102 } else if include_default_health {
2103 app = app.route(
2105 "/health",
2106 axum::routing::get(|| async {
2107 use mockforge_core::server_utils::health::HealthStatus;
2108 {
2109 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2111 Ok(value) => Json(value),
2112 Err(e) => {
2113 tracing::error!("Failed to serialize health status: {}", e);
2115 Json(serde_json::json!({
2116 "status": "healthy",
2117 "service": "mockforge-http",
2118 "uptime_seconds": 0
2119 }))
2120 }
2121 }
2122 }
2123 }),
2124 );
2125 }
2126
2127 app = app.merge(sse::sse_router());
2128 app = app.merge(file_server::file_serving_router());
2130
2131 let mgmt_spec = if let Some(ref sp) = spec_path {
2134 match OpenApiSpec::from_file(sp).await {
2135 Ok(s) => Some(Arc::new(s)),
2136 Err(e) => {
2137 debug!("Failed to load OpenAPI spec for management API: {}", e);
2138 None
2139 }
2140 }
2141 } else {
2142 None
2143 };
2144 let spec_path_clone = spec_path.clone();
2145 let mgmt_port = std::env::var("PORT")
2146 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2147 .ok()
2148 .and_then(|p| p.parse().ok())
2149 .unwrap_or(3000);
2150 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2151
2152 use std::sync::Arc;
2154 let ws_state = WsManagementState::new();
2155 let ws_broadcast = Arc::new(ws_state.tx.clone());
2156 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2157
2158 let management_state = if let Some(proxy_cfg) = proxy_config {
2160 use tokio::sync::RwLock;
2161 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2162 management_state.with_proxy_config(proxy_config_arc)
2163 } else {
2164 management_state
2165 };
2166
2167 #[cfg(feature = "smtp")]
2168 let management_state = {
2169 if let Some(smtp_reg) = smtp_registry {
2170 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2171 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2172 Err(e) => {
2173 error!(
2174 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2175 e.type_id()
2176 );
2177 management_state
2178 }
2179 }
2180 } else {
2181 management_state
2182 }
2183 };
2184 #[cfg(not(feature = "smtp"))]
2185 let management_state = {
2186 let _ = smtp_registry;
2187 management_state
2188 };
2189 #[cfg(feature = "mqtt")]
2190 let management_state = {
2191 if let Some(sessions) = mqtt_broker {
2194 match sessions.downcast::<mockforge_mqtt::SessionManager>() {
2195 Ok(sessions) => management_state.with_mqtt_sessions(sessions),
2196 Err(e) => {
2197 error!(
2198 "Invalid MQTT session manager passed to HTTP management state: {:?}",
2199 e.type_id()
2200 );
2201 management_state
2202 }
2203 }
2204 } else {
2205 management_state
2206 }
2207 };
2208 #[cfg(not(feature = "mqtt"))]
2209 let management_state = {
2210 let _ = mqtt_broker;
2211 management_state
2212 };
2213 #[cfg(feature = "amqp")]
2214 let management_state = {
2215 if let Some(broker) = amqp_broker {
2216 match broker.downcast::<mockforge_amqp::AmqpBroker>() {
2217 Ok(broker) => management_state.with_amqp_broker(broker),
2218 Err(e) => {
2219 error!(
2220 "Invalid AMQP broker passed to HTTP management state: {:?}",
2221 e.type_id()
2222 );
2223 management_state
2224 }
2225 }
2226 } else {
2227 management_state
2228 }
2229 };
2230 #[cfg(not(feature = "amqp"))]
2231 let management_state = {
2232 let _ = amqp_broker;
2233 management_state
2234 };
2235 #[cfg(feature = "kafka")]
2236 let management_state = {
2237 if let Some(broker) = kafka_broker {
2238 match broker.downcast::<mockforge_kafka::KafkaMockBroker>() {
2239 Ok(broker) => management_state.with_kafka_broker(broker),
2240 Err(e) => {
2241 error!(
2242 "Invalid Kafka broker passed to HTTP management state: {:?}",
2243 e.type_id()
2244 );
2245 management_state
2246 }
2247 }
2248 } else {
2249 management_state
2250 }
2251 };
2252 #[cfg(not(feature = "kafka"))]
2253 let management_state = {
2254 let _ = kafka_broker;
2255 management_state
2256 };
2257 let management_state_for_fallback = management_state.clone();
2258 app = app.nest("/__mockforge/api", management_router(management_state));
2259 app = app.fallback_service(
2261 axum::routing::any(management::dynamic_mock_fallback)
2262 .with_state(management_state_for_fallback),
2263 );
2264
2265 app = app.merge(verification_router());
2267
2268 {
2273 use crate::chain_handlers::{chains_router, create_chain_state};
2274 let chain_config = _circling_config.clone().unwrap_or_default();
2275 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2276 chain_config.clone(),
2277 ));
2278 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2279 chain_registry.clone(),
2280 chain_config,
2281 ));
2282 app = app.nest(
2283 "/__mockforge/chains",
2284 chains_router(create_chain_state(chain_registry, chain_engine)),
2285 );
2286 }
2287
2288 {
2293 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2294 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2295 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2296 }
2297
2298 {
2305 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2306 let fx_state = FixturesApiState::from_env();
2307 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2308 }
2309
2310 {
2317 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2318 let api_state = MockAiApiState::new(mockai.clone());
2319 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2320 }
2321
2322 app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2329
2330 {
2335 use crate::route_chaos_runtime::{
2336 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2337 };
2338 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2339 let middleware_state = runtime_state.clone();
2340 app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2341 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2342 }
2343
2344 {
2350 use crate::network_profile_runtime::{
2351 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2352 };
2353 let runtime_state = NetworkProfileRuntimeState::new(
2354 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2355 );
2356 let middleware_state = runtime_state.clone();
2357 app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2358 app = app
2359 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2360 }
2361
2362 use crate::auth::oidc::oidc_router;
2364 app = app.merge(oidc_router());
2365
2366 {
2368 use mockforge_core::security::get_global_access_review_service;
2369 if let Some(service) = get_global_access_review_service().await {
2370 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2371 let review_state = AccessReviewState { service };
2372 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2373 debug!("Access review API mounted at /api/v1/security/access-reviews");
2374 }
2375 }
2376
2377 {
2379 use mockforge_core::security::get_global_privileged_access_manager;
2380 if let Some(manager) = get_global_privileged_access_manager().await {
2381 use crate::handlers::privileged_access::{
2382 privileged_access_router, PrivilegedAccessState,
2383 };
2384 let privileged_state = PrivilegedAccessState { manager };
2385 app = app.nest(
2386 "/api/v1/security/privileged-access",
2387 privileged_access_router(privileged_state),
2388 );
2389 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2390 }
2391 }
2392
2393 {
2395 use mockforge_core::security::get_global_change_management_engine;
2396 if let Some(engine) = get_global_change_management_engine().await {
2397 use crate::handlers::change_management::{
2398 change_management_router, ChangeManagementState,
2399 };
2400 let change_state = ChangeManagementState { engine };
2401 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2402 debug!("Change management API mounted at /api/v1/change-management");
2403 }
2404 }
2405
2406 {
2408 use mockforge_core::security::get_global_risk_assessment_engine;
2409 if let Some(engine) = get_global_risk_assessment_engine().await {
2410 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2411 let risk_state = RiskAssessmentState { engine };
2412 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2413 debug!("Risk assessment API mounted at /api/v1/security/risks");
2414 }
2415 }
2416
2417 {
2419 use crate::auth::token_lifecycle::TokenLifecycleManager;
2420 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2421 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2422 let lifecycle_state = TokenLifecycleState {
2423 manager: lifecycle_manager,
2424 };
2425 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2426 debug!("Token lifecycle API mounted at /api/v1/auth");
2427 }
2428
2429 {
2431 use crate::auth::oidc::load_oidc_state;
2432 use crate::auth::token_lifecycle::TokenLifecycleManager;
2433 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2434 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2436 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2437 let oauth2_state = OAuth2ServerState {
2438 oidc_state,
2439 lifecycle_manager,
2440 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2441 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2442 };
2443 app = app.merge(oauth2_server_router(oauth2_state));
2444 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2445 }
2446
2447 {
2449 use crate::auth::oidc::load_oidc_state;
2450 use crate::auth::risk_engine::RiskEngine;
2451 use crate::auth::token_lifecycle::TokenLifecycleManager;
2452 use crate::handlers::consent::{consent_router, ConsentState};
2453 use crate::handlers::oauth2_server::OAuth2ServerState;
2454 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2456 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2457 let oauth2_state = OAuth2ServerState {
2458 oidc_state: oidc_state.clone(),
2459 lifecycle_manager: lifecycle_manager.clone(),
2460 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2461 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2462 };
2463 let risk_engine = Arc::new(RiskEngine::default());
2464 let consent_state = ConsentState {
2465 oauth2_state,
2466 risk_engine,
2467 };
2468 app = app.merge(consent_router(consent_state));
2469 debug!("Consent screen endpoints mounted at /consent");
2470 }
2471
2472 {
2474 use crate::auth::risk_engine::RiskEngine;
2475 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2476 let risk_engine = Arc::new(RiskEngine::default());
2477 let risk_state = RiskSimulationState { risk_engine };
2478 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2479 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2480 }
2481
2482 #[cfg(feature = "database")]
2484 let database = {
2485 use crate::database::Database;
2486 let database_url = std::env::var("DATABASE_URL").ok();
2487 match Database::connect_optional(database_url.as_deref()).await {
2488 Ok(db) => {
2489 if db.is_connected() {
2490 if let Err(e) = db.migrate_if_connected().await {
2492 warn!("Failed to run database migrations: {}", e);
2493 } else {
2494 info!("Database connected and migrations applied");
2495 }
2496 }
2497 Some(db)
2498 }
2499 Err(e) => {
2500 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2501 None
2502 }
2503 }
2504 };
2505
2506 let (drift_engine, incident_manager, drift_config) = {
2509 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2510 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2511 use std::sync::Arc;
2512
2513 let drift_config = DriftBudgetConfig::default();
2515 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2516
2517 let incident_store = Arc::new(IncidentStore::default());
2519 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2520
2521 (drift_engine, incident_manager, drift_config)
2522 };
2523
2524 {
2525 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2526 use crate::middleware::drift_tracking::DriftTrackingState;
2527 use mockforge_contracts::consumer_contracts::{
2528 ConsumerBreakingChangeDetector, UsageRecorder,
2529 };
2530 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2531 use std::sync::Arc;
2532
2533 let usage_recorder = Arc::new(UsageRecorder::default());
2535 let consumer_detector =
2536 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2537
2538 let diff_analyzer = if drift_config.enabled {
2540 match ContractDiffAnalyzer::new(
2541 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2542 ) {
2543 Ok(analyzer) => Some(Arc::new(analyzer)),
2544 Err(e) => {
2545 warn!("Failed to create contract diff analyzer: {}", e);
2546 None
2547 }
2548 }
2549 } else {
2550 None
2551 };
2552
2553 let spec = if let Some(ref spec_path) = spec_path {
2556 match OpenApiSpec::from_file(spec_path).await {
2557 Ok(s) => Some(Arc::new(s)),
2558 Err(e) => {
2559 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2560 None
2561 }
2562 }
2563 } else {
2564 None
2565 };
2566
2567 let drift_tracking_state = DriftTrackingState {
2569 diff_analyzer,
2570 spec,
2571 drift_engine: drift_engine.clone(),
2572 incident_manager: incident_manager.clone(),
2573 usage_recorder,
2574 consumer_detector,
2575 enabled: drift_config.enabled,
2576 };
2577
2578 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2580
2581 let drift_tracking_state_clone = drift_tracking_state.clone();
2584 app = app.layer(axum::middleware::from_fn(
2585 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2586 let state = drift_tracking_state_clone.clone();
2587 async move {
2588 if req.extensions().get::<DriftTrackingState>().is_none() {
2590 req.extensions_mut().insert(state);
2591 }
2592 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2594 .await
2595 }
2596 },
2597 ));
2598
2599 let drift_state = DriftBudgetState {
2600 engine: drift_engine.clone(),
2601 incident_manager: incident_manager.clone(),
2602 gitops_handler: None, };
2604
2605 app = app.merge(drift_budget_router(drift_state));
2606 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2607 }
2608
2609 #[cfg(feature = "pipelines")]
2611 {
2612 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2613
2614 let pipeline_state = PipelineState::new();
2615 app = app.merge(pipeline_router(pipeline_state));
2616 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2617 }
2618
2619 {
2621 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2622 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2623 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2624 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2625 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2626 use mockforge_intelligence::handlers::forecasting::{forecasting_router, ForecastingState};
2627 use mockforge_intelligence::handlers::semantic_drift::{
2628 semantic_drift_router, SemanticDriftState,
2629 };
2630 use mockforge_intelligence::handlers::threat_modeling::{
2631 threat_modeling_router, ThreatModelingState,
2632 };
2633 use std::sync::Arc;
2634
2635 let forecasting_config = ForecastingConfig::default();
2637 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2638 let forecasting_state = ForecastingState {
2639 forecaster,
2640 #[cfg(feature = "database")]
2641 database: database.clone(),
2642 };
2643
2644 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2646 let semantic_state = SemanticDriftState {
2647 manager: semantic_manager,
2648 #[cfg(feature = "database")]
2649 database: database.clone(),
2650 };
2651
2652 let threat_config = ThreatModelingConfig::default();
2654 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2655 Ok(analyzer) => Arc::new(analyzer),
2656 Err(e) => {
2657 warn!("Failed to create threat analyzer: {}. Using default.", e);
2658 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2659 |_| {
2660 ThreatAnalyzer::new(ThreatModelingConfig {
2662 enabled: false,
2663 ..Default::default()
2664 })
2665 .expect("Failed to create fallback threat analyzer")
2666 },
2667 ))
2668 }
2669 };
2670 let mut webhook_configs = Vec::new();
2672 let config_paths = [
2673 "config.yaml",
2674 "mockforge.yaml",
2675 "tools/mockforge/config.yaml",
2676 "../tools/mockforge/config.yaml",
2677 ];
2678
2679 for path in &config_paths {
2680 if let Ok(config) = mockforge_core::config::load_config(path).await {
2681 if !config.incidents.webhooks.is_empty() {
2682 webhook_configs = config.incidents.webhooks.clone();
2683 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2684 break;
2685 }
2686 }
2687 }
2688
2689 if webhook_configs.is_empty() {
2690 debug!("No webhook configs found in config files, using empty list");
2691 }
2692
2693 let threat_state = ThreatModelingState {
2694 analyzer: threat_analyzer,
2695 webhook_configs,
2696 #[cfg(feature = "database")]
2697 database: database.clone(),
2698 };
2699
2700 let contract_health_state = ContractHealthState {
2702 incident_manager: incident_manager.clone(),
2703 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2704 #[cfg(feature = "database")]
2705 database: database.clone(),
2706 };
2707
2708 app = app.merge(forecasting_router(forecasting_state));
2710 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2711
2712 app = app.merge(semantic_drift_router(semantic_state));
2713 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2714
2715 app = app.merge(threat_modeling_router(threat_state));
2716 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2717
2718 app = app.merge(contract_health_router(contract_health_state));
2719 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2720 }
2721
2722 {
2724 use crate::handlers::protocol_contracts::{
2725 protocol_contracts_router, ProtocolContractState,
2726 };
2727 use mockforge_core::contract_drift::{
2728 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2729 };
2730 use std::sync::Arc;
2731 use tokio::sync::RwLock;
2732
2733 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2735
2736 let mut fitness_registry = FitnessFunctionRegistry::new();
2738
2739 let config_paths = [
2741 "config.yaml",
2742 "mockforge.yaml",
2743 "tools/mockforge/config.yaml",
2744 "../tools/mockforge/config.yaml",
2745 ];
2746
2747 let mut config_loaded = false;
2748 for path in &config_paths {
2749 if let Ok(config) = mockforge_core::config::load_config(path).await {
2750 if !config.contracts.fitness_rules.is_empty() {
2751 if let Err(e) =
2752 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2753 {
2754 warn!("Failed to load fitness rules from config {}: {}", path, e);
2755 } else {
2756 info!(
2757 "Loaded {} fitness rules from config: {}",
2758 config.contracts.fitness_rules.len(),
2759 path
2760 );
2761 config_loaded = true;
2762 break;
2763 }
2764 }
2765 }
2766 }
2767
2768 if !config_loaded {
2769 debug!("No fitness rules found in config files, using empty registry");
2770 }
2771
2772 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2773
2774 let consumer_mapping_registry =
2778 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2779 let consumer_analyzer =
2780 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2781
2782 let protocol_state = ProtocolContractState {
2783 registry: contract_registry,
2784 drift_engine: Some(drift_engine.clone()),
2785 incident_manager: Some(incident_manager.clone()),
2786 fitness_registry: Some(fitness_registry),
2787 consumer_analyzer: Some(consumer_analyzer),
2788 };
2789
2790 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2791 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2792 }
2793
2794 #[cfg(feature = "behavioral-cloning")]
2796 {
2797 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2798 use std::path::PathBuf;
2799
2800 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2802 .ok()
2803 .map(PathBuf::from)
2804 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2805
2806 let bc_middleware_state = if let Some(path) = db_path {
2807 BehavioralCloningMiddlewareState::with_database_path(path)
2808 } else {
2809 BehavioralCloningMiddlewareState::new()
2810 };
2811
2812 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2814 .ok()
2815 .and_then(|v| v.parse::<bool>().ok())
2816 .unwrap_or(false);
2817
2818 if enabled {
2819 let bc_state_clone = bc_middleware_state.clone();
2820 app = app.layer(axum::middleware::from_fn(
2821 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2822 let state = bc_state_clone.clone();
2823 async move {
2824 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2826 req.extensions_mut().insert(state);
2827 }
2828 middleware::behavioral_cloning::behavioral_cloning_middleware(req, next)
2830 .await
2831 }
2832 },
2833 ));
2834 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2835 }
2836 }
2837
2838 {
2840 use crate::handlers::consumer_contracts::{
2841 consumer_contracts_router, ConsumerContractsState,
2842 };
2843 use mockforge_contracts::consumer_contracts::{
2844 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2845 };
2846 use std::sync::Arc;
2847
2848 let registry = Arc::new(ConsumerRegistry::default());
2850
2851 let usage_recorder = Arc::new(UsageRecorder::default());
2853
2854 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2856
2857 let consumer_state = ConsumerContractsState {
2858 registry,
2859 usage_recorder,
2860 detector,
2861 violations: Arc::new(RwLock::new(HashMap::new())),
2862 };
2863
2864 app = app.merge(consumer_contracts_router(consumer_state));
2865 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2866 }
2867
2868 #[cfg(feature = "behavioral-cloning")]
2870 {
2871 use crate::handlers::behavioral_cloning::{
2872 behavioral_cloning_router, BehavioralCloningState,
2873 };
2874 use std::path::PathBuf;
2875
2876 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2878 .ok()
2879 .map(PathBuf::from)
2880 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2881
2882 let bc_state = if let Some(path) = db_path {
2883 BehavioralCloningState::with_database_path(path)
2884 } else {
2885 BehavioralCloningState::new()
2886 };
2887
2888 app = app.merge(behavioral_cloning_router(bc_state));
2889 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2890 }
2891
2892 {
2894 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2895 use mockforge_intelligence::consistency::ConsistencyEngine;
2896 use mockforge_intelligence::handlers::consistency::{consistency_router, ConsistencyState};
2897 use std::sync::Arc;
2898
2899 let consistency_engine = Arc::new(ConsistencyEngine::new());
2901
2902 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2904 consistency_engine.register_adapter(http_adapter.clone()).await;
2905
2906 let consistency_state = ConsistencyState {
2908 engine: consistency_engine.clone(),
2909 };
2910
2911 use mockforge_intelligence::handlers::xray::XRayState;
2913 let xray_state = Arc::new(XRayState::new(consistency_engine.clone()));
2914
2915 let consistency_middleware_state = ConsistencyMiddlewareState {
2917 engine: consistency_engine.clone(),
2918 adapter: http_adapter,
2919 xray_state: Some(xray_state.clone()),
2920 };
2921
2922 if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2930 tracing::info!(
2931 upstream = %reality_cfg.upstream_base,
2932 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2933 );
2934 app = app.layer(axum::middleware::from_fn(
2935 move |req: axum::extract::Request, next: axum::middleware::Next| {
2936 let cfg = reality_cfg.clone();
2937 async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2938 },
2939 ));
2940 }
2941
2942 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2944 app = app.layer(axum::middleware::from_fn(
2945 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2946 let state = consistency_middleware_state_clone.clone();
2947 async move {
2948 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2950 req.extensions_mut().insert(state);
2951 }
2952 consistency::middleware::consistency_middleware(req, next).await
2954 }
2955 },
2956 ));
2957
2958 app = app.merge(consistency_router(consistency_state));
2960 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2961
2962 #[cfg(feature = "scenario-engine")]
2970 {
2971 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
2972 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
2973 Ok(s) => s,
2974 Err(e) => {
2975 tracing::warn!(
2976 error = %e,
2977 "Failed to init scenario storage; runtime scenarios API will list empty"
2978 );
2979 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
2982 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
2983 .expect("temp scenario storage")
2984 }
2985 };
2986 if let Err(e) = scenario_storage.load().await {
2987 tracing::warn!(
2988 error = %e,
2989 "Failed to load installed scenarios; API will list empty until scenarios are installed"
2990 );
2991 }
2992 let scenarios_state =
2993 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
2994 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
2995 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
2996 }
2997
2998 {
3000 use mockforge_intelligence::handlers::fidelity::{fidelity_router, FidelityState};
3001 let fidelity_state = FidelityState::new();
3002 app = app.merge(fidelity_router(fidelity_state));
3003 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
3004 }
3005
3006 {
3008 use mockforge_intelligence::handlers::scenario_studio::{
3009 scenario_studio_router, ScenarioStudioState,
3010 };
3011 let scenario_studio_state = ScenarioStudioState::new();
3012 app = app.merge(scenario_studio_router(scenario_studio_state));
3013 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
3014 }
3015
3016 {
3018 use crate::handlers::performance::{performance_router, PerformanceState};
3019 let performance_state = PerformanceState::new();
3020 app = app.nest("/api/performance", performance_router(performance_state));
3021 debug!("Performance mode endpoints mounted at /api/performance");
3022 }
3023
3024 {
3026 use crate::handlers::world_state::{world_state_router, WorldStateState};
3027 use mockforge_world_state::WorldStateEngine;
3028 use std::sync::Arc;
3029 use tokio::sync::RwLock;
3030
3031 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
3032 let world_state_state = WorldStateState {
3033 engine: world_state_engine,
3034 };
3035 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
3036 debug!("World state endpoints mounted at /api/world-state");
3037 }
3038
3039 {
3041 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
3042 use mockforge_core::snapshots::SnapshotManager;
3043 use std::path::PathBuf;
3044
3045 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
3046 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
3047
3048 let snapshot_state = SnapshotState {
3049 manager: snapshot_manager,
3050 consistency_engine: Some(consistency_engine.clone()),
3051 workspace_persistence: None, vbr_engine: None, recorder: None, };
3055
3056 app = app.merge(snapshot_router(snapshot_state));
3057 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
3058
3059 {
3061 use mockforge_intelligence::handlers::xray::xray_router;
3062 app = app.merge(xray_router((*xray_state).clone()));
3063 debug!("X-Ray API endpoints mounted at /api/v1/xray");
3064 }
3065 }
3066
3067 {
3069 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
3070 use crate::middleware::ab_testing::ab_testing_middleware;
3071
3072 let ab_testing_state = ABTestingState::new();
3073
3074 let ab_testing_state_clone = ab_testing_state.clone();
3076 app = app.layer(axum::middleware::from_fn(
3077 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
3078 let state = ab_testing_state_clone.clone();
3079 async move {
3080 if req.extensions().get::<ABTestingState>().is_none() {
3082 req.extensions_mut().insert(state);
3083 }
3084 ab_testing_middleware(req, next).await
3086 }
3087 },
3088 ));
3089
3090 app = app.merge(ab_testing_router(ab_testing_state));
3092 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
3093 }
3094 }
3095
3096 {
3098 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
3099 use mockforge_intelligence::pr_generation::{PRGenerator, PRProvider};
3100 use std::sync::Arc;
3101
3102 let pr_config = mockforge_intelligence::pr_generation::PRGenerationConfig::from_env();
3104
3105 let generator = pr_config.token.clone().filter(|_| pr_config.enabled).map(|token| {
3108 let generator = match pr_config.provider {
3109 PRProvider::GitHub => PRGenerator::new_github(
3110 pr_config.owner.clone(),
3111 pr_config.repo.clone(),
3112 token,
3113 pr_config.base_branch.clone(),
3114 ),
3115 PRProvider::GitLab => PRGenerator::new_gitlab(
3116 pr_config.owner.clone(),
3117 pr_config.repo.clone(),
3118 token,
3119 pr_config.base_branch.clone(),
3120 ),
3121 };
3122 Arc::new(generator)
3123 });
3124
3125 let pr_state = PRGenerationState {
3126 generator: generator.clone(),
3127 };
3128
3129 app = app.merge(pr_generation_router(pr_state));
3130 if generator.is_some() {
3131 debug!(
3132 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3133 pr_config.provider
3134 );
3135 } else {
3136 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3137 }
3138 }
3139
3140 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3142
3143 if let Some(mt_config) = multi_tenant_config {
3145 if mt_config.enabled {
3146 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3147 use std::sync::Arc;
3148
3149 info!(
3150 "Multi-tenant mode enabled with {} routing strategy",
3151 match mt_config.routing_strategy {
3152 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3153 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3154 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3155 }
3156 );
3157
3158 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3160
3161 let default_workspace =
3163 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3164 if let Err(e) =
3165 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3166 {
3167 warn!("Failed to register default workspace: {}", e);
3168 } else {
3169 info!("Registered default workspace: '{}'", mt_config.default_workspace);
3170 }
3171
3172 let registry = Arc::new(registry);
3174
3175 let _workspace_router = WorkspaceRouter::new(registry);
3177 info!("Workspace routing middleware initialized for HTTP server");
3178 }
3179 }
3180
3181 let mut final_cors_config = cors_config;
3183 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3184 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3186 let mut rate_limit_config = middleware::RateLimitConfig {
3187 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3188 .ok()
3189 .and_then(|v| v.parse().ok())
3190 .unwrap_or(1000),
3191 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3192 .ok()
3193 .and_then(|v| v.parse().ok())
3194 .unwrap_or(2000),
3195 per_ip: true,
3196 per_endpoint: false,
3197 };
3198
3199 if let Some(deploy_config) = &deceptive_deploy_config {
3200 if deploy_config.enabled {
3201 info!("Deceptive deploy mode enabled - applying production-like configuration");
3202
3203 if let Some(prod_cors) = &deploy_config.cors {
3205 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3206 enabled: true,
3207 allowed_origins: prod_cors.allowed_origins.clone(),
3208 allowed_methods: prod_cors.allowed_methods.clone(),
3209 allowed_headers: prod_cors.allowed_headers.clone(),
3210 allow_credentials: prod_cors.allow_credentials,
3211 });
3212 info!("Applied production-like CORS configuration");
3213 }
3214
3215 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3217 rate_limit_config = middleware::RateLimitConfig {
3218 requests_per_minute: prod_rate_limit.requests_per_minute,
3219 burst: prod_rate_limit.burst,
3220 per_ip: prod_rate_limit.per_ip,
3221 per_endpoint: false,
3222 };
3223 info!(
3224 "Applied production-like rate limiting: {} req/min, burst: {}",
3225 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3226 );
3227 }
3228
3229 if !deploy_config.headers.is_empty() {
3231 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3232 production_headers = Some(std::sync::Arc::new(headers_map));
3233 info!("Configured {} production headers", deploy_config.headers.len());
3234 }
3235
3236 if let Some(prod_oauth) = &deploy_config.oauth {
3238 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3239 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3240 oauth2: Some(oauth2_config),
3241 ..Default::default()
3242 });
3243 info!("Applied production-like OAuth configuration for deceptive deploy");
3244 }
3245 }
3246 }
3247
3248 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3250 let rate_limiter =
3251 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3252
3253 let mut state = HttpServerState::new();
3254 if rate_limit_disabled {
3255 info!(
3256 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3257 );
3258 } else {
3259 state = state.with_rate_limiter(rate_limiter.clone());
3260 }
3261
3262 if let Some(headers) = production_headers.clone() {
3264 state = state.with_production_headers(headers);
3265 }
3266
3267 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3269
3270 if state.production_headers.is_some() {
3272 app =
3273 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3274 }
3275
3276 if middleware::is_keepalive_hint_enabled() {
3281 info!(
3282 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3283 );
3284 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3285 }
3286
3287 if middleware::is_conn_log_enabled() {
3292 info!(
3293 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
3294 );
3295 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
3296 }
3297
3298 if let Some(auth_config) = deceptive_deploy_auth_config {
3300 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3301 use std::collections::HashMap;
3302 use std::sync::Arc;
3303 use tokio::sync::RwLock;
3304
3305 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3307 match create_oauth2_client(oauth2_config) {
3308 Ok(client) => Some(client),
3309 Err(e) => {
3310 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3311 None
3312 }
3313 }
3314 } else {
3315 None
3316 };
3317
3318 let auth_state = AuthState {
3320 config: auth_config,
3321 spec: None, oauth2_client,
3323 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3324 };
3325
3326 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3328 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3329 }
3330
3331 #[cfg(feature = "runtime-daemon")]
3333 {
3334 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3335 use std::sync::Arc;
3336
3337 let daemon_config = RuntimeDaemonConfig::from_env();
3339
3340 if daemon_config.enabled {
3341 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3342
3343 let management_api_url =
3345 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3346 let port =
3347 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3348 format!("http://localhost:{}", port)
3349 });
3350
3351 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3353
3354 let detector = NotFoundDetector::new(daemon_config.clone());
3356 detector.set_generator(generator).await;
3357
3358 let detector_clone = detector.clone();
3360 app = app.layer(axum::middleware::from_fn(
3361 move |req: axum::extract::Request, next: axum::middleware::Next| {
3362 let detector = detector_clone.clone();
3363 async move { detector.detect_and_auto_create(req, next).await }
3364 },
3365 ));
3366
3367 debug!("Runtime daemon 404 detection middleware added");
3368 }
3369 }
3370
3371 {
3373 let routes_state = HttpServerState::with_routes(captured_routes);
3374 let routes_router = Router::new()
3375 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3376 .with_state(routes_state);
3377 app = app.merge(routes_router);
3378 }
3379
3380 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3382
3383 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3388
3389 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3392
3393 app = apply_cors_middleware(app, final_cors_config);
3395
3396 app
3397}
3398
3399#[test]
3403fn test_route_info_clone() {
3404 let route = RouteInfo {
3405 method: "POST".to_string(),
3406 path: "/users".to_string(),
3407 operation_id: Some("createUser".to_string()),
3408 summary: None,
3409 description: None,
3410 parameters: vec![],
3411 };
3412
3413 let cloned = route.clone();
3414 assert_eq!(route.method, cloned.method);
3415 assert_eq!(route.path, cloned.path);
3416 assert_eq!(route.operation_id, cloned.operation_id);
3417}
3418
3419#[test]
3420fn test_http_server_state_new() {
3421 let state = HttpServerState::new();
3422 assert_eq!(state.routes.len(), 0);
3423}
3424
3425#[test]
3426fn test_http_server_state_with_routes() {
3427 let routes = vec![
3428 RouteInfo {
3429 method: "GET".to_string(),
3430 path: "/users".to_string(),
3431 operation_id: Some("getUsers".to_string()),
3432 summary: None,
3433 description: None,
3434 parameters: vec![],
3435 },
3436 RouteInfo {
3437 method: "POST".to_string(),
3438 path: "/users".to_string(),
3439 operation_id: Some("createUser".to_string()),
3440 summary: None,
3441 description: None,
3442 parameters: vec![],
3443 },
3444 ];
3445
3446 let state = HttpServerState::with_routes(routes.clone());
3447 assert_eq!(state.routes.len(), 2);
3448 assert_eq!(state.routes[0].method, "GET");
3449 assert_eq!(state.routes[1].method, "POST");
3450}
3451
3452#[test]
3453fn test_http_server_state_clone() {
3454 let routes = vec![RouteInfo {
3455 method: "GET".to_string(),
3456 path: "/test".to_string(),
3457 operation_id: None,
3458 summary: None,
3459 description: None,
3460 parameters: vec![],
3461 }];
3462
3463 let state = HttpServerState::with_routes(routes);
3464 let cloned = state.clone();
3465
3466 assert_eq!(state.routes.len(), cloned.routes.len());
3467 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3468}
3469
3470#[tokio::test]
3471async fn test_build_router_without_openapi() {
3472 let _router = build_router(None, None, None).await;
3473 }
3475
3476#[tokio::test]
3477async fn test_build_router_with_nonexistent_spec() {
3478 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3479 }
3481
3482#[tokio::test]
3483async fn test_build_router_with_auth_and_latency() {
3484 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3485 }
3487
3488#[tokio::test]
3489async fn test_build_router_with_latency() {
3490 let _router = build_router_with_latency(None, None, None).await;
3491 }
3493
3494#[tokio::test]
3495async fn test_build_router_with_auth() {
3496 let _router = build_router_with_auth(None, None, None).await;
3497 }
3499
3500#[tokio::test]
3501async fn test_build_router_with_chains() {
3502 let _router = build_router_with_chains(None, None, None).await;
3503 }
3505
3506#[test]
3507fn test_route_info_with_all_fields() {
3508 let route = RouteInfo {
3509 method: "PUT".to_string(),
3510 path: "/users/{id}".to_string(),
3511 operation_id: Some("updateUser".to_string()),
3512 summary: Some("Update user".to_string()),
3513 description: Some("Updates an existing user".to_string()),
3514 parameters: vec!["id".to_string(), "body".to_string()],
3515 };
3516
3517 assert!(route.operation_id.is_some());
3518 assert!(route.summary.is_some());
3519 assert!(route.description.is_some());
3520 assert_eq!(route.parameters.len(), 2);
3521}
3522
3523#[test]
3524fn test_route_info_with_minimal_fields() {
3525 let route = RouteInfo {
3526 method: "DELETE".to_string(),
3527 path: "/users/{id}".to_string(),
3528 operation_id: None,
3529 summary: None,
3530 description: None,
3531 parameters: vec![],
3532 };
3533
3534 assert!(route.operation_id.is_none());
3535 assert!(route.summary.is_none());
3536 assert!(route.description.is_none());
3537 assert_eq!(route.parameters.len(), 0);
3538}
3539
3540#[test]
3541fn test_http_server_state_empty_routes() {
3542 let state = HttpServerState::with_routes(vec![]);
3543 assert_eq!(state.routes.len(), 0);
3544}
3545
3546#[test]
3547fn test_http_server_state_multiple_routes() {
3548 let routes = vec![
3549 RouteInfo {
3550 method: "GET".to_string(),
3551 path: "/users".to_string(),
3552 operation_id: Some("listUsers".to_string()),
3553 summary: Some("List all users".to_string()),
3554 description: None,
3555 parameters: vec![],
3556 },
3557 RouteInfo {
3558 method: "GET".to_string(),
3559 path: "/users/{id}".to_string(),
3560 operation_id: Some("getUser".to_string()),
3561 summary: Some("Get a user".to_string()),
3562 description: None,
3563 parameters: vec!["id".to_string()],
3564 },
3565 RouteInfo {
3566 method: "POST".to_string(),
3567 path: "/users".to_string(),
3568 operation_id: Some("createUser".to_string()),
3569 summary: Some("Create a user".to_string()),
3570 description: None,
3571 parameters: vec!["body".to_string()],
3572 },
3573 ];
3574
3575 let state = HttpServerState::with_routes(routes);
3576 assert_eq!(state.routes.len(), 3);
3577
3578 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3580 assert!(methods.contains(&"GET"));
3581 assert!(methods.contains(&"POST"));
3582}
3583
3584#[test]
3585fn test_http_server_state_with_rate_limiter() {
3586 use std::sync::Arc;
3587
3588 let config = middleware::RateLimitConfig::default();
3589 let rate_limiter = Arc::new(middleware::GlobalRateLimiter::new(config));
3590
3591 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3592
3593 assert!(state.rate_limiter.is_some());
3594 assert_eq!(state.routes.len(), 0);
3595}
3596
3597#[tokio::test]
3598async fn test_build_router_includes_rate_limiter() {
3599 let _router = build_router(None, None, None).await;
3600 }