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