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