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 let Some(auth_config) = deceptive_deploy_auth_config {
1100 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1101 use std::collections::HashMap;
1102 use std::sync::Arc;
1103 use tokio::sync::RwLock;
1104
1105 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1107 match create_oauth2_client(oauth2_config) {
1108 Ok(client) => Some(client),
1109 Err(e) => {
1110 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1111 None
1112 }
1113 }
1114 } else {
1115 None
1116 };
1117
1118 let auth_state = AuthState {
1120 config: auth_config,
1121 spec: None, oauth2_client,
1123 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1124 };
1125
1126 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1128 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1129 }
1130
1131 app = apply_cors_middleware(app, final_cors_config);
1133
1134 if let Some(mt_config) = multi_tenant_config {
1136 if mt_config.enabled {
1137 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1138 use std::sync::Arc;
1139
1140 info!(
1141 "Multi-tenant mode enabled with {} routing strategy",
1142 match mt_config.routing_strategy {
1143 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1144 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1145 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1146 }
1147 );
1148
1149 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1151
1152 let default_workspace =
1154 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1155 if let Err(e) =
1156 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1157 {
1158 warn!("Failed to register default workspace: {}", e);
1159 } else {
1160 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1161 }
1162
1163 if mt_config.auto_discover {
1165 if let Some(config_dir) = &mt_config.config_directory {
1166 let config_path = Path::new(config_dir);
1167 if config_path.exists() && config_path.is_dir() {
1168 match fs::read_dir(config_path).await {
1169 Ok(mut entries) => {
1170 while let Ok(Some(entry)) = entries.next_entry().await {
1171 let path = entry.path();
1172 if path.extension() == Some(OsStr::new("yaml")) {
1173 match fs::read_to_string(&path).await {
1174 Ok(content) => {
1175 match serde_yaml::from_str::<
1176 mockforge_core::Workspace,
1177 >(
1178 &content
1179 ) {
1180 Ok(workspace) => {
1181 if let Err(e) = registry.register_workspace(
1182 workspace.id.clone(),
1183 workspace,
1184 ) {
1185 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1186 } else {
1187 info!("Auto-registered workspace from {:?}", path);
1188 }
1189 }
1190 Err(e) => {
1191 warn!("Failed to parse workspace from {:?}: {}", path, e);
1192 }
1193 }
1194 }
1195 Err(e) => {
1196 warn!(
1197 "Failed to read workspace file {:?}: {}",
1198 path, e
1199 );
1200 }
1201 }
1202 }
1203 }
1204 }
1205 Err(e) => {
1206 warn!("Failed to read config directory {:?}: {}", config_path, e);
1207 }
1208 }
1209 } else {
1210 warn!(
1211 "Config directory {:?} does not exist or is not a directory",
1212 config_path
1213 );
1214 }
1215 }
1216 }
1217
1218 let registry = Arc::new(registry);
1220
1221 let _workspace_router = WorkspaceRouter::new(registry);
1223
1224 info!("Workspace routing middleware initialized for HTTP server");
1227 }
1228 }
1229
1230 let total_startup_duration = startup_start.elapsed();
1231 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1232
1233 app
1234}
1235
1236pub async fn build_router_with_auth_and_latency(
1238 spec_path: Option<String>,
1239 _options: Option<()>,
1240 auth_config: Option<mockforge_core::config::AuthConfig>,
1241 latency_injector: Option<LatencyInjector>,
1242) -> Router {
1243 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1245
1246 if let Some(injector) = latency_injector {
1248 let injector = Arc::new(injector);
1249 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1250 let injector = injector.clone();
1251 async move {
1252 let _ = injector.inject_latency(&[]).await;
1253 next.run(req).await
1254 }
1255 }));
1256 }
1257
1258 app
1259}
1260
1261pub async fn build_router_with_latency(
1263 spec_path: Option<String>,
1264 options: Option<ValidationOptions>,
1265 latency_injector: Option<LatencyInjector>,
1266) -> Router {
1267 if let Some(spec) = &spec_path {
1268 match OpenApiSpec::from_file(spec).await {
1269 Ok(openapi) => {
1270 let registry = if let Some(opts) = options {
1271 OpenApiRouteRegistry::new_with_options(openapi, opts)
1272 } else {
1273 OpenApiRouteRegistry::new_with_env(openapi)
1274 };
1275
1276 if let Some(injector) = latency_injector {
1277 return registry.build_router_with_latency(injector);
1278 } else {
1279 return registry.build_router();
1280 }
1281 }
1282 Err(e) => {
1283 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1284 }
1285 }
1286 }
1287
1288 build_router(None, None, None).await
1289}
1290
1291pub async fn build_router_with_auth(
1293 spec_path: Option<String>,
1294 options: Option<ValidationOptions>,
1295 auth_config: Option<mockforge_core::config::AuthConfig>,
1296) -> Router {
1297 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1298 use std::sync::Arc;
1299
1300 #[cfg(feature = "data-faker")]
1302 {
1303 register_core_faker_provider();
1304 }
1305
1306 let spec = if let Some(spec_path) = &spec_path {
1308 match OpenApiSpec::from_file(&spec_path).await {
1309 Ok(spec) => Some(Arc::new(spec)),
1310 Err(e) => {
1311 warn!("Failed to load OpenAPI spec for auth: {}", e);
1312 None
1313 }
1314 }
1315 } else {
1316 None
1317 };
1318
1319 let oauth2_client = if let Some(auth_config) = &auth_config {
1321 if let Some(oauth2_config) = &auth_config.oauth2 {
1322 match create_oauth2_client(oauth2_config) {
1323 Ok(client) => Some(client),
1324 Err(e) => {
1325 warn!("Failed to create OAuth2 client: {}", e);
1326 None
1327 }
1328 }
1329 } else {
1330 None
1331 }
1332 } else {
1333 None
1334 };
1335
1336 let auth_state = AuthState {
1337 config: auth_config.unwrap_or_default(),
1338 spec,
1339 oauth2_client,
1340 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1341 };
1342
1343 let mut app = Router::new().with_state(auth_state.clone());
1345
1346 if let Some(spec_path) = spec_path {
1348 match OpenApiSpec::from_file(&spec_path).await {
1349 Ok(openapi) => {
1350 info!("Loaded OpenAPI spec from {}", spec_path);
1351 let registry = if let Some(opts) = options {
1352 OpenApiRouteRegistry::new_with_options(openapi, opts)
1353 } else {
1354 OpenApiRouteRegistry::new_with_env(openapi)
1355 };
1356
1357 app = registry.build_router();
1358 }
1359 Err(e) => {
1360 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1361 }
1362 }
1363 }
1364
1365 app = app.route(
1367 "/health",
1368 axum::routing::get(|| async {
1369 use mockforge_core::server_utils::health::HealthStatus;
1370 {
1371 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1373 Ok(value) => Json(value),
1374 Err(e) => {
1375 tracing::error!("Failed to serialize health status: {}", e);
1377 Json(serde_json::json!({
1378 "status": "healthy",
1379 "service": "mockforge-http",
1380 "uptime_seconds": 0
1381 }))
1382 }
1383 }
1384 }
1385 }),
1386 )
1387 .merge(sse::sse_router())
1389 .merge(file_server::file_serving_router())
1391 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1393 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1395
1396 app
1397}
1398
1399pub async fn serve_router(
1401 port: u16,
1402 app: Router,
1403) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1404 serve_router_with_tls(port, app, None).await
1405}
1406
1407pub async fn serve_router_with_tls(
1409 port: u16,
1410 app: Router,
1411 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1412) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1413 serve_router_with_tls_notify(port, app, tls_config, None).await
1414}
1415
1416pub async fn serve_router_with_tls_notify(
1424 port: u16,
1425 app: Router,
1426 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1427 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1428) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1429 serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1430}
1431
1432pub async fn serve_router_with_tls_notify_chaos(
1441 port: u16,
1442 app: Router,
1443 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1444 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1445 chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1446) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1447 use std::net::SocketAddr;
1448
1449 let addr = mockforge_core::wildcard_socket_addr(port);
1450
1451 if let Some(ref tls) = tls_config {
1452 if tls.enabled {
1453 info!("HTTPS listening on {}", addr);
1454 if let Some(tx) = bound_port_tx {
1455 let _ = tx.send(port);
1456 }
1457 return serve_with_tls(addr, app, tls).await;
1458 }
1459 }
1460
1461 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1462 format!(
1463 "Failed to bind HTTP server to port {}: {}\n\
1464 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 {}",
1465 port, e, port, port
1466 )
1467 })?;
1468
1469 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1470 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1471 if let Some(tx) = bound_port_tx {
1472 let _ = tx.send(actual_port);
1473 }
1474
1475 let odata_app = tower::ServiceBuilder::new()
1479 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1480 .service(app);
1481 if let Some(cfg) = chaos_config {
1482 info!("HTTP listener wrapped with chaos TCP listener (RST/FIN injection enabled)");
1483 let chaos_listener = mockforge_chaos::ChaosTcpListener::new(listener, cfg);
1484 let app_with_addr_compat = tower::ServiceBuilder::new()
1487 .layer(axum::middleware::from_fn(copy_chaos_addr_to_socketaddr))
1488 .service(odata_app);
1489 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1490 mockforge_chaos::ChaosClientAddr,
1491 >(app_with_addr_compat);
1492 let counted = counting_listener::CountingMakeService::new(make_svc);
1494 axum::serve(chaos_listener, counted).await?;
1495 } else {
1496 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1497 SocketAddr,
1498 >(odata_app);
1499 let counted = counting_listener::CountingMakeService::new(make_svc);
1502 axum::serve(listener, counted).await?;
1503 }
1504 Ok(())
1505}
1506
1507async fn copy_chaos_addr_to_socketaddr(
1511 mut req: Request<Body>,
1512 next: axum::middleware::Next,
1513) -> axum::response::Response {
1514 use axum::extract::ConnectInfo;
1515 if let Some(ConnectInfo(chaos_addr)) =
1516 req.extensions().get::<ConnectInfo<mockforge_chaos::ChaosClientAddr>>().copied()
1517 {
1518 let sock: std::net::SocketAddr = *chaos_addr;
1519 req.extensions_mut().insert(ConnectInfo(sock));
1520 }
1521 next.run(req).await
1522}
1523
1524async fn serve_with_tls(
1529 addr: std::net::SocketAddr,
1530 app: Router,
1531 tls_config: &mockforge_core::config::HttpTlsConfig,
1532) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1533 use axum_server::tls_rustls::RustlsConfig;
1534 use std::net::SocketAddr;
1535
1536 tls::init_crypto_provider();
1538
1539 info!("Loading TLS configuration for HTTPS server");
1540
1541 let server_config = tls::load_tls_server_config(tls_config)?;
1543
1544 let rustls_config = RustlsConfig::from_config(server_config);
1547
1548 info!("Starting HTTPS server on {}", addr);
1549
1550 let odata_app = tower::ServiceBuilder::new()
1554 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1555 .service(app);
1556 let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1557 SocketAddr,
1558 >(odata_app);
1559 let counted = counting_listener::CountingMakeService::new(make_svc);
1562
1563 axum_server::bind_rustls(addr, rustls_config)
1565 .serve(counted)
1566 .await
1567 .map_err(|e| format!("HTTPS server error: {}", e).into())
1568}
1569
1570pub async fn start(
1572 port: u16,
1573 spec_path: Option<String>,
1574 options: Option<ValidationOptions>,
1575) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1576 start_with_latency(port, spec_path, options, None).await
1577}
1578
1579pub async fn start_with_auth_and_latency(
1581 port: u16,
1582 spec_path: Option<String>,
1583 options: Option<ValidationOptions>,
1584 auth_config: Option<mockforge_core::config::AuthConfig>,
1585 latency_profile: Option<LatencyProfile>,
1586) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1587 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1588 .await
1589}
1590
1591pub async fn start_with_auth_and_injectors(
1593 port: u16,
1594 spec_path: Option<String>,
1595 options: Option<ValidationOptions>,
1596 auth_config: Option<mockforge_core::config::AuthConfig>,
1597 _latency_profile: Option<LatencyProfile>,
1598 _failure_injector: Option<FailureInjector>,
1599) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1600 let app = build_router_with_auth(spec_path, options, auth_config).await;
1602 serve_router(port, app).await
1603}
1604
1605pub async fn start_with_latency(
1607 port: u16,
1608 spec_path: Option<String>,
1609 options: Option<ValidationOptions>,
1610 latency_profile: Option<LatencyProfile>,
1611) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1612 let latency_injector =
1613 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1614
1615 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1616 serve_router(port, app).await
1617}
1618
1619pub async fn build_router_with_chains(
1621 spec_path: Option<String>,
1622 options: Option<ValidationOptions>,
1623 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1624) -> Router {
1625 build_router_with_chains_and_multi_tenant(
1626 spec_path,
1627 options,
1628 circling_config,
1629 None,
1630 None,
1631 None,
1632 None,
1633 None,
1634 None,
1635 None,
1636 false,
1637 None, None, None, None, )
1642 .await
1643}
1644
1645async fn apply_route_chaos(
1653 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1654 method: &http::Method,
1655 uri: &http::Uri,
1656) -> Option<axum::response::Response> {
1657 use axum::http::StatusCode;
1658 use axum::response::IntoResponse;
1659
1660 if let Some(injector) = injector {
1661 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1663 let mut response = Json(serde_json::json!({
1665 "error": fault_response.error_message,
1666 "fault_type": fault_response.fault_type,
1667 }))
1668 .into_response();
1669 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1670 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1671 return Some(response);
1672 }
1673
1674 if let Err(e) = injector.inject_latency(method, uri).await {
1676 tracing::warn!("Failed to inject latency: {}", e);
1677 }
1678 }
1679
1680 None }
1682
1683#[allow(clippy::too_many_arguments)]
1685#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1687 spec_path: Option<String>,
1688 options: Option<ValidationOptions>,
1689 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1690 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1691 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1692 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1693 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1694 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1695 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1696 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1697 traffic_shaping_enabled: bool,
1698 health_manager: Option<Arc<HealthManager>>,
1699 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1700 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1701 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1702) -> Router {
1703 use crate::latency_profiles::LatencyProfiles;
1704 use crate::op_middleware::Shared;
1705 use mockforge_core::Overrides;
1706
1707 let template_expand =
1709 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1710 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1711 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1712 .unwrap_or(false)
1713 });
1714
1715 let _shared = Shared {
1716 profiles: LatencyProfiles::default(),
1717 overrides: Overrides::default(),
1718 failure_injector: None,
1719 traffic_shaper,
1720 overrides_enabled: false,
1721 traffic_shaping_enabled,
1722 };
1723
1724 let mut app = Router::new();
1726 let mut include_default_health = true;
1727 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1728
1729 if let Some(ref spec) = spec_path {
1731 match OpenApiSpec::from_file(&spec).await {
1732 Ok(openapi) => {
1733 info!("Loaded OpenAPI spec from {}", spec);
1734
1735 let persona = load_persona_from_config().await;
1737
1738 let mut registry = if let Some(opts) = options {
1739 tracing::debug!("Using custom validation options");
1740 if let Some(ref persona) = persona {
1741 tracing::info!("Using persona '{}' for route generation", persona.name);
1742 }
1743 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1744 } else {
1745 tracing::debug!("Using environment-based options");
1746 if let Some(ref persona) = persona {
1747 tracing::info!("Using persona '{}' for route generation", persona.name);
1748 }
1749 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1750 };
1751
1752 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1754 .unwrap_or_else(|_| "/app/fixtures".to_string());
1755 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1756 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1757 .unwrap_or(true); if custom_fixtures_enabled {
1760 use mockforge_openapi::CustomFixtureLoader;
1761 use std::path::PathBuf;
1762 use std::sync::Arc;
1763
1764 let fixtures_path = PathBuf::from(&fixtures_dir);
1765 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1766
1767 if let Err(e) = custom_loader.load_fixtures().await {
1768 tracing::warn!("Failed to load custom fixtures: {}", e);
1769 } else {
1770 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1771 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1772 }
1773 }
1774
1775 if registry
1776 .routes()
1777 .iter()
1778 .any(|route| route.method == "GET" && route.path == "/health")
1779 {
1780 include_default_health = false;
1781 }
1782 captured_routes = registry
1784 .routes()
1785 .iter()
1786 .map(|r| RouteInfo {
1787 method: r.method.clone(),
1788 path: r.path.clone(),
1789 operation_id: r.operation.operation_id.clone(),
1790 summary: r.operation.summary.clone(),
1791 description: r.operation.description.clone(),
1792 parameters: r.parameters.clone(),
1793 })
1794 .collect();
1795
1796 {
1799 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1800 captured_routes
1801 .iter()
1802 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1803 method: r.method.clone(),
1804 path: r.path.clone(),
1805 operation_id: r.operation_id.clone(),
1806 summary: r.summary.clone(),
1807 description: r.description.clone(),
1808 parameters: r.parameters.clone(),
1809 })
1810 .collect();
1811 mockforge_core::request_logger::set_global_routes(global_routes);
1812 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1813 }
1814
1815 let spec_router = if let Some(ref mockai_instance) = mockai {
1817 tracing::debug!("Building router with MockAI support");
1818 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1819 } else {
1820 registry.build_router()
1821 };
1822 app = app.merge(spec_router);
1823 }
1824 Err(e) => {
1825 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1826 }
1827 }
1828 }
1829
1830 let route_chaos_injector: Option<
1834 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1835 > = if let Some(ref route_configs) = route_configs {
1836 if !route_configs.is_empty() {
1837 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1840 route_configs.to_vec();
1841 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1842 Ok(injector) => {
1843 info!(
1844 "Initialized advanced routing features for {} route(s)",
1845 route_configs.len()
1846 );
1847 Some(std::sync::Arc::new(injector)
1850 as std::sync::Arc<
1851 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1852 >)
1853 }
1854 Err(e) => {
1855 warn!(
1856 "Failed to initialize advanced routing features: {}. Using basic routing.",
1857 e
1858 );
1859 None
1860 }
1861 }
1862 } else {
1863 None
1864 }
1865 } else {
1866 None
1867 };
1868
1869 if let Some(route_configs) = route_configs {
1870 use axum::http::StatusCode;
1871 use axum::response::IntoResponse;
1872
1873 if !route_configs.is_empty() {
1874 info!("Registering {} custom route(s) from config", route_configs.len());
1875 }
1876
1877 let injector = route_chaos_injector.clone();
1878 for route_config in route_configs {
1879 let status = route_config.response.status;
1880 let body = route_config.response.body.clone();
1881 let headers = route_config.response.headers.clone();
1882 let path = route_config.path.clone();
1883 let method = route_config.method.clone();
1884
1885 let expected_method = method.to_uppercase();
1890 let injector_clone = injector.clone();
1894 app = app.route(
1895 &path,
1896 #[allow(clippy::non_send_fields_in_send_ty)]
1897 axum::routing::any(move |req: Request<Body>| {
1898 let body = body.clone();
1899 let headers = headers.clone();
1900 let expand = template_expand;
1901 let expected = expected_method.clone();
1902 let status_code = status;
1903 let injector_for_chaos = injector_clone.clone();
1905
1906 async move {
1907 if req.method().as_str() != expected.as_str() {
1909 return axum::response::Response::builder()
1911 .status(StatusCode::METHOD_NOT_ALLOWED)
1912 .header("Allow", &expected)
1913 .body(Body::empty())
1914 .unwrap()
1915 .into_response();
1916 }
1917
1918 if let Some(fault_response) = apply_route_chaos(
1922 injector_for_chaos.as_deref(),
1923 req.method(),
1924 req.uri(),
1925 )
1926 .await
1927 {
1928 return fault_response;
1929 }
1930
1931 let mut body_value = body.unwrap_or(serde_json::json!({}));
1933
1934 if expand {
1938 use mockforge_template_expansion::RequestContext;
1939 use serde_json::Value;
1940 use std::collections::HashMap;
1941
1942 let method = req.method().to_string();
1944 let path = req.uri().path().to_string();
1945
1946 let query_params: HashMap<String, Value> = req
1948 .uri()
1949 .query()
1950 .map(|q| {
1951 url::form_urlencoded::parse(q.as_bytes())
1952 .into_owned()
1953 .map(|(k, v)| (k, Value::String(v)))
1954 .collect()
1955 })
1956 .unwrap_or_default();
1957
1958 let headers: HashMap<String, Value> = req
1960 .headers()
1961 .iter()
1962 .map(|(k, v)| {
1963 (
1964 k.to_string(),
1965 Value::String(v.to_str().unwrap_or_default().to_string()),
1966 )
1967 })
1968 .collect();
1969
1970 let context = RequestContext {
1974 method,
1975 path,
1976 query_params,
1977 headers,
1978 body: None, path_params: HashMap::new(),
1980 multipart_fields: HashMap::new(),
1981 multipart_files: HashMap::new(),
1982 };
1983
1984 let body_value_clone = body_value.clone();
1988 let context_clone = context.clone();
1989 body_value = match tokio::task::spawn_blocking(move || {
1990 mockforge_template_expansion::expand_templates_in_json(
1991 body_value_clone,
1992 &context_clone,
1993 )
1994 })
1995 .await
1996 {
1997 Ok(result) => result,
1998 Err(_) => body_value, };
2000 }
2001
2002 let mut response = Json(body_value).into_response();
2003
2004 *response.status_mut() =
2006 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2007
2008 for (key, value) in headers {
2010 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2011 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2012 response.headers_mut().insert(header_name, header_value);
2013 }
2014 }
2015 }
2016
2017 response
2018 }
2019 }),
2020 );
2021
2022 debug!("Registered route: {} {}", method, path);
2023 }
2024 }
2025
2026 if let Some(health) = health_manager {
2028 app = app.merge(health::health_router(health));
2030 info!(
2031 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
2032 );
2033 } else if include_default_health {
2034 app = app.route(
2036 "/health",
2037 axum::routing::get(|| async {
2038 use mockforge_core::server_utils::health::HealthStatus;
2039 {
2040 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2042 Ok(value) => Json(value),
2043 Err(e) => {
2044 tracing::error!("Failed to serialize health status: {}", e);
2046 Json(serde_json::json!({
2047 "status": "healthy",
2048 "service": "mockforge-http",
2049 "uptime_seconds": 0
2050 }))
2051 }
2052 }
2053 }
2054 }),
2055 );
2056 }
2057
2058 app = app.merge(sse::sse_router());
2059 app = app.merge(file_server::file_serving_router());
2061
2062 let mgmt_spec = if let Some(ref sp) = spec_path {
2065 match OpenApiSpec::from_file(sp).await {
2066 Ok(s) => Some(Arc::new(s)),
2067 Err(e) => {
2068 debug!("Failed to load OpenAPI spec for management API: {}", e);
2069 None
2070 }
2071 }
2072 } else {
2073 None
2074 };
2075 let spec_path_clone = spec_path.clone();
2076 let mgmt_port = std::env::var("PORT")
2077 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2078 .ok()
2079 .and_then(|p| p.parse().ok())
2080 .unwrap_or(3000);
2081 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2082
2083 use std::sync::Arc;
2085 let ws_state = WsManagementState::new();
2086 let ws_broadcast = Arc::new(ws_state.tx.clone());
2087 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2088
2089 let management_state = if let Some(proxy_cfg) = proxy_config {
2091 use tokio::sync::RwLock;
2092 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2093 management_state.with_proxy_config(proxy_config_arc)
2094 } else {
2095 management_state
2096 };
2097
2098 #[cfg(feature = "smtp")]
2099 let management_state = {
2100 if let Some(smtp_reg) = smtp_registry {
2101 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2102 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2103 Err(e) => {
2104 error!(
2105 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2106 e.type_id()
2107 );
2108 management_state
2109 }
2110 }
2111 } else {
2112 management_state
2113 }
2114 };
2115 #[cfg(not(feature = "smtp"))]
2116 let management_state = {
2117 let _ = smtp_registry;
2118 management_state
2119 };
2120 #[cfg(feature = "mqtt")]
2121 let management_state = {
2122 if let Some(broker) = mqtt_broker {
2123 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
2124 Ok(broker) => management_state.with_mqtt_broker(broker),
2125 Err(e) => {
2126 error!(
2127 "Invalid MQTT broker passed to HTTP management state: {:?}",
2128 e.type_id()
2129 );
2130 management_state
2131 }
2132 }
2133 } else {
2134 management_state
2135 }
2136 };
2137 #[cfg(not(feature = "mqtt"))]
2138 let management_state = {
2139 let _ = mqtt_broker;
2140 management_state
2141 };
2142 let management_state_for_fallback = management_state.clone();
2143 app = app.nest("/__mockforge/api", management_router(management_state));
2144 app = app.fallback_service(
2146 axum::routing::any(management::dynamic_mock_fallback)
2147 .with_state(management_state_for_fallback),
2148 );
2149
2150 app = app.merge(verification_router());
2152
2153 {
2158 use crate::chain_handlers::{chains_router, create_chain_state};
2159 let chain_config = _circling_config.clone().unwrap_or_default();
2160 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2161 chain_config.clone(),
2162 ));
2163 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2164 chain_registry.clone(),
2165 chain_config,
2166 ));
2167 app = app.nest(
2168 "/__mockforge/chains",
2169 chains_router(create_chain_state(chain_registry, chain_engine)),
2170 );
2171 }
2172
2173 {
2178 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2179 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2180 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2181 }
2182
2183 {
2190 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2191 let fx_state = FixturesApiState::from_env();
2192 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2193 }
2194
2195 {
2202 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2203 let api_state = MockAiApiState::new(mockai.clone());
2204 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2205 }
2206
2207 app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2214
2215 {
2220 use crate::route_chaos_runtime::{
2221 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2222 };
2223 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2224 let middleware_state = runtime_state.clone();
2225 app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2226 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2227 }
2228
2229 {
2235 use crate::network_profile_runtime::{
2236 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2237 };
2238 let runtime_state = NetworkProfileRuntimeState::new(
2239 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2240 );
2241 let middleware_state = runtime_state.clone();
2242 app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2243 app = app
2244 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2245 }
2246
2247 use crate::auth::oidc::oidc_router;
2249 app = app.merge(oidc_router());
2250
2251 {
2253 use mockforge_core::security::get_global_access_review_service;
2254 if let Some(service) = get_global_access_review_service().await {
2255 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2256 let review_state = AccessReviewState { service };
2257 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2258 debug!("Access review API mounted at /api/v1/security/access-reviews");
2259 }
2260 }
2261
2262 {
2264 use mockforge_core::security::get_global_privileged_access_manager;
2265 if let Some(manager) = get_global_privileged_access_manager().await {
2266 use crate::handlers::privileged_access::{
2267 privileged_access_router, PrivilegedAccessState,
2268 };
2269 let privileged_state = PrivilegedAccessState { manager };
2270 app = app.nest(
2271 "/api/v1/security/privileged-access",
2272 privileged_access_router(privileged_state),
2273 );
2274 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2275 }
2276 }
2277
2278 {
2280 use mockforge_core::security::get_global_change_management_engine;
2281 if let Some(engine) = get_global_change_management_engine().await {
2282 use crate::handlers::change_management::{
2283 change_management_router, ChangeManagementState,
2284 };
2285 let change_state = ChangeManagementState { engine };
2286 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2287 debug!("Change management API mounted at /api/v1/change-management");
2288 }
2289 }
2290
2291 {
2293 use mockforge_core::security::get_global_risk_assessment_engine;
2294 if let Some(engine) = get_global_risk_assessment_engine().await {
2295 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2296 let risk_state = RiskAssessmentState { engine };
2297 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2298 debug!("Risk assessment API mounted at /api/v1/security/risks");
2299 }
2300 }
2301
2302 {
2304 use crate::auth::token_lifecycle::TokenLifecycleManager;
2305 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2306 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2307 let lifecycle_state = TokenLifecycleState {
2308 manager: lifecycle_manager,
2309 };
2310 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2311 debug!("Token lifecycle API mounted at /api/v1/auth");
2312 }
2313
2314 {
2316 use crate::auth::oidc::load_oidc_state;
2317 use crate::auth::token_lifecycle::TokenLifecycleManager;
2318 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2319 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2321 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2322 let oauth2_state = OAuth2ServerState {
2323 oidc_state,
2324 lifecycle_manager,
2325 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2326 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2327 };
2328 app = app.merge(oauth2_server_router(oauth2_state));
2329 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2330 }
2331
2332 {
2334 use crate::auth::oidc::load_oidc_state;
2335 use crate::auth::risk_engine::RiskEngine;
2336 use crate::auth::token_lifecycle::TokenLifecycleManager;
2337 use crate::handlers::consent::{consent_router, ConsentState};
2338 use crate::handlers::oauth2_server::OAuth2ServerState;
2339 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2341 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2342 let oauth2_state = OAuth2ServerState {
2343 oidc_state: oidc_state.clone(),
2344 lifecycle_manager: lifecycle_manager.clone(),
2345 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2346 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2347 };
2348 let risk_engine = Arc::new(RiskEngine::default());
2349 let consent_state = ConsentState {
2350 oauth2_state,
2351 risk_engine,
2352 };
2353 app = app.merge(consent_router(consent_state));
2354 debug!("Consent screen endpoints mounted at /consent");
2355 }
2356
2357 {
2359 use crate::auth::risk_engine::RiskEngine;
2360 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2361 let risk_engine = Arc::new(RiskEngine::default());
2362 let risk_state = RiskSimulationState { risk_engine };
2363 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2364 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2365 }
2366
2367 let database = {
2369 use crate::database::Database;
2370 let database_url = std::env::var("DATABASE_URL").ok();
2371 match Database::connect_optional(database_url.as_deref()).await {
2372 Ok(db) => {
2373 if db.is_connected() {
2374 if let Err(e) = db.migrate_if_connected().await {
2376 warn!("Failed to run database migrations: {}", e);
2377 } else {
2378 info!("Database connected and migrations applied");
2379 }
2380 }
2381 Some(db)
2382 }
2383 Err(e) => {
2384 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2385 None
2386 }
2387 }
2388 };
2389
2390 let (drift_engine, incident_manager, drift_config) = {
2393 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2394 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2395 use std::sync::Arc;
2396
2397 let drift_config = DriftBudgetConfig::default();
2399 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2400
2401 let incident_store = Arc::new(IncidentStore::default());
2403 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2404
2405 (drift_engine, incident_manager, drift_config)
2406 };
2407
2408 {
2409 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2410 use crate::middleware::drift_tracking::DriftTrackingState;
2411 use mockforge_contracts::consumer_contracts::{
2412 ConsumerBreakingChangeDetector, UsageRecorder,
2413 };
2414 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2415 use std::sync::Arc;
2416
2417 let usage_recorder = Arc::new(UsageRecorder::default());
2419 let consumer_detector =
2420 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2421
2422 let diff_analyzer = if drift_config.enabled {
2424 match ContractDiffAnalyzer::new(
2425 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2426 ) {
2427 Ok(analyzer) => Some(Arc::new(analyzer)),
2428 Err(e) => {
2429 warn!("Failed to create contract diff analyzer: {}", e);
2430 None
2431 }
2432 }
2433 } else {
2434 None
2435 };
2436
2437 let spec = if let Some(ref spec_path) = spec_path {
2440 match OpenApiSpec::from_file(spec_path).await {
2441 Ok(s) => Some(Arc::new(s)),
2442 Err(e) => {
2443 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2444 None
2445 }
2446 }
2447 } else {
2448 None
2449 };
2450
2451 let drift_tracking_state = DriftTrackingState {
2453 diff_analyzer,
2454 spec,
2455 drift_engine: drift_engine.clone(),
2456 incident_manager: incident_manager.clone(),
2457 usage_recorder,
2458 consumer_detector,
2459 enabled: drift_config.enabled,
2460 };
2461
2462 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2464
2465 let drift_tracking_state_clone = drift_tracking_state.clone();
2468 app = app.layer(axum::middleware::from_fn(
2469 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2470 let state = drift_tracking_state_clone.clone();
2471 async move {
2472 if req.extensions().get::<DriftTrackingState>().is_none() {
2474 req.extensions_mut().insert(state);
2475 }
2476 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2478 .await
2479 }
2480 },
2481 ));
2482
2483 let drift_state = DriftBudgetState {
2484 engine: drift_engine.clone(),
2485 incident_manager: incident_manager.clone(),
2486 gitops_handler: None, };
2488
2489 app = app.merge(drift_budget_router(drift_state));
2490 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2491 }
2492
2493 #[cfg(feature = "pipelines")]
2495 {
2496 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2497
2498 let pipeline_state = PipelineState::new();
2499 app = app.merge(pipeline_router(pipeline_state));
2500 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2501 }
2502
2503 {
2505 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2506 use crate::handlers::forecasting::{forecasting_router, ForecastingState};
2507 use crate::handlers::semantic_drift::{semantic_drift_router, SemanticDriftState};
2508 use crate::handlers::threat_modeling::{threat_modeling_router, ThreatModelingState};
2509 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2510 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2511 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2512 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2513 use std::sync::Arc;
2514
2515 let forecasting_config = ForecastingConfig::default();
2517 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2518 let forecasting_state = ForecastingState {
2519 forecaster,
2520 database: database.clone(),
2521 };
2522
2523 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2525 let semantic_state = SemanticDriftState {
2526 manager: semantic_manager,
2527 database: database.clone(),
2528 };
2529
2530 let threat_config = ThreatModelingConfig::default();
2532 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2533 Ok(analyzer) => Arc::new(analyzer),
2534 Err(e) => {
2535 warn!("Failed to create threat analyzer: {}. Using default.", e);
2536 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2537 |_| {
2538 ThreatAnalyzer::new(ThreatModelingConfig {
2540 enabled: false,
2541 ..Default::default()
2542 })
2543 .expect("Failed to create fallback threat analyzer")
2544 },
2545 ))
2546 }
2547 };
2548 let mut webhook_configs = Vec::new();
2550 let config_paths = [
2551 "config.yaml",
2552 "mockforge.yaml",
2553 "tools/mockforge/config.yaml",
2554 "../tools/mockforge/config.yaml",
2555 ];
2556
2557 for path in &config_paths {
2558 if let Ok(config) = mockforge_core::config::load_config(path).await {
2559 if !config.incidents.webhooks.is_empty() {
2560 webhook_configs = config.incidents.webhooks.clone();
2561 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2562 break;
2563 }
2564 }
2565 }
2566
2567 if webhook_configs.is_empty() {
2568 debug!("No webhook configs found in config files, using empty list");
2569 }
2570
2571 let threat_state = ThreatModelingState {
2572 analyzer: threat_analyzer,
2573 webhook_configs,
2574 database: database.clone(),
2575 };
2576
2577 let contract_health_state = ContractHealthState {
2579 incident_manager: incident_manager.clone(),
2580 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2581 database: database.clone(),
2582 };
2583
2584 app = app.merge(forecasting_router(forecasting_state));
2586 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2587
2588 app = app.merge(semantic_drift_router(semantic_state));
2589 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2590
2591 app = app.merge(threat_modeling_router(threat_state));
2592 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2593
2594 app = app.merge(contract_health_router(contract_health_state));
2595 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2596 }
2597
2598 {
2600 use crate::handlers::protocol_contracts::{
2601 protocol_contracts_router, ProtocolContractState,
2602 };
2603 use mockforge_core::contract_drift::{
2604 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2605 };
2606 use std::sync::Arc;
2607 use tokio::sync::RwLock;
2608
2609 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2611
2612 let mut fitness_registry = FitnessFunctionRegistry::new();
2614
2615 let config_paths = [
2617 "config.yaml",
2618 "mockforge.yaml",
2619 "tools/mockforge/config.yaml",
2620 "../tools/mockforge/config.yaml",
2621 ];
2622
2623 let mut config_loaded = false;
2624 for path in &config_paths {
2625 if let Ok(config) = mockforge_core::config::load_config(path).await {
2626 if !config.contracts.fitness_rules.is_empty() {
2627 if let Err(e) =
2628 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2629 {
2630 warn!("Failed to load fitness rules from config {}: {}", path, e);
2631 } else {
2632 info!(
2633 "Loaded {} fitness rules from config: {}",
2634 config.contracts.fitness_rules.len(),
2635 path
2636 );
2637 config_loaded = true;
2638 break;
2639 }
2640 }
2641 }
2642 }
2643
2644 if !config_loaded {
2645 debug!("No fitness rules found in config files, using empty registry");
2646 }
2647
2648 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2649
2650 let consumer_mapping_registry =
2654 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2655 let consumer_analyzer =
2656 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2657
2658 let protocol_state = ProtocolContractState {
2659 registry: contract_registry,
2660 drift_engine: Some(drift_engine.clone()),
2661 incident_manager: Some(incident_manager.clone()),
2662 fitness_registry: Some(fitness_registry),
2663 consumer_analyzer: Some(consumer_analyzer),
2664 };
2665
2666 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2667 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2668 }
2669
2670 #[cfg(feature = "behavioral-cloning")]
2672 {
2673 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2674 use std::path::PathBuf;
2675
2676 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2678 .ok()
2679 .map(PathBuf::from)
2680 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2681
2682 let bc_middleware_state = if let Some(path) = db_path {
2683 BehavioralCloningMiddlewareState::with_database_path(path)
2684 } else {
2685 BehavioralCloningMiddlewareState::new()
2686 };
2687
2688 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2690 .ok()
2691 .and_then(|v| v.parse::<bool>().ok())
2692 .unwrap_or(false);
2693
2694 if enabled {
2695 let bc_state_clone = bc_middleware_state.clone();
2696 app = app.layer(axum::middleware::from_fn(
2697 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2698 let state = bc_state_clone.clone();
2699 async move {
2700 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2702 req.extensions_mut().insert(state);
2703 }
2704 crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
2706 req, next,
2707 )
2708 .await
2709 }
2710 },
2711 ));
2712 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2713 }
2714 }
2715
2716 {
2718 use crate::handlers::consumer_contracts::{
2719 consumer_contracts_router, ConsumerContractsState,
2720 };
2721 use mockforge_contracts::consumer_contracts::{
2722 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2723 };
2724 use std::sync::Arc;
2725
2726 let registry = Arc::new(ConsumerRegistry::default());
2728
2729 let usage_recorder = Arc::new(UsageRecorder::default());
2731
2732 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2734
2735 let consumer_state = ConsumerContractsState {
2736 registry,
2737 usage_recorder,
2738 detector,
2739 violations: Arc::new(RwLock::new(HashMap::new())),
2740 };
2741
2742 app = app.merge(consumer_contracts_router(consumer_state));
2743 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2744 }
2745
2746 #[cfg(feature = "behavioral-cloning")]
2748 {
2749 use crate::handlers::behavioral_cloning::{
2750 behavioral_cloning_router, BehavioralCloningState,
2751 };
2752 use std::path::PathBuf;
2753
2754 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2756 .ok()
2757 .map(PathBuf::from)
2758 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2759
2760 let bc_state = if let Some(path) = db_path {
2761 BehavioralCloningState::with_database_path(path)
2762 } else {
2763 BehavioralCloningState::new()
2764 };
2765
2766 app = app.merge(behavioral_cloning_router(bc_state));
2767 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2768 }
2769
2770 {
2772 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2773 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2774 use mockforge_core::consistency::ConsistencyEngine;
2775 use std::sync::Arc;
2776
2777 let consistency_engine = Arc::new(ConsistencyEngine::new());
2779
2780 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2782 consistency_engine.register_adapter(http_adapter.clone()).await;
2783
2784 let consistency_state = ConsistencyState {
2786 engine: consistency_engine.clone(),
2787 };
2788
2789 use crate::handlers::xray::XRayState;
2791 let xray_state = Arc::new(XRayState {
2792 engine: consistency_engine.clone(),
2793 request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
2794 });
2795
2796 let consistency_middleware_state = ConsistencyMiddlewareState {
2798 engine: consistency_engine.clone(),
2799 adapter: http_adapter,
2800 xray_state: Some(xray_state.clone()),
2801 };
2802
2803 if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2811 tracing::info!(
2812 upstream = %reality_cfg.upstream_base,
2813 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2814 );
2815 app = app.layer(axum::middleware::from_fn(
2816 move |req: axum::extract::Request, next: axum::middleware::Next| {
2817 let cfg = reality_cfg.clone();
2818 async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2819 },
2820 ));
2821 }
2822
2823 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2825 app = app.layer(axum::middleware::from_fn(
2826 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2827 let state = consistency_middleware_state_clone.clone();
2828 async move {
2829 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2831 req.extensions_mut().insert(state);
2832 }
2833 consistency::middleware::consistency_middleware(req, next).await
2835 }
2836 },
2837 ));
2838
2839 app = app.merge(consistency_router(consistency_state));
2841 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2842
2843 #[cfg(feature = "scenario-engine")]
2851 {
2852 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
2853 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
2854 Ok(s) => s,
2855 Err(e) => {
2856 tracing::warn!(
2857 error = %e,
2858 "Failed to init scenario storage; runtime scenarios API will list empty"
2859 );
2860 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
2863 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
2864 .expect("temp scenario storage")
2865 }
2866 };
2867 if let Err(e) = scenario_storage.load().await {
2868 tracing::warn!(
2869 error = %e,
2870 "Failed to load installed scenarios; API will list empty until scenarios are installed"
2871 );
2872 }
2873 let scenarios_state =
2874 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
2875 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
2876 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
2877 }
2878
2879 {
2881 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2882 let fidelity_state = FidelityState::new();
2883 app = app.merge(fidelity_router(fidelity_state));
2884 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2885 }
2886
2887 {
2889 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2890 let scenario_studio_state = ScenarioStudioState::new();
2891 app = app.merge(scenario_studio_router(scenario_studio_state));
2892 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2893 }
2894
2895 {
2897 use crate::handlers::performance::{performance_router, PerformanceState};
2898 let performance_state = PerformanceState::new();
2899 app = app.nest("/api/performance", performance_router(performance_state));
2900 debug!("Performance mode endpoints mounted at /api/performance");
2901 }
2902
2903 {
2905 use crate::handlers::world_state::{world_state_router, WorldStateState};
2906 use mockforge_world_state::WorldStateEngine;
2907 use std::sync::Arc;
2908 use tokio::sync::RwLock;
2909
2910 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
2911 let world_state_state = WorldStateState {
2912 engine: world_state_engine,
2913 };
2914 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
2915 debug!("World state endpoints mounted at /api/world-state");
2916 }
2917
2918 {
2920 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2921 use mockforge_core::snapshots::SnapshotManager;
2922 use std::path::PathBuf;
2923
2924 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2925 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2926
2927 let snapshot_state = SnapshotState {
2928 manager: snapshot_manager,
2929 consistency_engine: Some(consistency_engine.clone()),
2930 workspace_persistence: None, vbr_engine: None, recorder: None, };
2934
2935 app = app.merge(snapshot_router(snapshot_state));
2936 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2937
2938 {
2940 use crate::handlers::xray::xray_router;
2941 app = app.merge(xray_router((*xray_state).clone()));
2942 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2943 }
2944 }
2945
2946 {
2948 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2949 use crate::middleware::ab_testing::ab_testing_middleware;
2950
2951 let ab_testing_state = ABTestingState::new();
2952
2953 let ab_testing_state_clone = ab_testing_state.clone();
2955 app = app.layer(axum::middleware::from_fn(
2956 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2957 let state = ab_testing_state_clone.clone();
2958 async move {
2959 if req.extensions().get::<ABTestingState>().is_none() {
2961 req.extensions_mut().insert(state);
2962 }
2963 ab_testing_middleware(req, next).await
2965 }
2966 },
2967 ));
2968
2969 app = app.merge(ab_testing_router(ab_testing_state));
2971 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2972 }
2973 }
2974
2975 {
2977 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
2978 use mockforge_core::pr_generation::{PRGenerator, PRProvider};
2979 use std::sync::Arc;
2980
2981 let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
2983
2984 let generator = if pr_config.enabled && pr_config.token.is_some() {
2985 let token = pr_config.token.as_ref().unwrap().clone();
2986 let generator = match pr_config.provider {
2987 PRProvider::GitHub => PRGenerator::new_github(
2988 pr_config.owner.clone(),
2989 pr_config.repo.clone(),
2990 token,
2991 pr_config.base_branch.clone(),
2992 ),
2993 PRProvider::GitLab => PRGenerator::new_gitlab(
2994 pr_config.owner.clone(),
2995 pr_config.repo.clone(),
2996 token,
2997 pr_config.base_branch.clone(),
2998 ),
2999 };
3000 Some(Arc::new(generator))
3001 } else {
3002 None
3003 };
3004
3005 let pr_state = PRGenerationState {
3006 generator: generator.clone(),
3007 };
3008
3009 app = app.merge(pr_generation_router(pr_state));
3010 if generator.is_some() {
3011 debug!(
3012 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3013 pr_config.provider
3014 );
3015 } else {
3016 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3017 }
3018 }
3019
3020 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3022
3023 if let Some(mt_config) = multi_tenant_config {
3025 if mt_config.enabled {
3026 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3027 use std::sync::Arc;
3028
3029 info!(
3030 "Multi-tenant mode enabled with {} routing strategy",
3031 match mt_config.routing_strategy {
3032 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3033 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3034 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3035 }
3036 );
3037
3038 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3040
3041 let default_workspace =
3043 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3044 if let Err(e) =
3045 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3046 {
3047 warn!("Failed to register default workspace: {}", e);
3048 } else {
3049 info!("Registered default workspace: '{}'", mt_config.default_workspace);
3050 }
3051
3052 let registry = Arc::new(registry);
3054
3055 let _workspace_router = WorkspaceRouter::new(registry);
3057 info!("Workspace routing middleware initialized for HTTP server");
3058 }
3059 }
3060
3061 let mut final_cors_config = cors_config;
3063 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3064 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3066 let mut rate_limit_config = middleware::RateLimitConfig {
3067 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3068 .ok()
3069 .and_then(|v| v.parse().ok())
3070 .unwrap_or(1000),
3071 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3072 .ok()
3073 .and_then(|v| v.parse().ok())
3074 .unwrap_or(2000),
3075 per_ip: true,
3076 per_endpoint: false,
3077 };
3078
3079 if let Some(deploy_config) = &deceptive_deploy_config {
3080 if deploy_config.enabled {
3081 info!("Deceptive deploy mode enabled - applying production-like configuration");
3082
3083 if let Some(prod_cors) = &deploy_config.cors {
3085 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3086 enabled: true,
3087 allowed_origins: prod_cors.allowed_origins.clone(),
3088 allowed_methods: prod_cors.allowed_methods.clone(),
3089 allowed_headers: prod_cors.allowed_headers.clone(),
3090 allow_credentials: prod_cors.allow_credentials,
3091 });
3092 info!("Applied production-like CORS configuration");
3093 }
3094
3095 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3097 rate_limit_config = middleware::RateLimitConfig {
3098 requests_per_minute: prod_rate_limit.requests_per_minute,
3099 burst: prod_rate_limit.burst,
3100 per_ip: prod_rate_limit.per_ip,
3101 per_endpoint: false,
3102 };
3103 info!(
3104 "Applied production-like rate limiting: {} req/min, burst: {}",
3105 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3106 );
3107 }
3108
3109 if !deploy_config.headers.is_empty() {
3111 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3112 production_headers = Some(std::sync::Arc::new(headers_map));
3113 info!("Configured {} production headers", deploy_config.headers.len());
3114 }
3115
3116 if let Some(prod_oauth) = &deploy_config.oauth {
3118 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3119 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3120 oauth2: Some(oauth2_config),
3121 ..Default::default()
3122 });
3123 info!("Applied production-like OAuth configuration for deceptive deploy");
3124 }
3125 }
3126 }
3127
3128 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3130 let rate_limiter =
3131 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3132
3133 let mut state = HttpServerState::new();
3134 if rate_limit_disabled {
3135 info!(
3136 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3137 );
3138 } else {
3139 state = state.with_rate_limiter(rate_limiter.clone());
3140 }
3141
3142 if let Some(headers) = production_headers.clone() {
3144 state = state.with_production_headers(headers);
3145 }
3146
3147 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3149
3150 if state.production_headers.is_some() {
3152 app =
3153 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3154 }
3155
3156 if middleware::is_keepalive_hint_enabled() {
3161 info!(
3162 "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3163 );
3164 app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3165 }
3166
3167 if let Some(auth_config) = deceptive_deploy_auth_config {
3169 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3170 use std::collections::HashMap;
3171 use std::sync::Arc;
3172 use tokio::sync::RwLock;
3173
3174 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3176 match create_oauth2_client(oauth2_config) {
3177 Ok(client) => Some(client),
3178 Err(e) => {
3179 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3180 None
3181 }
3182 }
3183 } else {
3184 None
3185 };
3186
3187 let auth_state = AuthState {
3189 config: auth_config,
3190 spec: None, oauth2_client,
3192 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3193 };
3194
3195 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3197 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3198 }
3199
3200 #[cfg(feature = "runtime-daemon")]
3202 {
3203 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3204 use std::sync::Arc;
3205
3206 let daemon_config = RuntimeDaemonConfig::from_env();
3208
3209 if daemon_config.enabled {
3210 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3211
3212 let management_api_url =
3214 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3215 let port =
3216 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3217 format!("http://localhost:{}", port)
3218 });
3219
3220 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3222
3223 let detector = NotFoundDetector::new(daemon_config.clone());
3225 detector.set_generator(generator).await;
3226
3227 let detector_clone = detector.clone();
3229 app = app.layer(axum::middleware::from_fn(
3230 move |req: axum::extract::Request, next: axum::middleware::Next| {
3231 let detector = detector_clone.clone();
3232 async move { detector.detect_and_auto_create(req, next).await }
3233 },
3234 ));
3235
3236 debug!("Runtime daemon 404 detection middleware added");
3237 }
3238 }
3239
3240 {
3242 let routes_state = HttpServerState::with_routes(captured_routes);
3243 let routes_router = Router::new()
3244 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3245 .with_state(routes_state);
3246 app = app.merge(routes_router);
3247 }
3248
3249 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3251
3252 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3257
3258 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3261
3262 app = apply_cors_middleware(app, final_cors_config);
3264
3265 app
3266}
3267
3268#[test]
3272fn test_route_info_clone() {
3273 let route = RouteInfo {
3274 method: "POST".to_string(),
3275 path: "/users".to_string(),
3276 operation_id: Some("createUser".to_string()),
3277 summary: None,
3278 description: None,
3279 parameters: vec![],
3280 };
3281
3282 let cloned = route.clone();
3283 assert_eq!(route.method, cloned.method);
3284 assert_eq!(route.path, cloned.path);
3285 assert_eq!(route.operation_id, cloned.operation_id);
3286}
3287
3288#[test]
3289fn test_http_server_state_new() {
3290 let state = HttpServerState::new();
3291 assert_eq!(state.routes.len(), 0);
3292}
3293
3294#[test]
3295fn test_http_server_state_with_routes() {
3296 let routes = vec![
3297 RouteInfo {
3298 method: "GET".to_string(),
3299 path: "/users".to_string(),
3300 operation_id: Some("getUsers".to_string()),
3301 summary: None,
3302 description: None,
3303 parameters: vec![],
3304 },
3305 RouteInfo {
3306 method: "POST".to_string(),
3307 path: "/users".to_string(),
3308 operation_id: Some("createUser".to_string()),
3309 summary: None,
3310 description: None,
3311 parameters: vec![],
3312 },
3313 ];
3314
3315 let state = HttpServerState::with_routes(routes.clone());
3316 assert_eq!(state.routes.len(), 2);
3317 assert_eq!(state.routes[0].method, "GET");
3318 assert_eq!(state.routes[1].method, "POST");
3319}
3320
3321#[test]
3322fn test_http_server_state_clone() {
3323 let routes = vec![RouteInfo {
3324 method: "GET".to_string(),
3325 path: "/test".to_string(),
3326 operation_id: None,
3327 summary: None,
3328 description: None,
3329 parameters: vec![],
3330 }];
3331
3332 let state = HttpServerState::with_routes(routes);
3333 let cloned = state.clone();
3334
3335 assert_eq!(state.routes.len(), cloned.routes.len());
3336 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3337}
3338
3339#[tokio::test]
3340async fn test_build_router_without_openapi() {
3341 let _router = build_router(None, None, None).await;
3342 }
3344
3345#[tokio::test]
3346async fn test_build_router_with_nonexistent_spec() {
3347 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3348 }
3350
3351#[tokio::test]
3352async fn test_build_router_with_auth_and_latency() {
3353 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3354 }
3356
3357#[tokio::test]
3358async fn test_build_router_with_latency() {
3359 let _router = build_router_with_latency(None, None, None).await;
3360 }
3362
3363#[tokio::test]
3364async fn test_build_router_with_auth() {
3365 let _router = build_router_with_auth(None, None, None).await;
3366 }
3368
3369#[tokio::test]
3370async fn test_build_router_with_chains() {
3371 let _router = build_router_with_chains(None, None, None).await;
3372 }
3374
3375#[test]
3376fn test_route_info_with_all_fields() {
3377 let route = RouteInfo {
3378 method: "PUT".to_string(),
3379 path: "/users/{id}".to_string(),
3380 operation_id: Some("updateUser".to_string()),
3381 summary: Some("Update user".to_string()),
3382 description: Some("Updates an existing user".to_string()),
3383 parameters: vec!["id".to_string(), "body".to_string()],
3384 };
3385
3386 assert!(route.operation_id.is_some());
3387 assert!(route.summary.is_some());
3388 assert!(route.description.is_some());
3389 assert_eq!(route.parameters.len(), 2);
3390}
3391
3392#[test]
3393fn test_route_info_with_minimal_fields() {
3394 let route = RouteInfo {
3395 method: "DELETE".to_string(),
3396 path: "/users/{id}".to_string(),
3397 operation_id: None,
3398 summary: None,
3399 description: None,
3400 parameters: vec![],
3401 };
3402
3403 assert!(route.operation_id.is_none());
3404 assert!(route.summary.is_none());
3405 assert!(route.description.is_none());
3406 assert_eq!(route.parameters.len(), 0);
3407}
3408
3409#[test]
3410fn test_http_server_state_empty_routes() {
3411 let state = HttpServerState::with_routes(vec![]);
3412 assert_eq!(state.routes.len(), 0);
3413}
3414
3415#[test]
3416fn test_http_server_state_multiple_routes() {
3417 let routes = vec![
3418 RouteInfo {
3419 method: "GET".to_string(),
3420 path: "/users".to_string(),
3421 operation_id: Some("listUsers".to_string()),
3422 summary: Some("List all users".to_string()),
3423 description: None,
3424 parameters: vec![],
3425 },
3426 RouteInfo {
3427 method: "GET".to_string(),
3428 path: "/users/{id}".to_string(),
3429 operation_id: Some("getUser".to_string()),
3430 summary: Some("Get a user".to_string()),
3431 description: None,
3432 parameters: vec!["id".to_string()],
3433 },
3434 RouteInfo {
3435 method: "POST".to_string(),
3436 path: "/users".to_string(),
3437 operation_id: Some("createUser".to_string()),
3438 summary: Some("Create a user".to_string()),
3439 description: None,
3440 parameters: vec!["body".to_string()],
3441 },
3442 ];
3443
3444 let state = HttpServerState::with_routes(routes);
3445 assert_eq!(state.routes.len(), 3);
3446
3447 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3449 assert!(methods.contains(&"GET"));
3450 assert!(methods.contains(&"POST"));
3451}
3452
3453#[test]
3454fn test_http_server_state_with_rate_limiter() {
3455 use std::sync::Arc;
3456
3457 let config = crate::middleware::RateLimitConfig::default();
3458 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
3459
3460 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3461
3462 assert!(state.rate_limiter.is_some());
3463 assert_eq!(state.routes.len(), 0);
3464}
3465
3466#[tokio::test]
3467async fn test_build_router_includes_rate_limiter() {
3468 let _router = build_router(None, None, None).await;
3469 }