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