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_auth;
241pub mod management_ws;
243pub mod mcp_mock;
245pub mod metrics_middleware;
246pub mod middleware;
247pub mod mockai_api;
249pub mod network_profile_runtime;
251pub mod op_middleware;
252pub mod protocol_server;
254pub mod proxy_server;
256pub mod quick_mock;
258pub mod rag_ai_generator;
260pub mod reality_proxy;
262pub mod replay_listing;
264pub mod request_logging;
265pub mod route_chaos_runtime;
266#[cfg(feature = "scenario-engine")]
268pub mod scenarios_runtime;
269pub mod spec_import;
271pub mod sse;
273pub mod state_machine_api;
275pub mod time_travel_api;
277pub mod tls;
279pub mod token_response;
281pub mod ui_builder;
283pub mod verification;
285
286pub mod handlers;
288
289pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
291pub use health::{HealthManager, ServiceStatus};
293
294pub use management::{
296 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
297 ServerConfig, ServerStats,
298};
299
300pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
302
303pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
305
306pub use verification::verification_router;
308
309pub use metrics_middleware::collect_http_metrics;
311
312pub use http_tracing_middleware::http_tracing_middleware;
314
315pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
317
318async fn load_persona_from_config() -> Option<Arc<Persona>> {
321 use mockforge_core::config::load_config;
322
323 let config_paths = [
325 "config.yaml",
326 "mockforge.yaml",
327 "tools/mockforge/config.yaml",
328 "../tools/mockforge/config.yaml",
329 ];
330
331 for path in &config_paths {
332 if let Ok(config) = load_config(path).await {
333 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
336 {
337 tracing::info!(
338 "Loaded active persona '{}' from config file: {}",
339 persona.name,
340 path
341 );
342 return Some(Arc::new(persona.clone()));
343 } else {
344 tracing::debug!(
345 "No active persona found in config file: {} (personas count: {})",
346 path,
347 config.mockai.intelligent_behavior.personas.personas.len()
348 );
349 }
350 } else {
351 tracing::debug!("Could not load config from: {}", path);
352 }
353 }
354
355 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
356 None
357}
358
359use axum::body::Body;
360use axum::extract::State;
361use axum::http::Request;
362use axum::middleware::from_fn_with_state;
363use axum::response::Json;
364use axum::Router;
365use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
366use mockforge_core::intelligent_behavior::config::Persona;
367use mockforge_foundation::latency::LatencyInjector;
368use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
369use mockforge_openapi::openapi_routes::ValidationOptions;
370use mockforge_openapi::OpenApiSpec;
371use std::sync::Arc;
372use tower_http::cors::{Any, CorsLayer};
373
374#[cfg(feature = "data-faker")]
375use mockforge_data::provider::register_core_faker_provider;
376use mockforge_foundation::latency::LatencyProfile;
377use std::collections::HashMap;
378use std::ffi::OsStr;
379use std::path::Path;
380use tokio::fs;
381use tokio::sync::RwLock;
382use tracing::*;
383
384#[derive(Clone)]
386pub struct RouteInfo {
387 pub method: String,
389 pub path: String,
391 pub operation_id: Option<String>,
393 pub summary: Option<String>,
395 pub description: Option<String>,
397 pub parameters: Vec<String>,
399}
400
401#[derive(Clone)]
403pub struct HttpServerState {
404 pub routes: Vec<RouteInfo>,
406 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
408 pub production_headers: Option<Arc<HashMap<String, String>>>,
410}
411
412impl Default for HttpServerState {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418impl HttpServerState {
419 pub fn new() -> Self {
421 Self {
422 routes: Vec::new(),
423 rate_limiter: None,
424 production_headers: None,
425 }
426 }
427
428 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
430 Self {
431 routes,
432 rate_limiter: None,
433 production_headers: None,
434 }
435 }
436
437 pub fn with_rate_limiter(
439 mut self,
440 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
441 ) -> Self {
442 self.rate_limiter = Some(rate_limiter);
443 self
444 }
445
446 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
448 self.production_headers = Some(headers);
449 self
450 }
451}
452
453async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
455 let route_info: Vec<serde_json::Value> = state
456 .routes
457 .iter()
458 .map(|route| {
459 serde_json::json!({
460 "method": route.method,
461 "path": route.path,
462 "operation_id": route.operation_id,
463 "summary": route.summary,
464 "description": route.description,
465 "parameters": route.parameters
466 })
467 })
468 .collect();
469
470 Json(serde_json::json!({
471 "routes": route_info,
472 "total": state.routes.len()
473 }))
474}
475
476async fn get_docs_handler() -> axum::response::Html<&'static str> {
478 axum::response::Html(include_str!("../static/docs.html"))
479}
480
481pub async fn build_router(
483 spec_path: Option<String>,
484 options: Option<ValidationOptions>,
485 failure_config: Option<FailureConfig>,
486) -> Router {
487 build_router_with_multi_tenant(
488 spec_path,
489 options,
490 failure_config,
491 None,
492 None,
493 None,
494 None,
495 None,
496 None,
497 None,
498 )
499 .await
500}
501
502fn apply_cors_middleware(
504 app: Router,
505 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
506) -> Router {
507 use http::Method;
508 use tower_http::cors::AllowOrigin;
509
510 if let Some(config) = cors_config {
511 if !config.enabled {
512 return app;
513 }
514
515 let mut cors_layer = CorsLayer::new();
516 let is_wildcard_origin;
517
518 if config.allowed_origins.contains(&"*".to_string()) {
520 cors_layer = cors_layer.allow_origin(Any);
521 is_wildcard_origin = true;
522 } else if !config.allowed_origins.is_empty() {
523 let origins: Vec<_> = config
525 .allowed_origins
526 .iter()
527 .filter_map(|origin| {
528 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
529 })
530 .collect();
531
532 if origins.is_empty() {
533 warn!("No valid CORS origins configured, using permissive CORS");
535 cors_layer = cors_layer.allow_origin(Any);
536 is_wildcard_origin = true;
537 } else {
538 if origins.len() == 1 {
541 cors_layer = cors_layer.allow_origin(origins[0].clone());
542 is_wildcard_origin = false;
543 } else {
544 warn!(
546 "Multiple CORS origins configured, using permissive CORS. \
547 Consider using '*' for all origins."
548 );
549 cors_layer = cors_layer.allow_origin(Any);
550 is_wildcard_origin = true;
551 }
552 }
553 } else {
554 cors_layer = cors_layer.allow_origin(Any);
556 is_wildcard_origin = true;
557 }
558
559 if !config.allowed_methods.is_empty() {
561 let methods: Vec<Method> =
562 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
563 if !methods.is_empty() {
564 cors_layer = cors_layer.allow_methods(methods);
565 }
566 } else {
567 cors_layer = cors_layer.allow_methods([
569 Method::GET,
570 Method::POST,
571 Method::PUT,
572 Method::DELETE,
573 Method::PATCH,
574 Method::OPTIONS,
575 ]);
576 }
577
578 if !config.allowed_headers.is_empty() {
580 let headers: Vec<_> = config
581 .allowed_headers
582 .iter()
583 .filter_map(|h| h.parse::<http::HeaderName>().ok())
584 .collect();
585 if !headers.is_empty() {
586 cors_layer = cors_layer.allow_headers(headers);
587 }
588 } else {
589 cors_layer =
591 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
592 }
593
594 let should_allow_credentials = if is_wildcard_origin {
598 false
600 } else {
601 config.allow_credentials
603 };
604
605 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
606
607 info!(
608 "CORS middleware enabled with configured settings (credentials: {})",
609 should_allow_credentials
610 );
611 app.layer(cors_layer)
612 } else {
613 debug!("No CORS config provided, using permissive CORS for development");
617 app.layer(CorsLayer::permissive().allow_credentials(false))
620 }
621}
622
623#[allow(clippy::too_many_arguments)]
625#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
627 spec_path: Option<String>,
628 options: Option<ValidationOptions>,
629 failure_config: Option<FailureConfig>,
630 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
631 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
632 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
633 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
634 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
635 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
636 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
637) -> Router {
638 use std::time::Instant;
639
640 let startup_start = Instant::now();
641
642 let mut app = Router::new();
644
645 let mut rate_limit_config = middleware::RateLimitConfig {
648 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
649 .ok()
650 .and_then(|v| v.parse().ok())
651 .unwrap_or(1000),
652 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
653 .ok()
654 .and_then(|v| v.parse().ok())
655 .unwrap_or(2000),
656 per_ip: true,
657 per_endpoint: false,
658 };
659
660 let mut final_cors_config = cors_config;
662 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
663 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
665
666 if let Some(deploy_config) = &deceptive_deploy_config {
667 if deploy_config.enabled {
668 info!("Deceptive deploy mode enabled - applying production-like configuration");
669
670 if let Some(prod_cors) = &deploy_config.cors {
672 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
673 enabled: true,
674 allowed_origins: prod_cors.allowed_origins.clone(),
675 allowed_methods: prod_cors.allowed_methods.clone(),
676 allowed_headers: prod_cors.allowed_headers.clone(),
677 allow_credentials: prod_cors.allow_credentials,
678 });
679 info!("Applied production-like CORS configuration");
680 }
681
682 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
684 rate_limit_config = middleware::RateLimitConfig {
685 requests_per_minute: prod_rate_limit.requests_per_minute,
686 burst: prod_rate_limit.burst,
687 per_ip: prod_rate_limit.per_ip,
688 per_endpoint: false,
689 };
690 info!(
691 "Applied production-like rate limiting: {} req/min, burst: {}",
692 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
693 );
694 }
695
696 if !deploy_config.headers.is_empty() {
698 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
699 production_headers = Some(std::sync::Arc::new(headers_map));
700 info!("Configured {} production headers", deploy_config.headers.len());
701 }
702
703 if let Some(prod_oauth) = &deploy_config.oauth {
705 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
706 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
707 oauth2: Some(oauth2_config),
708 ..Default::default()
709 });
710 info!("Applied production-like OAuth configuration for deceptive deploy");
711 }
712 }
713 }
714
715 let rate_limit_disabled = middleware::is_rate_limit_disabled();
716 let rate_limiter =
717 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
718
719 let mut state = HttpServerState::new();
720 if rate_limit_disabled {
721 info!(
722 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
723 );
724 } else {
725 state = state.with_rate_limiter(rate_limiter.clone());
726 }
727
728 if let Some(headers) = production_headers.clone() {
730 state = state.with_production_headers(headers);
731 }
732
733 let spec_path_for_mgmt = spec_path.clone();
735
736 if let Some(spec_path) = spec_path {
738 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
739
740 let spec_load_start = Instant::now();
742 match OpenApiSpec::from_file(&spec_path).await {
743 Ok(openapi) => {
744 let spec_load_duration = spec_load_start.elapsed();
745 info!(
746 "Successfully loaded OpenAPI spec from {} (took {:?})",
747 spec_path, spec_load_duration
748 );
749
750 tracing::debug!("Creating OpenAPI route registry...");
752 let registry_start = Instant::now();
753
754 let persona = load_persona_from_config().await;
756
757 let registry = if let Some(opts) = options {
758 tracing::debug!("Using custom validation options");
759 if let Some(ref persona) = persona {
760 tracing::info!("Using persona '{}' for route generation", persona.name);
761 }
762 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
763 } else {
764 tracing::debug!("Using environment-based options");
765 if let Some(ref persona) = persona {
766 tracing::info!("Using persona '{}' for route generation", persona.name);
767 }
768 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
769 };
770 let registry_duration = registry_start.elapsed();
771 info!(
772 "Created OpenAPI route registry with {} routes (took {:?})",
773 registry.routes().len(),
774 registry_duration
775 );
776
777 let extract_start = Instant::now();
779 let route_info: Vec<RouteInfo> = registry
780 .routes()
781 .iter()
782 .map(|route| RouteInfo {
783 method: route.method.clone(),
784 path: route.path.clone(),
785 operation_id: route.operation.operation_id.clone(),
786 summary: route.operation.summary.clone(),
787 description: route.operation.description.clone(),
788 parameters: route.parameters.clone(),
789 })
790 .collect();
791 state.routes = route_info;
792 let extract_duration = extract_start.elapsed();
793 debug!("Extracted route information (took {:?})", extract_duration);
794
795 let registry = registry.with_overrides(Arc::new(
796 mockforge_core::SharedOverrides::load(Vec::new()).await,
797 ));
798 let router_build_start = Instant::now();
799 let response_rewriter: Option<
800 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
801 > = Some(std::sync::Arc::new(
802 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(),
803 ));
804 let openapi_router = if let Some(mockai_instance) = &mockai {
805 tracing::debug!("Building router with MockAI support");
806 registry.build_router_with_mockai(Some(mockai_instance.clone()))
807 } else if let Some(ai_generator) = &ai_generator {
808 tracing::debug!("Building router with AI generator support");
809 registry.build_router_with_ai(Some(ai_generator.clone()))
810 } else if let Some(failure_config) = &failure_config {
811 tracing::debug!("Building router with failure injection and overrides");
812 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
813 registry.build_router_with_injectors_and_rewriter(
814 LatencyInjector::default(),
815 Some(failure_injector),
816 response_rewriter,
817 )
818 } else {
819 tracing::debug!("Building router with overrides");
820 registry.build_router_with_injectors_and_rewriter(
821 LatencyInjector::default(),
822 None,
823 response_rewriter,
824 )
825 };
826 let router_build_duration = router_build_start.elapsed();
827 debug!("Built OpenAPI router (took {:?})", router_build_duration);
828
829 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
837 .ok()
838 .and_then(|v| v.parse::<usize>().ok())
839 .unwrap_or(50);
840 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
841 let openapi_router =
842 openapi_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
843 tracing::info!(
844 body_limit_mb = body_limit_mb,
845 "Merging OpenAPI router with main router"
846 );
847 app = app.merge(openapi_router);
848 tracing::debug!("Router built successfully");
849 }
850 Err(e) => {
851 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
852 }
853 }
854 }
855
856 app = app.route(
858 "/health",
859 axum::routing::get(|| async {
860 use mockforge_core::server_utils::health::HealthStatus;
861 {
862 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
864 Ok(value) => Json(value),
865 Err(e) => {
866 tracing::error!("Failed to serialize health status: {}", e);
868 Json(serde_json::json!({
869 "status": "healthy",
870 "service": "mockforge-http",
871 "uptime_seconds": 0
872 }))
873 }
874 }
875 }
876 }),
877 )
878 .merge(sse::sse_router())
880 .merge(file_server::file_serving_router());
882
883 let state_for_routes = state.clone();
885
886 let routes_router = Router::new()
888 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
889 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
890 .with_state(state_for_routes);
891
892 app = app.merge(routes_router);
894
895 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
897
898 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
901 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
902
903 if Path::new(&coverage_html_path).exists() {
905 app = app.nest_service(
906 "/__mockforge/coverage.html",
907 tower_http::services::ServeFile::new(&coverage_html_path),
908 );
909 debug!("Serving coverage UI from: {}", coverage_html_path);
910 } else {
911 debug!(
912 "Coverage UI file not found at: {}. Skipping static file serving.",
913 coverage_html_path
914 );
915 }
916
917 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
920 match OpenApiSpec::from_file(sp).await {
921 Ok(s) => Some(Arc::new(s)),
922 Err(e) => {
923 debug!("Failed to load OpenAPI spec for management API: {}", e);
924 None
925 }
926 }
927 } else {
928 None
929 };
930 let mgmt_port = std::env::var("PORT")
931 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
932 .ok()
933 .and_then(|p| p.parse().ok())
934 .unwrap_or(3000);
935 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
936
937 use std::sync::Arc;
939 let ws_state = WsManagementState::new();
940 let ws_broadcast = Arc::new(ws_state.tx.clone());
941 let management_state = management_state.with_ws_broadcast(ws_broadcast);
942
943 #[cfg(feature = "smtp")]
947 let management_state = {
948 if let Some(smtp_reg) = smtp_registry {
949 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
950 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
951 Err(e) => {
952 error!(
953 "Invalid SMTP registry type passed to HTTP management state: {:?}",
954 e.type_id()
955 );
956 management_state
957 }
958 }
959 } else {
960 management_state
961 }
962 };
963 #[cfg(not(feature = "smtp"))]
964 let management_state = management_state;
965 #[cfg(not(feature = "smtp"))]
966 let _ = smtp_registry;
967 let management_state_for_fallback = management_state.clone();
968 app = app.nest("/__mockforge/api", management_router(management_state));
969 app = app.fallback_service(
974 axum::routing::any(management::dynamic_mock_fallback)
975 .with_state(management_state_for_fallback),
976 );
977
978 app = app.merge(verification_router());
980
981 use crate::auth::oidc::oidc_router;
983 app = app.merge(oidc_router());
984
985 {
987 use mockforge_core::security::get_global_access_review_service;
988 if let Some(service) = get_global_access_review_service().await {
989 use crate::handlers::access_review::{access_review_router, AccessReviewState};
990 let review_state = AccessReviewState { service };
991 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
992 debug!("Access review API mounted at /api/v1/security/access-reviews");
993 }
994 }
995
996 {
998 use mockforge_core::security::get_global_privileged_access_manager;
999 if let Some(manager) = get_global_privileged_access_manager().await {
1000 use crate::handlers::privileged_access::{
1001 privileged_access_router, PrivilegedAccessState,
1002 };
1003 let privileged_state = PrivilegedAccessState { manager };
1004 app = app.nest(
1005 "/api/v1/security/privileged-access",
1006 privileged_access_router(privileged_state),
1007 );
1008 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1009 }
1010 }
1011
1012 {
1014 use mockforge_core::security::get_global_change_management_engine;
1015 if let Some(engine) = get_global_change_management_engine().await {
1016 use crate::handlers::change_management::{
1017 change_management_router, ChangeManagementState,
1018 };
1019 let change_state = ChangeManagementState { engine };
1020 app = app.nest("/api/v1/change-management", change_management_router(change_state));
1021 debug!("Change management API mounted at /api/v1/change-management");
1022 }
1023 }
1024
1025 {
1027 use mockforge_core::security::get_global_risk_assessment_engine;
1028 if let Some(engine) = get_global_risk_assessment_engine().await {
1029 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1030 let risk_state = RiskAssessmentState { engine };
1031 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1032 debug!("Risk assessment API mounted at /api/v1/security/risks");
1033 }
1034 }
1035
1036 {
1038 use crate::auth::token_lifecycle::TokenLifecycleManager;
1039 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1040 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1041 let lifecycle_state = TokenLifecycleState {
1042 manager: lifecycle_manager,
1043 };
1044 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1045 debug!("Token lifecycle API mounted at /api/v1/auth");
1046 }
1047
1048 {
1050 use crate::auth::oidc::load_oidc_state;
1051 use crate::auth::token_lifecycle::TokenLifecycleManager;
1052 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1053 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1055 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1056 let oauth2_state = OAuth2ServerState {
1057 oidc_state,
1058 lifecycle_manager,
1059 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1060 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1061 };
1062 app = app.merge(oauth2_server_router(oauth2_state));
1063 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1064 }
1065
1066 {
1068 use crate::auth::oidc::load_oidc_state;
1069 use crate::auth::risk_engine::RiskEngine;
1070 use crate::auth::token_lifecycle::TokenLifecycleManager;
1071 use crate::handlers::consent::{consent_router, ConsentState};
1072 use crate::handlers::oauth2_server::OAuth2ServerState;
1073 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1075 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1076 let oauth2_state = OAuth2ServerState {
1077 oidc_state: oidc_state.clone(),
1078 lifecycle_manager: lifecycle_manager.clone(),
1079 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1080 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1081 };
1082 let risk_engine = Arc::new(RiskEngine::default());
1083 let consent_state = ConsentState {
1084 oauth2_state,
1085 risk_engine,
1086 };
1087 app = app.merge(consent_router(consent_state));
1088 debug!("Consent screen endpoints mounted at /consent");
1089 }
1090
1091 {
1093 use crate::auth::risk_engine::RiskEngine;
1094 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1095 let risk_engine = Arc::new(RiskEngine::default());
1096 let risk_state = RiskSimulationState { risk_engine };
1097 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1098 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1099 }
1100
1101 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1103
1104 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1106
1107 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1109
1110 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1113
1114 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1116
1117 if state.production_headers.is_some() {
1119 app =
1120 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1121 }
1122
1123 if middleware::is_keepalive_hint_enabled() {
1129 info!(
1130 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
1131 );
1132 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
1133 }
1134
1135 if middleware::is_conn_log_enabled() {
1140 info!(
1141 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
1142 );
1143 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
1144 }
1145
1146 if let Some(auth_config) = deceptive_deploy_auth_config {
1148 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1149 use std::collections::HashMap;
1150 use std::sync::Arc;
1151 use tokio::sync::RwLock;
1152
1153 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1155 match create_oauth2_client(oauth2_config) {
1156 Ok(client) => Some(client),
1157 Err(e) => {
1158 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1159 None
1160 }
1161 }
1162 } else {
1163 None
1164 };
1165
1166 let auth_state = AuthState {
1168 config: auth_config,
1169 spec: None, oauth2_client,
1171 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1172 };
1173
1174 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1176 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1177 }
1178
1179 app = apply_cors_middleware(app, final_cors_config);
1181
1182 if let Some(mt_config) = multi_tenant_config {
1184 if mt_config.enabled {
1185 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1186 use std::sync::Arc;
1187
1188 info!(
1189 "Multi-tenant mode enabled with {} routing strategy",
1190 match mt_config.routing_strategy {
1191 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1192 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1193 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1194 }
1195 );
1196
1197 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1199
1200 let default_workspace =
1202 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1203 if let Err(e) =
1204 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1205 {
1206 warn!("Failed to register default workspace: {}", e);
1207 } else {
1208 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1209 }
1210
1211 if mt_config.auto_discover {
1213 if let Some(config_dir) = &mt_config.config_directory {
1214 let config_path = Path::new(config_dir);
1215 if config_path.exists() && config_path.is_dir() {
1216 match fs::read_dir(config_path).await {
1217 Ok(mut entries) => {
1218 while let Ok(Some(entry)) = entries.next_entry().await {
1219 let path = entry.path();
1220 if path.extension() == Some(OsStr::new("yaml")) {
1221 match fs::read_to_string(&path).await {
1222 Ok(content) => {
1223 match serde_yaml::from_str::<
1224 mockforge_core::Workspace,
1225 >(
1226 &content
1227 ) {
1228 Ok(workspace) => {
1229 if let Err(e) = registry.register_workspace(
1230 workspace.id.clone(),
1231 workspace,
1232 ) {
1233 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1234 } else {
1235 info!("Auto-registered workspace from {:?}", path);
1236 }
1237 }
1238 Err(e) => {
1239 warn!("Failed to parse workspace from {:?}: {}", path, e);
1240 }
1241 }
1242 }
1243 Err(e) => {
1244 warn!(
1245 "Failed to read workspace file {:?}: {}",
1246 path, e
1247 );
1248 }
1249 }
1250 }
1251 }
1252 }
1253 Err(e) => {
1254 warn!("Failed to read config directory {:?}: {}", config_path, e);
1255 }
1256 }
1257 } else {
1258 warn!(
1259 "Config directory {:?} does not exist or is not a directory",
1260 config_path
1261 );
1262 }
1263 }
1264 }
1265
1266 let registry = Arc::new(registry);
1268
1269 let _workspace_router = WorkspaceRouter::new(registry);
1271
1272 info!("Workspace routing middleware initialized for HTTP server");
1275 }
1276 }
1277
1278 let total_startup_duration = startup_start.elapsed();
1279 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1280
1281 app
1282}
1283
1284pub async fn build_router_with_auth_and_latency(
1286 spec_path: Option<String>,
1287 _options: Option<()>,
1288 auth_config: Option<mockforge_core::config::AuthConfig>,
1289 latency_injector: Option<LatencyInjector>,
1290) -> Router {
1291 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1293
1294 if let Some(injector) = latency_injector {
1296 let injector = Arc::new(injector);
1297 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1298 let injector = injector.clone();
1299 async move {
1300 let _ = injector.inject_latency(&[]).await;
1301 next.run(req).await
1302 }
1303 }));
1304 }
1305
1306 app
1307}
1308
1309pub async fn build_router_with_latency(
1311 spec_path: Option<String>,
1312 options: Option<ValidationOptions>,
1313 latency_injector: Option<LatencyInjector>,
1314) -> Router {
1315 if let Some(spec) = &spec_path {
1316 match OpenApiSpec::from_file(spec).await {
1317 Ok(openapi) => {
1318 let registry = if let Some(opts) = options {
1319 OpenApiRouteRegistry::new_with_options(openapi, opts)
1320 } else {
1321 OpenApiRouteRegistry::new_with_env(openapi)
1322 };
1323
1324 if let Some(injector) = latency_injector {
1325 return registry.build_router_with_latency(injector);
1326 } else {
1327 return registry.build_router();
1328 }
1329 }
1330 Err(e) => {
1331 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1332 }
1333 }
1334 }
1335
1336 build_router(None, None, None).await
1337}
1338
1339pub async fn build_router_with_auth(
1341 spec_path: Option<String>,
1342 options: Option<ValidationOptions>,
1343 auth_config: Option<mockforge_core::config::AuthConfig>,
1344) -> Router {
1345 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1346 use std::sync::Arc;
1347
1348 #[cfg(feature = "data-faker")]
1350 {
1351 register_core_faker_provider();
1352 }
1353
1354 let spec = if let Some(spec_path) = &spec_path {
1356 match OpenApiSpec::from_file(&spec_path).await {
1357 Ok(spec) => Some(Arc::new(spec)),
1358 Err(e) => {
1359 warn!("Failed to load OpenAPI spec for auth: {}", e);
1360 None
1361 }
1362 }
1363 } else {
1364 None
1365 };
1366
1367 let oauth2_client = if let Some(auth_config) = &auth_config {
1369 if let Some(oauth2_config) = &auth_config.oauth2 {
1370 match create_oauth2_client(oauth2_config) {
1371 Ok(client) => Some(client),
1372 Err(e) => {
1373 warn!("Failed to create OAuth2 client: {}", e);
1374 None
1375 }
1376 }
1377 } else {
1378 None
1379 }
1380 } else {
1381 None
1382 };
1383
1384 let auth_state = AuthState {
1385 config: auth_config.unwrap_or_default(),
1386 spec,
1387 oauth2_client,
1388 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1389 };
1390
1391 let mut app = Router::new().with_state(auth_state.clone());
1393
1394 if let Some(spec_path) = spec_path {
1396 match OpenApiSpec::from_file(&spec_path).await {
1397 Ok(openapi) => {
1398 info!("Loaded OpenAPI spec from {}", spec_path);
1399 let registry = if let Some(opts) = options {
1400 OpenApiRouteRegistry::new_with_options(openapi, opts)
1401 } else {
1402 OpenApiRouteRegistry::new_with_env(openapi)
1403 };
1404
1405 app = registry.build_router();
1406 }
1407 Err(e) => {
1408 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1409 }
1410 }
1411 }
1412
1413 app = app.route(
1415 "/health",
1416 axum::routing::get(|| async {
1417 use mockforge_core::server_utils::health::HealthStatus;
1418 {
1419 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1421 Ok(value) => Json(value),
1422 Err(e) => {
1423 tracing::error!("Failed to serialize health status: {}", e);
1425 Json(serde_json::json!({
1426 "status": "healthy",
1427 "service": "mockforge-http",
1428 "uptime_seconds": 0
1429 }))
1430 }
1431 }
1432 }
1433 }),
1434 )
1435 .merge(sse::sse_router())
1437 .merge(file_server::file_serving_router())
1439 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1441 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1443
1444 app
1445}
1446
1447pub async fn serve_router(
1449 port: u16,
1450 app: Router,
1451) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1452 serve_router_with_tls(port, app, None).await
1453}
1454
1455pub async fn serve_router_with_tls(
1457 port: u16,
1458 app: Router,
1459 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1460) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1461 serve_router_with_tls_notify(port, app, tls_config, None).await
1462}
1463
1464pub async fn serve_router_with_tls_notify(
1472 port: u16,
1473 app: Router,
1474 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1475 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1476) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1477 serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1478}
1479
1480pub async fn serve_router_with_tls_notify_chaos(
1489 port: u16,
1490 app: Router,
1491 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1492 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1493 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1494) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1495 serve_router_with_tls_notify_chaos_shutdown(
1496 port,
1497 app,
1498 tls_config,
1499 bound_port_tx,
1500 chaos_config,
1501 std::future::pending(),
1502 )
1503 .await
1504}
1505
1506pub async fn serve_router_with_tls_notify_chaos_shutdown(
1511 port: u16,
1512 app: Router,
1513 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1514 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1515 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1516 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
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, shutdown).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).with_graceful_shutdown(shutdown).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).with_graceful_shutdown(shutdown).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 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
1604) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1605 use axum_server::tls_rustls::RustlsConfig;
1606 use std::net::SocketAddr;
1607
1608 tls::init_crypto_provider();
1610
1611 info!("Loading TLS configuration for HTTPS server");
1612
1613 let server_config = tls::load_tls_server_config(tls_config)?;
1615
1616 let rustls_config = RustlsConfig::from_config(server_config);
1619
1620 info!("Starting HTTPS server on {}", addr);
1621
1622 let odata_app = tower::ServiceBuilder::new()
1626 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1627 .service(app);
1628 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1629 SocketAddr,
1630 >(odata_app);
1631 let counted = counting_listener::CountingMakeService::new(make_svc);
1634
1635 let handle = axum_server::Handle::new();
1637 let server = axum_server::bind_rustls(addr, rustls_config)
1638 .handle(handle.clone())
1639 .serve(counted);
1640 tokio::pin!(server);
1641 tokio::select! {
1642 result = &mut server => result,
1643 _ = shutdown => {
1644 handle.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
1645 server.await
1646 }
1647 }
1648 .map_err(|e| format!("HTTPS server error: {}", e).into())
1649}
1650
1651pub async fn start(
1653 port: u16,
1654 spec_path: Option<String>,
1655 options: Option<ValidationOptions>,
1656) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1657 start_with_latency(port, spec_path, options, None).await
1658}
1659
1660pub async fn start_with_auth_and_latency(
1662 port: u16,
1663 spec_path: Option<String>,
1664 options: Option<ValidationOptions>,
1665 auth_config: Option<mockforge_core::config::AuthConfig>,
1666 latency_profile: Option<LatencyProfile>,
1667) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1668 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1669 .await
1670}
1671
1672pub async fn start_with_auth_and_injectors(
1674 port: u16,
1675 spec_path: Option<String>,
1676 options: Option<ValidationOptions>,
1677 auth_config: Option<mockforge_core::config::AuthConfig>,
1678 _latency_profile: Option<LatencyProfile>,
1679 _failure_injector: Option<FailureInjector>,
1680) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1681 let app = build_router_with_auth(spec_path, options, auth_config).await;
1683 serve_router(port, app).await
1684}
1685
1686pub async fn start_with_latency(
1688 port: u16,
1689 spec_path: Option<String>,
1690 options: Option<ValidationOptions>,
1691 latency_profile: Option<LatencyProfile>,
1692) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1693 let latency_injector =
1694 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1695
1696 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1697 serve_router(port, app).await
1698}
1699
1700pub async fn build_router_with_chains(
1702 spec_path: Option<String>,
1703 options: Option<ValidationOptions>,
1704 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1705) -> Router {
1706 build_router_with_chains_and_multi_tenant(
1707 spec_path,
1708 options,
1709 circling_config,
1710 None,
1711 None,
1712 None,
1713 None,
1714 None, None, None, None, None, false,
1720 None, None, None, None, mockforge_core::SharedOverrides::load(Vec::new()).await,
1725 )
1726 .await
1727}
1728
1729async fn apply_route_chaos(
1737 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1738 method: &http::Method,
1739 uri: &http::Uri,
1740) -> Option<axum::response::Response> {
1741 use axum::http::StatusCode;
1742 use axum::response::IntoResponse;
1743
1744 if let Some(injector) = injector {
1745 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1747 let mut response = Json(serde_json::json!({
1749 "error": fault_response.error_message,
1750 "fault_type": fault_response.fault_type,
1751 }))
1752 .into_response();
1753 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1754 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1755 return Some(response);
1756 }
1757
1758 if let Err(e) = injector.inject_latency(method, uri).await {
1760 tracing::warn!("Failed to inject latency: {}", e);
1761 }
1762 }
1763
1764 None }
1766
1767#[allow(clippy::too_many_arguments)]
1769#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1771 spec_path: Option<String>,
1772 options: Option<ValidationOptions>,
1773 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1774 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1775 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1776 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1777 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1778 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1779 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1780 amqp_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1781 kafka_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1782 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1783 traffic_shaping_enabled: bool,
1784 health_manager: Option<Arc<HealthManager>>,
1785 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1786 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1787 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1788 overrides: mockforge_core::SharedOverrides,
1789) -> Router {
1790 use crate::latency_profiles::LatencyProfiles;
1791 use crate::op_middleware::Shared;
1792
1793 let template_expand =
1795 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1796 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1797 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1798 .unwrap_or(false)
1799 });
1800
1801 let _shared = Shared {
1802 profiles: LatencyProfiles::default(),
1803 failure_injector: None,
1804 traffic_shaper,
1805 traffic_shaping_enabled,
1806 };
1807
1808 let mut app = Router::new();
1810 let mut mock_health_paths: Vec<String> = route_configs
1812 .as_ref()
1813 .into_iter()
1814 .flatten()
1815 .filter(|route| health::HEALTH_PATHS.contains(&route.path.as_str()))
1816 .map(|route| route.path.clone())
1817 .collect();
1818 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1819
1820 if let Some(ref spec) = spec_path {
1822 match OpenApiSpec::from_file(&spec).await {
1823 Ok(openapi) => {
1824 info!("Loaded OpenAPI spec from {}", spec);
1825
1826 let persona = load_persona_from_config().await;
1828
1829 let mut registry = if let Some(opts) = options {
1830 tracing::debug!("Using custom validation options");
1831 if let Some(ref persona) = persona {
1832 tracing::info!("Using persona '{}' for route generation", persona.name);
1833 }
1834 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1835 } else {
1836 tracing::debug!("Using environment-based options");
1837 if let Some(ref persona) = persona {
1838 tracing::info!("Using persona '{}' for route generation", persona.name);
1839 }
1840 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1841 };
1842
1843 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1845 .unwrap_or_else(|_| "/app/fixtures".to_string());
1846 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1847 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1848 .unwrap_or(true); if custom_fixtures_enabled {
1851 use mockforge_openapi::CustomFixtureLoader;
1852 use std::path::PathBuf;
1853 use std::sync::Arc;
1854
1855 let fixtures_path = PathBuf::from(&fixtures_dir);
1856 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1857
1858 if let Err(e) = custom_loader.load_fixtures().await {
1859 tracing::warn!("Failed to load custom fixtures: {}", e);
1860 } else {
1861 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1862 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1863 }
1864 }
1865 registry = registry.with_overrides(Arc::new(overrides.clone()));
1866
1867 mock_health_paths.extend(
1868 registry
1869 .routes()
1870 .iter()
1871 .filter(|route| {
1872 route.method.eq_ignore_ascii_case("GET")
1873 && health::HEALTH_PATHS.contains(&route.path.as_str())
1874 })
1875 .map(|route| route.path.clone()),
1876 );
1877 captured_routes = registry
1879 .routes()
1880 .iter()
1881 .map(|r| RouteInfo {
1882 method: r.method.clone(),
1883 path: r.path.clone(),
1884 operation_id: r.operation.operation_id.clone(),
1885 summary: r.operation.summary.clone(),
1886 description: r.operation.description.clone(),
1887 parameters: r.parameters.clone(),
1888 })
1889 .collect();
1890
1891 {
1894 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1895 captured_routes
1896 .iter()
1897 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1898 method: r.method.clone(),
1899 path: r.path.clone(),
1900 operation_id: r.operation_id.clone(),
1901 summary: r.summary.clone(),
1902 description: r.description.clone(),
1903 parameters: r.parameters.clone(),
1904 })
1905 .collect();
1906 mockforge_core::request_logger::set_global_routes(global_routes);
1907 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1908 }
1909
1910 let spec_router = if let Some(ref mockai_instance) = mockai {
1912 tracing::debug!("Building router with MockAI support");
1913 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1914 } else {
1915 registry.build_router()
1916 };
1917 let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
1925 .ok()
1926 .and_then(|v| v.parse::<usize>().ok())
1927 .unwrap_or(50);
1928 let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
1929 let spec_router =
1930 spec_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
1931 tracing::info!(
1932 body_limit_mb = body_limit_mb,
1933 "Merging OpenAPI router with main router"
1934 );
1935 app = app.merge(spec_router);
1936 }
1937 Err(e) => {
1938 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1939 }
1940 }
1941 }
1942
1943 let route_chaos_injector: Option<
1947 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1948 > = if let Some(ref route_configs) = route_configs {
1949 if !route_configs.is_empty() {
1950 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1953 route_configs.to_vec();
1954 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1955 Ok(injector) => {
1956 info!(
1957 "Initialized advanced routing features for {} route(s)",
1958 route_configs.len()
1959 );
1960 Some(std::sync::Arc::new(injector)
1963 as std::sync::Arc<
1964 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1965 >)
1966 }
1967 Err(e) => {
1968 warn!(
1969 "Failed to initialize advanced routing features: {}. Using basic routing.",
1970 e
1971 );
1972 None
1973 }
1974 }
1975 } else {
1976 None
1977 }
1978 } else {
1979 None
1980 };
1981
1982 if let Some(route_configs) = route_configs {
1983 use axum::http::StatusCode;
1984 use axum::response::IntoResponse;
1985
1986 if !route_configs.is_empty() {
1987 info!("Registering {} custom route(s) from config", route_configs.len());
1988 }
1989
1990 let injector = route_chaos_injector.clone();
1991 for route_config in route_configs {
1992 let status = route_config.response.status;
1993 let body = route_config.response.body.clone();
1994 let headers = route_config.response.headers.clone();
1995 let path = route_config.path.clone();
1996 let method = route_config.method.clone();
1997
1998 let expected_method = method.to_uppercase();
2003 let injector_clone = injector.clone();
2007 app = app.route(
2008 &path,
2009 #[allow(clippy::non_send_fields_in_send_ty)]
2010 axum::routing::any(move |req: Request<Body>| {
2011 let body = body.clone();
2012 let headers = headers.clone();
2013 let expand = template_expand;
2014 let expected = expected_method.clone();
2015 let status_code = status;
2016 let injector_for_chaos = injector_clone.clone();
2018
2019 async move {
2020 if req.method().as_str() != expected.as_str() {
2022 return axum::response::Response::builder()
2024 .status(StatusCode::METHOD_NOT_ALLOWED)
2025 .header("Allow", &expected)
2026 .body(Body::empty())
2027 .unwrap()
2028 .into_response();
2029 }
2030
2031 if let Some(fault_response) = apply_route_chaos(
2035 injector_for_chaos.as_deref(),
2036 req.method(),
2037 req.uri(),
2038 )
2039 .await
2040 {
2041 return fault_response;
2042 }
2043
2044 let mut body_value = body.unwrap_or(serde_json::json!({}));
2046
2047 if expand {
2051 use mockforge_template_expansion::RequestContext;
2052 use serde_json::Value;
2053 use std::collections::HashMap;
2054
2055 let method = req.method().to_string();
2057 let path = req.uri().path().to_string();
2058
2059 let query_params: HashMap<String, Value> = req
2061 .uri()
2062 .query()
2063 .map(|q| {
2064 url::form_urlencoded::parse(q.as_bytes())
2065 .into_owned()
2066 .map(|(k, v)| (k, Value::String(v)))
2067 .collect()
2068 })
2069 .unwrap_or_default();
2070
2071 let headers: HashMap<String, Value> = req
2073 .headers()
2074 .iter()
2075 .map(|(k, v)| {
2076 (
2077 k.to_string(),
2078 Value::String(v.to_str().unwrap_or_default().to_string()),
2079 )
2080 })
2081 .collect();
2082
2083 let context = RequestContext {
2087 method,
2088 path,
2089 query_params,
2090 headers,
2091 body: None, path_params: HashMap::new(),
2093 multipart_fields: HashMap::new(),
2094 multipart_files: HashMap::new(),
2095 };
2096
2097 let body_value_clone = body_value.clone();
2101 let context_clone = context.clone();
2102 body_value = match tokio::task::spawn_blocking(move || {
2103 mockforge_template_expansion::expand_templates_in_json(
2104 body_value_clone,
2105 &context_clone,
2106 )
2107 })
2108 .await
2109 {
2110 Ok(result) => result,
2111 Err(_) => body_value, };
2113 }
2114
2115 let mut response = Json(body_value).into_response();
2116
2117 *response.status_mut() =
2119 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2120
2121 for (key, value) in headers {
2123 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2124 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2125 response.headers_mut().insert(header_name, header_value);
2126 }
2127 }
2128 }
2129
2130 response
2131 }
2132 }),
2133 );
2134
2135 debug!("Registered route: {} {}", method, path);
2136 }
2137 }
2138
2139 if let Some(health) = health_manager {
2141 let excluded_paths: Vec<&str> = mock_health_paths.iter().map(String::as_str).collect();
2143 app = app.merge(health::health_router_with_exclusions(health, &excluded_paths));
2144 info!("Default health probes enabled on paths not supplied by mock routes");
2145 } else if !mock_health_paths.iter().any(|path| path == "/health") {
2146 app = app.route(
2148 "/health",
2149 axum::routing::get(|| async {
2150 use mockforge_core::server_utils::health::HealthStatus;
2151 {
2152 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2154 Ok(value) => Json(value),
2155 Err(e) => {
2156 tracing::error!("Failed to serialize health status: {}", e);
2158 Json(serde_json::json!({
2159 "status": "healthy",
2160 "service": "mockforge-http",
2161 "uptime_seconds": 0
2162 }))
2163 }
2164 }
2165 }
2166 }),
2167 );
2168 }
2169
2170 app = app.merge(sse::sse_router());
2171 app = app.merge(file_server::file_serving_router());
2173
2174 let mgmt_spec = if let Some(ref sp) = spec_path {
2177 match OpenApiSpec::from_file(sp).await {
2178 Ok(s) => Some(Arc::new(s)),
2179 Err(e) => {
2180 debug!("Failed to load OpenAPI spec for management API: {}", e);
2181 None
2182 }
2183 }
2184 } else {
2185 None
2186 };
2187 let spec_path_clone = spec_path.clone();
2188 let mgmt_port = std::env::var("PORT")
2189 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2190 .ok()
2191 .and_then(|p| p.parse().ok())
2192 .unwrap_or(3000);
2193 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2194
2195 use std::sync::Arc;
2197 let ws_state = WsManagementState::new();
2198 let ws_broadcast = Arc::new(ws_state.tx.clone());
2199 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2200
2201 let management_state = if let Some(proxy_cfg) = proxy_config {
2203 use tokio::sync::RwLock;
2204 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2205 management_state.with_proxy_config(proxy_config_arc)
2206 } else {
2207 management_state
2208 };
2209
2210 #[cfg(feature = "smtp")]
2211 let management_state = {
2212 if let Some(smtp_reg) = smtp_registry {
2213 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2214 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2215 Err(e) => {
2216 error!(
2217 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2218 e.type_id()
2219 );
2220 management_state
2221 }
2222 }
2223 } else {
2224 management_state
2225 }
2226 };
2227 #[cfg(not(feature = "smtp"))]
2228 let management_state = {
2229 let _ = smtp_registry;
2230 management_state
2231 };
2232 #[cfg(feature = "mqtt")]
2233 let management_state = {
2234 if let Some(sessions) = mqtt_broker {
2237 match sessions.downcast::<mockforge_mqtt::SessionManager>() {
2238 Ok(sessions) => management_state.with_mqtt_sessions(sessions),
2239 Err(e) => {
2240 error!(
2241 "Invalid MQTT session manager passed to HTTP management state: {:?}",
2242 e.type_id()
2243 );
2244 management_state
2245 }
2246 }
2247 } else {
2248 management_state
2249 }
2250 };
2251 #[cfg(not(feature = "mqtt"))]
2252 let management_state = {
2253 let _ = mqtt_broker;
2254 management_state
2255 };
2256 #[cfg(feature = "amqp")]
2257 let management_state = {
2258 if let Some(broker) = amqp_broker {
2259 match broker.downcast::<mockforge_amqp::AmqpBroker>() {
2260 Ok(broker) => management_state.with_amqp_broker(broker),
2261 Err(e) => {
2262 error!(
2263 "Invalid AMQP broker passed to HTTP management state: {:?}",
2264 e.type_id()
2265 );
2266 management_state
2267 }
2268 }
2269 } else {
2270 management_state
2271 }
2272 };
2273 #[cfg(not(feature = "amqp"))]
2274 let management_state = {
2275 let _ = amqp_broker;
2276 management_state
2277 };
2278 #[cfg(feature = "kafka")]
2279 let management_state = {
2280 if let Some(broker) = kafka_broker {
2281 match broker.downcast::<mockforge_kafka::KafkaMockBroker>() {
2282 Ok(broker) => management_state.with_kafka_broker(broker),
2283 Err(e) => {
2284 error!(
2285 "Invalid Kafka broker passed to HTTP management state: {:?}",
2286 e.type_id()
2287 );
2288 management_state
2289 }
2290 }
2291 } else {
2292 management_state
2293 }
2294 };
2295 #[cfg(not(feature = "kafka"))]
2296 let management_state = {
2297 let _ = kafka_broker;
2298 management_state
2299 };
2300 let management_state_for_fallback = management_state.clone();
2301 app = app.nest("/__mockforge/api", management_router(management_state));
2302 app = app.fallback_service(
2304 axum::routing::any(management::dynamic_mock_fallback)
2305 .with_state(management_state_for_fallback),
2306 );
2307
2308 app = app.merge(verification_router());
2310
2311 {
2316 use crate::chain_handlers::{chains_router, create_chain_state};
2317 let chain_config = _circling_config.clone().unwrap_or_default();
2318 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2319 chain_config.clone(),
2320 ));
2321 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2322 chain_registry.clone(),
2323 chain_config,
2324 ));
2325 app = app.nest(
2326 "/__mockforge/chains",
2327 chains_router(create_chain_state(chain_registry, chain_engine)),
2328 );
2329 }
2330
2331 {
2336 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2337 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2338 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2339 }
2340
2341 {
2348 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2349 let fx_state = FixturesApiState::from_env();
2350 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2351 }
2352
2353 {
2360 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2361 let api_state = MockAiApiState::new(mockai.clone());
2362 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2363 }
2364
2365 app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2372
2373 {
2378 use crate::route_chaos_runtime::{
2379 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2380 };
2381 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2382 let middleware_state = runtime_state.clone();
2383 app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2384 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2385 }
2386
2387 {
2393 use crate::network_profile_runtime::{
2394 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2395 };
2396 let runtime_state = NetworkProfileRuntimeState::new(
2397 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2398 );
2399 let middleware_state = runtime_state.clone();
2400 app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2401 app = app
2402 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2403 }
2404
2405 use crate::auth::oidc::oidc_router;
2407 app = app.merge(oidc_router());
2408
2409 {
2411 use mockforge_core::security::get_global_access_review_service;
2412 if let Some(service) = get_global_access_review_service().await {
2413 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2414 let review_state = AccessReviewState { service };
2415 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2416 debug!("Access review API mounted at /api/v1/security/access-reviews");
2417 }
2418 }
2419
2420 {
2422 use mockforge_core::security::get_global_privileged_access_manager;
2423 if let Some(manager) = get_global_privileged_access_manager().await {
2424 use crate::handlers::privileged_access::{
2425 privileged_access_router, PrivilegedAccessState,
2426 };
2427 let privileged_state = PrivilegedAccessState { manager };
2428 app = app.nest(
2429 "/api/v1/security/privileged-access",
2430 privileged_access_router(privileged_state),
2431 );
2432 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2433 }
2434 }
2435
2436 {
2438 use mockforge_core::security::get_global_change_management_engine;
2439 if let Some(engine) = get_global_change_management_engine().await {
2440 use crate::handlers::change_management::{
2441 change_management_router, ChangeManagementState,
2442 };
2443 let change_state = ChangeManagementState { engine };
2444 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2445 debug!("Change management API mounted at /api/v1/change-management");
2446 }
2447 }
2448
2449 {
2451 use mockforge_core::security::get_global_risk_assessment_engine;
2452 if let Some(engine) = get_global_risk_assessment_engine().await {
2453 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2454 let risk_state = RiskAssessmentState { engine };
2455 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2456 debug!("Risk assessment API mounted at /api/v1/security/risks");
2457 }
2458 }
2459
2460 {
2462 use crate::auth::token_lifecycle::TokenLifecycleManager;
2463 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2464 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2465 let lifecycle_state = TokenLifecycleState {
2466 manager: lifecycle_manager,
2467 };
2468 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2469 debug!("Token lifecycle API mounted at /api/v1/auth");
2470 }
2471
2472 {
2474 use crate::auth::oidc::load_oidc_state;
2475 use crate::auth::token_lifecycle::TokenLifecycleManager;
2476 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2477 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2479 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2480 let oauth2_state = OAuth2ServerState {
2481 oidc_state,
2482 lifecycle_manager,
2483 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2484 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2485 };
2486 app = app.merge(oauth2_server_router(oauth2_state));
2487 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2488 }
2489
2490 {
2492 use crate::auth::oidc::load_oidc_state;
2493 use crate::auth::risk_engine::RiskEngine;
2494 use crate::auth::token_lifecycle::TokenLifecycleManager;
2495 use crate::handlers::consent::{consent_router, ConsentState};
2496 use crate::handlers::oauth2_server::OAuth2ServerState;
2497 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2499 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2500 let oauth2_state = OAuth2ServerState {
2501 oidc_state: oidc_state.clone(),
2502 lifecycle_manager: lifecycle_manager.clone(),
2503 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2504 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2505 };
2506 let risk_engine = Arc::new(RiskEngine::default());
2507 let consent_state = ConsentState {
2508 oauth2_state,
2509 risk_engine,
2510 };
2511 app = app.merge(consent_router(consent_state));
2512 debug!("Consent screen endpoints mounted at /consent");
2513 }
2514
2515 {
2517 use crate::auth::risk_engine::RiskEngine;
2518 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2519 let risk_engine = Arc::new(RiskEngine::default());
2520 let risk_state = RiskSimulationState { risk_engine };
2521 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2522 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2523 }
2524
2525 #[cfg(feature = "database")]
2527 let database = {
2528 use crate::database::Database;
2529 let database_url = std::env::var("DATABASE_URL").ok();
2530 match Database::connect_optional(database_url.as_deref()).await {
2531 Ok(db) => {
2532 if db.is_connected() {
2533 if let Err(e) = db.migrate_if_connected().await {
2535 warn!("Failed to run database migrations: {}", e);
2536 } else {
2537 info!("Database connected and migrations applied");
2538 }
2539 }
2540 Some(db)
2541 }
2542 Err(e) => {
2543 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2544 None
2545 }
2546 }
2547 };
2548
2549 let (drift_engine, incident_manager, drift_config) = {
2552 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2553 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2554 use std::sync::Arc;
2555
2556 let drift_config = DriftBudgetConfig::default();
2558 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2559
2560 let incident_store = Arc::new(IncidentStore::default());
2562 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2563
2564 (drift_engine, incident_manager, drift_config)
2565 };
2566
2567 {
2568 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2569 use crate::middleware::drift_tracking::DriftTrackingState;
2570 use mockforge_contracts::consumer_contracts::{
2571 ConsumerBreakingChangeDetector, UsageRecorder,
2572 };
2573 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2574 use std::sync::Arc;
2575
2576 let usage_recorder = Arc::new(UsageRecorder::default());
2578 let consumer_detector =
2579 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2580
2581 let diff_analyzer = if drift_config.enabled {
2583 match ContractDiffAnalyzer::new(
2584 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2585 ) {
2586 Ok(analyzer) => Some(Arc::new(analyzer)),
2587 Err(e) => {
2588 warn!("Failed to create contract diff analyzer: {}", e);
2589 None
2590 }
2591 }
2592 } else {
2593 None
2594 };
2595
2596 let spec = if let Some(ref spec_path) = spec_path {
2599 match OpenApiSpec::from_file(spec_path).await {
2600 Ok(s) => Some(Arc::new(s)),
2601 Err(e) => {
2602 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2603 None
2604 }
2605 }
2606 } else {
2607 None
2608 };
2609
2610 let drift_tracking_state = DriftTrackingState {
2612 diff_analyzer,
2613 spec,
2614 drift_engine: drift_engine.clone(),
2615 incident_manager: incident_manager.clone(),
2616 usage_recorder,
2617 consumer_detector,
2618 enabled: drift_config.enabled,
2619 };
2620
2621 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2623
2624 let drift_tracking_state_clone = drift_tracking_state.clone();
2627 app = app.layer(axum::middleware::from_fn(
2628 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2629 let state = drift_tracking_state_clone.clone();
2630 async move {
2631 if req.extensions().get::<DriftTrackingState>().is_none() {
2633 req.extensions_mut().insert(state);
2634 }
2635 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2637 .await
2638 }
2639 },
2640 ));
2641
2642 let drift_state = DriftBudgetState {
2643 engine: drift_engine.clone(),
2644 incident_manager: incident_manager.clone(),
2645 gitops_handler: None, };
2647
2648 app = app.merge(drift_budget_router(drift_state));
2649 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2650 }
2651
2652 #[cfg(feature = "pipelines")]
2654 {
2655 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2656
2657 let pipeline_state = PipelineState::new();
2658 app = app.merge(pipeline_router(pipeline_state));
2659 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2660 }
2661
2662 {
2664 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2665 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2666 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2667 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2668 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2669 use mockforge_intelligence::handlers::forecasting::{forecasting_router, ForecastingState};
2670 use mockforge_intelligence::handlers::semantic_drift::{
2671 semantic_drift_router, SemanticDriftState,
2672 };
2673 use mockforge_intelligence::handlers::threat_modeling::{
2674 threat_modeling_router, ThreatModelingState,
2675 };
2676 use std::sync::Arc;
2677
2678 let forecasting_config = ForecastingConfig::default();
2680 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2681 let forecasting_state = ForecastingState::new(forecaster);
2682 #[cfg(feature = "database")]
2683 let forecasting_state = forecasting_state.with_database(database.clone());
2684
2685 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2687 let semantic_state = SemanticDriftState::new(semantic_manager);
2688 #[cfg(feature = "database")]
2689 let semantic_state = semantic_state.with_database(database.clone());
2690
2691 let threat_config = ThreatModelingConfig::default();
2693 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2694 Ok(analyzer) => Arc::new(analyzer),
2695 Err(e) => {
2696 warn!("Failed to create threat analyzer: {}. Using default.", e);
2697 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2698 |_| {
2699 ThreatAnalyzer::new(ThreatModelingConfig {
2701 enabled: false,
2702 ..Default::default()
2703 })
2704 .expect("Failed to create fallback threat analyzer")
2705 },
2706 ))
2707 }
2708 };
2709 let mut webhook_configs = Vec::new();
2711 let config_paths = [
2712 "config.yaml",
2713 "mockforge.yaml",
2714 "tools/mockforge/config.yaml",
2715 "../tools/mockforge/config.yaml",
2716 ];
2717
2718 for path in &config_paths {
2719 if let Ok(config) = mockforge_core::config::load_config(path).await {
2720 if !config.incidents.webhooks.is_empty() {
2721 webhook_configs = config.incidents.webhooks.clone();
2722 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2723 break;
2724 }
2725 }
2726 }
2727
2728 if webhook_configs.is_empty() {
2729 debug!("No webhook configs found in config files, using empty list");
2730 }
2731
2732 let threat_state = ThreatModelingState::new(threat_analyzer, webhook_configs);
2733 #[cfg(feature = "database")]
2734 let threat_state = threat_state.with_database(database.clone());
2735
2736 let contract_health_state = ContractHealthState {
2738 incident_manager: incident_manager.clone(),
2739 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2740 #[cfg(feature = "database")]
2741 database: database.clone(),
2742 };
2743
2744 app = app.merge(forecasting_router(forecasting_state));
2746 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2747
2748 app = app.merge(semantic_drift_router(semantic_state));
2749 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2750
2751 app = app.merge(threat_modeling_router(threat_state));
2752 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2753
2754 app = app.merge(contract_health_router(contract_health_state));
2755 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2756 }
2757
2758 {
2760 use crate::handlers::protocol_contracts::{
2761 protocol_contracts_router, ProtocolContractState,
2762 };
2763 use mockforge_core::contract_drift::{
2764 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2765 };
2766 use std::sync::Arc;
2767 use tokio::sync::RwLock;
2768
2769 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2771
2772 let mut fitness_registry = FitnessFunctionRegistry::new();
2774
2775 let config_paths = [
2777 "config.yaml",
2778 "mockforge.yaml",
2779 "tools/mockforge/config.yaml",
2780 "../tools/mockforge/config.yaml",
2781 ];
2782
2783 let mut config_loaded = false;
2784 for path in &config_paths {
2785 if let Ok(config) = mockforge_core::config::load_config(path).await {
2786 if !config.contracts.fitness_rules.is_empty() {
2787 if let Err(e) =
2788 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2789 {
2790 warn!("Failed to load fitness rules from config {}: {}", path, e);
2791 } else {
2792 info!(
2793 "Loaded {} fitness rules from config: {}",
2794 config.contracts.fitness_rules.len(),
2795 path
2796 );
2797 config_loaded = true;
2798 break;
2799 }
2800 }
2801 }
2802 }
2803
2804 if !config_loaded {
2805 debug!("No fitness rules found in config files, using empty registry");
2806 }
2807
2808 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2809
2810 let consumer_mapping_registry =
2814 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2815 let consumer_analyzer =
2816 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2817
2818 let protocol_state = ProtocolContractState {
2819 registry: contract_registry,
2820 drift_engine: Some(drift_engine.clone()),
2821 incident_manager: Some(incident_manager.clone()),
2822 fitness_registry: Some(fitness_registry),
2823 consumer_analyzer: Some(consumer_analyzer),
2824 };
2825
2826 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2827 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2828 }
2829
2830 #[cfg(feature = "behavioral-cloning")]
2832 {
2833 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2834 use std::path::PathBuf;
2835
2836 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2838 .ok()
2839 .map(PathBuf::from)
2840 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2841
2842 let bc_middleware_state = if let Some(path) = db_path {
2843 BehavioralCloningMiddlewareState::with_database_path(path)
2844 } else {
2845 BehavioralCloningMiddlewareState::new()
2846 };
2847
2848 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2850 .ok()
2851 .and_then(|v| v.parse::<bool>().ok())
2852 .unwrap_or(false);
2853
2854 if enabled {
2855 let bc_state_clone = bc_middleware_state.clone();
2856 app = app.layer(axum::middleware::from_fn(
2857 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2858 let state = bc_state_clone.clone();
2859 async move {
2860 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2862 req.extensions_mut().insert(state);
2863 }
2864 middleware::behavioral_cloning::behavioral_cloning_middleware(req, next)
2866 .await
2867 }
2868 },
2869 ));
2870 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2871 }
2872 }
2873
2874 {
2876 use crate::handlers::consumer_contracts::{
2877 consumer_contracts_router, ConsumerContractsState,
2878 };
2879 use mockforge_contracts::consumer_contracts::{
2880 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2881 };
2882 use std::sync::Arc;
2883
2884 let registry = Arc::new(ConsumerRegistry::default());
2886
2887 let usage_recorder = Arc::new(UsageRecorder::default());
2889
2890 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2892
2893 let consumer_state = ConsumerContractsState {
2894 registry,
2895 usage_recorder,
2896 detector,
2897 violations: Arc::new(RwLock::new(HashMap::new())),
2898 };
2899
2900 app = app.merge(consumer_contracts_router(consumer_state));
2901 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2902 }
2903
2904 #[cfg(feature = "behavioral-cloning")]
2906 {
2907 use crate::handlers::behavioral_cloning::{
2908 behavioral_cloning_router, BehavioralCloningState,
2909 };
2910 use std::path::PathBuf;
2911
2912 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2914 .ok()
2915 .map(PathBuf::from)
2916 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2917
2918 let bc_state = if let Some(path) = db_path {
2919 BehavioralCloningState::with_database_path(path)
2920 } else {
2921 BehavioralCloningState::new()
2922 };
2923
2924 app = app.merge(behavioral_cloning_router(bc_state));
2925 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2926 }
2927
2928 {
2930 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2931 use mockforge_intelligence::consistency::ConsistencyEngine;
2932 use mockforge_intelligence::handlers::consistency::{consistency_router, ConsistencyState};
2933 use std::sync::Arc;
2934
2935 let consistency_engine = Arc::new(ConsistencyEngine::new());
2937
2938 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2940 consistency_engine.register_adapter(http_adapter.clone()).await;
2941
2942 let consistency_state = ConsistencyState {
2944 engine: consistency_engine.clone(),
2945 };
2946
2947 use mockforge_intelligence::handlers::xray::XRayState;
2949 let xray_state = Arc::new(XRayState::new(consistency_engine.clone()));
2950
2951 let consistency_middleware_state = ConsistencyMiddlewareState {
2953 engine: consistency_engine.clone(),
2954 adapter: http_adapter,
2955 xray_state: Some(xray_state.clone()),
2956 };
2957
2958 if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2966 tracing::info!(
2967 upstream = %reality_cfg.upstream_base,
2968 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2969 );
2970 app = app.layer(axum::middleware::from_fn(
2971 move |req: axum::extract::Request, next: axum::middleware::Next| {
2972 let cfg = reality_cfg.clone();
2973 async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2974 },
2975 ));
2976 }
2977
2978 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2980 app = app.layer(axum::middleware::from_fn(
2981 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2982 let state = consistency_middleware_state_clone.clone();
2983 async move {
2984 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2986 req.extensions_mut().insert(state);
2987 }
2988 consistency::middleware::consistency_middleware(req, next).await
2990 }
2991 },
2992 ));
2993
2994 app = app.merge(consistency_router(consistency_state));
2996 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2997
2998 #[cfg(feature = "scenario-engine")]
3006 {
3007 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
3008 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
3009 Ok(s) => s,
3010 Err(e) => {
3011 tracing::warn!(
3012 error = %e,
3013 "Failed to init scenario storage; runtime scenarios API will list empty"
3014 );
3015 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
3018 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
3019 .expect("temp scenario storage")
3020 }
3021 };
3022 if let Err(e) = scenario_storage.load().await {
3023 tracing::warn!(
3024 error = %e,
3025 "Failed to load installed scenarios; API will list empty until scenarios are installed"
3026 );
3027 }
3028 let scenarios_state =
3029 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
3030 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
3031 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
3032 }
3033
3034 {
3036 use mockforge_intelligence::handlers::fidelity::{fidelity_router, FidelityState};
3037 let fidelity_state = FidelityState::new();
3038 app = app.merge(fidelity_router(fidelity_state));
3039 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
3040 }
3041
3042 {
3044 use mockforge_intelligence::handlers::scenario_studio::{
3045 scenario_studio_router, ScenarioStudioState,
3046 };
3047 let scenario_studio_state = ScenarioStudioState::new();
3048 app = app.merge(scenario_studio_router(scenario_studio_state));
3049 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
3050 }
3051
3052 {
3054 use crate::handlers::performance::{performance_router, PerformanceState};
3055 let performance_state = PerformanceState::new();
3056 app = app.nest("/api/performance", performance_router(performance_state));
3057 debug!("Performance mode endpoints mounted at /api/performance");
3058 }
3059
3060 {
3062 use crate::handlers::world_state::{world_state_router, WorldStateState};
3063 use mockforge_world_state::WorldStateEngine;
3064 use std::sync::Arc;
3065 use tokio::sync::RwLock;
3066
3067 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
3068 let world_state_state = WorldStateState {
3069 engine: world_state_engine,
3070 };
3071 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
3072 debug!("World state endpoints mounted at /api/world-state");
3073 }
3074
3075 {
3077 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
3078 use mockforge_core::snapshots::SnapshotManager;
3079 use std::path::PathBuf;
3080
3081 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
3082 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
3083
3084 let snapshot_state = SnapshotState {
3085 manager: snapshot_manager,
3086 consistency_engine: Some(consistency_engine.clone()),
3087 workspace_persistence: None, vbr_engine: None, recorder: None, };
3091
3092 app = app.merge(snapshot_router(snapshot_state));
3093 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
3094
3095 {
3097 use mockforge_intelligence::handlers::xray::xray_router;
3098 app = app.merge(xray_router((*xray_state).clone()));
3099 debug!("X-Ray API endpoints mounted at /api/v1/xray");
3100 }
3101 }
3102
3103 {
3105 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
3106 use crate::middleware::ab_testing::ab_testing_middleware;
3107
3108 let ab_testing_state = ABTestingState::new();
3109
3110 let ab_testing_state_clone = ab_testing_state.clone();
3112 app = app.layer(axum::middleware::from_fn(
3113 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
3114 let state = ab_testing_state_clone.clone();
3115 async move {
3116 if req.extensions().get::<ABTestingState>().is_none() {
3118 req.extensions_mut().insert(state);
3119 }
3120 ab_testing_middleware(req, next).await
3122 }
3123 },
3124 ));
3125
3126 app = app.merge(ab_testing_router(ab_testing_state));
3128 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
3129 }
3130 }
3131
3132 {
3134 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
3135 use mockforge_intelligence::pr_generation::{PRGenerator, PRProvider};
3136 use std::sync::Arc;
3137
3138 let pr_config = mockforge_intelligence::pr_generation::PRGenerationConfig::from_env();
3140
3141 let generator = pr_config.token.clone().filter(|_| pr_config.enabled).map(|token| {
3144 let generator = match pr_config.provider {
3145 PRProvider::GitHub => PRGenerator::new_github(
3146 pr_config.owner.clone(),
3147 pr_config.repo.clone(),
3148 token,
3149 pr_config.base_branch.clone(),
3150 ),
3151 PRProvider::GitLab => PRGenerator::new_gitlab(
3152 pr_config.owner.clone(),
3153 pr_config.repo.clone(),
3154 token,
3155 pr_config.base_branch.clone(),
3156 ),
3157 };
3158 Arc::new(generator)
3159 });
3160
3161 let pr_state = PRGenerationState {
3162 generator: generator.clone(),
3163 };
3164
3165 app = app.merge(pr_generation_router(pr_state));
3166 if generator.is_some() {
3167 debug!(
3168 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3169 pr_config.provider
3170 );
3171 } else {
3172 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3173 }
3174 }
3175
3176 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3178
3179 if let Some(mt_config) = multi_tenant_config {
3181 if mt_config.enabled {
3182 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3183 use std::sync::Arc;
3184
3185 info!(
3186 "Multi-tenant mode enabled with {} routing strategy",
3187 match mt_config.routing_strategy {
3188 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3189 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3190 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3191 }
3192 );
3193
3194 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3196
3197 let default_workspace =
3199 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3200 if let Err(e) =
3201 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3202 {
3203 warn!("Failed to register default workspace: {}", e);
3204 } else {
3205 info!("Registered default workspace: '{}'", mt_config.default_workspace);
3206 }
3207
3208 let registry = Arc::new(registry);
3210
3211 let _workspace_router = WorkspaceRouter::new(registry);
3213 info!("Workspace routing middleware initialized for HTTP server");
3214 }
3215 }
3216
3217 let mut final_cors_config = cors_config;
3219 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3220 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3222 let mut rate_limit_config = middleware::RateLimitConfig {
3223 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3224 .ok()
3225 .and_then(|v| v.parse().ok())
3226 .unwrap_or(1000),
3227 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3228 .ok()
3229 .and_then(|v| v.parse().ok())
3230 .unwrap_or(2000),
3231 per_ip: true,
3232 per_endpoint: false,
3233 };
3234
3235 if let Some(deploy_config) = &deceptive_deploy_config {
3236 if deploy_config.enabled {
3237 info!("Deceptive deploy mode enabled - applying production-like configuration");
3238
3239 if let Some(prod_cors) = &deploy_config.cors {
3241 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3242 enabled: true,
3243 allowed_origins: prod_cors.allowed_origins.clone(),
3244 allowed_methods: prod_cors.allowed_methods.clone(),
3245 allowed_headers: prod_cors.allowed_headers.clone(),
3246 allow_credentials: prod_cors.allow_credentials,
3247 });
3248 info!("Applied production-like CORS configuration");
3249 }
3250
3251 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3253 rate_limit_config = middleware::RateLimitConfig {
3254 requests_per_minute: prod_rate_limit.requests_per_minute,
3255 burst: prod_rate_limit.burst,
3256 per_ip: prod_rate_limit.per_ip,
3257 per_endpoint: false,
3258 };
3259 info!(
3260 "Applied production-like rate limiting: {} req/min, burst: {}",
3261 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3262 );
3263 }
3264
3265 if !deploy_config.headers.is_empty() {
3267 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3268 production_headers = Some(std::sync::Arc::new(headers_map));
3269 info!("Configured {} production headers", deploy_config.headers.len());
3270 }
3271
3272 if let Some(prod_oauth) = &deploy_config.oauth {
3274 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3275 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3276 oauth2: Some(oauth2_config),
3277 ..Default::default()
3278 });
3279 info!("Applied production-like OAuth configuration for deceptive deploy");
3280 }
3281 }
3282 }
3283
3284 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3286 let rate_limiter =
3287 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3288
3289 let mut state = HttpServerState::new();
3290 if rate_limit_disabled {
3291 info!(
3292 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3293 );
3294 } else {
3295 state = state.with_rate_limiter(rate_limiter.clone());
3296 }
3297
3298 if let Some(headers) = production_headers.clone() {
3300 state = state.with_production_headers(headers);
3301 }
3302
3303 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3305
3306 if state.production_headers.is_some() {
3308 app =
3309 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3310 }
3311
3312 if middleware::is_keepalive_hint_enabled() {
3317 info!(
3318 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3319 );
3320 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3321 }
3322
3323 if middleware::is_conn_log_enabled() {
3328 info!(
3329 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
3330 );
3331 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
3332 }
3333
3334 if let Some(auth_config) = deceptive_deploy_auth_config {
3336 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3337 use std::collections::HashMap;
3338 use std::sync::Arc;
3339 use tokio::sync::RwLock;
3340
3341 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3343 match create_oauth2_client(oauth2_config) {
3344 Ok(client) => Some(client),
3345 Err(e) => {
3346 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3347 None
3348 }
3349 }
3350 } else {
3351 None
3352 };
3353
3354 let auth_state = AuthState {
3356 config: auth_config,
3357 spec: None, oauth2_client,
3359 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3360 };
3361
3362 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3364 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3365 }
3366
3367 #[cfg(feature = "runtime-daemon")]
3369 {
3370 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3371 use std::sync::Arc;
3372
3373 let daemon_config = RuntimeDaemonConfig::from_env();
3375
3376 if daemon_config.enabled {
3377 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3378
3379 let management_api_url =
3381 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3382 let port =
3383 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3384 format!("http://localhost:{}", port)
3385 });
3386
3387 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3389
3390 let detector = NotFoundDetector::new(daemon_config.clone());
3392 detector.set_generator(generator).await;
3393
3394 let detector_clone = detector.clone();
3396 app = app.layer(axum::middleware::from_fn(
3397 move |req: axum::extract::Request, next: axum::middleware::Next| {
3398 let detector = detector_clone.clone();
3399 async move { detector.detect_and_auto_create(req, next).await }
3400 },
3401 ));
3402
3403 debug!("Runtime daemon 404 detection middleware added");
3404 }
3405 }
3406
3407 {
3409 let routes_state = HttpServerState::with_routes(captured_routes);
3410 let routes_router = Router::new()
3411 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3412 .with_state(routes_state);
3413 app = app.merge(routes_router);
3414 }
3415
3416 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3418
3419 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3424
3425 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3428
3429 app = apply_cors_middleware(app, final_cors_config);
3431
3432 app
3433}
3434
3435#[test]
3439fn test_route_info_clone() {
3440 let route = RouteInfo {
3441 method: "POST".to_string(),
3442 path: "/users".to_string(),
3443 operation_id: Some("createUser".to_string()),
3444 summary: None,
3445 description: None,
3446 parameters: vec![],
3447 };
3448
3449 let cloned = route.clone();
3450 assert_eq!(route.method, cloned.method);
3451 assert_eq!(route.path, cloned.path);
3452 assert_eq!(route.operation_id, cloned.operation_id);
3453}
3454
3455#[test]
3456fn test_http_server_state_new() {
3457 let state = HttpServerState::new();
3458 assert_eq!(state.routes.len(), 0);
3459}
3460
3461#[test]
3462fn test_http_server_state_with_routes() {
3463 let routes = vec![
3464 RouteInfo {
3465 method: "GET".to_string(),
3466 path: "/users".to_string(),
3467 operation_id: Some("getUsers".to_string()),
3468 summary: None,
3469 description: None,
3470 parameters: vec![],
3471 },
3472 RouteInfo {
3473 method: "POST".to_string(),
3474 path: "/users".to_string(),
3475 operation_id: Some("createUser".to_string()),
3476 summary: None,
3477 description: None,
3478 parameters: vec![],
3479 },
3480 ];
3481
3482 let state = HttpServerState::with_routes(routes.clone());
3483 assert_eq!(state.routes.len(), 2);
3484 assert_eq!(state.routes[0].method, "GET");
3485 assert_eq!(state.routes[1].method, "POST");
3486}
3487
3488#[test]
3489fn test_http_server_state_clone() {
3490 let routes = vec![RouteInfo {
3491 method: "GET".to_string(),
3492 path: "/test".to_string(),
3493 operation_id: None,
3494 summary: None,
3495 description: None,
3496 parameters: vec![],
3497 }];
3498
3499 let state = HttpServerState::with_routes(routes);
3500 let cloned = state.clone();
3501
3502 assert_eq!(state.routes.len(), cloned.routes.len());
3503 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3504}
3505
3506#[tokio::test]
3507async fn test_build_router_without_openapi() {
3508 let _router = build_router(None, None, None).await;
3509 }
3511
3512#[tokio::test]
3513async fn test_build_router_with_nonexistent_spec() {
3514 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3515 }
3517
3518#[tokio::test]
3519async fn test_build_router_with_auth_and_latency() {
3520 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3521 }
3523
3524#[tokio::test]
3525async fn test_build_router_with_latency() {
3526 let _router = build_router_with_latency(None, None, None).await;
3527 }
3529
3530#[tokio::test]
3531async fn test_build_router_with_auth() {
3532 let _router = build_router_with_auth(None, None, None).await;
3533 }
3535
3536#[tokio::test]
3537async fn test_build_router_with_chains() {
3538 let _router = build_router_with_chains(None, None, None).await;
3539 }
3541
3542#[test]
3543fn test_route_info_with_all_fields() {
3544 let route = RouteInfo {
3545 method: "PUT".to_string(),
3546 path: "/users/{id}".to_string(),
3547 operation_id: Some("updateUser".to_string()),
3548 summary: Some("Update user".to_string()),
3549 description: Some("Updates an existing user".to_string()),
3550 parameters: vec!["id".to_string(), "body".to_string()],
3551 };
3552
3553 assert!(route.operation_id.is_some());
3554 assert!(route.summary.is_some());
3555 assert!(route.description.is_some());
3556 assert_eq!(route.parameters.len(), 2);
3557}
3558
3559#[test]
3560fn test_route_info_with_minimal_fields() {
3561 let route = RouteInfo {
3562 method: "DELETE".to_string(),
3563 path: "/users/{id}".to_string(),
3564 operation_id: None,
3565 summary: None,
3566 description: None,
3567 parameters: vec![],
3568 };
3569
3570 assert!(route.operation_id.is_none());
3571 assert!(route.summary.is_none());
3572 assert!(route.description.is_none());
3573 assert_eq!(route.parameters.len(), 0);
3574}
3575
3576#[test]
3577fn test_http_server_state_empty_routes() {
3578 let state = HttpServerState::with_routes(vec![]);
3579 assert_eq!(state.routes.len(), 0);
3580}
3581
3582#[test]
3583fn test_http_server_state_multiple_routes() {
3584 let routes = vec![
3585 RouteInfo {
3586 method: "GET".to_string(),
3587 path: "/users".to_string(),
3588 operation_id: Some("listUsers".to_string()),
3589 summary: Some("List all users".to_string()),
3590 description: None,
3591 parameters: vec![],
3592 },
3593 RouteInfo {
3594 method: "GET".to_string(),
3595 path: "/users/{id}".to_string(),
3596 operation_id: Some("getUser".to_string()),
3597 summary: Some("Get a user".to_string()),
3598 description: None,
3599 parameters: vec!["id".to_string()],
3600 },
3601 RouteInfo {
3602 method: "POST".to_string(),
3603 path: "/users".to_string(),
3604 operation_id: Some("createUser".to_string()),
3605 summary: Some("Create a user".to_string()),
3606 description: None,
3607 parameters: vec!["body".to_string()],
3608 },
3609 ];
3610
3611 let state = HttpServerState::with_routes(routes);
3612 assert_eq!(state.routes.len(), 3);
3613
3614 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3616 assert!(methods.contains(&"GET"));
3617 assert!(methods.contains(&"POST"));
3618}
3619
3620#[test]
3621fn test_http_server_state_with_rate_limiter() {
3622 use std::sync::Arc;
3623
3624 let config = middleware::RateLimitConfig::default();
3625 let rate_limiter = Arc::new(middleware::GlobalRateLimiter::new(config));
3626
3627 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3628
3629 assert!(state.rate_limiter.is_some());
3630 assert_eq!(state.routes.len(), 0);
3631}
3632
3633#[tokio::test]
3634async fn test_build_router_includes_rate_limiter() {
3635 let _router = build_router(None, None, None).await;
3636 }