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