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