1pub mod ai_handler;
204pub mod auth;
205pub mod chain_handlers;
206pub mod consistency;
208pub mod contract_diff_api;
210pub mod contract_diff_middleware;
212pub mod counting_listener;
215pub mod coverage;
216#[cfg(feature = "database")]
222pub mod database {
223 pub use mockforge_intelligence::database::*;
224}
225pub mod file_generator;
227pub mod file_server;
229pub mod fixtures_api;
231pub mod health;
233pub mod http_tracing_middleware;
234pub mod latency_profiles;
236
237pub mod llm_mock;
239
240pub mod management;
242pub mod management_auth;
244pub mod management_ws;
246pub mod mcp_mock;
248pub mod metrics_middleware;
249pub mod middleware;
250pub mod mockai_api;
252pub mod network_profile_runtime;
254pub mod op_middleware;
255pub mod protocol_server;
257pub mod proxy_server;
259pub mod quick_mock;
261pub mod rag_ai_generator;
263pub mod reality_proxy;
265pub mod replay_listing;
267pub mod request_logging;
268#[cfg(feature = "route-chaos")]
269pub mod route_chaos_runtime;
270#[cfg(feature = "scenario-engine")]
272pub mod scenarios_runtime;
273pub mod spec_import;
275pub mod sse;
277#[cfg(feature = "scenario-engine")]
279pub mod state_machine_api;
280pub mod time_travel_api;
282pub mod tls;
284pub mod token_response;
286pub mod ui_builder;
288pub mod verification;
290
291pub mod handlers;
293
294pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
296pub use health::{HealthManager, ServiceStatus};
298
299pub use management::{
301 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
302 ServerConfig, ServerStats,
303};
304
305pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
307
308pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
310
311pub use verification::verification_router;
313
314pub use metrics_middleware::collect_http_metrics;
316
317pub use http_tracing_middleware::http_tracing_middleware;
319
320pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
322
323async fn load_persona_from_config() -> Option<Arc<Persona>> {
326 use mockforge_core::config::load_config;
327
328 let config_paths = [
330 "config.yaml",
331 "mockforge.yaml",
332 "tools/mockforge/config.yaml",
333 "../tools/mockforge/config.yaml",
334 ];
335
336 for path in &config_paths {
337 if let Ok(config) = load_config(path).await {
338 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
341 {
342 tracing::info!(
343 "Loaded active persona '{}' from config file: {}",
344 persona.name,
345 path
346 );
347 return Some(Arc::new(persona.clone()));
348 } else {
349 tracing::debug!(
350 "No active persona found in config file: {} (personas count: {})",
351 path,
352 config.mockai.intelligent_behavior.personas.personas.len()
353 );
354 }
355 } else {
356 tracing::debug!("Could not load config from: {}", path);
357 }
358 }
359
360 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
361 None
362}
363
364use axum::body::Body;
365use axum::extract::State;
366use axum::http::Request;
367use axum::middleware::from_fn_with_state;
368use axum::response::Json;
369use axum::Router;
370use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
371use mockforge_core::intelligent_behavior::config::Persona;
372use mockforge_foundation::latency::LatencyInjector;
373use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
374use mockforge_openapi::openapi_routes::ValidationOptions;
375use mockforge_openapi::OpenApiSpec;
376use std::sync::Arc;
377use tower_http::cors::{Any, CorsLayer};
378
379#[cfg(feature = "data-faker")]
380use mockforge_data::provider::register_core_faker_provider;
381use mockforge_foundation::latency::LatencyProfile;
382use std::collections::HashMap;
383use std::ffi::OsStr;
384use std::path::Path;
385use tokio::fs;
386use tokio::sync::RwLock;
387use tracing::*;
388
389#[derive(Clone)]
391pub struct RouteInfo {
392 pub method: String,
394 pub path: String,
396 pub operation_id: Option<String>,
398 pub summary: Option<String>,
400 pub description: Option<String>,
402 pub parameters: Vec<String>,
404}
405
406#[derive(Clone)]
408pub struct HttpServerState {
409 pub routes: Vec<RouteInfo>,
411 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
413 pub production_headers: Option<Arc<HashMap<String, String>>>,
415}
416
417impl Default for HttpServerState {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423impl HttpServerState {
424 pub fn new() -> Self {
426 Self {
427 routes: Vec::new(),
428 rate_limiter: None,
429 production_headers: None,
430 }
431 }
432
433 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
435 Self {
436 routes,
437 rate_limiter: None,
438 production_headers: None,
439 }
440 }
441
442 pub fn with_rate_limiter(
444 mut self,
445 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
446 ) -> Self {
447 self.rate_limiter = Some(rate_limiter);
448 self
449 }
450
451 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
453 self.production_headers = Some(headers);
454 self
455 }
456}
457
458async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
460 let route_info: Vec<serde_json::Value> = state
461 .routes
462 .iter()
463 .map(|route| {
464 serde_json::json!({
465 "method": route.method,
466 "path": route.path,
467 "operation_id": route.operation_id,
468 "summary": route.summary,
469 "description": route.description,
470 "parameters": route.parameters
471 })
472 })
473 .collect();
474
475 Json(serde_json::json!({
476 "routes": route_info,
477 "total": state.routes.len()
478 }))
479}
480
481async fn get_docs_handler() -> axum::response::Html<&'static str> {
483 axum::response::Html(include_str!("../static/docs.html"))
484}
485
486pub async fn build_router(
488 spec_path: Option<String>,
489 options: Option<ValidationOptions>,
490 failure_config: Option<FailureConfig>,
491) -> Router {
492 build_router_with_multi_tenant(
493 spec_path,
494 options,
495 failure_config,
496 None,
497 None,
498 None,
499 None,
500 None,
501 None,
502 None,
503 )
504 .await
505}
506
507fn apply_cors_middleware(
509 app: Router,
510 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
511) -> Router {
512 use http::Method;
513 use tower_http::cors::AllowOrigin;
514
515 if let Some(config) = cors_config {
516 if !config.enabled {
517 return app;
518 }
519
520 let mut cors_layer = CorsLayer::new();
521 let is_wildcard_origin;
522
523 if config.allowed_origins.contains(&"*".to_string()) {
525 cors_layer = cors_layer.allow_origin(Any);
526 is_wildcard_origin = true;
527 } else if !config.allowed_origins.is_empty() {
528 let origins: Vec<_> = config
530 .allowed_origins
531 .iter()
532 .filter_map(|origin| {
533 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
534 })
535 .collect();
536
537 if origins.is_empty() {
538 warn!("No valid CORS origins configured, using permissive CORS");
540 cors_layer = cors_layer.allow_origin(Any);
541 is_wildcard_origin = true;
542 } else {
543 if origins.len() == 1 {
546 cors_layer = cors_layer.allow_origin(origins[0].clone());
547 is_wildcard_origin = false;
548 } else {
549 warn!(
551 "Multiple CORS origins configured, using permissive CORS. \
552 Consider using '*' for all origins."
553 );
554 cors_layer = cors_layer.allow_origin(Any);
555 is_wildcard_origin = true;
556 }
557 }
558 } else {
559 cors_layer = cors_layer.allow_origin(Any);
561 is_wildcard_origin = true;
562 }
563
564 if !config.allowed_methods.is_empty() {
566 let methods: Vec<Method> =
567 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
568 if !methods.is_empty() {
569 cors_layer = cors_layer.allow_methods(methods);
570 }
571 } else {
572 cors_layer = cors_layer.allow_methods([
574 Method::GET,
575 Method::POST,
576 Method::PUT,
577 Method::DELETE,
578 Method::PATCH,
579 Method::OPTIONS,
580 ]);
581 }
582
583 if !config.allowed_headers.is_empty() {
585 let headers: Vec<_> = config
586 .allowed_headers
587 .iter()
588 .filter_map(|h| h.parse::<http::HeaderName>().ok())
589 .collect();
590 if !headers.is_empty() {
591 cors_layer = cors_layer.allow_headers(headers);
592 }
593 } else {
594 cors_layer =
596 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
597 }
598
599 let should_allow_credentials = if is_wildcard_origin {
603 false
605 } else {
606 config.allow_credentials
608 };
609
610 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
611
612 info!(
613 "CORS middleware enabled with configured settings (credentials: {})",
614 should_allow_credentials
615 );
616 app.layer(cors_layer)
617 } else {
618 debug!("No CORS config provided, using permissive CORS for development");
622 app.layer(CorsLayer::permissive().allow_credentials(false))
625 }
626}
627
628#[allow(clippy::too_many_arguments)]
630#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
632 spec_path: Option<String>,
633 options: Option<ValidationOptions>,
634 failure_config: Option<FailureConfig>,
635 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
636 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
637 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
638 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
639 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
640 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
641 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
642) -> Router {
643 use std::time::Instant;
644
645 let startup_start = Instant::now();
646
647 let mut app = Router::new();
649
650 let mut rate_limit_config = middleware::RateLimitConfig {
653 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
654 .ok()
655 .and_then(|v| v.parse().ok())
656 .unwrap_or(1000),
657 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
658 .ok()
659 .and_then(|v| v.parse().ok())
660 .unwrap_or(2000),
661 per_ip: true,
662 per_endpoint: false,
663 };
664
665 let mut final_cors_config = cors_config;
667 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
668 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
670
671 if let Some(deploy_config) = &deceptive_deploy_config {
672 if deploy_config.enabled {
673 info!("Deceptive deploy mode enabled - applying production-like configuration");
674
675 if let Some(prod_cors) = &deploy_config.cors {
677 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
678 enabled: true,
679 allowed_origins: prod_cors.allowed_origins.clone(),
680 allowed_methods: prod_cors.allowed_methods.clone(),
681 allowed_headers: prod_cors.allowed_headers.clone(),
682 allow_credentials: prod_cors.allow_credentials,
683 });
684 info!("Applied production-like CORS configuration");
685 }
686
687 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
689 rate_limit_config = middleware::RateLimitConfig {
690 requests_per_minute: prod_rate_limit.requests_per_minute,
691 burst: prod_rate_limit.burst,
692 per_ip: prod_rate_limit.per_ip,
693 per_endpoint: false,
694 };
695 info!(
696 "Applied production-like rate limiting: {} req/min, burst: {}",
697 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
698 );
699 }
700
701 if !deploy_config.headers.is_empty() {
703 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
704 production_headers = Some(std::sync::Arc::new(headers_map));
705 info!("Configured {} production headers", deploy_config.headers.len());
706 }
707
708 if let Some(prod_oauth) = &deploy_config.oauth {
710 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
711 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
712 oauth2: Some(oauth2_config),
713 ..Default::default()
714 });
715 info!("Applied production-like OAuth configuration for deceptive deploy");
716 }
717 }
718 }
719
720 let rate_limit_disabled = middleware::is_rate_limit_disabled();
721 let rate_limiter =
722 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
723
724 let mut state = HttpServerState::new();
725 if rate_limit_disabled {
726 info!(
727 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
728 );
729 } else {
730 state = state.with_rate_limiter(rate_limiter.clone());
731 }
732
733 if let Some(headers) = production_headers.clone() {
735 state = state.with_production_headers(headers);
736 }
737
738 let spec_path_for_mgmt = spec_path.clone();
740
741 if let Some(spec_path) = spec_path {
743 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
744
745 let spec_load_start = Instant::now();
747 match OpenApiSpec::from_file(&spec_path).await {
748 Ok(openapi) => {
749 let spec_load_duration = spec_load_start.elapsed();
750 info!(
751 "Successfully loaded OpenAPI spec from {} (took {:?})",
752 spec_path, spec_load_duration
753 );
754
755 tracing::debug!("Creating OpenAPI route registry...");
757 let registry_start = Instant::now();
758
759 let persona = load_persona_from_config().await;
761
762 let registry = if let Some(opts) = options {
763 tracing::debug!("Using custom validation options");
764 if let Some(ref persona) = persona {
765 tracing::info!("Using persona '{}' for route generation", persona.name);
766 }
767 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
768 } else {
769 tracing::debug!("Using environment-based options");
770 if let Some(ref persona) = persona {
771 tracing::info!("Using persona '{}' for route generation", persona.name);
772 }
773 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
774 };
775 let registry_duration = registry_start.elapsed();
776 info!(
777 "Created OpenAPI route registry with {} routes (took {:?})",
778 registry.routes().len(),
779 registry_duration
780 );
781
782 let extract_start = Instant::now();
784 let route_info: Vec<RouteInfo> = registry
785 .routes()
786 .iter()
787 .map(|route| RouteInfo {
788 method: route.method.clone(),
789 path: route.path.clone(),
790 operation_id: route.operation.operation_id.clone(),
791 summary: route.operation.summary.clone(),
792 description: route.operation.description.clone(),
793 parameters: route.parameters.clone(),
794 })
795 .collect();
796 state.routes = route_info;
797 let extract_duration = extract_start.elapsed();
798 debug!("Extracted route information (took {:?})", extract_duration);
799
800 let registry = registry.with_overrides(Arc::new(
801 mockforge_core::SharedOverrides::load(Vec::new()).await,
802 ));
803 let router_build_start = Instant::now();
804 let response_rewriter: Option<
805 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
806 > = Some(std::sync::Arc::new(
807 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(),
808 ));
809 let openapi_router = if let Some(mockai_instance) = &mockai {
810 tracing::debug!("Building router with MockAI support");
811 registry.build_router_with_mockai(Some(mockai_instance.clone()))
812 } else if let Some(ai_generator) = &ai_generator {
813 tracing::debug!("Building router with AI generator support");
814 registry.build_router_with_ai(Some(ai_generator.clone()))
815 } else if let Some(failure_config) = &failure_config {
816 tracing::debug!("Building router with failure injection and overrides");
817 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
818 registry.build_router_with_injectors_and_rewriter(
819 LatencyInjector::default(),
820 Some(failure_injector),
821 response_rewriter,
822 )
823 } else {
824 tracing::debug!("Building router with overrides");
825 registry.build_router_with_injectors_and_rewriter(
826 LatencyInjector::default(),
827 None,
828 response_rewriter,
829 )
830 };
831 let router_build_duration = router_build_start.elapsed();
832 debug!("Built OpenAPI router (took {:?})", router_build_duration);
833
834 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
842 .ok()
843 .and_then(|v| v.parse::<usize>().ok())
844 .unwrap_or(50);
845 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
846 let openapi_router =
847 openapi_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
848 tracing::info!(
849 body_limit_mb = body_limit_mb,
850 "Merging OpenAPI router with main router"
851 );
852 app = app.merge(openapi_router);
853 tracing::debug!("Router built successfully");
854 }
855 Err(e) => {
856 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
857 }
858 }
859 }
860
861 app = app.route(
863 "/health",
864 axum::routing::get(|| async {
865 use mockforge_core::server_utils::health::HealthStatus;
866 {
867 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
869 Ok(value) => Json(value),
870 Err(e) => {
871 tracing::error!("Failed to serialize health status: {}", e);
873 Json(serde_json::json!({
874 "status": "healthy",
875 "service": "mockforge-http",
876 "uptime_seconds": 0
877 }))
878 }
879 }
880 }
881 }),
882 )
883 .merge(sse::sse_router())
885 .merge(file_server::file_serving_router());
887
888 let state_for_routes = state.clone();
890
891 let routes_router = Router::new()
893 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
894 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
895 .with_state(state_for_routes);
896
897 app = app.merge(routes_router);
899
900 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
902
903 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
906 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
907
908 if Path::new(&coverage_html_path).exists() {
910 app = app.nest_service(
911 "/__mockforge/coverage.html",
912 tower_http::services::ServeFile::new(&coverage_html_path),
913 );
914 debug!("Serving coverage UI from: {}", coverage_html_path);
915 } else {
916 debug!(
917 "Coverage UI file not found at: {}. Skipping static file serving.",
918 coverage_html_path
919 );
920 }
921
922 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
925 match OpenApiSpec::from_file(sp).await {
926 Ok(s) => Some(Arc::new(s)),
927 Err(e) => {
928 debug!("Failed to load OpenAPI spec for management API: {}", e);
929 None
930 }
931 }
932 } else {
933 None
934 };
935 let mgmt_port = std::env::var("PORT")
936 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
937 .ok()
938 .and_then(|p| p.parse().ok())
939 .unwrap_or(3000);
940 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
941
942 use std::sync::Arc;
944 let ws_state = WsManagementState::new();
945 let ws_broadcast = Arc::new(ws_state.tx.clone());
946 let management_state = management_state.with_ws_broadcast(ws_broadcast);
947
948 #[cfg(feature = "smtp")]
952 let management_state = {
953 if let Some(smtp_reg) = smtp_registry {
954 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
955 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
956 Err(e) => {
957 error!(
958 "Invalid SMTP registry type passed to HTTP management state: {:?}",
959 e.type_id()
960 );
961 management_state
962 }
963 }
964 } else {
965 management_state
966 }
967 };
968 #[cfg(not(feature = "smtp"))]
969 let management_state = management_state;
970 #[cfg(not(feature = "smtp"))]
971 let _ = smtp_registry;
972 let management_state_for_fallback = management_state.clone();
973 app = app.nest("/__mockforge/api", management_router(management_state));
974 app = app.fallback_service(
979 axum::routing::any(management::dynamic_mock_fallback)
980 .with_state(management_state_for_fallback),
981 );
982
983 app = app.merge(verification_router());
985
986 use crate::auth::oidc::oidc_router;
988 app = app.merge(oidc_router());
989
990 {
992 use mockforge_core::security::get_global_access_review_service;
993 if let Some(service) = get_global_access_review_service().await {
994 use crate::handlers::access_review::{access_review_router, AccessReviewState};
995 let review_state = AccessReviewState { service };
996 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
997 debug!("Access review API mounted at /api/v1/security/access-reviews");
998 }
999 }
1000
1001 {
1003 use mockforge_core::security::get_global_privileged_access_manager;
1004 if let Some(manager) = get_global_privileged_access_manager().await {
1005 use crate::handlers::privileged_access::{
1006 privileged_access_router, PrivilegedAccessState,
1007 };
1008 let privileged_state = PrivilegedAccessState { manager };
1009 app = app.nest(
1010 "/api/v1/security/privileged-access",
1011 privileged_access_router(privileged_state),
1012 );
1013 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1014 }
1015 }
1016
1017 {
1019 use mockforge_core::security::get_global_change_management_engine;
1020 if let Some(engine) = get_global_change_management_engine().await {
1021 use crate::handlers::change_management::{
1022 change_management_router, ChangeManagementState,
1023 };
1024 let change_state = ChangeManagementState { engine };
1025 app = app.nest("/api/v1/change-management", change_management_router(change_state));
1026 debug!("Change management API mounted at /api/v1/change-management");
1027 }
1028 }
1029
1030 {
1032 use mockforge_core::security::get_global_risk_assessment_engine;
1033 if let Some(engine) = get_global_risk_assessment_engine().await {
1034 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1035 let risk_state = RiskAssessmentState { engine };
1036 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1037 debug!("Risk assessment API mounted at /api/v1/security/risks");
1038 }
1039 }
1040
1041 {
1043 use crate::auth::token_lifecycle::TokenLifecycleManager;
1044 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1045 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1046 let lifecycle_state = TokenLifecycleState {
1047 manager: lifecycle_manager,
1048 };
1049 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1050 debug!("Token lifecycle API mounted at /api/v1/auth");
1051 }
1052
1053 {
1055 use crate::auth::oidc::load_oidc_state;
1056 use crate::auth::token_lifecycle::TokenLifecycleManager;
1057 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1058 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1060 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1061 let oauth2_state = OAuth2ServerState {
1062 oidc_state,
1063 lifecycle_manager,
1064 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1065 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1066 };
1067 app = app.merge(oauth2_server_router(oauth2_state));
1068 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1069 }
1070
1071 {
1073 use crate::auth::oidc::load_oidc_state;
1074 use crate::auth::risk_engine::RiskEngine;
1075 use crate::auth::token_lifecycle::TokenLifecycleManager;
1076 use crate::handlers::consent::{consent_router, ConsentState};
1077 use crate::handlers::oauth2_server::OAuth2ServerState;
1078 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1080 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1081 let oauth2_state = OAuth2ServerState {
1082 oidc_state: oidc_state.clone(),
1083 lifecycle_manager: lifecycle_manager.clone(),
1084 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1085 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1086 };
1087 let risk_engine = Arc::new(RiskEngine::default());
1088 let consent_state = ConsentState {
1089 oauth2_state,
1090 risk_engine,
1091 };
1092 app = app.merge(consent_router(consent_state));
1093 debug!("Consent screen endpoints mounted at /consent");
1094 }
1095
1096 {
1098 use crate::auth::risk_engine::RiskEngine;
1099 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1100 let risk_engine = Arc::new(RiskEngine::default());
1101 let risk_state = RiskSimulationState { risk_engine };
1102 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1103 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1104 }
1105
1106 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1108
1109 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1111
1112 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1114
1115 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1118
1119 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1121
1122 if state.production_headers.is_some() {
1124 app =
1125 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1126 }
1127
1128 if middleware::is_keepalive_hint_enabled() {
1134 info!(
1135 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
1136 );
1137 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
1138 }
1139
1140 if middleware::is_conn_log_enabled() {
1145 info!(
1146 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
1147 );
1148 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
1149 }
1150
1151 if let Some(auth_config) = deceptive_deploy_auth_config {
1153 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1154 use std::collections::HashMap;
1155 use std::sync::Arc;
1156 use tokio::sync::RwLock;
1157
1158 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1160 match create_oauth2_client(oauth2_config) {
1161 Ok(client) => Some(client),
1162 Err(e) => {
1163 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1164 None
1165 }
1166 }
1167 } else {
1168 None
1169 };
1170
1171 let auth_state = AuthState {
1173 config: auth_config,
1174 spec: None, oauth2_client,
1176 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1177 };
1178
1179 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1181 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1182 }
1183
1184 app = apply_cors_middleware(app, final_cors_config);
1186
1187 if let Some(mt_config) = multi_tenant_config {
1189 if mt_config.enabled {
1190 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1191 use std::sync::Arc;
1192
1193 info!(
1194 "Multi-tenant mode enabled with {} routing strategy",
1195 match mt_config.routing_strategy {
1196 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1197 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1198 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1199 }
1200 );
1201
1202 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1204
1205 let default_workspace =
1207 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1208 if let Err(e) =
1209 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1210 {
1211 warn!("Failed to register default workspace: {}", e);
1212 } else {
1213 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1214 }
1215
1216 if mt_config.auto_discover {
1218 if let Some(config_dir) = &mt_config.config_directory {
1219 let config_path = Path::new(config_dir);
1220 if config_path.exists() && config_path.is_dir() {
1221 match fs::read_dir(config_path).await {
1222 Ok(mut entries) => {
1223 while let Ok(Some(entry)) = entries.next_entry().await {
1224 let path = entry.path();
1225 if path.extension() == Some(OsStr::new("yaml")) {
1226 match fs::read_to_string(&path).await {
1227 Ok(content) => {
1228 match serde_yaml::from_str::<
1229 mockforge_core::Workspace,
1230 >(
1231 &content
1232 ) {
1233 Ok(workspace) => {
1234 if let Err(e) = registry.register_workspace(
1235 workspace.id.clone(),
1236 workspace,
1237 ) {
1238 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1239 } else {
1240 info!("Auto-registered workspace from {:?}", path);
1241 }
1242 }
1243 Err(e) => {
1244 warn!("Failed to parse workspace from {:?}: {}", path, e);
1245 }
1246 }
1247 }
1248 Err(e) => {
1249 warn!(
1250 "Failed to read workspace file {:?}: {}",
1251 path, e
1252 );
1253 }
1254 }
1255 }
1256 }
1257 }
1258 Err(e) => {
1259 warn!("Failed to read config directory {:?}: {}", config_path, e);
1260 }
1261 }
1262 } else {
1263 warn!(
1264 "Config directory {:?} does not exist or is not a directory",
1265 config_path
1266 );
1267 }
1268 }
1269 }
1270
1271 let registry = Arc::new(registry);
1273
1274 let _workspace_router = WorkspaceRouter::new(registry);
1276
1277 info!("Workspace routing middleware initialized for HTTP server");
1280 }
1281 }
1282
1283 let total_startup_duration = startup_start.elapsed();
1284 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1285
1286 app
1287}
1288
1289pub async fn build_router_with_auth_and_latency(
1291 spec_path: Option<String>,
1292 _options: Option<()>,
1293 auth_config: Option<mockforge_core::config::AuthConfig>,
1294 latency_injector: Option<LatencyInjector>,
1295) -> Router {
1296 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1298
1299 if let Some(injector) = latency_injector {
1301 let injector = Arc::new(injector);
1302 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1303 let injector = injector.clone();
1304 async move {
1305 let _ = injector.inject_latency(&[]).await;
1306 next.run(req).await
1307 }
1308 }));
1309 }
1310
1311 app
1312}
1313
1314pub async fn build_router_with_latency(
1316 spec_path: Option<String>,
1317 options: Option<ValidationOptions>,
1318 latency_injector: Option<LatencyInjector>,
1319) -> Router {
1320 if let Some(spec) = &spec_path {
1321 match OpenApiSpec::from_file(spec).await {
1322 Ok(openapi) => {
1323 let registry = if let Some(opts) = options {
1324 OpenApiRouteRegistry::new_with_options(openapi, opts)
1325 } else {
1326 OpenApiRouteRegistry::new_with_env(openapi)
1327 };
1328
1329 if let Some(injector) = latency_injector {
1330 return registry.build_router_with_latency(injector);
1331 } else {
1332 return registry.build_router();
1333 }
1334 }
1335 Err(e) => {
1336 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1337 }
1338 }
1339 }
1340
1341 build_router(None, None, None).await
1342}
1343
1344pub async fn build_router_with_auth(
1346 spec_path: Option<String>,
1347 options: Option<ValidationOptions>,
1348 auth_config: Option<mockforge_core::config::AuthConfig>,
1349) -> Router {
1350 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1351 use std::sync::Arc;
1352
1353 #[cfg(feature = "data-faker")]
1355 {
1356 register_core_faker_provider();
1357 }
1358
1359 let spec = if let Some(spec_path) = &spec_path {
1361 match OpenApiSpec::from_file(&spec_path).await {
1362 Ok(spec) => Some(Arc::new(spec)),
1363 Err(e) => {
1364 warn!("Failed to load OpenAPI spec for auth: {}", e);
1365 None
1366 }
1367 }
1368 } else {
1369 None
1370 };
1371
1372 let oauth2_client = if let Some(auth_config) = &auth_config {
1374 if let Some(oauth2_config) = &auth_config.oauth2 {
1375 match create_oauth2_client(oauth2_config) {
1376 Ok(client) => Some(client),
1377 Err(e) => {
1378 warn!("Failed to create OAuth2 client: {}", e);
1379 None
1380 }
1381 }
1382 } else {
1383 None
1384 }
1385 } else {
1386 None
1387 };
1388
1389 let auth_state = AuthState {
1390 config: auth_config.unwrap_or_default(),
1391 spec,
1392 oauth2_client,
1393 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1394 };
1395
1396 let mut app = Router::new().with_state(auth_state.clone());
1398
1399 if let Some(spec_path) = spec_path {
1401 match OpenApiSpec::from_file(&spec_path).await {
1402 Ok(openapi) => {
1403 info!("Loaded OpenAPI spec from {}", spec_path);
1404 let registry = if let Some(opts) = options {
1405 OpenApiRouteRegistry::new_with_options(openapi, opts)
1406 } else {
1407 OpenApiRouteRegistry::new_with_env(openapi)
1408 };
1409
1410 app = registry.build_router();
1411 }
1412 Err(e) => {
1413 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1414 }
1415 }
1416 }
1417
1418 app = app.route(
1420 "/health",
1421 axum::routing::get(|| async {
1422 use mockforge_core::server_utils::health::HealthStatus;
1423 {
1424 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1426 Ok(value) => Json(value),
1427 Err(e) => {
1428 tracing::error!("Failed to serialize health status: {}", e);
1430 Json(serde_json::json!({
1431 "status": "healthy",
1432 "service": "mockforge-http",
1433 "uptime_seconds": 0
1434 }))
1435 }
1436 }
1437 }
1438 }),
1439 )
1440 .merge(sse::sse_router())
1442 .merge(file_server::file_serving_router())
1444 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1446 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1448
1449 app
1450}
1451
1452pub async fn serve_router(
1454 port: u16,
1455 app: Router,
1456) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1457 serve_router_with_tls(port, app, None).await
1458}
1459
1460pub async fn serve_router_with_tls(
1462 port: u16,
1463 app: Router,
1464 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1465) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1466 serve_router_with_tls_notify(port, app, tls_config, None).await
1467}
1468
1469pub async fn serve_router_with_tls_notify(
1477 port: u16,
1478 app: Router,
1479 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1480 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1481) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1482 serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1483}
1484
1485pub async fn serve_router_with_tls_notify_chaos(
1494 port: u16,
1495 app: Router,
1496 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1497 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1498 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1499) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1500 serve_router_with_tls_notify_chaos_shutdown(
1501 port,
1502 app,
1503 tls_config,
1504 bound_port_tx,
1505 chaos_config,
1506 std::future::pending(),
1507 )
1508 .await
1509}
1510
1511pub async fn serve_router_with_tls_notify_chaos_shutdown(
1516 port: u16,
1517 app: Router,
1518 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1519 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1520 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1521 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
1522) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1523 use std::net::SocketAddr;
1524
1525 let addr = mockforge_core::wildcard_socket_addr(port);
1526
1527 if let Some(ref tls) = tls_config {
1528 if tls.enabled {
1529 info!("HTTPS listening on {}", addr);
1530 if let Some(tx) = bound_port_tx {
1531 let _ = tx.send(port);
1532 }
1533 return serve_with_tls(addr, app, tls, shutdown).await;
1534 }
1535 }
1536
1537 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1538 format!(
1539 "Failed to bind HTTP server to port {}: {}\n\
1540 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 {}",
1541 port, e, port, port
1542 )
1543 })?;
1544
1545 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1546 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1547 if let Some(tx) = bound_port_tx {
1548 let _ = tx.send(actual_port);
1549 }
1550
1551 let odata_app = tower::ServiceBuilder::new()
1555 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1556 .service(app);
1557 if let Some(cfg) = chaos_config {
1558 info!("HTTP listener wrapped with chaos TCP listener (RST/FIN injection enabled)");
1559 let chaos_listener = mockforge_chaos::ChaosTcpListener::new(listener, cfg);
1560 let app_with_addr_compat = tower::ServiceBuilder::new()
1563 .layer(axum::middleware::from_fn(copy_chaos_addr_to_socketaddr))
1564 .service(odata_app);
1565 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1566 mockforge_chaos::ChaosClientAddr,
1567 >(app_with_addr_compat);
1568 let counted = counting_listener::CountingMakeService::new(make_svc);
1570 axum::serve(chaos_listener, counted).with_graceful_shutdown(shutdown).await?;
1571 } else {
1572 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1573 SocketAddr,
1574 >(odata_app);
1575 let counted = counting_listener::CountingMakeService::new(make_svc);
1578 axum::serve(listener, counted).with_graceful_shutdown(shutdown).await?;
1579 }
1580 Ok(())
1581}
1582
1583async fn copy_chaos_addr_to_socketaddr(
1587 mut req: Request<Body>,
1588 next: axum::middleware::Next,
1589) -> axum::response::Response {
1590 use axum::extract::ConnectInfo;
1591 if let Some(ConnectInfo(chaos_addr)) =
1592 req.extensions().get::<ConnectInfo<mockforge_chaos::ChaosClientAddr>>().copied()
1593 {
1594 let sock: std::net::SocketAddr = *chaos_addr;
1595 req.extensions_mut().insert(ConnectInfo(sock));
1596 }
1597 next.run(req).await
1598}
1599
1600async fn serve_with_tls(
1605 addr: std::net::SocketAddr,
1606 app: Router,
1607 tls_config: &mockforge_core::config::HttpTlsConfig,
1608 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
1609) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1610 use axum_server::tls_rustls::RustlsConfig;
1611 use std::net::SocketAddr;
1612
1613 tls::init_crypto_provider();
1615
1616 info!("Loading TLS configuration for HTTPS server");
1617
1618 let server_config = tls::load_tls_server_config(tls_config)?;
1620
1621 let rustls_config = RustlsConfig::from_config(server_config);
1624
1625 info!("Starting HTTPS server on {}", addr);
1626
1627 let odata_app = tower::ServiceBuilder::new()
1631 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1632 .service(app);
1633 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1634 SocketAddr,
1635 >(odata_app);
1636 let counted = counting_listener::CountingMakeService::new(make_svc);
1639
1640 let handle = axum_server::Handle::new();
1642 let server = axum_server::bind_rustls(addr, rustls_config)
1643 .handle(handle.clone())
1644 .serve(counted);
1645 tokio::pin!(server);
1646 tokio::select! {
1647 result = &mut server => result,
1648 _ = shutdown => {
1649 handle.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
1650 server.await
1651 }
1652 }
1653 .map_err(|e| format!("HTTPS server error: {}", e).into())
1654}
1655
1656pub async fn start(
1658 port: u16,
1659 spec_path: Option<String>,
1660 options: Option<ValidationOptions>,
1661) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1662 start_with_latency(port, spec_path, options, None).await
1663}
1664
1665pub async fn start_with_auth_and_latency(
1667 port: u16,
1668 spec_path: Option<String>,
1669 options: Option<ValidationOptions>,
1670 auth_config: Option<mockforge_core::config::AuthConfig>,
1671 latency_profile: Option<LatencyProfile>,
1672) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1673 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1674 .await
1675}
1676
1677pub async fn start_with_auth_and_injectors(
1679 port: u16,
1680 spec_path: Option<String>,
1681 options: Option<ValidationOptions>,
1682 auth_config: Option<mockforge_core::config::AuthConfig>,
1683 _latency_profile: Option<LatencyProfile>,
1684 _failure_injector: Option<FailureInjector>,
1685) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1686 let app = build_router_with_auth(spec_path, options, auth_config).await;
1688 serve_router(port, app).await
1689}
1690
1691pub async fn start_with_latency(
1693 port: u16,
1694 spec_path: Option<String>,
1695 options: Option<ValidationOptions>,
1696 latency_profile: Option<LatencyProfile>,
1697) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1698 let latency_injector =
1699 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1700
1701 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1702 serve_router(port, app).await
1703}
1704
1705pub async fn build_router_with_chains(
1707 spec_path: Option<String>,
1708 options: Option<ValidationOptions>,
1709 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1710) -> Router {
1711 build_router_with_chains_and_multi_tenant(
1712 spec_path,
1713 options,
1714 circling_config,
1715 None,
1716 None,
1717 None,
1718 None,
1719 None, None, None, None, None, false,
1725 None, None, None, None, mockforge_core::SharedOverrides::load(Vec::new()).await,
1730 )
1731 .await
1732}
1733
1734async fn apply_route_chaos(
1742 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1743 method: &http::Method,
1744 uri: &http::Uri,
1745) -> Option<axum::response::Response> {
1746 use axum::http::StatusCode;
1747 use axum::response::IntoResponse;
1748
1749 if let Some(injector) = injector {
1750 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1752 let mut response = Json(serde_json::json!({
1754 "error": fault_response.error_message,
1755 "fault_type": fault_response.fault_type,
1756 }))
1757 .into_response();
1758 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1759 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1760 return Some(response);
1761 }
1762
1763 if let Err(e) = injector.inject_latency(method, uri).await {
1765 tracing::warn!("Failed to inject latency: {}", e);
1766 }
1767 }
1768
1769 None }
1771
1772#[allow(clippy::too_many_arguments)]
1774#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1776 spec_path: Option<String>,
1777 options: Option<ValidationOptions>,
1778 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1779 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1780 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1781 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1782 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1783 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1784 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1785 amqp_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1786 kafka_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1787 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1788 traffic_shaping_enabled: bool,
1789 health_manager: Option<Arc<HealthManager>>,
1790 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1791 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1792 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1793 overrides: mockforge_core::SharedOverrides,
1794) -> Router {
1795 use crate::latency_profiles::LatencyProfiles;
1796 use crate::op_middleware::Shared;
1797
1798 let template_expand =
1800 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1801 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1802 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1803 .unwrap_or(false)
1804 });
1805
1806 let _shared = Shared {
1807 profiles: LatencyProfiles::default(),
1808 failure_injector: None,
1809 traffic_shaper,
1810 traffic_shaping_enabled,
1811 };
1812
1813 let mut app = Router::new();
1815 let mut mock_health_paths: Vec<String> = route_configs
1817 .as_ref()
1818 .into_iter()
1819 .flatten()
1820 .filter(|route| health::HEALTH_PATHS.contains(&route.path.as_str()))
1821 .map(|route| route.path.clone())
1822 .collect();
1823 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1824
1825 if let Some(ref spec) = spec_path {
1827 match OpenApiSpec::from_file(&spec).await {
1828 Ok(openapi) => {
1829 info!("Loaded OpenAPI spec from {}", spec);
1830
1831 let persona = load_persona_from_config().await;
1833
1834 let mut registry = if let Some(opts) = options {
1835 tracing::debug!("Using custom validation options");
1836 if let Some(ref persona) = persona {
1837 tracing::info!("Using persona '{}' for route generation", persona.name);
1838 }
1839 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1840 } else {
1841 tracing::debug!("Using environment-based options");
1842 if let Some(ref persona) = persona {
1843 tracing::info!("Using persona '{}' for route generation", persona.name);
1844 }
1845 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1846 };
1847
1848 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1850 .unwrap_or_else(|_| "/app/fixtures".to_string());
1851 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1852 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1853 .unwrap_or(true); if custom_fixtures_enabled {
1856 use mockforge_openapi::CustomFixtureLoader;
1857 use std::path::PathBuf;
1858 use std::sync::Arc;
1859
1860 let fixtures_path = PathBuf::from(&fixtures_dir);
1861 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1862
1863 if let Err(e) = custom_loader.load_fixtures().await {
1864 tracing::warn!("Failed to load custom fixtures: {}", e);
1865 } else {
1866 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1867 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1868 }
1869 }
1870 registry = registry.with_overrides(Arc::new(overrides.clone()));
1871
1872 mock_health_paths.extend(
1873 registry
1874 .routes()
1875 .iter()
1876 .filter(|route| {
1877 route.method.eq_ignore_ascii_case("GET")
1878 && health::HEALTH_PATHS.contains(&route.path.as_str())
1879 })
1880 .map(|route| route.path.clone()),
1881 );
1882 captured_routes = registry
1884 .routes()
1885 .iter()
1886 .map(|r| RouteInfo {
1887 method: r.method.clone(),
1888 path: r.path.clone(),
1889 operation_id: r.operation.operation_id.clone(),
1890 summary: r.operation.summary.clone(),
1891 description: r.operation.description.clone(),
1892 parameters: r.parameters.clone(),
1893 })
1894 .collect();
1895
1896 {
1899 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1900 captured_routes
1901 .iter()
1902 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1903 method: r.method.clone(),
1904 path: r.path.clone(),
1905 operation_id: r.operation_id.clone(),
1906 summary: r.summary.clone(),
1907 description: r.description.clone(),
1908 parameters: r.parameters.clone(),
1909 })
1910 .collect();
1911 mockforge_core::request_logger::set_global_routes(global_routes);
1912 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1913 }
1914
1915 let spec_router = if let Some(ref mockai_instance) = mockai {
1917 tracing::debug!("Building router with MockAI support");
1918 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1919 } else {
1920 registry.build_router()
1921 };
1922 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
1930 .ok()
1931 .and_then(|v| v.parse::<usize>().ok())
1932 .unwrap_or(50);
1933 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
1934 let spec_router =
1935 spec_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
1936 tracing::info!(
1937 body_limit_mb = body_limit_mb,
1938 "Merging OpenAPI router with main router"
1939 );
1940 app = app.merge(spec_router);
1941 }
1942 Err(e) => {
1943 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1944 }
1945 }
1946 }
1947
1948 #[cfg(feature = "route-chaos")]
1952 let route_chaos_injector: Option<
1953 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1954 > = if let Some(ref route_configs) = route_configs {
1955 if !route_configs.is_empty() {
1956 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1959 route_configs.to_vec();
1960 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1961 Ok(injector) => {
1962 info!(
1963 "Initialized advanced routing features for {} route(s)",
1964 route_configs.len()
1965 );
1966 Some(std::sync::Arc::new(injector)
1969 as std::sync::Arc<
1970 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1971 >)
1972 }
1973 Err(e) => {
1974 warn!(
1975 "Failed to initialize advanced routing features: {}. Using basic routing.",
1976 e
1977 );
1978 None
1979 }
1980 }
1981 } else {
1982 None
1983 }
1984 } else {
1985 None
1986 };
1987
1988 #[cfg(not(feature = "route-chaos"))]
1989 let route_chaos_injector: Option<
1990 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1991 > = None;
1992
1993 if let Some(route_configs) = route_configs {
1994 use axum::http::StatusCode;
1995 use axum::response::IntoResponse;
1996
1997 if !route_configs.is_empty() {
1998 info!("Registering {} custom route(s) from config", route_configs.len());
1999 }
2000
2001 let injector = route_chaos_injector.clone();
2002 for route_config in route_configs {
2003 let status = route_config.response.status;
2004 let body = route_config.response.body.clone();
2005 let headers = route_config.response.headers.clone();
2006 let path = route_config.path.clone();
2007 let method = route_config.method.clone();
2008
2009 let expected_method = method.to_uppercase();
2014 let injector_clone = injector.clone();
2018 app = app.route(
2019 &path,
2020 #[allow(clippy::non_send_fields_in_send_ty)]
2021 axum::routing::any(move |req: Request<Body>| {
2022 let body = body.clone();
2023 let headers = headers.clone();
2024 let expand = template_expand;
2025 let expected = expected_method.clone();
2026 let status_code = status;
2027 let injector_for_chaos = injector_clone.clone();
2029
2030 async move {
2031 if req.method().as_str() != expected.as_str() {
2033 return axum::response::Response::builder()
2035 .status(StatusCode::METHOD_NOT_ALLOWED)
2036 .header("Allow", &expected)
2037 .body(Body::empty())
2038 .unwrap()
2039 .into_response();
2040 }
2041
2042 if let Some(fault_response) = apply_route_chaos(
2046 injector_for_chaos.as_deref(),
2047 req.method(),
2048 req.uri(),
2049 )
2050 .await
2051 {
2052 return fault_response;
2053 }
2054
2055 let mut body_value = body.unwrap_or(serde_json::json!({}));
2057
2058 if expand {
2062 use mockforge_template_expansion::RequestContext;
2063 use serde_json::Value;
2064 use std::collections::HashMap;
2065
2066 let method = req.method().to_string();
2068 let path = req.uri().path().to_string();
2069
2070 let query_params: HashMap<String, Value> = req
2072 .uri()
2073 .query()
2074 .map(|q| {
2075 url::form_urlencoded::parse(q.as_bytes())
2076 .into_owned()
2077 .map(|(k, v)| (k, Value::String(v)))
2078 .collect()
2079 })
2080 .unwrap_or_default();
2081
2082 let headers: HashMap<String, Value> = req
2084 .headers()
2085 .iter()
2086 .map(|(k, v)| {
2087 (
2088 k.to_string(),
2089 Value::String(v.to_str().unwrap_or_default().to_string()),
2090 )
2091 })
2092 .collect();
2093
2094 let context = RequestContext {
2098 method,
2099 path,
2100 query_params,
2101 headers,
2102 body: None, path_params: HashMap::new(),
2104 multipart_fields: HashMap::new(),
2105 multipart_files: HashMap::new(),
2106 };
2107
2108 let body_value_clone = body_value.clone();
2112 let context_clone = context.clone();
2113 body_value = match tokio::task::spawn_blocking(move || {
2114 mockforge_template_expansion::expand_templates_in_json(
2115 body_value_clone,
2116 &context_clone,
2117 )
2118 })
2119 .await
2120 {
2121 Ok(result) => result,
2122 Err(_) => body_value, };
2124 }
2125
2126 let mut response = Json(body_value).into_response();
2127
2128 *response.status_mut() =
2130 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2131
2132 for (key, value) in headers {
2134 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2135 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2136 response.headers_mut().insert(header_name, header_value);
2137 }
2138 }
2139 }
2140
2141 response
2142 }
2143 }),
2144 );
2145
2146 debug!("Registered route: {} {}", method, path);
2147 }
2148 }
2149
2150 if let Some(health) = health_manager {
2152 let excluded_paths: Vec<&str> = mock_health_paths.iter().map(String::as_str).collect();
2154 app = app.merge(health::health_router_with_exclusions(health, &excluded_paths));
2155 info!("Default health probes enabled on paths not supplied by mock routes");
2156 } else if !mock_health_paths.iter().any(|path| path == "/health") {
2157 app = app.route(
2159 "/health",
2160 axum::routing::get(|| async {
2161 use mockforge_core::server_utils::health::HealthStatus;
2162 {
2163 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2165 Ok(value) => Json(value),
2166 Err(e) => {
2167 tracing::error!("Failed to serialize health status: {}", e);
2169 Json(serde_json::json!({
2170 "status": "healthy",
2171 "service": "mockforge-http",
2172 "uptime_seconds": 0
2173 }))
2174 }
2175 }
2176 }
2177 }),
2178 );
2179 }
2180
2181 app = app.merge(sse::sse_router());
2182 app = app.merge(file_server::file_serving_router());
2184
2185 let mgmt_spec = if let Some(ref sp) = spec_path {
2188 match OpenApiSpec::from_file(sp).await {
2189 Ok(s) => Some(Arc::new(s)),
2190 Err(e) => {
2191 debug!("Failed to load OpenAPI spec for management API: {}", e);
2192 None
2193 }
2194 }
2195 } else {
2196 None
2197 };
2198 let spec_path_clone = spec_path.clone();
2199 let mgmt_port = std::env::var("PORT")
2200 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2201 .ok()
2202 .and_then(|p| p.parse().ok())
2203 .unwrap_or(3000);
2204 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2205
2206 use std::sync::Arc;
2208 let ws_state = WsManagementState::new();
2209 let ws_broadcast = Arc::new(ws_state.tx.clone());
2210 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2211
2212 let management_state = if let Some(proxy_cfg) = proxy_config {
2214 use tokio::sync::RwLock;
2215 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2216 management_state.with_proxy_config(proxy_config_arc)
2217 } else {
2218 management_state
2219 };
2220
2221 #[cfg(feature = "smtp")]
2222 let management_state = {
2223 if let Some(smtp_reg) = smtp_registry {
2224 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2225 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2226 Err(e) => {
2227 error!(
2228 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2229 e.type_id()
2230 );
2231 management_state
2232 }
2233 }
2234 } else {
2235 management_state
2236 }
2237 };
2238 #[cfg(not(feature = "smtp"))]
2239 let management_state = {
2240 let _ = smtp_registry;
2241 management_state
2242 };
2243 #[cfg(feature = "mqtt")]
2244 let management_state = {
2245 if let Some(sessions) = mqtt_broker {
2248 match sessions.downcast::<mockforge_mqtt::SessionManager>() {
2249 Ok(sessions) => management_state.with_mqtt_sessions(sessions),
2250 Err(e) => {
2251 error!(
2252 "Invalid MQTT session manager passed to HTTP management state: {:?}",
2253 e.type_id()
2254 );
2255 management_state
2256 }
2257 }
2258 } else {
2259 management_state
2260 }
2261 };
2262 #[cfg(not(feature = "mqtt"))]
2263 let management_state = {
2264 let _ = mqtt_broker;
2265 management_state
2266 };
2267 #[cfg(feature = "amqp")]
2268 let management_state = {
2269 if let Some(broker) = amqp_broker {
2270 match broker.downcast::<mockforge_amqp::AmqpBroker>() {
2271 Ok(broker) => management_state.with_amqp_broker(broker),
2272 Err(e) => {
2273 error!(
2274 "Invalid AMQP broker passed to HTTP management state: {:?}",
2275 e.type_id()
2276 );
2277 management_state
2278 }
2279 }
2280 } else {
2281 management_state
2282 }
2283 };
2284 #[cfg(not(feature = "amqp"))]
2285 let management_state = {
2286 let _ = amqp_broker;
2287 management_state
2288 };
2289 #[cfg(feature = "kafka")]
2290 let management_state = {
2291 if let Some(broker) = kafka_broker {
2292 match broker.downcast::<mockforge_kafka::KafkaMockBroker>() {
2293 Ok(broker) => management_state.with_kafka_broker(broker),
2294 Err(e) => {
2295 error!(
2296 "Invalid Kafka broker passed to HTTP management state: {:?}",
2297 e.type_id()
2298 );
2299 management_state
2300 }
2301 }
2302 } else {
2303 management_state
2304 }
2305 };
2306 #[cfg(not(feature = "kafka"))]
2307 let management_state = {
2308 let _ = kafka_broker;
2309 management_state
2310 };
2311 let management_state_for_fallback = management_state.clone();
2312 app = app.nest("/__mockforge/api", management_router(management_state));
2313 app = app.fallback_service(
2315 axum::routing::any(management::dynamic_mock_fallback)
2316 .with_state(management_state_for_fallback),
2317 );
2318
2319 app = app.merge(verification_router());
2321
2322 {
2327 use crate::chain_handlers::{chains_router, create_chain_state};
2328 let chain_config = _circling_config.clone().unwrap_or_default();
2329 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2330 chain_config.clone(),
2331 ));
2332 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2333 chain_registry.clone(),
2334 chain_config,
2335 ));
2336 app = app.nest(
2337 "/__mockforge/chains",
2338 chains_router(create_chain_state(chain_registry, chain_engine)),
2339 );
2340 }
2341
2342 {
2347 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2348 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2349 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2350 }
2351
2352 {
2359 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2360 let fx_state = FixturesApiState::from_env();
2361 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2362 }
2363
2364 {
2371 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2372 let api_state = MockAiApiState::new(mockai.clone());
2373 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2374 }
2375
2376 app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2383
2384 #[cfg(feature = "route-chaos")]
2389 {
2390 use crate::route_chaos_runtime::{
2391 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2392 };
2393 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2394 let middleware_state = runtime_state.clone();
2395 app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2396 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2397 }
2398
2399 {
2405 use crate::network_profile_runtime::{
2406 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2407 };
2408 let runtime_state = NetworkProfileRuntimeState::new(
2409 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2410 );
2411 let middleware_state = runtime_state.clone();
2412 app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2413 app = app
2414 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2415 }
2416
2417 use crate::auth::oidc::oidc_router;
2419 app = app.merge(oidc_router());
2420
2421 {
2423 use mockforge_core::security::get_global_access_review_service;
2424 if let Some(service) = get_global_access_review_service().await {
2425 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2426 let review_state = AccessReviewState { service };
2427 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2428 debug!("Access review API mounted at /api/v1/security/access-reviews");
2429 }
2430 }
2431
2432 {
2434 use mockforge_core::security::get_global_privileged_access_manager;
2435 if let Some(manager) = get_global_privileged_access_manager().await {
2436 use crate::handlers::privileged_access::{
2437 privileged_access_router, PrivilegedAccessState,
2438 };
2439 let privileged_state = PrivilegedAccessState { manager };
2440 app = app.nest(
2441 "/api/v1/security/privileged-access",
2442 privileged_access_router(privileged_state),
2443 );
2444 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2445 }
2446 }
2447
2448 {
2450 use mockforge_core::security::get_global_change_management_engine;
2451 if let Some(engine) = get_global_change_management_engine().await {
2452 use crate::handlers::change_management::{
2453 change_management_router, ChangeManagementState,
2454 };
2455 let change_state = ChangeManagementState { engine };
2456 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2457 debug!("Change management API mounted at /api/v1/change-management");
2458 }
2459 }
2460
2461 {
2463 use mockforge_core::security::get_global_risk_assessment_engine;
2464 if let Some(engine) = get_global_risk_assessment_engine().await {
2465 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2466 let risk_state = RiskAssessmentState { engine };
2467 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2468 debug!("Risk assessment API mounted at /api/v1/security/risks");
2469 }
2470 }
2471
2472 {
2474 use crate::auth::token_lifecycle::TokenLifecycleManager;
2475 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2476 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2477 let lifecycle_state = TokenLifecycleState {
2478 manager: lifecycle_manager,
2479 };
2480 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2481 debug!("Token lifecycle API mounted at /api/v1/auth");
2482 }
2483
2484 {
2486 use crate::auth::oidc::load_oidc_state;
2487 use crate::auth::token_lifecycle::TokenLifecycleManager;
2488 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2489 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2491 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2492 let oauth2_state = OAuth2ServerState {
2493 oidc_state,
2494 lifecycle_manager,
2495 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2496 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2497 };
2498 app = app.merge(oauth2_server_router(oauth2_state));
2499 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2500 }
2501
2502 {
2504 use crate::auth::oidc::load_oidc_state;
2505 use crate::auth::risk_engine::RiskEngine;
2506 use crate::auth::token_lifecycle::TokenLifecycleManager;
2507 use crate::handlers::consent::{consent_router, ConsentState};
2508 use crate::handlers::oauth2_server::OAuth2ServerState;
2509 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2511 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2512 let oauth2_state = OAuth2ServerState {
2513 oidc_state: oidc_state.clone(),
2514 lifecycle_manager: lifecycle_manager.clone(),
2515 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2516 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2517 };
2518 let risk_engine = Arc::new(RiskEngine::default());
2519 let consent_state = ConsentState {
2520 oauth2_state,
2521 risk_engine,
2522 };
2523 app = app.merge(consent_router(consent_state));
2524 debug!("Consent screen endpoints mounted at /consent");
2525 }
2526
2527 {
2529 use crate::auth::risk_engine::RiskEngine;
2530 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2531 let risk_engine = Arc::new(RiskEngine::default());
2532 let risk_state = RiskSimulationState { risk_engine };
2533 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2534 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2535 }
2536
2537 #[cfg(feature = "database")]
2539 let database = {
2540 use crate::database::Database;
2541 let database_url = std::env::var("DATABASE_URL").ok();
2542 match Database::connect_optional(database_url.as_deref()).await {
2543 Ok(db) => {
2544 if db.is_connected() {
2545 if let Err(e) = db.migrate_if_connected().await {
2547 warn!("Failed to run database migrations: {}", e);
2548 } else {
2549 info!("Database connected and migrations applied");
2550 }
2551 }
2552 Some(db)
2553 }
2554 Err(e) => {
2555 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2556 None
2557 }
2558 }
2559 };
2560
2561 let (drift_engine, incident_manager, drift_config) = {
2564 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2565 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2566 use std::sync::Arc;
2567
2568 let drift_config = DriftBudgetConfig::default();
2570 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2571
2572 let incident_store = Arc::new(IncidentStore::default());
2574 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2575
2576 (drift_engine, incident_manager, drift_config)
2577 };
2578
2579 {
2580 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2581 use crate::middleware::drift_tracking::DriftTrackingState;
2582 use mockforge_contracts::consumer_contracts::{
2583 ConsumerBreakingChangeDetector, UsageRecorder,
2584 };
2585 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2586 use std::sync::Arc;
2587
2588 let usage_recorder = Arc::new(UsageRecorder::default());
2590 let consumer_detector =
2591 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2592
2593 let diff_analyzer = if drift_config.enabled {
2595 match ContractDiffAnalyzer::new(
2596 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2597 ) {
2598 Ok(analyzer) => Some(Arc::new(analyzer)),
2599 Err(e) => {
2600 warn!("Failed to create contract diff analyzer: {}", e);
2601 None
2602 }
2603 }
2604 } else {
2605 None
2606 };
2607
2608 let spec = if let Some(ref spec_path) = spec_path {
2611 match OpenApiSpec::from_file(spec_path).await {
2612 Ok(s) => Some(Arc::new(s)),
2613 Err(e) => {
2614 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2615 None
2616 }
2617 }
2618 } else {
2619 None
2620 };
2621
2622 let drift_tracking_state = DriftTrackingState {
2624 diff_analyzer,
2625 spec,
2626 drift_engine: drift_engine.clone(),
2627 incident_manager: incident_manager.clone(),
2628 usage_recorder,
2629 consumer_detector,
2630 enabled: drift_config.enabled,
2631 };
2632
2633 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2635
2636 let drift_tracking_state_clone = drift_tracking_state.clone();
2639 app = app.layer(axum::middleware::from_fn(
2640 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2641 let state = drift_tracking_state_clone.clone();
2642 async move {
2643 if req.extensions().get::<DriftTrackingState>().is_none() {
2645 req.extensions_mut().insert(state);
2646 }
2647 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2649 .await
2650 }
2651 },
2652 ));
2653
2654 let drift_state = DriftBudgetState {
2655 engine: drift_engine.clone(),
2656 incident_manager: incident_manager.clone(),
2657 gitops_handler: None, };
2659
2660 app = app.merge(drift_budget_router(drift_state));
2661 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2662 }
2663
2664 #[cfg(feature = "pipelines")]
2666 {
2667 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2668
2669 let pipeline_state = PipelineState::new();
2670 app = app.merge(pipeline_router(pipeline_state));
2671 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2672 }
2673
2674 {
2676 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2677 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2678 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2679 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2680 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2681 use mockforge_intelligence::handlers::forecasting::{forecasting_router, ForecastingState};
2682 use mockforge_intelligence::handlers::semantic_drift::{
2683 semantic_drift_router, SemanticDriftState,
2684 };
2685 use mockforge_intelligence::handlers::threat_modeling::{
2686 threat_modeling_router, ThreatModelingState,
2687 };
2688 use std::sync::Arc;
2689
2690 let forecasting_config = ForecastingConfig::default();
2692 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2693 let forecasting_state = ForecastingState::new(forecaster);
2694 #[cfg(feature = "database")]
2695 let forecasting_state = forecasting_state.with_database(database.clone());
2696
2697 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2699 let semantic_state = SemanticDriftState::new(semantic_manager);
2700 #[cfg(feature = "database")]
2701 let semantic_state = semantic_state.with_database(database.clone());
2702
2703 let threat_config = ThreatModelingConfig::default();
2705 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2706 Ok(analyzer) => Arc::new(analyzer),
2707 Err(e) => {
2708 warn!("Failed to create threat analyzer: {}. Using default.", e);
2709 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2710 |_| {
2711 ThreatAnalyzer::new(ThreatModelingConfig {
2713 enabled: false,
2714 ..Default::default()
2715 })
2716 .expect("Failed to create fallback threat analyzer")
2717 },
2718 ))
2719 }
2720 };
2721 let mut webhook_configs = Vec::new();
2723 let config_paths = [
2724 "config.yaml",
2725 "mockforge.yaml",
2726 "tools/mockforge/config.yaml",
2727 "../tools/mockforge/config.yaml",
2728 ];
2729
2730 for path in &config_paths {
2731 if let Ok(config) = mockforge_core::config::load_config(path).await {
2732 if !config.incidents.webhooks.is_empty() {
2733 webhook_configs = config.incidents.webhooks.clone();
2734 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2735 break;
2736 }
2737 }
2738 }
2739
2740 if webhook_configs.is_empty() {
2741 debug!("No webhook configs found in config files, using empty list");
2742 }
2743
2744 let threat_state = ThreatModelingState::new(threat_analyzer, webhook_configs);
2745 #[cfg(feature = "database")]
2746 let threat_state = threat_state.with_database(database.clone());
2747
2748 let contract_health_state = ContractHealthState {
2750 incident_manager: incident_manager.clone(),
2751 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2752 #[cfg(feature = "database")]
2753 database: database.clone(),
2754 };
2755
2756 app = app.merge(forecasting_router(forecasting_state));
2758 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2759
2760 app = app.merge(semantic_drift_router(semantic_state));
2761 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2762
2763 app = app.merge(threat_modeling_router(threat_state));
2764 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2765
2766 app = app.merge(contract_health_router(contract_health_state));
2767 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2768 }
2769
2770 {
2772 use crate::handlers::protocol_contracts::{
2773 protocol_contracts_router, ProtocolContractState,
2774 };
2775 use mockforge_core::contract_drift::{
2776 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2777 };
2778 use std::sync::Arc;
2779 use tokio::sync::RwLock;
2780
2781 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2783
2784 let mut fitness_registry = FitnessFunctionRegistry::new();
2786
2787 let config_paths = [
2789 "config.yaml",
2790 "mockforge.yaml",
2791 "tools/mockforge/config.yaml",
2792 "../tools/mockforge/config.yaml",
2793 ];
2794
2795 let mut config_loaded = false;
2796 for path in &config_paths {
2797 if let Ok(config) = mockforge_core::config::load_config(path).await {
2798 if !config.contracts.fitness_rules.is_empty() {
2799 if let Err(e) =
2800 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2801 {
2802 warn!("Failed to load fitness rules from config {}: {}", path, e);
2803 } else {
2804 info!(
2805 "Loaded {} fitness rules from config: {}",
2806 config.contracts.fitness_rules.len(),
2807 path
2808 );
2809 config_loaded = true;
2810 break;
2811 }
2812 }
2813 }
2814 }
2815
2816 if !config_loaded {
2817 debug!("No fitness rules found in config files, using empty registry");
2818 }
2819
2820 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2821
2822 let consumer_mapping_registry =
2826 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2827 let consumer_analyzer =
2828 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2829
2830 let protocol_state = ProtocolContractState {
2831 registry: contract_registry,
2832 drift_engine: Some(drift_engine.clone()),
2833 incident_manager: Some(incident_manager.clone()),
2834 fitness_registry: Some(fitness_registry),
2835 consumer_analyzer: Some(consumer_analyzer),
2836 };
2837
2838 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2839 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2840 }
2841
2842 #[cfg(feature = "behavioral-cloning")]
2844 {
2845 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2846 use std::path::PathBuf;
2847
2848 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2850 .ok()
2851 .map(PathBuf::from)
2852 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2853
2854 let bc_middleware_state = if let Some(path) = db_path {
2855 BehavioralCloningMiddlewareState::with_database_path(path)
2856 } else {
2857 BehavioralCloningMiddlewareState::new()
2858 };
2859
2860 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2862 .ok()
2863 .and_then(|v| v.parse::<bool>().ok())
2864 .unwrap_or(false);
2865
2866 if enabled {
2867 let bc_state_clone = bc_middleware_state.clone();
2868 app = app.layer(axum::middleware::from_fn(
2869 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2870 let state = bc_state_clone.clone();
2871 async move {
2872 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2874 req.extensions_mut().insert(state);
2875 }
2876 middleware::behavioral_cloning::behavioral_cloning_middleware(req, next)
2878 .await
2879 }
2880 },
2881 ));
2882 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2883 }
2884 }
2885
2886 {
2888 use crate::handlers::consumer_contracts::{
2889 consumer_contracts_router, ConsumerContractsState,
2890 };
2891 use mockforge_contracts::consumer_contracts::{
2892 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2893 };
2894 use std::sync::Arc;
2895
2896 let registry = Arc::new(ConsumerRegistry::default());
2898
2899 let usage_recorder = Arc::new(UsageRecorder::default());
2901
2902 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2904
2905 let consumer_state = ConsumerContractsState {
2906 registry,
2907 usage_recorder,
2908 detector,
2909 violations: Arc::new(RwLock::new(HashMap::new())),
2910 };
2911
2912 app = app.merge(consumer_contracts_router(consumer_state));
2913 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2914 }
2915
2916 #[cfg(feature = "behavioral-cloning")]
2918 {
2919 use crate::handlers::behavioral_cloning::{
2920 behavioral_cloning_router, BehavioralCloningState,
2921 };
2922 use std::path::PathBuf;
2923
2924 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2926 .ok()
2927 .map(PathBuf::from)
2928 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2929
2930 let bc_state = if let Some(path) = db_path {
2931 BehavioralCloningState::with_database_path(path)
2932 } else {
2933 BehavioralCloningState::new()
2934 };
2935
2936 app = app.merge(behavioral_cloning_router(bc_state));
2937 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2938 }
2939
2940 {
2942 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2943 use mockforge_intelligence::consistency::ConsistencyEngine;
2944 use mockforge_intelligence::handlers::consistency::{consistency_router, ConsistencyState};
2945 use std::sync::Arc;
2946
2947 let consistency_engine = Arc::new(ConsistencyEngine::new());
2949
2950 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2952 consistency_engine.register_adapter(http_adapter.clone()).await;
2953
2954 let consistency_state = ConsistencyState {
2956 engine: consistency_engine.clone(),
2957 };
2958
2959 use mockforge_intelligence::handlers::xray::XRayState;
2961 let xray_state = Arc::new(XRayState::new(consistency_engine.clone()));
2962
2963 let consistency_middleware_state = ConsistencyMiddlewareState {
2965 engine: consistency_engine.clone(),
2966 adapter: http_adapter,
2967 xray_state: Some(xray_state.clone()),
2968 };
2969
2970 if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2978 tracing::info!(
2979 upstream = %reality_cfg.upstream_base,
2980 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2981 );
2982 app = app.layer(axum::middleware::from_fn(
2983 move |req: axum::extract::Request, next: axum::middleware::Next| {
2984 let cfg = reality_cfg.clone();
2985 async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2986 },
2987 ));
2988 }
2989
2990 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2992 app = app.layer(axum::middleware::from_fn(
2993 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2994 let state = consistency_middleware_state_clone.clone();
2995 async move {
2996 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2998 req.extensions_mut().insert(state);
2999 }
3000 consistency::middleware::consistency_middleware(req, next).await
3002 }
3003 },
3004 ));
3005
3006 app = app.merge(consistency_router(consistency_state));
3008 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
3009
3010 #[cfg(feature = "scenario-engine")]
3018 {
3019 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
3020 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
3021 Ok(s) => s,
3022 Err(e) => {
3023 tracing::warn!(
3024 error = %e,
3025 "Failed to init scenario storage; runtime scenarios API will list empty"
3026 );
3027 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
3030 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
3031 .expect("temp scenario storage")
3032 }
3033 };
3034 if let Err(e) = scenario_storage.load().await {
3035 tracing::warn!(
3036 error = %e,
3037 "Failed to load installed scenarios; API will list empty until scenarios are installed"
3038 );
3039 }
3040 let scenarios_state =
3041 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
3042 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
3043 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
3044 }
3045
3046 {
3048 use mockforge_intelligence::handlers::fidelity::{fidelity_router, FidelityState};
3049 let fidelity_state = FidelityState::new();
3050 app = app.merge(fidelity_router(fidelity_state));
3051 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
3052 }
3053
3054 {
3056 use mockforge_intelligence::handlers::scenario_studio::{
3057 scenario_studio_router, ScenarioStudioState,
3058 };
3059 let scenario_studio_state = ScenarioStudioState::new();
3060 app = app.merge(scenario_studio_router(scenario_studio_state));
3061 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
3062 }
3063
3064 #[cfg(feature = "performance-profiling")]
3066 {
3067 use crate::handlers::performance::{performance_router, PerformanceState};
3068 let performance_state = PerformanceState::new();
3069 app = app.nest("/api/performance", performance_router(performance_state));
3070 debug!("Performance mode endpoints mounted at /api/performance");
3071 }
3072
3073 #[cfg(feature = "world-state")]
3075 {
3076 use crate::handlers::world_state::{world_state_router, WorldStateState};
3077 use mockforge_world_state::WorldStateEngine;
3078 use std::sync::Arc;
3079 use tokio::sync::RwLock;
3080
3081 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
3082 let world_state_state = WorldStateState {
3083 engine: world_state_engine,
3084 };
3085 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
3086 debug!("World state endpoints mounted at /api/world-state");
3087 }
3088
3089 {
3091 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
3092 use mockforge_core::snapshots::SnapshotManager;
3093 use std::path::PathBuf;
3094
3095 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
3096 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
3097
3098 let snapshot_state = SnapshotState {
3099 manager: snapshot_manager,
3100 consistency_engine: Some(consistency_engine.clone()),
3101 workspace_persistence: None, vbr_engine: None, recorder: None, };
3105
3106 app = app.merge(snapshot_router(snapshot_state));
3107 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
3108
3109 {
3111 use mockforge_intelligence::handlers::xray::xray_router;
3112 app = app.merge(xray_router((*xray_state).clone()));
3113 debug!("X-Ray API endpoints mounted at /api/v1/xray");
3114 }
3115 }
3116
3117 {
3119 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
3120 use crate::middleware::ab_testing::ab_testing_middleware;
3121
3122 let ab_testing_state = ABTestingState::new();
3123
3124 let ab_testing_state_clone = ab_testing_state.clone();
3126 app = app.layer(axum::middleware::from_fn(
3127 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
3128 let state = ab_testing_state_clone.clone();
3129 async move {
3130 if req.extensions().get::<ABTestingState>().is_none() {
3132 req.extensions_mut().insert(state);
3133 }
3134 ab_testing_middleware(req, next).await
3136 }
3137 },
3138 ));
3139
3140 app = app.merge(ab_testing_router(ab_testing_state));
3142 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
3143 }
3144 }
3145
3146 {
3148 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
3149 use mockforge_intelligence::pr_generation::{PRGenerator, PRProvider};
3150 use std::sync::Arc;
3151
3152 let pr_config = mockforge_intelligence::pr_generation::PRGenerationConfig::from_env();
3154
3155 let generator = pr_config.token.clone().filter(|_| pr_config.enabled).map(|token| {
3158 let generator = match pr_config.provider {
3159 PRProvider::GitHub => PRGenerator::new_github(
3160 pr_config.owner.clone(),
3161 pr_config.repo.clone(),
3162 token,
3163 pr_config.base_branch.clone(),
3164 ),
3165 PRProvider::GitLab => PRGenerator::new_gitlab(
3166 pr_config.owner.clone(),
3167 pr_config.repo.clone(),
3168 token,
3169 pr_config.base_branch.clone(),
3170 ),
3171 };
3172 Arc::new(generator)
3173 });
3174
3175 let pr_state = PRGenerationState {
3176 generator: generator.clone(),
3177 };
3178
3179 app = app.merge(pr_generation_router(pr_state));
3180 if generator.is_some() {
3181 debug!(
3182 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3183 pr_config.provider
3184 );
3185 } else {
3186 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3187 }
3188 }
3189
3190 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3192
3193 if let Some(mt_config) = multi_tenant_config {
3195 if mt_config.enabled {
3196 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3197 use std::sync::Arc;
3198
3199 info!(
3200 "Multi-tenant mode enabled with {} routing strategy",
3201 match mt_config.routing_strategy {
3202 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3203 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3204 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3205 }
3206 );
3207
3208 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3210
3211 let default_workspace =
3213 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3214 if let Err(e) =
3215 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3216 {
3217 warn!("Failed to register default workspace: {}", e);
3218 } else {
3219 info!("Registered default workspace: '{}'", mt_config.default_workspace);
3220 }
3221
3222 let registry = Arc::new(registry);
3224
3225 let _workspace_router = WorkspaceRouter::new(registry);
3227 info!("Workspace routing middleware initialized for HTTP server");
3228 }
3229 }
3230
3231 let mut final_cors_config = cors_config;
3233 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3234 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3236 let mut rate_limit_config = middleware::RateLimitConfig {
3237 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3238 .ok()
3239 .and_then(|v| v.parse().ok())
3240 .unwrap_or(1000),
3241 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3242 .ok()
3243 .and_then(|v| v.parse().ok())
3244 .unwrap_or(2000),
3245 per_ip: true,
3246 per_endpoint: false,
3247 };
3248
3249 if let Some(deploy_config) = &deceptive_deploy_config {
3250 if deploy_config.enabled {
3251 info!("Deceptive deploy mode enabled - applying production-like configuration");
3252
3253 if let Some(prod_cors) = &deploy_config.cors {
3255 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3256 enabled: true,
3257 allowed_origins: prod_cors.allowed_origins.clone(),
3258 allowed_methods: prod_cors.allowed_methods.clone(),
3259 allowed_headers: prod_cors.allowed_headers.clone(),
3260 allow_credentials: prod_cors.allow_credentials,
3261 });
3262 info!("Applied production-like CORS configuration");
3263 }
3264
3265 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3267 rate_limit_config = middleware::RateLimitConfig {
3268 requests_per_minute: prod_rate_limit.requests_per_minute,
3269 burst: prod_rate_limit.burst,
3270 per_ip: prod_rate_limit.per_ip,
3271 per_endpoint: false,
3272 };
3273 info!(
3274 "Applied production-like rate limiting: {} req/min, burst: {}",
3275 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3276 );
3277 }
3278
3279 if !deploy_config.headers.is_empty() {
3281 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3282 production_headers = Some(std::sync::Arc::new(headers_map));
3283 info!("Configured {} production headers", deploy_config.headers.len());
3284 }
3285
3286 if let Some(prod_oauth) = &deploy_config.oauth {
3288 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3289 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3290 oauth2: Some(oauth2_config),
3291 ..Default::default()
3292 });
3293 info!("Applied production-like OAuth configuration for deceptive deploy");
3294 }
3295 }
3296 }
3297
3298 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3300 let rate_limiter =
3301 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3302
3303 let mut state = HttpServerState::new();
3304 if rate_limit_disabled {
3305 info!(
3306 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3307 );
3308 } else {
3309 state = state.with_rate_limiter(rate_limiter.clone());
3310 }
3311
3312 if let Some(headers) = production_headers.clone() {
3314 state = state.with_production_headers(headers);
3315 }
3316
3317 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3319
3320 if state.production_headers.is_some() {
3322 app =
3323 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3324 }
3325
3326 if middleware::is_keepalive_hint_enabled() {
3331 info!(
3332 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3333 );
3334 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3335 }
3336
3337 if middleware::is_conn_log_enabled() {
3342 info!(
3343 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
3344 );
3345 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
3346 }
3347
3348 if let Some(auth_config) = deceptive_deploy_auth_config {
3350 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3351 use std::collections::HashMap;
3352 use std::sync::Arc;
3353 use tokio::sync::RwLock;
3354
3355 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3357 match create_oauth2_client(oauth2_config) {
3358 Ok(client) => Some(client),
3359 Err(e) => {
3360 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3361 None
3362 }
3363 }
3364 } else {
3365 None
3366 };
3367
3368 let auth_state = AuthState {
3370 config: auth_config,
3371 spec: None, oauth2_client,
3373 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3374 };
3375
3376 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3378 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3379 }
3380
3381 #[cfg(feature = "runtime-daemon")]
3383 {
3384 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3385 use std::sync::Arc;
3386
3387 let daemon_config = RuntimeDaemonConfig::from_env();
3389
3390 if daemon_config.enabled {
3391 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3392
3393 let management_api_url =
3395 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3396 let port =
3397 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3398 format!("http://localhost:{}", port)
3399 });
3400
3401 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3403
3404 let detector = NotFoundDetector::new(daemon_config.clone());
3406 detector.set_generator(generator).await;
3407
3408 let detector_clone = detector.clone();
3410 app = app.layer(axum::middleware::from_fn(
3411 move |req: axum::extract::Request, next: axum::middleware::Next| {
3412 let detector = detector_clone.clone();
3413 async move { detector.detect_and_auto_create(req, next).await }
3414 },
3415 ));
3416
3417 debug!("Runtime daemon 404 detection middleware added");
3418 }
3419 }
3420
3421 {
3423 let routes_state = HttpServerState::with_routes(captured_routes);
3424 let routes_router = Router::new()
3425 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3426 .with_state(routes_state);
3427 app = app.merge(routes_router);
3428 }
3429
3430 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3432
3433 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3438
3439 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3442
3443 app = apply_cors_middleware(app, final_cors_config);
3445
3446 app
3447}
3448
3449#[test]
3453fn test_route_info_clone() {
3454 let route = RouteInfo {
3455 method: "POST".to_string(),
3456 path: "/users".to_string(),
3457 operation_id: Some("createUser".to_string()),
3458 summary: None,
3459 description: None,
3460 parameters: vec![],
3461 };
3462
3463 let cloned = route.clone();
3464 assert_eq!(route.method, cloned.method);
3465 assert_eq!(route.path, cloned.path);
3466 assert_eq!(route.operation_id, cloned.operation_id);
3467}
3468
3469#[test]
3470fn test_http_server_state_new() {
3471 let state = HttpServerState::new();
3472 assert_eq!(state.routes.len(), 0);
3473}
3474
3475#[test]
3476fn test_http_server_state_with_routes() {
3477 let routes = vec![
3478 RouteInfo {
3479 method: "GET".to_string(),
3480 path: "/users".to_string(),
3481 operation_id: Some("getUsers".to_string()),
3482 summary: None,
3483 description: None,
3484 parameters: vec![],
3485 },
3486 RouteInfo {
3487 method: "POST".to_string(),
3488 path: "/users".to_string(),
3489 operation_id: Some("createUser".to_string()),
3490 summary: None,
3491 description: None,
3492 parameters: vec![],
3493 },
3494 ];
3495
3496 let state = HttpServerState::with_routes(routes.clone());
3497 assert_eq!(state.routes.len(), 2);
3498 assert_eq!(state.routes[0].method, "GET");
3499 assert_eq!(state.routes[1].method, "POST");
3500}
3501
3502#[test]
3503fn test_http_server_state_clone() {
3504 let routes = vec![RouteInfo {
3505 method: "GET".to_string(),
3506 path: "/test".to_string(),
3507 operation_id: None,
3508 summary: None,
3509 description: None,
3510 parameters: vec![],
3511 }];
3512
3513 let state = HttpServerState::with_routes(routes);
3514 let cloned = state.clone();
3515
3516 assert_eq!(state.routes.len(), cloned.routes.len());
3517 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3518}
3519
3520#[tokio::test]
3521async fn test_build_router_without_openapi() {
3522 let _router = build_router(None, None, None).await;
3523 }
3525
3526#[tokio::test]
3527async fn test_build_router_with_nonexistent_spec() {
3528 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3529 }
3531
3532#[tokio::test]
3533async fn test_build_router_with_auth_and_latency() {
3534 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3535 }
3537
3538#[tokio::test]
3539async fn test_build_router_with_latency() {
3540 let _router = build_router_with_latency(None, None, None).await;
3541 }
3543
3544#[tokio::test]
3545async fn test_build_router_with_auth() {
3546 let _router = build_router_with_auth(None, None, None).await;
3547 }
3549
3550#[tokio::test]
3551async fn test_build_router_with_chains() {
3552 let _router = build_router_with_chains(None, None, None).await;
3553 }
3555
3556#[test]
3557fn test_route_info_with_all_fields() {
3558 let route = RouteInfo {
3559 method: "PUT".to_string(),
3560 path: "/users/{id}".to_string(),
3561 operation_id: Some("updateUser".to_string()),
3562 summary: Some("Update user".to_string()),
3563 description: Some("Updates an existing user".to_string()),
3564 parameters: vec!["id".to_string(), "body".to_string()],
3565 };
3566
3567 assert!(route.operation_id.is_some());
3568 assert!(route.summary.is_some());
3569 assert!(route.description.is_some());
3570 assert_eq!(route.parameters.len(), 2);
3571}
3572
3573#[test]
3574fn test_route_info_with_minimal_fields() {
3575 let route = RouteInfo {
3576 method: "DELETE".to_string(),
3577 path: "/users/{id}".to_string(),
3578 operation_id: None,
3579 summary: None,
3580 description: None,
3581 parameters: vec![],
3582 };
3583
3584 assert!(route.operation_id.is_none());
3585 assert!(route.summary.is_none());
3586 assert!(route.description.is_none());
3587 assert_eq!(route.parameters.len(), 0);
3588}
3589
3590#[test]
3591fn test_http_server_state_empty_routes() {
3592 let state = HttpServerState::with_routes(vec![]);
3593 assert_eq!(state.routes.len(), 0);
3594}
3595
3596#[test]
3597fn test_http_server_state_multiple_routes() {
3598 let routes = vec![
3599 RouteInfo {
3600 method: "GET".to_string(),
3601 path: "/users".to_string(),
3602 operation_id: Some("listUsers".to_string()),
3603 summary: Some("List all users".to_string()),
3604 description: None,
3605 parameters: vec![],
3606 },
3607 RouteInfo {
3608 method: "GET".to_string(),
3609 path: "/users/{id}".to_string(),
3610 operation_id: Some("getUser".to_string()),
3611 summary: Some("Get a user".to_string()),
3612 description: None,
3613 parameters: vec!["id".to_string()],
3614 },
3615 RouteInfo {
3616 method: "POST".to_string(),
3617 path: "/users".to_string(),
3618 operation_id: Some("createUser".to_string()),
3619 summary: Some("Create a user".to_string()),
3620 description: None,
3621 parameters: vec!["body".to_string()],
3622 },
3623 ];
3624
3625 let state = HttpServerState::with_routes(routes);
3626 assert_eq!(state.routes.len(), 3);
3627
3628 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3630 assert!(methods.contains(&"GET"));
3631 assert!(methods.contains(&"POST"));
3632}
3633
3634#[test]
3635fn test_http_server_state_with_rate_limiter() {
3636 use std::sync::Arc;
3637
3638 let config = middleware::RateLimitConfig::default();
3639 let rate_limiter = Arc::new(middleware::GlobalRateLimiter::new(config));
3640
3641 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3642
3643 assert!(state.rate_limiter.is_some());
3644 assert_eq!(state.routes.len(), 0);
3645}
3646
3647#[tokio::test]
3648async fn test_build_router_includes_rate_limiter() {
3649 let _router = build_router(None, None, None).await;
3650 }