1pub mod ai_handler;
172pub mod auth;
173pub mod chain_handlers;
174pub mod consistency;
176pub mod contract_diff_api;
178pub mod contract_diff_middleware;
180pub mod counting_listener;
183pub mod coverage;
184#[cfg(feature = "database")]
190pub mod database {
191 pub use mockforge_intelligence::database::*;
192}
193pub mod file_generator;
195pub mod file_server;
197pub mod fixtures_api;
199pub mod health;
201pub mod http_tracing_middleware;
202pub mod latency_profiles;
204pub mod management;
206pub mod management_ws;
208pub mod metrics_middleware;
209pub mod middleware;
210pub mod mockai_api;
212pub mod network_profile_runtime;
214pub mod op_middleware;
215pub mod protocol_server;
217pub mod proxy_server;
219pub mod quick_mock;
221pub mod rag_ai_generator;
223pub mod reality_proxy;
225pub mod replay_listing;
227pub mod request_logging;
228pub mod route_chaos_runtime;
230#[cfg(feature = "scenario-engine")]
232pub mod scenarios_runtime;
233pub mod spec_import;
235pub mod sse;
237pub mod state_machine_api;
239pub mod time_travel_api;
241pub mod tls;
243pub mod token_response;
245pub mod ui_builder;
247pub mod verification;
249
250pub mod handlers;
252
253pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
255pub use health::{HealthManager, ServiceStatus};
257
258pub use management::{
260 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
261 ServerConfig, ServerStats,
262};
263
264pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
266
267pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
269
270pub use verification::verification_router;
272
273pub use metrics_middleware::collect_http_metrics;
275
276pub use http_tracing_middleware::http_tracing_middleware;
278
279pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
281
282async fn load_persona_from_config() -> Option<Arc<Persona>> {
285 use mockforge_core::config::load_config;
286
287 let config_paths = [
289 "config.yaml",
290 "mockforge.yaml",
291 "tools/mockforge/config.yaml",
292 "../tools/mockforge/config.yaml",
293 ];
294
295 for path in &config_paths {
296 if let Ok(config) = load_config(path).await {
297 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
300 {
301 tracing::info!(
302 "Loaded active persona '{}' from config file: {}",
303 persona.name,
304 path
305 );
306 return Some(Arc::new(persona.clone()));
307 } else {
308 tracing::debug!(
309 "No active persona found in config file: {} (personas count: {})",
310 path,
311 config.mockai.intelligent_behavior.personas.personas.len()
312 );
313 }
314 } else {
315 tracing::debug!("Could not load config from: {}", path);
316 }
317 }
318
319 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
320 None
321}
322
323use axum::body::Body;
324use axum::extract::State;
325use axum::http::Request;
326use axum::middleware::from_fn_with_state;
327use axum::response::Json;
328use axum::Router;
329use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
330use mockforge_core::intelligent_behavior::config::Persona;
331use mockforge_foundation::latency::LatencyInjector;
332use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
333use mockforge_openapi::openapi_routes::ValidationOptions;
334use mockforge_openapi::OpenApiSpec;
335use std::sync::Arc;
336use tower_http::cors::{Any, CorsLayer};
337
338#[cfg(feature = "data-faker")]
339use mockforge_data::provider::register_core_faker_provider;
340use mockforge_foundation::latency::LatencyProfile;
341use std::collections::HashMap;
342use std::ffi::OsStr;
343use std::path::Path;
344use tokio::fs;
345use tokio::sync::RwLock;
346use tracing::*;
347
348#[derive(Clone)]
350pub struct RouteInfo {
351 pub method: String,
353 pub path: String,
355 pub operation_id: Option<String>,
357 pub summary: Option<String>,
359 pub description: Option<String>,
361 pub parameters: Vec<String>,
363}
364
365#[derive(Clone)]
367pub struct HttpServerState {
368 pub routes: Vec<RouteInfo>,
370 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
372 pub production_headers: Option<Arc<HashMap<String, String>>>,
374}
375
376impl Default for HttpServerState {
377 fn default() -> Self {
378 Self::new()
379 }
380}
381
382impl HttpServerState {
383 pub fn new() -> Self {
385 Self {
386 routes: Vec::new(),
387 rate_limiter: None,
388 production_headers: None,
389 }
390 }
391
392 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
394 Self {
395 routes,
396 rate_limiter: None,
397 production_headers: None,
398 }
399 }
400
401 pub fn with_rate_limiter(
403 mut self,
404 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
405 ) -> Self {
406 self.rate_limiter = Some(rate_limiter);
407 self
408 }
409
410 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
412 self.production_headers = Some(headers);
413 self
414 }
415}
416
417async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
419 let route_info: Vec<serde_json::Value> = state
420 .routes
421 .iter()
422 .map(|route| {
423 serde_json::json!({
424 "method": route.method,
425 "path": route.path,
426 "operation_id": route.operation_id,
427 "summary": route.summary,
428 "description": route.description,
429 "parameters": route.parameters
430 })
431 })
432 .collect();
433
434 Json(serde_json::json!({
435 "routes": route_info,
436 "total": state.routes.len()
437 }))
438}
439
440async fn get_docs_handler() -> axum::response::Html<&'static str> {
442 axum::response::Html(include_str!("../static/docs.html"))
443}
444
445pub async fn build_router(
447 spec_path: Option<String>,
448 options: Option<ValidationOptions>,
449 failure_config: Option<FailureConfig>,
450) -> Router {
451 build_router_with_multi_tenant(
452 spec_path,
453 options,
454 failure_config,
455 None,
456 None,
457 None,
458 None,
459 None,
460 None,
461 None,
462 )
463 .await
464}
465
466fn apply_cors_middleware(
468 app: Router,
469 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
470) -> Router {
471 use http::Method;
472 use tower_http::cors::AllowOrigin;
473
474 if let Some(config) = cors_config {
475 if !config.enabled {
476 return app;
477 }
478
479 let mut cors_layer = CorsLayer::new();
480 let is_wildcard_origin;
481
482 if config.allowed_origins.contains(&"*".to_string()) {
484 cors_layer = cors_layer.allow_origin(Any);
485 is_wildcard_origin = true;
486 } else if !config.allowed_origins.is_empty() {
487 let origins: Vec<_> = config
489 .allowed_origins
490 .iter()
491 .filter_map(|origin| {
492 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
493 })
494 .collect();
495
496 if origins.is_empty() {
497 warn!("No valid CORS origins configured, using permissive CORS");
499 cors_layer = cors_layer.allow_origin(Any);
500 is_wildcard_origin = true;
501 } else {
502 if origins.len() == 1 {
505 cors_layer = cors_layer.allow_origin(origins[0].clone());
506 is_wildcard_origin = false;
507 } else {
508 warn!(
510 "Multiple CORS origins configured, using permissive CORS. \
511 Consider using '*' for all origins."
512 );
513 cors_layer = cors_layer.allow_origin(Any);
514 is_wildcard_origin = true;
515 }
516 }
517 } else {
518 cors_layer = cors_layer.allow_origin(Any);
520 is_wildcard_origin = true;
521 }
522
523 if !config.allowed_methods.is_empty() {
525 let methods: Vec<Method> =
526 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
527 if !methods.is_empty() {
528 cors_layer = cors_layer.allow_methods(methods);
529 }
530 } else {
531 cors_layer = cors_layer.allow_methods([
533 Method::GET,
534 Method::POST,
535 Method::PUT,
536 Method::DELETE,
537 Method::PATCH,
538 Method::OPTIONS,
539 ]);
540 }
541
542 if !config.allowed_headers.is_empty() {
544 let headers: Vec<_> = config
545 .allowed_headers
546 .iter()
547 .filter_map(|h| h.parse::<http::HeaderName>().ok())
548 .collect();
549 if !headers.is_empty() {
550 cors_layer = cors_layer.allow_headers(headers);
551 }
552 } else {
553 cors_layer =
555 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
556 }
557
558 let should_allow_credentials = if is_wildcard_origin {
562 false
564 } else {
565 config.allow_credentials
567 };
568
569 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
570
571 info!(
572 "CORS middleware enabled with configured settings (credentials: {})",
573 should_allow_credentials
574 );
575 app.layer(cors_layer)
576 } else {
577 debug!("No CORS config provided, using permissive CORS for development");
581 app.layer(CorsLayer::permissive().allow_credentials(false))
584 }
585}
586
587#[allow(clippy::too_many_arguments)]
589#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
591 spec_path: Option<String>,
592 options: Option<ValidationOptions>,
593 failure_config: Option<FailureConfig>,
594 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
595 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
596 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
597 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
598 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
599 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
600 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
601) -> Router {
602 use std::time::Instant;
603
604 let startup_start = Instant::now();
605
606 let mut app = Router::new();
608
609 let mut rate_limit_config = middleware::RateLimitConfig {
612 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
613 .ok()
614 .and_then(|v| v.parse().ok())
615 .unwrap_or(1000),
616 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
617 .ok()
618 .and_then(|v| v.parse().ok())
619 .unwrap_or(2000),
620 per_ip: true,
621 per_endpoint: false,
622 };
623
624 let mut final_cors_config = cors_config;
626 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
627 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
629
630 if let Some(deploy_config) = &deceptive_deploy_config {
631 if deploy_config.enabled {
632 info!("Deceptive deploy mode enabled - applying production-like configuration");
633
634 if let Some(prod_cors) = &deploy_config.cors {
636 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
637 enabled: true,
638 allowed_origins: prod_cors.allowed_origins.clone(),
639 allowed_methods: prod_cors.allowed_methods.clone(),
640 allowed_headers: prod_cors.allowed_headers.clone(),
641 allow_credentials: prod_cors.allow_credentials,
642 });
643 info!("Applied production-like CORS configuration");
644 }
645
646 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
648 rate_limit_config = middleware::RateLimitConfig {
649 requests_per_minute: prod_rate_limit.requests_per_minute,
650 burst: prod_rate_limit.burst,
651 per_ip: prod_rate_limit.per_ip,
652 per_endpoint: false,
653 };
654 info!(
655 "Applied production-like rate limiting: {} req/min, burst: {}",
656 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
657 );
658 }
659
660 if !deploy_config.headers.is_empty() {
662 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
663 production_headers = Some(std::sync::Arc::new(headers_map));
664 info!("Configured {} production headers", deploy_config.headers.len());
665 }
666
667 if let Some(prod_oauth) = &deploy_config.oauth {
669 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
670 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
671 oauth2: Some(oauth2_config),
672 ..Default::default()
673 });
674 info!("Applied production-like OAuth configuration for deceptive deploy");
675 }
676 }
677 }
678
679 let rate_limit_disabled = middleware::is_rate_limit_disabled();
680 let rate_limiter =
681 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
682
683 let mut state = HttpServerState::new();
684 if rate_limit_disabled {
685 info!(
686 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
687 );
688 } else {
689 state = state.with_rate_limiter(rate_limiter.clone());
690 }
691
692 if let Some(headers) = production_headers.clone() {
694 state = state.with_production_headers(headers);
695 }
696
697 let spec_path_for_mgmt = spec_path.clone();
699
700 if let Some(spec_path) = spec_path {
702 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
703
704 let spec_load_start = Instant::now();
706 match OpenApiSpec::from_file(&spec_path).await {
707 Ok(openapi) => {
708 let spec_load_duration = spec_load_start.elapsed();
709 info!(
710 "Successfully loaded OpenAPI spec from {} (took {:?})",
711 spec_path, spec_load_duration
712 );
713
714 tracing::debug!("Creating OpenAPI route registry...");
716 let registry_start = Instant::now();
717
718 let persona = load_persona_from_config().await;
720
721 let registry = if let Some(opts) = options {
722 tracing::debug!("Using custom validation options");
723 if let Some(ref persona) = persona {
724 tracing::info!("Using persona '{}' for route generation", persona.name);
725 }
726 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
727 } else {
728 tracing::debug!("Using environment-based options");
729 if let Some(ref persona) = persona {
730 tracing::info!("Using persona '{}' for route generation", persona.name);
731 }
732 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
733 };
734 let registry_duration = registry_start.elapsed();
735 info!(
736 "Created OpenAPI route registry with {} routes (took {:?})",
737 registry.routes().len(),
738 registry_duration
739 );
740
741 let extract_start = Instant::now();
743 let route_info: Vec<RouteInfo> = registry
744 .routes()
745 .iter()
746 .map(|route| RouteInfo {
747 method: route.method.clone(),
748 path: route.path.clone(),
749 operation_id: route.operation.operation_id.clone(),
750 summary: route.operation.summary.clone(),
751 description: route.operation.description.clone(),
752 parameters: route.parameters.clone(),
753 })
754 .collect();
755 state.routes = route_info;
756 let extract_duration = extract_start.elapsed();
757 debug!("Extracted route information (took {:?})", extract_duration);
758
759 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
761 tracing::debug!("Loading overrides from environment variable");
762 let overrides_start = Instant::now();
763 match mockforge_core::Overrides::load_from_globs(&[]).await {
764 Ok(overrides) => {
765 let overrides_duration = overrides_start.elapsed();
766 info!(
767 "Loaded {} override rules (took {:?})",
768 overrides.rules().len(),
769 overrides_duration
770 );
771 Some(overrides)
772 }
773 Err(e) => {
774 tracing::warn!("Failed to load overrides: {}", e);
775 None
776 }
777 }
778 } else {
779 None
780 };
781
782 let router_build_start = Instant::now();
784 let overrides_enabled = overrides.is_some();
785 let response_rewriter: Option<
786 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
787 > = Some(std::sync::Arc::new(
788 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(overrides),
789 ));
790 let openapi_router = if let Some(mockai_instance) = &mockai {
791 tracing::debug!("Building router with MockAI support");
792 registry.build_router_with_mockai(Some(mockai_instance.clone()))
793 } else if let Some(ai_generator) = &ai_generator {
794 tracing::debug!("Building router with AI generator support");
795 registry.build_router_with_ai(Some(ai_generator.clone()))
796 } else if let Some(failure_config) = &failure_config {
797 tracing::debug!("Building router with failure injection and overrides");
798 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
799 registry.build_router_with_injectors_and_overrides(
800 LatencyInjector::default(),
801 Some(failure_injector),
802 response_rewriter,
803 overrides_enabled,
804 )
805 } else {
806 tracing::debug!("Building router with overrides");
807 registry.build_router_with_injectors_and_overrides(
808 LatencyInjector::default(),
809 None,
810 response_rewriter,
811 overrides_enabled,
812 )
813 };
814 let router_build_duration = router_build_start.elapsed();
815 debug!("Built OpenAPI router (took {:?})", router_build_duration);
816
817 tracing::debug!("Merging OpenAPI router with main router");
818 app = app.merge(openapi_router);
819 tracing::debug!("Router built successfully");
820 }
821 Err(e) => {
822 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
823 }
824 }
825 }
826
827 app = app.route(
829 "/health",
830 axum::routing::get(|| async {
831 use mockforge_core::server_utils::health::HealthStatus;
832 {
833 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
835 Ok(value) => Json(value),
836 Err(e) => {
837 tracing::error!("Failed to serialize health status: {}", e);
839 Json(serde_json::json!({
840 "status": "healthy",
841 "service": "mockforge-http",
842 "uptime_seconds": 0
843 }))
844 }
845 }
846 }
847 }),
848 )
849 .merge(sse::sse_router())
851 .merge(file_server::file_serving_router());
853
854 let state_for_routes = state.clone();
856
857 let routes_router = Router::new()
859 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
860 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
861 .with_state(state_for_routes);
862
863 app = app.merge(routes_router);
865
866 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
868
869 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
872 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
873
874 if Path::new(&coverage_html_path).exists() {
876 app = app.nest_service(
877 "/__mockforge/coverage.html",
878 tower_http::services::ServeFile::new(&coverage_html_path),
879 );
880 debug!("Serving coverage UI from: {}", coverage_html_path);
881 } else {
882 debug!(
883 "Coverage UI file not found at: {}. Skipping static file serving.",
884 coverage_html_path
885 );
886 }
887
888 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
891 match OpenApiSpec::from_file(sp).await {
892 Ok(s) => Some(Arc::new(s)),
893 Err(e) => {
894 debug!("Failed to load OpenAPI spec for management API: {}", e);
895 None
896 }
897 }
898 } else {
899 None
900 };
901 let mgmt_port = std::env::var("PORT")
902 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
903 .ok()
904 .and_then(|p| p.parse().ok())
905 .unwrap_or(3000);
906 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
907
908 use std::sync::Arc;
910 let ws_state = WsManagementState::new();
911 let ws_broadcast = Arc::new(ws_state.tx.clone());
912 let management_state = management_state.with_ws_broadcast(ws_broadcast);
913
914 #[cfg(feature = "smtp")]
918 let management_state = {
919 if let Some(smtp_reg) = smtp_registry {
920 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
921 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
922 Err(e) => {
923 error!(
924 "Invalid SMTP registry type passed to HTTP management state: {:?}",
925 e.type_id()
926 );
927 management_state
928 }
929 }
930 } else {
931 management_state
932 }
933 };
934 #[cfg(not(feature = "smtp"))]
935 let management_state = management_state;
936 #[cfg(not(feature = "smtp"))]
937 let _ = smtp_registry;
938 let management_state_for_fallback = management_state.clone();
939 app = app.nest("/__mockforge/api", management_router(management_state));
940 app = app.fallback_service(
945 axum::routing::any(management::dynamic_mock_fallback)
946 .with_state(management_state_for_fallback),
947 );
948
949 app = app.merge(verification_router());
951
952 use crate::auth::oidc::oidc_router;
954 app = app.merge(oidc_router());
955
956 {
958 use mockforge_core::security::get_global_access_review_service;
959 if let Some(service) = get_global_access_review_service().await {
960 use crate::handlers::access_review::{access_review_router, AccessReviewState};
961 let review_state = AccessReviewState { service };
962 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
963 debug!("Access review API mounted at /api/v1/security/access-reviews");
964 }
965 }
966
967 {
969 use mockforge_core::security::get_global_privileged_access_manager;
970 if let Some(manager) = get_global_privileged_access_manager().await {
971 use crate::handlers::privileged_access::{
972 privileged_access_router, PrivilegedAccessState,
973 };
974 let privileged_state = PrivilegedAccessState { manager };
975 app = app.nest(
976 "/api/v1/security/privileged-access",
977 privileged_access_router(privileged_state),
978 );
979 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
980 }
981 }
982
983 {
985 use mockforge_core::security::get_global_change_management_engine;
986 if let Some(engine) = get_global_change_management_engine().await {
987 use crate::handlers::change_management::{
988 change_management_router, ChangeManagementState,
989 };
990 let change_state = ChangeManagementState { engine };
991 app = app.nest("/api/v1/change-management", change_management_router(change_state));
992 debug!("Change management API mounted at /api/v1/change-management");
993 }
994 }
995
996 {
998 use mockforge_core::security::get_global_risk_assessment_engine;
999 if let Some(engine) = get_global_risk_assessment_engine().await {
1000 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1001 let risk_state = RiskAssessmentState { engine };
1002 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1003 debug!("Risk assessment API mounted at /api/v1/security/risks");
1004 }
1005 }
1006
1007 {
1009 use crate::auth::token_lifecycle::TokenLifecycleManager;
1010 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1011 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1012 let lifecycle_state = TokenLifecycleState {
1013 manager: lifecycle_manager,
1014 };
1015 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1016 debug!("Token lifecycle API mounted at /api/v1/auth");
1017 }
1018
1019 {
1021 use crate::auth::oidc::load_oidc_state;
1022 use crate::auth::token_lifecycle::TokenLifecycleManager;
1023 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1024 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1026 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1027 let oauth2_state = OAuth2ServerState {
1028 oidc_state,
1029 lifecycle_manager,
1030 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1031 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1032 };
1033 app = app.merge(oauth2_server_router(oauth2_state));
1034 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1035 }
1036
1037 {
1039 use crate::auth::oidc::load_oidc_state;
1040 use crate::auth::risk_engine::RiskEngine;
1041 use crate::auth::token_lifecycle::TokenLifecycleManager;
1042 use crate::handlers::consent::{consent_router, ConsentState};
1043 use crate::handlers::oauth2_server::OAuth2ServerState;
1044 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1046 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1047 let oauth2_state = OAuth2ServerState {
1048 oidc_state: oidc_state.clone(),
1049 lifecycle_manager: lifecycle_manager.clone(),
1050 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1051 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1052 };
1053 let risk_engine = Arc::new(RiskEngine::default());
1054 let consent_state = ConsentState {
1055 oauth2_state,
1056 risk_engine,
1057 };
1058 app = app.merge(consent_router(consent_state));
1059 debug!("Consent screen endpoints mounted at /consent");
1060 }
1061
1062 {
1064 use crate::auth::risk_engine::RiskEngine;
1065 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1066 let risk_engine = Arc::new(RiskEngine::default());
1067 let risk_state = RiskSimulationState { risk_engine };
1068 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1069 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1070 }
1071
1072 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1074
1075 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1077
1078 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1080
1081 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1084
1085 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1087
1088 if state.production_headers.is_some() {
1090 app =
1091 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1092 }
1093
1094 if middleware::is_keepalive_hint_enabled() {
1100 info!(
1101 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
1102 );
1103 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
1104 }
1105
1106 if middleware::is_conn_log_enabled() {
1111 info!(
1112 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
1113 );
1114 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
1115 }
1116
1117 if let Some(auth_config) = deceptive_deploy_auth_config {
1119 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1120 use std::collections::HashMap;
1121 use std::sync::Arc;
1122 use tokio::sync::RwLock;
1123
1124 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1126 match create_oauth2_client(oauth2_config) {
1127 Ok(client) => Some(client),
1128 Err(e) => {
1129 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1130 None
1131 }
1132 }
1133 } else {
1134 None
1135 };
1136
1137 let auth_state = AuthState {
1139 config: auth_config,
1140 spec: None, oauth2_client,
1142 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1143 };
1144
1145 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1147 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1148 }
1149
1150 app = apply_cors_middleware(app, final_cors_config);
1152
1153 if let Some(mt_config) = multi_tenant_config {
1155 if mt_config.enabled {
1156 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1157 use std::sync::Arc;
1158
1159 info!(
1160 "Multi-tenant mode enabled with {} routing strategy",
1161 match mt_config.routing_strategy {
1162 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1163 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1164 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1165 }
1166 );
1167
1168 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1170
1171 let default_workspace =
1173 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1174 if let Err(e) =
1175 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1176 {
1177 warn!("Failed to register default workspace: {}", e);
1178 } else {
1179 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1180 }
1181
1182 if mt_config.auto_discover {
1184 if let Some(config_dir) = &mt_config.config_directory {
1185 let config_path = Path::new(config_dir);
1186 if config_path.exists() && config_path.is_dir() {
1187 match fs::read_dir(config_path).await {
1188 Ok(mut entries) => {
1189 while let Ok(Some(entry)) = entries.next_entry().await {
1190 let path = entry.path();
1191 if path.extension() == Some(OsStr::new("yaml")) {
1192 match fs::read_to_string(&path).await {
1193 Ok(content) => {
1194 match serde_yaml::from_str::<
1195 mockforge_core::Workspace,
1196 >(
1197 &content
1198 ) {
1199 Ok(workspace) => {
1200 if let Err(e) = registry.register_workspace(
1201 workspace.id.clone(),
1202 workspace,
1203 ) {
1204 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1205 } else {
1206 info!("Auto-registered workspace from {:?}", path);
1207 }
1208 }
1209 Err(e) => {
1210 warn!("Failed to parse workspace from {:?}: {}", path, e);
1211 }
1212 }
1213 }
1214 Err(e) => {
1215 warn!(
1216 "Failed to read workspace file {:?}: {}",
1217 path, e
1218 );
1219 }
1220 }
1221 }
1222 }
1223 }
1224 Err(e) => {
1225 warn!("Failed to read config directory {:?}: {}", config_path, e);
1226 }
1227 }
1228 } else {
1229 warn!(
1230 "Config directory {:?} does not exist or is not a directory",
1231 config_path
1232 );
1233 }
1234 }
1235 }
1236
1237 let registry = Arc::new(registry);
1239
1240 let _workspace_router = WorkspaceRouter::new(registry);
1242
1243 info!("Workspace routing middleware initialized for HTTP server");
1246 }
1247 }
1248
1249 let total_startup_duration = startup_start.elapsed();
1250 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1251
1252 app
1253}
1254
1255pub async fn build_router_with_auth_and_latency(
1257 spec_path: Option<String>,
1258 _options: Option<()>,
1259 auth_config: Option<mockforge_core::config::AuthConfig>,
1260 latency_injector: Option<LatencyInjector>,
1261) -> Router {
1262 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1264
1265 if let Some(injector) = latency_injector {
1267 let injector = Arc::new(injector);
1268 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1269 let injector = injector.clone();
1270 async move {
1271 let _ = injector.inject_latency(&[]).await;
1272 next.run(req).await
1273 }
1274 }));
1275 }
1276
1277 app
1278}
1279
1280pub async fn build_router_with_latency(
1282 spec_path: Option<String>,
1283 options: Option<ValidationOptions>,
1284 latency_injector: Option<LatencyInjector>,
1285) -> Router {
1286 if let Some(spec) = &spec_path {
1287 match OpenApiSpec::from_file(spec).await {
1288 Ok(openapi) => {
1289 let registry = if let Some(opts) = options {
1290 OpenApiRouteRegistry::new_with_options(openapi, opts)
1291 } else {
1292 OpenApiRouteRegistry::new_with_env(openapi)
1293 };
1294
1295 if let Some(injector) = latency_injector {
1296 return registry.build_router_with_latency(injector);
1297 } else {
1298 return registry.build_router();
1299 }
1300 }
1301 Err(e) => {
1302 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1303 }
1304 }
1305 }
1306
1307 build_router(None, None, None).await
1308}
1309
1310pub async fn build_router_with_auth(
1312 spec_path: Option<String>,
1313 options: Option<ValidationOptions>,
1314 auth_config: Option<mockforge_core::config::AuthConfig>,
1315) -> Router {
1316 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1317 use std::sync::Arc;
1318
1319 #[cfg(feature = "data-faker")]
1321 {
1322 register_core_faker_provider();
1323 }
1324
1325 let spec = if let Some(spec_path) = &spec_path {
1327 match OpenApiSpec::from_file(&spec_path).await {
1328 Ok(spec) => Some(Arc::new(spec)),
1329 Err(e) => {
1330 warn!("Failed to load OpenAPI spec for auth: {}", e);
1331 None
1332 }
1333 }
1334 } else {
1335 None
1336 };
1337
1338 let oauth2_client = if let Some(auth_config) = &auth_config {
1340 if let Some(oauth2_config) = &auth_config.oauth2 {
1341 match create_oauth2_client(oauth2_config) {
1342 Ok(client) => Some(client),
1343 Err(e) => {
1344 warn!("Failed to create OAuth2 client: {}", e);
1345 None
1346 }
1347 }
1348 } else {
1349 None
1350 }
1351 } else {
1352 None
1353 };
1354
1355 let auth_state = AuthState {
1356 config: auth_config.unwrap_or_default(),
1357 spec,
1358 oauth2_client,
1359 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1360 };
1361
1362 let mut app = Router::new().with_state(auth_state.clone());
1364
1365 if let Some(spec_path) = spec_path {
1367 match OpenApiSpec::from_file(&spec_path).await {
1368 Ok(openapi) => {
1369 info!("Loaded OpenAPI spec from {}", spec_path);
1370 let registry = if let Some(opts) = options {
1371 OpenApiRouteRegistry::new_with_options(openapi, opts)
1372 } else {
1373 OpenApiRouteRegistry::new_with_env(openapi)
1374 };
1375
1376 app = registry.build_router();
1377 }
1378 Err(e) => {
1379 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1380 }
1381 }
1382 }
1383
1384 app = app.route(
1386 "/health",
1387 axum::routing::get(|| async {
1388 use mockforge_core::server_utils::health::HealthStatus;
1389 {
1390 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1392 Ok(value) => Json(value),
1393 Err(e) => {
1394 tracing::error!("Failed to serialize health status: {}", e);
1396 Json(serde_json::json!({
1397 "status": "healthy",
1398 "service": "mockforge-http",
1399 "uptime_seconds": 0
1400 }))
1401 }
1402 }
1403 }
1404 }),
1405 )
1406 .merge(sse::sse_router())
1408 .merge(file_server::file_serving_router())
1410 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1412 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1414
1415 app
1416}
1417
1418pub async fn serve_router(
1420 port: u16,
1421 app: Router,
1422) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1423 serve_router_with_tls(port, app, None).await
1424}
1425
1426pub async fn serve_router_with_tls(
1428 port: u16,
1429 app: Router,
1430 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1431) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1432 serve_router_with_tls_notify(port, app, tls_config, None).await
1433}
1434
1435pub async fn serve_router_with_tls_notify(
1443 port: u16,
1444 app: Router,
1445 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1446 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1447) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1448 serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1449}
1450
1451pub async fn serve_router_with_tls_notify_chaos(
1460 port: u16,
1461 app: Router,
1462 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1463 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1464 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1465) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1466 use std::net::SocketAddr;
1467
1468 let addr = mockforge_core::wildcard_socket_addr(port);
1469
1470 if let Some(ref tls) = tls_config {
1471 if tls.enabled {
1472 info!("HTTPS listening on {}", addr);
1473 if let Some(tx) = bound_port_tx {
1474 let _ = tx.send(port);
1475 }
1476 return serve_with_tls(addr, app, tls).await;
1477 }
1478 }
1479
1480 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1481 format!(
1482 "Failed to bind HTTP server to port {}: {}\n\
1483 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 {}",
1484 port, e, port, port
1485 )
1486 })?;
1487
1488 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1489 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1490 if let Some(tx) = bound_port_tx {
1491 let _ = tx.send(actual_port);
1492 }
1493
1494 let odata_app = tower::ServiceBuilder::new()
1498 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1499 .service(app);
1500 if let Some(cfg) = chaos_config {
1501 info!("HTTP listener wrapped with chaos TCP listener (RST/FIN injection enabled)");
1502 let chaos_listener = mockforge_chaos::ChaosTcpListener::new(listener, cfg);
1503 let app_with_addr_compat = tower::ServiceBuilder::new()
1506 .layer(axum::middleware::from_fn(copy_chaos_addr_to_socketaddr))
1507 .service(odata_app);
1508 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1509 mockforge_chaos::ChaosClientAddr,
1510 >(app_with_addr_compat);
1511 let counted = counting_listener::CountingMakeService::new(make_svc);
1513 axum::serve(chaos_listener, counted).await?;
1514 } else {
1515 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1516 SocketAddr,
1517 >(odata_app);
1518 let counted = counting_listener::CountingMakeService::new(make_svc);
1521 axum::serve(listener, counted).await?;
1522 }
1523 Ok(())
1524}
1525
1526async fn copy_chaos_addr_to_socketaddr(
1530 mut req: Request<Body>,
1531 next: axum::middleware::Next,
1532) -> axum::response::Response {
1533 use axum::extract::ConnectInfo;
1534 if let Some(ConnectInfo(chaos_addr)) =
1535 req.extensions().get::<ConnectInfo<mockforge_chaos::ChaosClientAddr>>().copied()
1536 {
1537 let sock: std::net::SocketAddr = *chaos_addr;
1538 req.extensions_mut().insert(ConnectInfo(sock));
1539 }
1540 next.run(req).await
1541}
1542
1543async fn serve_with_tls(
1548 addr: std::net::SocketAddr,
1549 app: Router,
1550 tls_config: &mockforge_core::config::HttpTlsConfig,
1551) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1552 use axum_server::tls_rustls::RustlsConfig;
1553 use std::net::SocketAddr;
1554
1555 tls::init_crypto_provider();
1557
1558 info!("Loading TLS configuration for HTTPS server");
1559
1560 let server_config = tls::load_tls_server_config(tls_config)?;
1562
1563 let rustls_config = RustlsConfig::from_config(server_config);
1566
1567 info!("Starting HTTPS server on {}", addr);
1568
1569 let odata_app = tower::ServiceBuilder::new()
1573 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1574 .service(app);
1575 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1576 SocketAddr,
1577 >(odata_app);
1578 let counted = counting_listener::CountingMakeService::new(make_svc);
1581
1582 axum_server::bind_rustls(addr, rustls_config)
1584 .serve(counted)
1585 .await
1586 .map_err(|e| format!("HTTPS server error: {}", e).into())
1587}
1588
1589pub async fn start(
1591 port: u16,
1592 spec_path: Option<String>,
1593 options: Option<ValidationOptions>,
1594) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1595 start_with_latency(port, spec_path, options, None).await
1596}
1597
1598pub async fn start_with_auth_and_latency(
1600 port: u16,
1601 spec_path: Option<String>,
1602 options: Option<ValidationOptions>,
1603 auth_config: Option<mockforge_core::config::AuthConfig>,
1604 latency_profile: Option<LatencyProfile>,
1605) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1606 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1607 .await
1608}
1609
1610pub async fn start_with_auth_and_injectors(
1612 port: u16,
1613 spec_path: Option<String>,
1614 options: Option<ValidationOptions>,
1615 auth_config: Option<mockforge_core::config::AuthConfig>,
1616 _latency_profile: Option<LatencyProfile>,
1617 _failure_injector: Option<FailureInjector>,
1618) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1619 let app = build_router_with_auth(spec_path, options, auth_config).await;
1621 serve_router(port, app).await
1622}
1623
1624pub async fn start_with_latency(
1626 port: u16,
1627 spec_path: Option<String>,
1628 options: Option<ValidationOptions>,
1629 latency_profile: Option<LatencyProfile>,
1630) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1631 let latency_injector =
1632 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1633
1634 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1635 serve_router(port, app).await
1636}
1637
1638pub async fn build_router_with_chains(
1640 spec_path: Option<String>,
1641 options: Option<ValidationOptions>,
1642 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1643) -> Router {
1644 build_router_with_chains_and_multi_tenant(
1645 spec_path,
1646 options,
1647 circling_config,
1648 None,
1649 None,
1650 None,
1651 None,
1652 None,
1653 None,
1654 None,
1655 false,
1656 None, None, None, None, )
1661 .await
1662}
1663
1664async fn apply_route_chaos(
1672 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1673 method: &http::Method,
1674 uri: &http::Uri,
1675) -> Option<axum::response::Response> {
1676 use axum::http::StatusCode;
1677 use axum::response::IntoResponse;
1678
1679 if let Some(injector) = injector {
1680 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1682 let mut response = Json(serde_json::json!({
1684 "error": fault_response.error_message,
1685 "fault_type": fault_response.fault_type,
1686 }))
1687 .into_response();
1688 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1689 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1690 return Some(response);
1691 }
1692
1693 if let Err(e) = injector.inject_latency(method, uri).await {
1695 tracing::warn!("Failed to inject latency: {}", e);
1696 }
1697 }
1698
1699 None }
1701
1702#[allow(clippy::too_many_arguments)]
1704#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1706 spec_path: Option<String>,
1707 options: Option<ValidationOptions>,
1708 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1709 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1710 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1711 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1712 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1713 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1714 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1715 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1716 traffic_shaping_enabled: bool,
1717 health_manager: Option<Arc<HealthManager>>,
1718 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1719 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1720 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1721) -> Router {
1722 use crate::latency_profiles::LatencyProfiles;
1723 use crate::op_middleware::Shared;
1724 use mockforge_core::Overrides;
1725
1726 let template_expand =
1728 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1729 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1730 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1731 .unwrap_or(false)
1732 });
1733
1734 let _shared = Shared {
1735 profiles: LatencyProfiles::default(),
1736 overrides: Overrides::default(),
1737 failure_injector: None,
1738 traffic_shaper,
1739 overrides_enabled: false,
1740 traffic_shaping_enabled,
1741 };
1742
1743 let mut app = Router::new();
1745 let mut include_default_health = true;
1746 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1747
1748 if let Some(ref spec) = spec_path {
1750 match OpenApiSpec::from_file(&spec).await {
1751 Ok(openapi) => {
1752 info!("Loaded OpenAPI spec from {}", spec);
1753
1754 let persona = load_persona_from_config().await;
1756
1757 let mut registry = if let Some(opts) = options {
1758 tracing::debug!("Using custom validation options");
1759 if let Some(ref persona) = persona {
1760 tracing::info!("Using persona '{}' for route generation", persona.name);
1761 }
1762 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1763 } else {
1764 tracing::debug!("Using environment-based options");
1765 if let Some(ref persona) = persona {
1766 tracing::info!("Using persona '{}' for route generation", persona.name);
1767 }
1768 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1769 };
1770
1771 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1773 .unwrap_or_else(|_| "/app/fixtures".to_string());
1774 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1775 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1776 .unwrap_or(true); if custom_fixtures_enabled {
1779 use mockforge_openapi::CustomFixtureLoader;
1780 use std::path::PathBuf;
1781 use std::sync::Arc;
1782
1783 let fixtures_path = PathBuf::from(&fixtures_dir);
1784 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1785
1786 if let Err(e) = custom_loader.load_fixtures().await {
1787 tracing::warn!("Failed to load custom fixtures: {}", e);
1788 } else {
1789 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1790 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1791 }
1792 }
1793
1794 if registry
1795 .routes()
1796 .iter()
1797 .any(|route| route.method == "GET" && route.path == "/health")
1798 {
1799 include_default_health = false;
1800 }
1801 captured_routes = registry
1803 .routes()
1804 .iter()
1805 .map(|r| RouteInfo {
1806 method: r.method.clone(),
1807 path: r.path.clone(),
1808 operation_id: r.operation.operation_id.clone(),
1809 summary: r.operation.summary.clone(),
1810 description: r.operation.description.clone(),
1811 parameters: r.parameters.clone(),
1812 })
1813 .collect();
1814
1815 {
1818 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1819 captured_routes
1820 .iter()
1821 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1822 method: r.method.clone(),
1823 path: r.path.clone(),
1824 operation_id: r.operation_id.clone(),
1825 summary: r.summary.clone(),
1826 description: r.description.clone(),
1827 parameters: r.parameters.clone(),
1828 })
1829 .collect();
1830 mockforge_core::request_logger::set_global_routes(global_routes);
1831 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1832 }
1833
1834 let spec_router = if let Some(ref mockai_instance) = mockai {
1836 tracing::debug!("Building router with MockAI support");
1837 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1838 } else {
1839 registry.build_router()
1840 };
1841 app = app.merge(spec_router);
1842 }
1843 Err(e) => {
1844 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1845 }
1846 }
1847 }
1848
1849 let route_chaos_injector: Option<
1853 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1854 > = if let Some(ref route_configs) = route_configs {
1855 if !route_configs.is_empty() {
1856 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1859 route_configs.to_vec();
1860 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1861 Ok(injector) => {
1862 info!(
1863 "Initialized advanced routing features for {} route(s)",
1864 route_configs.len()
1865 );
1866 Some(std::sync::Arc::new(injector)
1869 as std::sync::Arc<
1870 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1871 >)
1872 }
1873 Err(e) => {
1874 warn!(
1875 "Failed to initialize advanced routing features: {}. Using basic routing.",
1876 e
1877 );
1878 None
1879 }
1880 }
1881 } else {
1882 None
1883 }
1884 } else {
1885 None
1886 };
1887
1888 if let Some(route_configs) = route_configs {
1889 use axum::http::StatusCode;
1890 use axum::response::IntoResponse;
1891
1892 if !route_configs.is_empty() {
1893 info!("Registering {} custom route(s) from config", route_configs.len());
1894 }
1895
1896 let injector = route_chaos_injector.clone();
1897 for route_config in route_configs {
1898 let status = route_config.response.status;
1899 let body = route_config.response.body.clone();
1900 let headers = route_config.response.headers.clone();
1901 let path = route_config.path.clone();
1902 let method = route_config.method.clone();
1903
1904 let expected_method = method.to_uppercase();
1909 let injector_clone = injector.clone();
1913 app = app.route(
1914 &path,
1915 #[allow(clippy::non_send_fields_in_send_ty)]
1916 axum::routing::any(move |req: Request<Body>| {
1917 let body = body.clone();
1918 let headers = headers.clone();
1919 let expand = template_expand;
1920 let expected = expected_method.clone();
1921 let status_code = status;
1922 let injector_for_chaos = injector_clone.clone();
1924
1925 async move {
1926 if req.method().as_str() != expected.as_str() {
1928 return axum::response::Response::builder()
1930 .status(StatusCode::METHOD_NOT_ALLOWED)
1931 .header("Allow", &expected)
1932 .body(Body::empty())
1933 .unwrap()
1934 .into_response();
1935 }
1936
1937 if let Some(fault_response) = apply_route_chaos(
1941 injector_for_chaos.as_deref(),
1942 req.method(),
1943 req.uri(),
1944 )
1945 .await
1946 {
1947 return fault_response;
1948 }
1949
1950 let mut body_value = body.unwrap_or(serde_json::json!({}));
1952
1953 if expand {
1957 use mockforge_template_expansion::RequestContext;
1958 use serde_json::Value;
1959 use std::collections::HashMap;
1960
1961 let method = req.method().to_string();
1963 let path = req.uri().path().to_string();
1964
1965 let query_params: HashMap<String, Value> = req
1967 .uri()
1968 .query()
1969 .map(|q| {
1970 url::form_urlencoded::parse(q.as_bytes())
1971 .into_owned()
1972 .map(|(k, v)| (k, Value::String(v)))
1973 .collect()
1974 })
1975 .unwrap_or_default();
1976
1977 let headers: HashMap<String, Value> = req
1979 .headers()
1980 .iter()
1981 .map(|(k, v)| {
1982 (
1983 k.to_string(),
1984 Value::String(v.to_str().unwrap_or_default().to_string()),
1985 )
1986 })
1987 .collect();
1988
1989 let context = RequestContext {
1993 method,
1994 path,
1995 query_params,
1996 headers,
1997 body: None, path_params: HashMap::new(),
1999 multipart_fields: HashMap::new(),
2000 multipart_files: HashMap::new(),
2001 };
2002
2003 let body_value_clone = body_value.clone();
2007 let context_clone = context.clone();
2008 body_value = match tokio::task::spawn_blocking(move || {
2009 mockforge_template_expansion::expand_templates_in_json(
2010 body_value_clone,
2011 &context_clone,
2012 )
2013 })
2014 .await
2015 {
2016 Ok(result) => result,
2017 Err(_) => body_value, };
2019 }
2020
2021 let mut response = Json(body_value).into_response();
2022
2023 *response.status_mut() =
2025 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2026
2027 for (key, value) in headers {
2029 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2030 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2031 response.headers_mut().insert(header_name, header_value);
2032 }
2033 }
2034 }
2035
2036 response
2037 }
2038 }),
2039 );
2040
2041 debug!("Registered route: {} {}", method, path);
2042 }
2043 }
2044
2045 if let Some(health) = health_manager {
2047 app = app.merge(health::health_router(health));
2049 info!(
2050 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
2051 );
2052 } else if include_default_health {
2053 app = app.route(
2055 "/health",
2056 axum::routing::get(|| async {
2057 use mockforge_core::server_utils::health::HealthStatus;
2058 {
2059 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2061 Ok(value) => Json(value),
2062 Err(e) => {
2063 tracing::error!("Failed to serialize health status: {}", e);
2065 Json(serde_json::json!({
2066 "status": "healthy",
2067 "service": "mockforge-http",
2068 "uptime_seconds": 0
2069 }))
2070 }
2071 }
2072 }
2073 }),
2074 );
2075 }
2076
2077 app = app.merge(sse::sse_router());
2078 app = app.merge(file_server::file_serving_router());
2080
2081 let mgmt_spec = if let Some(ref sp) = spec_path {
2084 match OpenApiSpec::from_file(sp).await {
2085 Ok(s) => Some(Arc::new(s)),
2086 Err(e) => {
2087 debug!("Failed to load OpenAPI spec for management API: {}", e);
2088 None
2089 }
2090 }
2091 } else {
2092 None
2093 };
2094 let spec_path_clone = spec_path.clone();
2095 let mgmt_port = std::env::var("PORT")
2096 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2097 .ok()
2098 .and_then(|p| p.parse().ok())
2099 .unwrap_or(3000);
2100 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2101
2102 use std::sync::Arc;
2104 let ws_state = WsManagementState::new();
2105 let ws_broadcast = Arc::new(ws_state.tx.clone());
2106 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2107
2108 let management_state = if let Some(proxy_cfg) = proxy_config {
2110 use tokio::sync::RwLock;
2111 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2112 management_state.with_proxy_config(proxy_config_arc)
2113 } else {
2114 management_state
2115 };
2116
2117 #[cfg(feature = "smtp")]
2118 let management_state = {
2119 if let Some(smtp_reg) = smtp_registry {
2120 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2121 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2122 Err(e) => {
2123 error!(
2124 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2125 e.type_id()
2126 );
2127 management_state
2128 }
2129 }
2130 } else {
2131 management_state
2132 }
2133 };
2134 #[cfg(not(feature = "smtp"))]
2135 let management_state = {
2136 let _ = smtp_registry;
2137 management_state
2138 };
2139 #[cfg(feature = "mqtt")]
2140 let management_state = {
2141 if let Some(broker) = mqtt_broker {
2142 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
2143 Ok(broker) => management_state.with_mqtt_broker(broker),
2144 Err(e) => {
2145 error!(
2146 "Invalid MQTT broker passed to HTTP management state: {:?}",
2147 e.type_id()
2148 );
2149 management_state
2150 }
2151 }
2152 } else {
2153 management_state
2154 }
2155 };
2156 #[cfg(not(feature = "mqtt"))]
2157 let management_state = {
2158 let _ = mqtt_broker;
2159 management_state
2160 };
2161 let management_state_for_fallback = management_state.clone();
2162 app = app.nest("/__mockforge/api", management_router(management_state));
2163 app = app.fallback_service(
2165 axum::routing::any(management::dynamic_mock_fallback)
2166 .with_state(management_state_for_fallback),
2167 );
2168
2169 app = app.merge(verification_router());
2171
2172 {
2177 use crate::chain_handlers::{chains_router, create_chain_state};
2178 let chain_config = _circling_config.clone().unwrap_or_default();
2179 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2180 chain_config.clone(),
2181 ));
2182 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2183 chain_registry.clone(),
2184 chain_config,
2185 ));
2186 app = app.nest(
2187 "/__mockforge/chains",
2188 chains_router(create_chain_state(chain_registry, chain_engine)),
2189 );
2190 }
2191
2192 {
2197 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2198 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2199 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2200 }
2201
2202 {
2209 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2210 let fx_state = FixturesApiState::from_env();
2211 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2212 }
2213
2214 {
2221 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2222 let api_state = MockAiApiState::new(mockai.clone());
2223 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2224 }
2225
2226 app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2233
2234 {
2239 use crate::route_chaos_runtime::{
2240 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2241 };
2242 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2243 let middleware_state = runtime_state.clone();
2244 app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2245 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2246 }
2247
2248 {
2254 use crate::network_profile_runtime::{
2255 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2256 };
2257 let runtime_state = NetworkProfileRuntimeState::new(
2258 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2259 );
2260 let middleware_state = runtime_state.clone();
2261 app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2262 app = app
2263 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2264 }
2265
2266 use crate::auth::oidc::oidc_router;
2268 app = app.merge(oidc_router());
2269
2270 {
2272 use mockforge_core::security::get_global_access_review_service;
2273 if let Some(service) = get_global_access_review_service().await {
2274 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2275 let review_state = AccessReviewState { service };
2276 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2277 debug!("Access review API mounted at /api/v1/security/access-reviews");
2278 }
2279 }
2280
2281 {
2283 use mockforge_core::security::get_global_privileged_access_manager;
2284 if let Some(manager) = get_global_privileged_access_manager().await {
2285 use crate::handlers::privileged_access::{
2286 privileged_access_router, PrivilegedAccessState,
2287 };
2288 let privileged_state = PrivilegedAccessState { manager };
2289 app = app.nest(
2290 "/api/v1/security/privileged-access",
2291 privileged_access_router(privileged_state),
2292 );
2293 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2294 }
2295 }
2296
2297 {
2299 use mockforge_core::security::get_global_change_management_engine;
2300 if let Some(engine) = get_global_change_management_engine().await {
2301 use crate::handlers::change_management::{
2302 change_management_router, ChangeManagementState,
2303 };
2304 let change_state = ChangeManagementState { engine };
2305 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2306 debug!("Change management API mounted at /api/v1/change-management");
2307 }
2308 }
2309
2310 {
2312 use mockforge_core::security::get_global_risk_assessment_engine;
2313 if let Some(engine) = get_global_risk_assessment_engine().await {
2314 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2315 let risk_state = RiskAssessmentState { engine };
2316 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2317 debug!("Risk assessment API mounted at /api/v1/security/risks");
2318 }
2319 }
2320
2321 {
2323 use crate::auth::token_lifecycle::TokenLifecycleManager;
2324 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2325 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2326 let lifecycle_state = TokenLifecycleState {
2327 manager: lifecycle_manager,
2328 };
2329 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2330 debug!("Token lifecycle API mounted at /api/v1/auth");
2331 }
2332
2333 {
2335 use crate::auth::oidc::load_oidc_state;
2336 use crate::auth::token_lifecycle::TokenLifecycleManager;
2337 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2338 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2340 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2341 let oauth2_state = OAuth2ServerState {
2342 oidc_state,
2343 lifecycle_manager,
2344 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2345 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2346 };
2347 app = app.merge(oauth2_server_router(oauth2_state));
2348 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2349 }
2350
2351 {
2353 use crate::auth::oidc::load_oidc_state;
2354 use crate::auth::risk_engine::RiskEngine;
2355 use crate::auth::token_lifecycle::TokenLifecycleManager;
2356 use crate::handlers::consent::{consent_router, ConsentState};
2357 use crate::handlers::oauth2_server::OAuth2ServerState;
2358 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2360 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2361 let oauth2_state = OAuth2ServerState {
2362 oidc_state: oidc_state.clone(),
2363 lifecycle_manager: lifecycle_manager.clone(),
2364 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2365 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2366 };
2367 let risk_engine = Arc::new(RiskEngine::default());
2368 let consent_state = ConsentState {
2369 oauth2_state,
2370 risk_engine,
2371 };
2372 app = app.merge(consent_router(consent_state));
2373 debug!("Consent screen endpoints mounted at /consent");
2374 }
2375
2376 {
2378 use crate::auth::risk_engine::RiskEngine;
2379 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2380 let risk_engine = Arc::new(RiskEngine::default());
2381 let risk_state = RiskSimulationState { risk_engine };
2382 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2383 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2384 }
2385
2386 #[cfg(feature = "database")]
2388 let database = {
2389 use crate::database::Database;
2390 let database_url = std::env::var("DATABASE_URL").ok();
2391 match Database::connect_optional(database_url.as_deref()).await {
2392 Ok(db) => {
2393 if db.is_connected() {
2394 if let Err(e) = db.migrate_if_connected().await {
2396 warn!("Failed to run database migrations: {}", e);
2397 } else {
2398 info!("Database connected and migrations applied");
2399 }
2400 }
2401 Some(db)
2402 }
2403 Err(e) => {
2404 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2405 None
2406 }
2407 }
2408 };
2409
2410 let (drift_engine, incident_manager, drift_config) = {
2413 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2414 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2415 use std::sync::Arc;
2416
2417 let drift_config = DriftBudgetConfig::default();
2419 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2420
2421 let incident_store = Arc::new(IncidentStore::default());
2423 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2424
2425 (drift_engine, incident_manager, drift_config)
2426 };
2427
2428 {
2429 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2430 use crate::middleware::drift_tracking::DriftTrackingState;
2431 use mockforge_contracts::consumer_contracts::{
2432 ConsumerBreakingChangeDetector, UsageRecorder,
2433 };
2434 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2435 use std::sync::Arc;
2436
2437 let usage_recorder = Arc::new(UsageRecorder::default());
2439 let consumer_detector =
2440 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2441
2442 let diff_analyzer = if drift_config.enabled {
2444 match ContractDiffAnalyzer::new(
2445 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2446 ) {
2447 Ok(analyzer) => Some(Arc::new(analyzer)),
2448 Err(e) => {
2449 warn!("Failed to create contract diff analyzer: {}", e);
2450 None
2451 }
2452 }
2453 } else {
2454 None
2455 };
2456
2457 let spec = if let Some(ref spec_path) = spec_path {
2460 match OpenApiSpec::from_file(spec_path).await {
2461 Ok(s) => Some(Arc::new(s)),
2462 Err(e) => {
2463 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2464 None
2465 }
2466 }
2467 } else {
2468 None
2469 };
2470
2471 let drift_tracking_state = DriftTrackingState {
2473 diff_analyzer,
2474 spec,
2475 drift_engine: drift_engine.clone(),
2476 incident_manager: incident_manager.clone(),
2477 usage_recorder,
2478 consumer_detector,
2479 enabled: drift_config.enabled,
2480 };
2481
2482 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2484
2485 let drift_tracking_state_clone = drift_tracking_state.clone();
2488 app = app.layer(axum::middleware::from_fn(
2489 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2490 let state = drift_tracking_state_clone.clone();
2491 async move {
2492 if req.extensions().get::<DriftTrackingState>().is_none() {
2494 req.extensions_mut().insert(state);
2495 }
2496 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2498 .await
2499 }
2500 },
2501 ));
2502
2503 let drift_state = DriftBudgetState {
2504 engine: drift_engine.clone(),
2505 incident_manager: incident_manager.clone(),
2506 gitops_handler: None, };
2508
2509 app = app.merge(drift_budget_router(drift_state));
2510 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2511 }
2512
2513 #[cfg(feature = "pipelines")]
2515 {
2516 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2517
2518 let pipeline_state = PipelineState::new();
2519 app = app.merge(pipeline_router(pipeline_state));
2520 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2521 }
2522
2523 {
2525 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2526 use crate::handlers::forecasting::{forecasting_router, ForecastingState};
2527 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2528 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2529 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2530 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2531 use mockforge_intelligence::handlers::semantic_drift::{
2532 semantic_drift_router, SemanticDriftState,
2533 };
2534 use mockforge_intelligence::handlers::threat_modeling::{
2535 threat_modeling_router, ThreatModelingState,
2536 };
2537 use std::sync::Arc;
2538
2539 let forecasting_config = ForecastingConfig::default();
2541 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2542 let forecasting_state = ForecastingState {
2543 forecaster,
2544 #[cfg(feature = "database")]
2545 database: database.clone(),
2546 };
2547
2548 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2550 let semantic_state = SemanticDriftState {
2551 manager: semantic_manager,
2552 #[cfg(feature = "database")]
2553 database: database.clone(),
2554 };
2555
2556 let threat_config = ThreatModelingConfig::default();
2558 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2559 Ok(analyzer) => Arc::new(analyzer),
2560 Err(e) => {
2561 warn!("Failed to create threat analyzer: {}. Using default.", e);
2562 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2563 |_| {
2564 ThreatAnalyzer::new(ThreatModelingConfig {
2566 enabled: false,
2567 ..Default::default()
2568 })
2569 .expect("Failed to create fallback threat analyzer")
2570 },
2571 ))
2572 }
2573 };
2574 let mut webhook_configs = Vec::new();
2576 let config_paths = [
2577 "config.yaml",
2578 "mockforge.yaml",
2579 "tools/mockforge/config.yaml",
2580 "../tools/mockforge/config.yaml",
2581 ];
2582
2583 for path in &config_paths {
2584 if let Ok(config) = mockforge_core::config::load_config(path).await {
2585 if !config.incidents.webhooks.is_empty() {
2586 webhook_configs = config.incidents.webhooks.clone();
2587 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2588 break;
2589 }
2590 }
2591 }
2592
2593 if webhook_configs.is_empty() {
2594 debug!("No webhook configs found in config files, using empty list");
2595 }
2596
2597 let threat_state = ThreatModelingState {
2598 analyzer: threat_analyzer,
2599 webhook_configs,
2600 #[cfg(feature = "database")]
2601 database: database.clone(),
2602 };
2603
2604 let contract_health_state = ContractHealthState {
2606 incident_manager: incident_manager.clone(),
2607 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2608 #[cfg(feature = "database")]
2609 database: database.clone(),
2610 };
2611
2612 app = app.merge(forecasting_router(forecasting_state));
2614 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2615
2616 app = app.merge(semantic_drift_router(semantic_state));
2617 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2618
2619 app = app.merge(threat_modeling_router(threat_state));
2620 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2621
2622 app = app.merge(contract_health_router(contract_health_state));
2623 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2624 }
2625
2626 {
2628 use crate::handlers::protocol_contracts::{
2629 protocol_contracts_router, ProtocolContractState,
2630 };
2631 use mockforge_core::contract_drift::{
2632 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2633 };
2634 use std::sync::Arc;
2635 use tokio::sync::RwLock;
2636
2637 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2639
2640 let mut fitness_registry = FitnessFunctionRegistry::new();
2642
2643 let config_paths = [
2645 "config.yaml",
2646 "mockforge.yaml",
2647 "tools/mockforge/config.yaml",
2648 "../tools/mockforge/config.yaml",
2649 ];
2650
2651 let mut config_loaded = false;
2652 for path in &config_paths {
2653 if let Ok(config) = mockforge_core::config::load_config(path).await {
2654 if !config.contracts.fitness_rules.is_empty() {
2655 if let Err(e) =
2656 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2657 {
2658 warn!("Failed to load fitness rules from config {}: {}", path, e);
2659 } else {
2660 info!(
2661 "Loaded {} fitness rules from config: {}",
2662 config.contracts.fitness_rules.len(),
2663 path
2664 );
2665 config_loaded = true;
2666 break;
2667 }
2668 }
2669 }
2670 }
2671
2672 if !config_loaded {
2673 debug!("No fitness rules found in config files, using empty registry");
2674 }
2675
2676 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2677
2678 let consumer_mapping_registry =
2682 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2683 let consumer_analyzer =
2684 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2685
2686 let protocol_state = ProtocolContractState {
2687 registry: contract_registry,
2688 drift_engine: Some(drift_engine.clone()),
2689 incident_manager: Some(incident_manager.clone()),
2690 fitness_registry: Some(fitness_registry),
2691 consumer_analyzer: Some(consumer_analyzer),
2692 };
2693
2694 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2695 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2696 }
2697
2698 #[cfg(feature = "behavioral-cloning")]
2700 {
2701 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2702 use std::path::PathBuf;
2703
2704 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2706 .ok()
2707 .map(PathBuf::from)
2708 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2709
2710 let bc_middleware_state = if let Some(path) = db_path {
2711 BehavioralCloningMiddlewareState::with_database_path(path)
2712 } else {
2713 BehavioralCloningMiddlewareState::new()
2714 };
2715
2716 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2718 .ok()
2719 .and_then(|v| v.parse::<bool>().ok())
2720 .unwrap_or(false);
2721
2722 if enabled {
2723 let bc_state_clone = bc_middleware_state.clone();
2724 app = app.layer(axum::middleware::from_fn(
2725 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2726 let state = bc_state_clone.clone();
2727 async move {
2728 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2730 req.extensions_mut().insert(state);
2731 }
2732 middleware::behavioral_cloning::behavioral_cloning_middleware(req, next)
2734 .await
2735 }
2736 },
2737 ));
2738 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2739 }
2740 }
2741
2742 {
2744 use crate::handlers::consumer_contracts::{
2745 consumer_contracts_router, ConsumerContractsState,
2746 };
2747 use mockforge_contracts::consumer_contracts::{
2748 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2749 };
2750 use std::sync::Arc;
2751
2752 let registry = Arc::new(ConsumerRegistry::default());
2754
2755 let usage_recorder = Arc::new(UsageRecorder::default());
2757
2758 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2760
2761 let consumer_state = ConsumerContractsState {
2762 registry,
2763 usage_recorder,
2764 detector,
2765 violations: Arc::new(RwLock::new(HashMap::new())),
2766 };
2767
2768 app = app.merge(consumer_contracts_router(consumer_state));
2769 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2770 }
2771
2772 #[cfg(feature = "behavioral-cloning")]
2774 {
2775 use crate::handlers::behavioral_cloning::{
2776 behavioral_cloning_router, BehavioralCloningState,
2777 };
2778 use std::path::PathBuf;
2779
2780 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2782 .ok()
2783 .map(PathBuf::from)
2784 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2785
2786 let bc_state = if let Some(path) = db_path {
2787 BehavioralCloningState::with_database_path(path)
2788 } else {
2789 BehavioralCloningState::new()
2790 };
2791
2792 app = app.merge(behavioral_cloning_router(bc_state));
2793 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2794 }
2795
2796 {
2798 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2799 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2800 use mockforge_core::consistency::ConsistencyEngine;
2801 use std::sync::Arc;
2802
2803 let consistency_engine = Arc::new(ConsistencyEngine::new());
2805
2806 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2808 consistency_engine.register_adapter(http_adapter.clone()).await;
2809
2810 let consistency_state = ConsistencyState {
2812 engine: consistency_engine.clone(),
2813 };
2814
2815 use crate::handlers::xray::XRayState;
2817 let xray_state = Arc::new(XRayState {
2818 engine: consistency_engine.clone(),
2819 request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
2820 });
2821
2822 let consistency_middleware_state = ConsistencyMiddlewareState {
2824 engine: consistency_engine.clone(),
2825 adapter: http_adapter,
2826 xray_state: Some(xray_state.clone()),
2827 };
2828
2829 if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2837 tracing::info!(
2838 upstream = %reality_cfg.upstream_base,
2839 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2840 );
2841 app = app.layer(axum::middleware::from_fn(
2842 move |req: axum::extract::Request, next: axum::middleware::Next| {
2843 let cfg = reality_cfg.clone();
2844 async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2845 },
2846 ));
2847 }
2848
2849 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2851 app = app.layer(axum::middleware::from_fn(
2852 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2853 let state = consistency_middleware_state_clone.clone();
2854 async move {
2855 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2857 req.extensions_mut().insert(state);
2858 }
2859 consistency::middleware::consistency_middleware(req, next).await
2861 }
2862 },
2863 ));
2864
2865 app = app.merge(consistency_router(consistency_state));
2867 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2868
2869 #[cfg(feature = "scenario-engine")]
2877 {
2878 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
2879 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
2880 Ok(s) => s,
2881 Err(e) => {
2882 tracing::warn!(
2883 error = %e,
2884 "Failed to init scenario storage; runtime scenarios API will list empty"
2885 );
2886 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
2889 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
2890 .expect("temp scenario storage")
2891 }
2892 };
2893 if let Err(e) = scenario_storage.load().await {
2894 tracing::warn!(
2895 error = %e,
2896 "Failed to load installed scenarios; API will list empty until scenarios are installed"
2897 );
2898 }
2899 let scenarios_state =
2900 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
2901 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
2902 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
2903 }
2904
2905 {
2907 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2908 let fidelity_state = FidelityState::new();
2909 app = app.merge(fidelity_router(fidelity_state));
2910 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2911 }
2912
2913 {
2915 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2916 let scenario_studio_state = ScenarioStudioState::new();
2917 app = app.merge(scenario_studio_router(scenario_studio_state));
2918 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2919 }
2920
2921 {
2923 use crate::handlers::performance::{performance_router, PerformanceState};
2924 let performance_state = PerformanceState::new();
2925 app = app.nest("/api/performance", performance_router(performance_state));
2926 debug!("Performance mode endpoints mounted at /api/performance");
2927 }
2928
2929 {
2931 use crate::handlers::world_state::{world_state_router, WorldStateState};
2932 use mockforge_world_state::WorldStateEngine;
2933 use std::sync::Arc;
2934 use tokio::sync::RwLock;
2935
2936 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
2937 let world_state_state = WorldStateState {
2938 engine: world_state_engine,
2939 };
2940 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
2941 debug!("World state endpoints mounted at /api/world-state");
2942 }
2943
2944 {
2946 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2947 use mockforge_core::snapshots::SnapshotManager;
2948 use std::path::PathBuf;
2949
2950 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2951 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2952
2953 let snapshot_state = SnapshotState {
2954 manager: snapshot_manager,
2955 consistency_engine: Some(consistency_engine.clone()),
2956 workspace_persistence: None, vbr_engine: None, recorder: None, };
2960
2961 app = app.merge(snapshot_router(snapshot_state));
2962 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2963
2964 {
2966 use crate::handlers::xray::xray_router;
2967 app = app.merge(xray_router((*xray_state).clone()));
2968 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2969 }
2970 }
2971
2972 {
2974 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2975 use crate::middleware::ab_testing::ab_testing_middleware;
2976
2977 let ab_testing_state = ABTestingState::new();
2978
2979 let ab_testing_state_clone = ab_testing_state.clone();
2981 app = app.layer(axum::middleware::from_fn(
2982 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2983 let state = ab_testing_state_clone.clone();
2984 async move {
2985 if req.extensions().get::<ABTestingState>().is_none() {
2987 req.extensions_mut().insert(state);
2988 }
2989 ab_testing_middleware(req, next).await
2991 }
2992 },
2993 ));
2994
2995 app = app.merge(ab_testing_router(ab_testing_state));
2997 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2998 }
2999 }
3000
3001 {
3003 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
3004 use mockforge_intelligence::pr_generation::{PRGenerator, PRProvider};
3005 use std::sync::Arc;
3006
3007 let pr_config = mockforge_intelligence::pr_generation::PRGenerationConfig::from_env();
3009
3010 let generator = if pr_config.enabled && pr_config.token.is_some() {
3011 let token = pr_config.token.as_ref().unwrap().clone();
3012 let generator = match pr_config.provider {
3013 PRProvider::GitHub => PRGenerator::new_github(
3014 pr_config.owner.clone(),
3015 pr_config.repo.clone(),
3016 token,
3017 pr_config.base_branch.clone(),
3018 ),
3019 PRProvider::GitLab => PRGenerator::new_gitlab(
3020 pr_config.owner.clone(),
3021 pr_config.repo.clone(),
3022 token,
3023 pr_config.base_branch.clone(),
3024 ),
3025 };
3026 Some(Arc::new(generator))
3027 } else {
3028 None
3029 };
3030
3031 let pr_state = PRGenerationState {
3032 generator: generator.clone(),
3033 };
3034
3035 app = app.merge(pr_generation_router(pr_state));
3036 if generator.is_some() {
3037 debug!(
3038 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3039 pr_config.provider
3040 );
3041 } else {
3042 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3043 }
3044 }
3045
3046 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3048
3049 if let Some(mt_config) = multi_tenant_config {
3051 if mt_config.enabled {
3052 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3053 use std::sync::Arc;
3054
3055 info!(
3056 "Multi-tenant mode enabled with {} routing strategy",
3057 match mt_config.routing_strategy {
3058 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3059 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3060 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3061 }
3062 );
3063
3064 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3066
3067 let default_workspace =
3069 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3070 if let Err(e) =
3071 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3072 {
3073 warn!("Failed to register default workspace: {}", e);
3074 } else {
3075 info!("Registered default workspace: '{}'", mt_config.default_workspace);
3076 }
3077
3078 let registry = Arc::new(registry);
3080
3081 let _workspace_router = WorkspaceRouter::new(registry);
3083 info!("Workspace routing middleware initialized for HTTP server");
3084 }
3085 }
3086
3087 let mut final_cors_config = cors_config;
3089 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3090 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3092 let mut rate_limit_config = middleware::RateLimitConfig {
3093 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3094 .ok()
3095 .and_then(|v| v.parse().ok())
3096 .unwrap_or(1000),
3097 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3098 .ok()
3099 .and_then(|v| v.parse().ok())
3100 .unwrap_or(2000),
3101 per_ip: true,
3102 per_endpoint: false,
3103 };
3104
3105 if let Some(deploy_config) = &deceptive_deploy_config {
3106 if deploy_config.enabled {
3107 info!("Deceptive deploy mode enabled - applying production-like configuration");
3108
3109 if let Some(prod_cors) = &deploy_config.cors {
3111 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3112 enabled: true,
3113 allowed_origins: prod_cors.allowed_origins.clone(),
3114 allowed_methods: prod_cors.allowed_methods.clone(),
3115 allowed_headers: prod_cors.allowed_headers.clone(),
3116 allow_credentials: prod_cors.allow_credentials,
3117 });
3118 info!("Applied production-like CORS configuration");
3119 }
3120
3121 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3123 rate_limit_config = middleware::RateLimitConfig {
3124 requests_per_minute: prod_rate_limit.requests_per_minute,
3125 burst: prod_rate_limit.burst,
3126 per_ip: prod_rate_limit.per_ip,
3127 per_endpoint: false,
3128 };
3129 info!(
3130 "Applied production-like rate limiting: {} req/min, burst: {}",
3131 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3132 );
3133 }
3134
3135 if !deploy_config.headers.is_empty() {
3137 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3138 production_headers = Some(std::sync::Arc::new(headers_map));
3139 info!("Configured {} production headers", deploy_config.headers.len());
3140 }
3141
3142 if let Some(prod_oauth) = &deploy_config.oauth {
3144 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3145 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3146 oauth2: Some(oauth2_config),
3147 ..Default::default()
3148 });
3149 info!("Applied production-like OAuth configuration for deceptive deploy");
3150 }
3151 }
3152 }
3153
3154 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3156 let rate_limiter =
3157 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3158
3159 let mut state = HttpServerState::new();
3160 if rate_limit_disabled {
3161 info!(
3162 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3163 );
3164 } else {
3165 state = state.with_rate_limiter(rate_limiter.clone());
3166 }
3167
3168 if let Some(headers) = production_headers.clone() {
3170 state = state.with_production_headers(headers);
3171 }
3172
3173 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3175
3176 if state.production_headers.is_some() {
3178 app =
3179 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3180 }
3181
3182 if middleware::is_keepalive_hint_enabled() {
3187 info!(
3188 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3189 );
3190 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3191 }
3192
3193 if middleware::is_conn_log_enabled() {
3198 info!(
3199 "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
3200 );
3201 app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
3202 }
3203
3204 if let Some(auth_config) = deceptive_deploy_auth_config {
3206 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3207 use std::collections::HashMap;
3208 use std::sync::Arc;
3209 use tokio::sync::RwLock;
3210
3211 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3213 match create_oauth2_client(oauth2_config) {
3214 Ok(client) => Some(client),
3215 Err(e) => {
3216 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3217 None
3218 }
3219 }
3220 } else {
3221 None
3222 };
3223
3224 let auth_state = AuthState {
3226 config: auth_config,
3227 spec: None, oauth2_client,
3229 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3230 };
3231
3232 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3234 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3235 }
3236
3237 #[cfg(feature = "runtime-daemon")]
3239 {
3240 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3241 use std::sync::Arc;
3242
3243 let daemon_config = RuntimeDaemonConfig::from_env();
3245
3246 if daemon_config.enabled {
3247 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3248
3249 let management_api_url =
3251 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3252 let port =
3253 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3254 format!("http://localhost:{}", port)
3255 });
3256
3257 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3259
3260 let detector = NotFoundDetector::new(daemon_config.clone());
3262 detector.set_generator(generator).await;
3263
3264 let detector_clone = detector.clone();
3266 app = app.layer(axum::middleware::from_fn(
3267 move |req: axum::extract::Request, next: axum::middleware::Next| {
3268 let detector = detector_clone.clone();
3269 async move { detector.detect_and_auto_create(req, next).await }
3270 },
3271 ));
3272
3273 debug!("Runtime daemon 404 detection middleware added");
3274 }
3275 }
3276
3277 {
3279 let routes_state = HttpServerState::with_routes(captured_routes);
3280 let routes_router = Router::new()
3281 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3282 .with_state(routes_state);
3283 app = app.merge(routes_router);
3284 }
3285
3286 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3288
3289 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3294
3295 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3298
3299 app = apply_cors_middleware(app, final_cors_config);
3301
3302 app
3303}
3304
3305#[test]
3309fn test_route_info_clone() {
3310 let route = RouteInfo {
3311 method: "POST".to_string(),
3312 path: "/users".to_string(),
3313 operation_id: Some("createUser".to_string()),
3314 summary: None,
3315 description: None,
3316 parameters: vec![],
3317 };
3318
3319 let cloned = route.clone();
3320 assert_eq!(route.method, cloned.method);
3321 assert_eq!(route.path, cloned.path);
3322 assert_eq!(route.operation_id, cloned.operation_id);
3323}
3324
3325#[test]
3326fn test_http_server_state_new() {
3327 let state = HttpServerState::new();
3328 assert_eq!(state.routes.len(), 0);
3329}
3330
3331#[test]
3332fn test_http_server_state_with_routes() {
3333 let routes = vec![
3334 RouteInfo {
3335 method: "GET".to_string(),
3336 path: "/users".to_string(),
3337 operation_id: Some("getUsers".to_string()),
3338 summary: None,
3339 description: None,
3340 parameters: vec![],
3341 },
3342 RouteInfo {
3343 method: "POST".to_string(),
3344 path: "/users".to_string(),
3345 operation_id: Some("createUser".to_string()),
3346 summary: None,
3347 description: None,
3348 parameters: vec![],
3349 },
3350 ];
3351
3352 let state = HttpServerState::with_routes(routes.clone());
3353 assert_eq!(state.routes.len(), 2);
3354 assert_eq!(state.routes[0].method, "GET");
3355 assert_eq!(state.routes[1].method, "POST");
3356}
3357
3358#[test]
3359fn test_http_server_state_clone() {
3360 let routes = vec![RouteInfo {
3361 method: "GET".to_string(),
3362 path: "/test".to_string(),
3363 operation_id: None,
3364 summary: None,
3365 description: None,
3366 parameters: vec![],
3367 }];
3368
3369 let state = HttpServerState::with_routes(routes);
3370 let cloned = state.clone();
3371
3372 assert_eq!(state.routes.len(), cloned.routes.len());
3373 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3374}
3375
3376#[tokio::test]
3377async fn test_build_router_without_openapi() {
3378 let _router = build_router(None, None, None).await;
3379 }
3381
3382#[tokio::test]
3383async fn test_build_router_with_nonexistent_spec() {
3384 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3385 }
3387
3388#[tokio::test]
3389async fn test_build_router_with_auth_and_latency() {
3390 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3391 }
3393
3394#[tokio::test]
3395async fn test_build_router_with_latency() {
3396 let _router = build_router_with_latency(None, None, None).await;
3397 }
3399
3400#[tokio::test]
3401async fn test_build_router_with_auth() {
3402 let _router = build_router_with_auth(None, None, None).await;
3403 }
3405
3406#[tokio::test]
3407async fn test_build_router_with_chains() {
3408 let _router = build_router_with_chains(None, None, None).await;
3409 }
3411
3412#[test]
3413fn test_route_info_with_all_fields() {
3414 let route = RouteInfo {
3415 method: "PUT".to_string(),
3416 path: "/users/{id}".to_string(),
3417 operation_id: Some("updateUser".to_string()),
3418 summary: Some("Update user".to_string()),
3419 description: Some("Updates an existing user".to_string()),
3420 parameters: vec!["id".to_string(), "body".to_string()],
3421 };
3422
3423 assert!(route.operation_id.is_some());
3424 assert!(route.summary.is_some());
3425 assert!(route.description.is_some());
3426 assert_eq!(route.parameters.len(), 2);
3427}
3428
3429#[test]
3430fn test_route_info_with_minimal_fields() {
3431 let route = RouteInfo {
3432 method: "DELETE".to_string(),
3433 path: "/users/{id}".to_string(),
3434 operation_id: None,
3435 summary: None,
3436 description: None,
3437 parameters: vec![],
3438 };
3439
3440 assert!(route.operation_id.is_none());
3441 assert!(route.summary.is_none());
3442 assert!(route.description.is_none());
3443 assert_eq!(route.parameters.len(), 0);
3444}
3445
3446#[test]
3447fn test_http_server_state_empty_routes() {
3448 let state = HttpServerState::with_routes(vec![]);
3449 assert_eq!(state.routes.len(), 0);
3450}
3451
3452#[test]
3453fn test_http_server_state_multiple_routes() {
3454 let routes = vec![
3455 RouteInfo {
3456 method: "GET".to_string(),
3457 path: "/users".to_string(),
3458 operation_id: Some("listUsers".to_string()),
3459 summary: Some("List all users".to_string()),
3460 description: None,
3461 parameters: vec![],
3462 },
3463 RouteInfo {
3464 method: "GET".to_string(),
3465 path: "/users/{id}".to_string(),
3466 operation_id: Some("getUser".to_string()),
3467 summary: Some("Get a user".to_string()),
3468 description: None,
3469 parameters: vec!["id".to_string()],
3470 },
3471 RouteInfo {
3472 method: "POST".to_string(),
3473 path: "/users".to_string(),
3474 operation_id: Some("createUser".to_string()),
3475 summary: Some("Create a user".to_string()),
3476 description: None,
3477 parameters: vec!["body".to_string()],
3478 },
3479 ];
3480
3481 let state = HttpServerState::with_routes(routes);
3482 assert_eq!(state.routes.len(), 3);
3483
3484 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3486 assert!(methods.contains(&"GET"));
3487 assert!(methods.contains(&"POST"));
3488}
3489
3490#[test]
3491fn test_http_server_state_with_rate_limiter() {
3492 use std::sync::Arc;
3493
3494 let config = middleware::RateLimitConfig::default();
3495 let rate_limiter = Arc::new(middleware::GlobalRateLimiter::new(config));
3496
3497 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3498
3499 assert!(state.rate_limiter.is_some());
3500 assert_eq!(state.routes.len(), 0);
3501}
3502
3503#[tokio::test]
3504async fn test_build_router_includes_rate_limiter() {
3505 let _router = build_router(None, None, None).await;
3506 }