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::extract::State;
313use axum::middleware::from_fn_with_state;
314use axum::response::Json;
315use axum::Router;
316use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
317use mockforge_core::intelligent_behavior::config::Persona;
318use mockforge_foundation::latency::LatencyInjector;
319use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
320use mockforge_openapi::openapi_routes::ValidationOptions;
321use mockforge_openapi::OpenApiSpec;
322use std::sync::Arc;
323use tower_http::cors::{Any, CorsLayer};
324
325#[cfg(feature = "data-faker")]
326use mockforge_data::provider::register_core_faker_provider;
327use mockforge_foundation::latency::LatencyProfile;
328use std::collections::HashMap;
329use std::ffi::OsStr;
330use std::path::Path;
331use tokio::fs;
332use tokio::sync::RwLock;
333use tracing::*;
334
335#[derive(Clone)]
337pub struct RouteInfo {
338 pub method: String,
340 pub path: String,
342 pub operation_id: Option<String>,
344 pub summary: Option<String>,
346 pub description: Option<String>,
348 pub parameters: Vec<String>,
350}
351
352#[derive(Clone)]
354pub struct HttpServerState {
355 pub routes: Vec<RouteInfo>,
357 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
359 pub production_headers: Option<Arc<HashMap<String, String>>>,
361}
362
363impl Default for HttpServerState {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl HttpServerState {
370 pub fn new() -> Self {
372 Self {
373 routes: Vec::new(),
374 rate_limiter: None,
375 production_headers: None,
376 }
377 }
378
379 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
381 Self {
382 routes,
383 rate_limiter: None,
384 production_headers: None,
385 }
386 }
387
388 pub fn with_rate_limiter(
390 mut self,
391 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
392 ) -> Self {
393 self.rate_limiter = Some(rate_limiter);
394 self
395 }
396
397 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
399 self.production_headers = Some(headers);
400 self
401 }
402}
403
404async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
406 let route_info: Vec<serde_json::Value> = state
407 .routes
408 .iter()
409 .map(|route| {
410 serde_json::json!({
411 "method": route.method,
412 "path": route.path,
413 "operation_id": route.operation_id,
414 "summary": route.summary,
415 "description": route.description,
416 "parameters": route.parameters
417 })
418 })
419 .collect();
420
421 Json(serde_json::json!({
422 "routes": route_info,
423 "total": state.routes.len()
424 }))
425}
426
427async fn get_docs_handler() -> axum::response::Html<&'static str> {
429 axum::response::Html(include_str!("../static/docs.html"))
430}
431
432pub async fn build_router(
434 spec_path: Option<String>,
435 options: Option<ValidationOptions>,
436 failure_config: Option<FailureConfig>,
437) -> Router {
438 build_router_with_multi_tenant(
439 spec_path,
440 options,
441 failure_config,
442 None,
443 None,
444 None,
445 None,
446 None,
447 None,
448 None,
449 )
450 .await
451}
452
453fn apply_cors_middleware(
455 app: Router,
456 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
457) -> Router {
458 use http::Method;
459 use tower_http::cors::AllowOrigin;
460
461 if let Some(config) = cors_config {
462 if !config.enabled {
463 return app;
464 }
465
466 let mut cors_layer = CorsLayer::new();
467 let is_wildcard_origin;
468
469 if config.allowed_origins.contains(&"*".to_string()) {
471 cors_layer = cors_layer.allow_origin(Any);
472 is_wildcard_origin = true;
473 } else if !config.allowed_origins.is_empty() {
474 let origins: Vec<_> = config
476 .allowed_origins
477 .iter()
478 .filter_map(|origin| {
479 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
480 })
481 .collect();
482
483 if origins.is_empty() {
484 warn!("No valid CORS origins configured, using permissive CORS");
486 cors_layer = cors_layer.allow_origin(Any);
487 is_wildcard_origin = true;
488 } else {
489 if origins.len() == 1 {
492 cors_layer = cors_layer.allow_origin(origins[0].clone());
493 is_wildcard_origin = false;
494 } else {
495 warn!(
497 "Multiple CORS origins configured, using permissive CORS. \
498 Consider using '*' for all origins."
499 );
500 cors_layer = cors_layer.allow_origin(Any);
501 is_wildcard_origin = true;
502 }
503 }
504 } else {
505 cors_layer = cors_layer.allow_origin(Any);
507 is_wildcard_origin = true;
508 }
509
510 if !config.allowed_methods.is_empty() {
512 let methods: Vec<Method> =
513 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
514 if !methods.is_empty() {
515 cors_layer = cors_layer.allow_methods(methods);
516 }
517 } else {
518 cors_layer = cors_layer.allow_methods([
520 Method::GET,
521 Method::POST,
522 Method::PUT,
523 Method::DELETE,
524 Method::PATCH,
525 Method::OPTIONS,
526 ]);
527 }
528
529 if !config.allowed_headers.is_empty() {
531 let headers: Vec<_> = config
532 .allowed_headers
533 .iter()
534 .filter_map(|h| h.parse::<http::HeaderName>().ok())
535 .collect();
536 if !headers.is_empty() {
537 cors_layer = cors_layer.allow_headers(headers);
538 }
539 } else {
540 cors_layer =
542 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
543 }
544
545 let should_allow_credentials = if is_wildcard_origin {
549 false
551 } else {
552 config.allow_credentials
554 };
555
556 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
557
558 info!(
559 "CORS middleware enabled with configured settings (credentials: {})",
560 should_allow_credentials
561 );
562 app.layer(cors_layer)
563 } else {
564 debug!("No CORS config provided, using permissive CORS for development");
568 app.layer(CorsLayer::permissive().allow_credentials(false))
571 }
572}
573
574#[allow(clippy::too_many_arguments)]
576#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
578 spec_path: Option<String>,
579 options: Option<ValidationOptions>,
580 failure_config: Option<FailureConfig>,
581 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
582 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
583 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
584 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
585 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
586 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
587 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
588) -> Router {
589 use std::time::Instant;
590
591 let startup_start = Instant::now();
592
593 let mut app = Router::new();
595
596 let mut rate_limit_config = middleware::RateLimitConfig {
599 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
600 .ok()
601 .and_then(|v| v.parse().ok())
602 .unwrap_or(1000),
603 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
604 .ok()
605 .and_then(|v| v.parse().ok())
606 .unwrap_or(2000),
607 per_ip: true,
608 per_endpoint: false,
609 };
610
611 let mut final_cors_config = cors_config;
613 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
614 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
616
617 if let Some(deploy_config) = &deceptive_deploy_config {
618 if deploy_config.enabled {
619 info!("Deceptive deploy mode enabled - applying production-like configuration");
620
621 if let Some(prod_cors) = &deploy_config.cors {
623 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
624 enabled: true,
625 allowed_origins: prod_cors.allowed_origins.clone(),
626 allowed_methods: prod_cors.allowed_methods.clone(),
627 allowed_headers: prod_cors.allowed_headers.clone(),
628 allow_credentials: prod_cors.allow_credentials,
629 });
630 info!("Applied production-like CORS configuration");
631 }
632
633 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
635 rate_limit_config = middleware::RateLimitConfig {
636 requests_per_minute: prod_rate_limit.requests_per_minute,
637 burst: prod_rate_limit.burst,
638 per_ip: prod_rate_limit.per_ip,
639 per_endpoint: false,
640 };
641 info!(
642 "Applied production-like rate limiting: {} req/min, burst: {}",
643 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
644 );
645 }
646
647 if !deploy_config.headers.is_empty() {
649 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
650 production_headers = Some(std::sync::Arc::new(headers_map));
651 info!("Configured {} production headers", deploy_config.headers.len());
652 }
653
654 if let Some(prod_oauth) = &deploy_config.oauth {
656 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
657 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
658 oauth2: Some(oauth2_config),
659 ..Default::default()
660 });
661 info!("Applied production-like OAuth configuration for deceptive deploy");
662 }
663 }
664 }
665
666 let rate_limit_disabled = middleware::is_rate_limit_disabled();
667 let rate_limiter =
668 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
669
670 let mut state = HttpServerState::new();
671 if rate_limit_disabled {
672 info!(
673 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
674 );
675 } else {
676 state = state.with_rate_limiter(rate_limiter.clone());
677 }
678
679 if let Some(headers) = production_headers.clone() {
681 state = state.with_production_headers(headers);
682 }
683
684 let spec_path_for_mgmt = spec_path.clone();
686
687 if let Some(spec_path) = spec_path {
689 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
690
691 let spec_load_start = Instant::now();
693 match OpenApiSpec::from_file(&spec_path).await {
694 Ok(openapi) => {
695 let spec_load_duration = spec_load_start.elapsed();
696 info!(
697 "Successfully loaded OpenAPI spec from {} (took {:?})",
698 spec_path, spec_load_duration
699 );
700
701 tracing::debug!("Creating OpenAPI route registry...");
703 let registry_start = Instant::now();
704
705 let persona = load_persona_from_config().await;
707
708 let registry = if let Some(opts) = options {
709 tracing::debug!("Using custom validation options");
710 if let Some(ref persona) = persona {
711 tracing::info!("Using persona '{}' for route generation", persona.name);
712 }
713 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
714 } else {
715 tracing::debug!("Using environment-based options");
716 if let Some(ref persona) = persona {
717 tracing::info!("Using persona '{}' for route generation", persona.name);
718 }
719 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
720 };
721 let registry_duration = registry_start.elapsed();
722 info!(
723 "Created OpenAPI route registry with {} routes (took {:?})",
724 registry.routes().len(),
725 registry_duration
726 );
727
728 let extract_start = Instant::now();
730 let route_info: Vec<RouteInfo> = registry
731 .routes()
732 .iter()
733 .map(|route| RouteInfo {
734 method: route.method.clone(),
735 path: route.path.clone(),
736 operation_id: route.operation.operation_id.clone(),
737 summary: route.operation.summary.clone(),
738 description: route.operation.description.clone(),
739 parameters: route.parameters.clone(),
740 })
741 .collect();
742 state.routes = route_info;
743 let extract_duration = extract_start.elapsed();
744 debug!("Extracted route information (took {:?})", extract_duration);
745
746 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
748 tracing::debug!("Loading overrides from environment variable");
749 let overrides_start = Instant::now();
750 match mockforge_core::Overrides::load_from_globs(&[]).await {
751 Ok(overrides) => {
752 let overrides_duration = overrides_start.elapsed();
753 info!(
754 "Loaded {} override rules (took {:?})",
755 overrides.rules().len(),
756 overrides_duration
757 );
758 Some(overrides)
759 }
760 Err(e) => {
761 tracing::warn!("Failed to load overrides: {}", e);
762 None
763 }
764 }
765 } else {
766 None
767 };
768
769 let router_build_start = Instant::now();
771 let overrides_enabled = overrides.is_some();
772 let response_rewriter: Option<
773 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
774 > = Some(std::sync::Arc::new(
775 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(overrides),
776 ));
777 let openapi_router = if let Some(mockai_instance) = &mockai {
778 tracing::debug!("Building router with MockAI support");
779 registry.build_router_with_mockai(Some(mockai_instance.clone()))
780 } else if let Some(ai_generator) = &ai_generator {
781 tracing::debug!("Building router with AI generator support");
782 registry.build_router_with_ai(Some(ai_generator.clone()))
783 } else if let Some(failure_config) = &failure_config {
784 tracing::debug!("Building router with failure injection and overrides");
785 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
786 registry.build_router_with_injectors_and_overrides(
787 LatencyInjector::default(),
788 Some(failure_injector),
789 response_rewriter,
790 overrides_enabled,
791 )
792 } else {
793 tracing::debug!("Building router with overrides");
794 registry.build_router_with_injectors_and_overrides(
795 LatencyInjector::default(),
796 None,
797 response_rewriter,
798 overrides_enabled,
799 )
800 };
801 let router_build_duration = router_build_start.elapsed();
802 debug!("Built OpenAPI router (took {:?})", router_build_duration);
803
804 tracing::debug!("Merging OpenAPI router with main router");
805 app = app.merge(openapi_router);
806 tracing::debug!("Router built successfully");
807 }
808 Err(e) => {
809 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
810 }
811 }
812 }
813
814 app = app.route(
816 "/health",
817 axum::routing::get(|| async {
818 use mockforge_core::server_utils::health::HealthStatus;
819 {
820 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
822 Ok(value) => Json(value),
823 Err(e) => {
824 tracing::error!("Failed to serialize health status: {}", e);
826 Json(serde_json::json!({
827 "status": "healthy",
828 "service": "mockforge-http",
829 "uptime_seconds": 0
830 }))
831 }
832 }
833 }
834 }),
835 )
836 .merge(sse::sse_router())
838 .merge(file_server::file_serving_router());
840
841 let state_for_routes = state.clone();
843
844 let routes_router = Router::new()
846 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
847 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
848 .with_state(state_for_routes);
849
850 app = app.merge(routes_router);
852
853 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
855
856 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
859 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
860
861 if Path::new(&coverage_html_path).exists() {
863 app = app.nest_service(
864 "/__mockforge/coverage.html",
865 tower_http::services::ServeFile::new(&coverage_html_path),
866 );
867 debug!("Serving coverage UI from: {}", coverage_html_path);
868 } else {
869 debug!(
870 "Coverage UI file not found at: {}. Skipping static file serving.",
871 coverage_html_path
872 );
873 }
874
875 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
878 match OpenApiSpec::from_file(sp).await {
879 Ok(s) => Some(Arc::new(s)),
880 Err(e) => {
881 debug!("Failed to load OpenAPI spec for management API: {}", e);
882 None
883 }
884 }
885 } else {
886 None
887 };
888 let mgmt_port = std::env::var("PORT")
889 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
890 .ok()
891 .and_then(|p| p.parse().ok())
892 .unwrap_or(3000);
893 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
894
895 use std::sync::Arc;
897 let ws_state = WsManagementState::new();
898 let ws_broadcast = Arc::new(ws_state.tx.clone());
899 let management_state = management_state.with_ws_broadcast(ws_broadcast);
900
901 #[cfg(feature = "smtp")]
905 let management_state = {
906 if let Some(smtp_reg) = smtp_registry {
907 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
908 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
909 Err(e) => {
910 error!(
911 "Invalid SMTP registry type passed to HTTP management state: {:?}",
912 e.type_id()
913 );
914 management_state
915 }
916 }
917 } else {
918 management_state
919 }
920 };
921 #[cfg(not(feature = "smtp"))]
922 let management_state = management_state;
923 #[cfg(not(feature = "smtp"))]
924 let _ = smtp_registry;
925 let management_state_for_fallback = management_state.clone();
926 app = app.nest("/__mockforge/api", management_router(management_state));
927 app = app.fallback_service(
932 axum::routing::any(crate::management::dynamic_mock_fallback)
933 .with_state(management_state_for_fallback),
934 );
935
936 app = app.merge(verification_router());
938
939 use crate::auth::oidc::oidc_router;
941 app = app.merge(oidc_router());
942
943 {
945 use mockforge_core::security::get_global_access_review_service;
946 if let Some(service) = get_global_access_review_service().await {
947 use crate::handlers::access_review::{access_review_router, AccessReviewState};
948 let review_state = AccessReviewState { service };
949 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
950 debug!("Access review API mounted at /api/v1/security/access-reviews");
951 }
952 }
953
954 {
956 use mockforge_core::security::get_global_privileged_access_manager;
957 if let Some(manager) = get_global_privileged_access_manager().await {
958 use crate::handlers::privileged_access::{
959 privileged_access_router, PrivilegedAccessState,
960 };
961 let privileged_state = PrivilegedAccessState { manager };
962 app = app.nest(
963 "/api/v1/security/privileged-access",
964 privileged_access_router(privileged_state),
965 );
966 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
967 }
968 }
969
970 {
972 use mockforge_core::security::get_global_change_management_engine;
973 if let Some(engine) = get_global_change_management_engine().await {
974 use crate::handlers::change_management::{
975 change_management_router, ChangeManagementState,
976 };
977 let change_state = ChangeManagementState { engine };
978 app = app.nest("/api/v1/change-management", change_management_router(change_state));
979 debug!("Change management API mounted at /api/v1/change-management");
980 }
981 }
982
983 {
985 use mockforge_core::security::get_global_risk_assessment_engine;
986 if let Some(engine) = get_global_risk_assessment_engine().await {
987 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
988 let risk_state = RiskAssessmentState { engine };
989 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
990 debug!("Risk assessment API mounted at /api/v1/security/risks");
991 }
992 }
993
994 {
996 use crate::auth::token_lifecycle::TokenLifecycleManager;
997 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
998 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
999 let lifecycle_state = TokenLifecycleState {
1000 manager: lifecycle_manager,
1001 };
1002 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1003 debug!("Token lifecycle API mounted at /api/v1/auth");
1004 }
1005
1006 {
1008 use crate::auth::oidc::load_oidc_state;
1009 use crate::auth::token_lifecycle::TokenLifecycleManager;
1010 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1011 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1013 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1014 let oauth2_state = OAuth2ServerState {
1015 oidc_state,
1016 lifecycle_manager,
1017 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1018 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1019 };
1020 app = app.merge(oauth2_server_router(oauth2_state));
1021 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1022 }
1023
1024 {
1026 use crate::auth::oidc::load_oidc_state;
1027 use crate::auth::risk_engine::RiskEngine;
1028 use crate::auth::token_lifecycle::TokenLifecycleManager;
1029 use crate::handlers::consent::{consent_router, ConsentState};
1030 use crate::handlers::oauth2_server::OAuth2ServerState;
1031 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1033 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1034 let oauth2_state = OAuth2ServerState {
1035 oidc_state: oidc_state.clone(),
1036 lifecycle_manager: lifecycle_manager.clone(),
1037 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1038 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1039 };
1040 let risk_engine = Arc::new(RiskEngine::default());
1041 let consent_state = ConsentState {
1042 oauth2_state,
1043 risk_engine,
1044 };
1045 app = app.merge(consent_router(consent_state));
1046 debug!("Consent screen endpoints mounted at /consent");
1047 }
1048
1049 {
1051 use crate::auth::risk_engine::RiskEngine;
1052 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1053 let risk_engine = Arc::new(RiskEngine::default());
1054 let risk_state = RiskSimulationState { risk_engine };
1055 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1056 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1057 }
1058
1059 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1061
1062 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1064
1065 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1067
1068 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1071
1072 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1074
1075 if state.production_headers.is_some() {
1077 app =
1078 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1079 }
1080
1081 if let Some(auth_config) = deceptive_deploy_auth_config {
1083 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1084 use std::collections::HashMap;
1085 use std::sync::Arc;
1086 use tokio::sync::RwLock;
1087
1088 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1090 match create_oauth2_client(oauth2_config) {
1091 Ok(client) => Some(client),
1092 Err(e) => {
1093 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1094 None
1095 }
1096 }
1097 } else {
1098 None
1099 };
1100
1101 let auth_state = AuthState {
1103 config: auth_config,
1104 spec: None, oauth2_client,
1106 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1107 };
1108
1109 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1111 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1112 }
1113
1114 app = apply_cors_middleware(app, final_cors_config);
1116
1117 if let Some(mt_config) = multi_tenant_config {
1119 if mt_config.enabled {
1120 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1121 use std::sync::Arc;
1122
1123 info!(
1124 "Multi-tenant mode enabled with {} routing strategy",
1125 match mt_config.routing_strategy {
1126 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1127 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1128 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1129 }
1130 );
1131
1132 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1134
1135 let default_workspace =
1137 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1138 if let Err(e) =
1139 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1140 {
1141 warn!("Failed to register default workspace: {}", e);
1142 } else {
1143 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1144 }
1145
1146 if mt_config.auto_discover {
1148 if let Some(config_dir) = &mt_config.config_directory {
1149 let config_path = Path::new(config_dir);
1150 if config_path.exists() && config_path.is_dir() {
1151 match fs::read_dir(config_path).await {
1152 Ok(mut entries) => {
1153 while let Ok(Some(entry)) = entries.next_entry().await {
1154 let path = entry.path();
1155 if path.extension() == Some(OsStr::new("yaml")) {
1156 match fs::read_to_string(&path).await {
1157 Ok(content) => {
1158 match serde_yaml::from_str::<
1159 mockforge_core::Workspace,
1160 >(
1161 &content
1162 ) {
1163 Ok(workspace) => {
1164 if let Err(e) = registry.register_workspace(
1165 workspace.id.clone(),
1166 workspace,
1167 ) {
1168 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1169 } else {
1170 info!("Auto-registered workspace from {:?}", path);
1171 }
1172 }
1173 Err(e) => {
1174 warn!("Failed to parse workspace from {:?}: {}", path, e);
1175 }
1176 }
1177 }
1178 Err(e) => {
1179 warn!(
1180 "Failed to read workspace file {:?}: {}",
1181 path, e
1182 );
1183 }
1184 }
1185 }
1186 }
1187 }
1188 Err(e) => {
1189 warn!("Failed to read config directory {:?}: {}", config_path, e);
1190 }
1191 }
1192 } else {
1193 warn!(
1194 "Config directory {:?} does not exist or is not a directory",
1195 config_path
1196 );
1197 }
1198 }
1199 }
1200
1201 let registry = Arc::new(registry);
1203
1204 let _workspace_router = WorkspaceRouter::new(registry);
1206
1207 info!("Workspace routing middleware initialized for HTTP server");
1210 }
1211 }
1212
1213 let total_startup_duration = startup_start.elapsed();
1214 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1215
1216 app
1217}
1218
1219pub async fn build_router_with_auth_and_latency(
1221 spec_path: Option<String>,
1222 _options: Option<()>,
1223 auth_config: Option<mockforge_core::config::AuthConfig>,
1224 latency_injector: Option<LatencyInjector>,
1225) -> Router {
1226 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1228
1229 if let Some(injector) = latency_injector {
1231 let injector = Arc::new(injector);
1232 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1233 let injector = injector.clone();
1234 async move {
1235 let _ = injector.inject_latency(&[]).await;
1236 next.run(req).await
1237 }
1238 }));
1239 }
1240
1241 app
1242}
1243
1244pub async fn build_router_with_latency(
1246 spec_path: Option<String>,
1247 options: Option<ValidationOptions>,
1248 latency_injector: Option<LatencyInjector>,
1249) -> Router {
1250 if let Some(spec) = &spec_path {
1251 match OpenApiSpec::from_file(spec).await {
1252 Ok(openapi) => {
1253 let registry = if let Some(opts) = options {
1254 OpenApiRouteRegistry::new_with_options(openapi, opts)
1255 } else {
1256 OpenApiRouteRegistry::new_with_env(openapi)
1257 };
1258
1259 if let Some(injector) = latency_injector {
1260 return registry.build_router_with_latency(injector);
1261 } else {
1262 return registry.build_router();
1263 }
1264 }
1265 Err(e) => {
1266 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1267 }
1268 }
1269 }
1270
1271 build_router(None, None, None).await
1272}
1273
1274pub async fn build_router_with_auth(
1276 spec_path: Option<String>,
1277 options: Option<ValidationOptions>,
1278 auth_config: Option<mockforge_core::config::AuthConfig>,
1279) -> Router {
1280 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1281 use std::sync::Arc;
1282
1283 #[cfg(feature = "data-faker")]
1285 {
1286 register_core_faker_provider();
1287 }
1288
1289 let spec = if let Some(spec_path) = &spec_path {
1291 match OpenApiSpec::from_file(&spec_path).await {
1292 Ok(spec) => Some(Arc::new(spec)),
1293 Err(e) => {
1294 warn!("Failed to load OpenAPI spec for auth: {}", e);
1295 None
1296 }
1297 }
1298 } else {
1299 None
1300 };
1301
1302 let oauth2_client = if let Some(auth_config) = &auth_config {
1304 if let Some(oauth2_config) = &auth_config.oauth2 {
1305 match create_oauth2_client(oauth2_config) {
1306 Ok(client) => Some(client),
1307 Err(e) => {
1308 warn!("Failed to create OAuth2 client: {}", e);
1309 None
1310 }
1311 }
1312 } else {
1313 None
1314 }
1315 } else {
1316 None
1317 };
1318
1319 let auth_state = AuthState {
1320 config: auth_config.unwrap_or_default(),
1321 spec,
1322 oauth2_client,
1323 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1324 };
1325
1326 let mut app = Router::new().with_state(auth_state.clone());
1328
1329 if let Some(spec_path) = spec_path {
1331 match OpenApiSpec::from_file(&spec_path).await {
1332 Ok(openapi) => {
1333 info!("Loaded OpenAPI spec from {}", spec_path);
1334 let registry = if let Some(opts) = options {
1335 OpenApiRouteRegistry::new_with_options(openapi, opts)
1336 } else {
1337 OpenApiRouteRegistry::new_with_env(openapi)
1338 };
1339
1340 app = registry.build_router();
1341 }
1342 Err(e) => {
1343 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1344 }
1345 }
1346 }
1347
1348 app = app.route(
1350 "/health",
1351 axum::routing::get(|| async {
1352 use mockforge_core::server_utils::health::HealthStatus;
1353 {
1354 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1356 Ok(value) => Json(value),
1357 Err(e) => {
1358 tracing::error!("Failed to serialize health status: {}", e);
1360 Json(serde_json::json!({
1361 "status": "healthy",
1362 "service": "mockforge-http",
1363 "uptime_seconds": 0
1364 }))
1365 }
1366 }
1367 }
1368 }),
1369 )
1370 .merge(sse::sse_router())
1372 .merge(file_server::file_serving_router())
1374 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1376 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1378
1379 app
1380}
1381
1382pub async fn serve_router(
1384 port: u16,
1385 app: Router,
1386) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1387 serve_router_with_tls(port, app, None).await
1388}
1389
1390pub async fn serve_router_with_tls(
1392 port: u16,
1393 app: Router,
1394 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1395) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1396 serve_router_with_tls_notify(port, app, tls_config, None).await
1397}
1398
1399pub async fn serve_router_with_tls_notify(
1407 port: u16,
1408 app: Router,
1409 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1410 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1411) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1412 use std::net::SocketAddr;
1413
1414 let addr = mockforge_core::wildcard_socket_addr(port);
1415
1416 if let Some(ref tls) = tls_config {
1417 if tls.enabled {
1418 info!("HTTPS listening on {}", addr);
1419 if let Some(tx) = bound_port_tx {
1420 let _ = tx.send(port);
1421 }
1422 return serve_with_tls(addr, app, tls).await;
1423 }
1424 }
1425
1426 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1427 format!(
1428 "Failed to bind HTTP server to port {}: {}\n\
1429 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 {}",
1430 port, e, port, port
1431 )
1432 })?;
1433
1434 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1435 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1436 if let Some(tx) = bound_port_tx {
1437 let _ = tx.send(actual_port);
1438 }
1439
1440 let odata_app = tower::ServiceBuilder::new()
1444 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1445 .service(app);
1446 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1447 axum::serve(listener, make_svc).await?;
1448 Ok(())
1449}
1450
1451async fn serve_with_tls(
1456 addr: std::net::SocketAddr,
1457 app: Router,
1458 tls_config: &mockforge_core::config::HttpTlsConfig,
1459) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1460 use axum_server::tls_rustls::RustlsConfig;
1461 use std::net::SocketAddr;
1462
1463 tls::init_crypto_provider();
1465
1466 info!("Loading TLS configuration for HTTPS server");
1467
1468 let server_config = tls::load_tls_server_config(tls_config)?;
1470
1471 let rustls_config = RustlsConfig::from_config(server_config);
1474
1475 info!("Starting HTTPS server on {}", addr);
1476
1477 let odata_app = tower::ServiceBuilder::new()
1481 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1482 .service(app);
1483 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1484
1485 axum_server::bind_rustls(addr, rustls_config)
1487 .serve(make_svc)
1488 .await
1489 .map_err(|e| format!("HTTPS server error: {}", e).into())
1490}
1491
1492pub async fn start(
1494 port: u16,
1495 spec_path: Option<String>,
1496 options: Option<ValidationOptions>,
1497) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1498 start_with_latency(port, spec_path, options, None).await
1499}
1500
1501pub async fn start_with_auth_and_latency(
1503 port: u16,
1504 spec_path: Option<String>,
1505 options: Option<ValidationOptions>,
1506 auth_config: Option<mockforge_core::config::AuthConfig>,
1507 latency_profile: Option<LatencyProfile>,
1508) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1509 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1510 .await
1511}
1512
1513pub async fn start_with_auth_and_injectors(
1515 port: u16,
1516 spec_path: Option<String>,
1517 options: Option<ValidationOptions>,
1518 auth_config: Option<mockforge_core::config::AuthConfig>,
1519 _latency_profile: Option<LatencyProfile>,
1520 _failure_injector: Option<FailureInjector>,
1521) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1522 let app = build_router_with_auth(spec_path, options, auth_config).await;
1524 serve_router(port, app).await
1525}
1526
1527pub async fn start_with_latency(
1529 port: u16,
1530 spec_path: Option<String>,
1531 options: Option<ValidationOptions>,
1532 latency_profile: Option<LatencyProfile>,
1533) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1534 let latency_injector =
1535 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1536
1537 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1538 serve_router(port, app).await
1539}
1540
1541pub async fn build_router_with_chains(
1543 spec_path: Option<String>,
1544 options: Option<ValidationOptions>,
1545 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1546) -> Router {
1547 build_router_with_chains_and_multi_tenant(
1548 spec_path,
1549 options,
1550 circling_config,
1551 None,
1552 None,
1553 None,
1554 None,
1555 None,
1556 None,
1557 None,
1558 false,
1559 None, None, None, None, )
1564 .await
1565}
1566
1567async fn apply_route_chaos(
1575 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1576 method: &http::Method,
1577 uri: &http::Uri,
1578) -> Option<axum::response::Response> {
1579 use axum::http::StatusCode;
1580 use axum::response::IntoResponse;
1581
1582 if let Some(injector) = injector {
1583 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1585 let mut response = Json(serde_json::json!({
1587 "error": fault_response.error_message,
1588 "fault_type": fault_response.fault_type,
1589 }))
1590 .into_response();
1591 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1592 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1593 return Some(response);
1594 }
1595
1596 if let Err(e) = injector.inject_latency(method, uri).await {
1598 tracing::warn!("Failed to inject latency: {}", e);
1599 }
1600 }
1601
1602 None }
1604
1605#[allow(clippy::too_many_arguments)]
1607#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1609 spec_path: Option<String>,
1610 options: Option<ValidationOptions>,
1611 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1612 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1613 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1614 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1615 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1616 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1617 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1618 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1619 traffic_shaping_enabled: bool,
1620 health_manager: Option<Arc<HealthManager>>,
1621 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1622 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1623 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1624) -> Router {
1625 use crate::latency_profiles::LatencyProfiles;
1626 use crate::op_middleware::Shared;
1627 use mockforge_core::Overrides;
1628
1629 let template_expand =
1631 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1632 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1633 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1634 .unwrap_or(false)
1635 });
1636
1637 let _shared = Shared {
1638 profiles: LatencyProfiles::default(),
1639 overrides: Overrides::default(),
1640 failure_injector: None,
1641 traffic_shaper,
1642 overrides_enabled: false,
1643 traffic_shaping_enabled,
1644 };
1645
1646 let mut app = Router::new();
1648 let mut include_default_health = true;
1649 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1650
1651 if let Some(ref spec) = spec_path {
1653 match OpenApiSpec::from_file(&spec).await {
1654 Ok(openapi) => {
1655 info!("Loaded OpenAPI spec from {}", spec);
1656
1657 let persona = load_persona_from_config().await;
1659
1660 let mut registry = if let Some(opts) = options {
1661 tracing::debug!("Using custom validation options");
1662 if let Some(ref persona) = persona {
1663 tracing::info!("Using persona '{}' for route generation", persona.name);
1664 }
1665 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1666 } else {
1667 tracing::debug!("Using environment-based options");
1668 if let Some(ref persona) = persona {
1669 tracing::info!("Using persona '{}' for route generation", persona.name);
1670 }
1671 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1672 };
1673
1674 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1676 .unwrap_or_else(|_| "/app/fixtures".to_string());
1677 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1678 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1679 .unwrap_or(true); if custom_fixtures_enabled {
1682 use mockforge_openapi::CustomFixtureLoader;
1683 use std::path::PathBuf;
1684 use std::sync::Arc;
1685
1686 let fixtures_path = PathBuf::from(&fixtures_dir);
1687 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1688
1689 if let Err(e) = custom_loader.load_fixtures().await {
1690 tracing::warn!("Failed to load custom fixtures: {}", e);
1691 } else {
1692 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1693 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1694 }
1695 }
1696
1697 if registry
1698 .routes()
1699 .iter()
1700 .any(|route| route.method == "GET" && route.path == "/health")
1701 {
1702 include_default_health = false;
1703 }
1704 captured_routes = registry
1706 .routes()
1707 .iter()
1708 .map(|r| RouteInfo {
1709 method: r.method.clone(),
1710 path: r.path.clone(),
1711 operation_id: r.operation.operation_id.clone(),
1712 summary: r.operation.summary.clone(),
1713 description: r.operation.description.clone(),
1714 parameters: r.parameters.clone(),
1715 })
1716 .collect();
1717
1718 {
1721 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1722 captured_routes
1723 .iter()
1724 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1725 method: r.method.clone(),
1726 path: r.path.clone(),
1727 operation_id: r.operation_id.clone(),
1728 summary: r.summary.clone(),
1729 description: r.description.clone(),
1730 parameters: r.parameters.clone(),
1731 })
1732 .collect();
1733 mockforge_core::request_logger::set_global_routes(global_routes);
1734 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1735 }
1736
1737 let spec_router = if let Some(ref mockai_instance) = mockai {
1739 tracing::debug!("Building router with MockAI support");
1740 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1741 } else {
1742 registry.build_router()
1743 };
1744 app = app.merge(spec_router);
1745 }
1746 Err(e) => {
1747 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1748 }
1749 }
1750 }
1751
1752 let route_chaos_injector: Option<
1756 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1757 > = if let Some(ref route_configs) = route_configs {
1758 if !route_configs.is_empty() {
1759 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1762 route_configs.to_vec();
1763 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1764 Ok(injector) => {
1765 info!(
1766 "Initialized advanced routing features for {} route(s)",
1767 route_configs.len()
1768 );
1769 Some(std::sync::Arc::new(injector)
1772 as std::sync::Arc<
1773 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1774 >)
1775 }
1776 Err(e) => {
1777 warn!(
1778 "Failed to initialize advanced routing features: {}. Using basic routing.",
1779 e
1780 );
1781 None
1782 }
1783 }
1784 } else {
1785 None
1786 }
1787 } else {
1788 None
1789 };
1790
1791 if let Some(route_configs) = route_configs {
1792 use axum::http::StatusCode;
1793 use axum::response::IntoResponse;
1794
1795 if !route_configs.is_empty() {
1796 info!("Registering {} custom route(s) from config", route_configs.len());
1797 }
1798
1799 let injector = route_chaos_injector.clone();
1800 for route_config in route_configs {
1801 let status = route_config.response.status;
1802 let body = route_config.response.body.clone();
1803 let headers = route_config.response.headers.clone();
1804 let path = route_config.path.clone();
1805 let method = route_config.method.clone();
1806
1807 let expected_method = method.to_uppercase();
1812 let injector_clone = injector.clone();
1816 app = app.route(
1817 &path,
1818 #[allow(clippy::non_send_fields_in_send_ty)]
1819 axum::routing::any(move |req: http::Request<axum::body::Body>| {
1820 let body = body.clone();
1821 let headers = headers.clone();
1822 let expand = template_expand;
1823 let expected = expected_method.clone();
1824 let status_code = status;
1825 let injector_for_chaos = injector_clone.clone();
1827
1828 async move {
1829 if req.method().as_str() != expected.as_str() {
1831 return axum::response::Response::builder()
1833 .status(StatusCode::METHOD_NOT_ALLOWED)
1834 .header("Allow", &expected)
1835 .body(axum::body::Body::empty())
1836 .unwrap()
1837 .into_response();
1838 }
1839
1840 if let Some(fault_response) = apply_route_chaos(
1844 injector_for_chaos.as_deref(),
1845 req.method(),
1846 req.uri(),
1847 )
1848 .await
1849 {
1850 return fault_response;
1851 }
1852
1853 let mut body_value = body.unwrap_or(serde_json::json!({}));
1855
1856 if expand {
1860 use mockforge_template_expansion::RequestContext;
1861 use serde_json::Value;
1862 use std::collections::HashMap;
1863
1864 let method = req.method().to_string();
1866 let path = req.uri().path().to_string();
1867
1868 let query_params: HashMap<String, Value> = req
1870 .uri()
1871 .query()
1872 .map(|q| {
1873 url::form_urlencoded::parse(q.as_bytes())
1874 .into_owned()
1875 .map(|(k, v)| (k, Value::String(v)))
1876 .collect()
1877 })
1878 .unwrap_or_default();
1879
1880 let headers: HashMap<String, Value> = req
1882 .headers()
1883 .iter()
1884 .map(|(k, v)| {
1885 (
1886 k.to_string(),
1887 Value::String(v.to_str().unwrap_or_default().to_string()),
1888 )
1889 })
1890 .collect();
1891
1892 let context = RequestContext {
1896 method,
1897 path,
1898 query_params,
1899 headers,
1900 body: None, path_params: HashMap::new(),
1902 multipart_fields: HashMap::new(),
1903 multipart_files: HashMap::new(),
1904 };
1905
1906 let body_value_clone = body_value.clone();
1910 let context_clone = context.clone();
1911 body_value = match tokio::task::spawn_blocking(move || {
1912 mockforge_template_expansion::expand_templates_in_json(
1913 body_value_clone,
1914 &context_clone,
1915 )
1916 })
1917 .await
1918 {
1919 Ok(result) => result,
1920 Err(_) => body_value, };
1922 }
1923
1924 let mut response = Json(body_value).into_response();
1925
1926 *response.status_mut() =
1928 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
1929
1930 for (key, value) in headers {
1932 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
1933 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
1934 response.headers_mut().insert(header_name, header_value);
1935 }
1936 }
1937 }
1938
1939 response
1940 }
1941 }),
1942 );
1943
1944 debug!("Registered route: {} {}", method, path);
1945 }
1946 }
1947
1948 if let Some(health) = health_manager {
1950 app = app.merge(health::health_router(health));
1952 info!(
1953 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1954 );
1955 } else if include_default_health {
1956 app = app.route(
1958 "/health",
1959 axum::routing::get(|| async {
1960 use mockforge_core::server_utils::health::HealthStatus;
1961 {
1962 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1964 Ok(value) => Json(value),
1965 Err(e) => {
1966 tracing::error!("Failed to serialize health status: {}", e);
1968 Json(serde_json::json!({
1969 "status": "healthy",
1970 "service": "mockforge-http",
1971 "uptime_seconds": 0
1972 }))
1973 }
1974 }
1975 }
1976 }),
1977 );
1978 }
1979
1980 app = app.merge(sse::sse_router());
1981 app = app.merge(file_server::file_serving_router());
1983
1984 let mgmt_spec = if let Some(ref sp) = spec_path {
1987 match OpenApiSpec::from_file(sp).await {
1988 Ok(s) => Some(Arc::new(s)),
1989 Err(e) => {
1990 debug!("Failed to load OpenAPI spec for management API: {}", e);
1991 None
1992 }
1993 }
1994 } else {
1995 None
1996 };
1997 let spec_path_clone = spec_path.clone();
1998 let mgmt_port = std::env::var("PORT")
1999 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2000 .ok()
2001 .and_then(|p| p.parse().ok())
2002 .unwrap_or(3000);
2003 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2004
2005 use std::sync::Arc;
2007 let ws_state = WsManagementState::new();
2008 let ws_broadcast = Arc::new(ws_state.tx.clone());
2009 let management_state = management_state.with_ws_broadcast(ws_broadcast);
2010
2011 let management_state = if let Some(proxy_cfg) = proxy_config {
2013 use tokio::sync::RwLock;
2014 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2015 management_state.with_proxy_config(proxy_config_arc)
2016 } else {
2017 management_state
2018 };
2019
2020 #[cfg(feature = "smtp")]
2021 let management_state = {
2022 if let Some(smtp_reg) = smtp_registry {
2023 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2024 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2025 Err(e) => {
2026 error!(
2027 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2028 e.type_id()
2029 );
2030 management_state
2031 }
2032 }
2033 } else {
2034 management_state
2035 }
2036 };
2037 #[cfg(not(feature = "smtp"))]
2038 let management_state = {
2039 let _ = smtp_registry;
2040 management_state
2041 };
2042 #[cfg(feature = "mqtt")]
2043 let management_state = {
2044 if let Some(broker) = mqtt_broker {
2045 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
2046 Ok(broker) => management_state.with_mqtt_broker(broker),
2047 Err(e) => {
2048 error!(
2049 "Invalid MQTT broker passed to HTTP management state: {:?}",
2050 e.type_id()
2051 );
2052 management_state
2053 }
2054 }
2055 } else {
2056 management_state
2057 }
2058 };
2059 #[cfg(not(feature = "mqtt"))]
2060 let management_state = {
2061 let _ = mqtt_broker;
2062 management_state
2063 };
2064 let management_state_for_fallback = management_state.clone();
2065 app = app.nest("/__mockforge/api", management_router(management_state));
2066 app = app.fallback_service(
2068 axum::routing::any(crate::management::dynamic_mock_fallback)
2069 .with_state(management_state_for_fallback),
2070 );
2071
2072 app = app.merge(verification_router());
2074
2075 {
2080 use crate::chain_handlers::{chains_router, create_chain_state};
2081 let chain_config = _circling_config.clone().unwrap_or_default();
2082 let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2083 chain_config.clone(),
2084 ));
2085 let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2086 chain_registry.clone(),
2087 chain_config,
2088 ));
2089 app = app.nest(
2090 "/__mockforge/chains",
2091 chains_router(create_chain_state(chain_registry, chain_engine)),
2092 );
2093 }
2094
2095 {
2100 use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2101 let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2102 app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2103 }
2104
2105 {
2112 use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2113 let fx_state = FixturesApiState::from_env();
2114 app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2115 }
2116
2117 {
2124 use crate::mockai_api::{mockai_api_router, MockAiApiState};
2125 let api_state = MockAiApiState::new(mockai.clone());
2126 app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2127 }
2128
2129 app = app.nest("/__mockforge/time-travel", crate::time_travel_api::time_travel_router());
2136
2137 {
2142 use crate::route_chaos_runtime::{
2143 route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2144 };
2145 let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2146 let middleware_state = runtime_state.clone();
2147 app = app.layer(axum::middleware::from_fn_with_state(
2148 middleware_state,
2149 runtime_route_chaos_middleware,
2150 ));
2151 app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2152 }
2153
2154 {
2160 use crate::network_profile_runtime::{
2161 network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2162 };
2163 let runtime_state = NetworkProfileRuntimeState::new(
2164 mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2165 );
2166 let middleware_state = runtime_state.clone();
2167 app = app.layer(axum::middleware::from_fn_with_state(
2168 middleware_state,
2169 network_profile_middleware,
2170 ));
2171 app = app
2172 .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2173 }
2174
2175 use crate::auth::oidc::oidc_router;
2177 app = app.merge(oidc_router());
2178
2179 {
2181 use mockforge_core::security::get_global_access_review_service;
2182 if let Some(service) = get_global_access_review_service().await {
2183 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2184 let review_state = AccessReviewState { service };
2185 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2186 debug!("Access review API mounted at /api/v1/security/access-reviews");
2187 }
2188 }
2189
2190 {
2192 use mockforge_core::security::get_global_privileged_access_manager;
2193 if let Some(manager) = get_global_privileged_access_manager().await {
2194 use crate::handlers::privileged_access::{
2195 privileged_access_router, PrivilegedAccessState,
2196 };
2197 let privileged_state = PrivilegedAccessState { manager };
2198 app = app.nest(
2199 "/api/v1/security/privileged-access",
2200 privileged_access_router(privileged_state),
2201 );
2202 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2203 }
2204 }
2205
2206 {
2208 use mockforge_core::security::get_global_change_management_engine;
2209 if let Some(engine) = get_global_change_management_engine().await {
2210 use crate::handlers::change_management::{
2211 change_management_router, ChangeManagementState,
2212 };
2213 let change_state = ChangeManagementState { engine };
2214 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2215 debug!("Change management API mounted at /api/v1/change-management");
2216 }
2217 }
2218
2219 {
2221 use mockforge_core::security::get_global_risk_assessment_engine;
2222 if let Some(engine) = get_global_risk_assessment_engine().await {
2223 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2224 let risk_state = RiskAssessmentState { engine };
2225 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2226 debug!("Risk assessment API mounted at /api/v1/security/risks");
2227 }
2228 }
2229
2230 {
2232 use crate::auth::token_lifecycle::TokenLifecycleManager;
2233 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2234 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2235 let lifecycle_state = TokenLifecycleState {
2236 manager: lifecycle_manager,
2237 };
2238 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2239 debug!("Token lifecycle API mounted at /api/v1/auth");
2240 }
2241
2242 {
2244 use crate::auth::oidc::load_oidc_state;
2245 use crate::auth::token_lifecycle::TokenLifecycleManager;
2246 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2247 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2249 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2250 let oauth2_state = OAuth2ServerState {
2251 oidc_state,
2252 lifecycle_manager,
2253 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2254 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2255 };
2256 app = app.merge(oauth2_server_router(oauth2_state));
2257 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2258 }
2259
2260 {
2262 use crate::auth::oidc::load_oidc_state;
2263 use crate::auth::risk_engine::RiskEngine;
2264 use crate::auth::token_lifecycle::TokenLifecycleManager;
2265 use crate::handlers::consent::{consent_router, ConsentState};
2266 use crate::handlers::oauth2_server::OAuth2ServerState;
2267 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2269 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2270 let oauth2_state = OAuth2ServerState {
2271 oidc_state: oidc_state.clone(),
2272 lifecycle_manager: lifecycle_manager.clone(),
2273 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2274 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2275 };
2276 let risk_engine = Arc::new(RiskEngine::default());
2277 let consent_state = ConsentState {
2278 oauth2_state,
2279 risk_engine,
2280 };
2281 app = app.merge(consent_router(consent_state));
2282 debug!("Consent screen endpoints mounted at /consent");
2283 }
2284
2285 {
2287 use crate::auth::risk_engine::RiskEngine;
2288 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2289 let risk_engine = Arc::new(RiskEngine::default());
2290 let risk_state = RiskSimulationState { risk_engine };
2291 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2292 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2293 }
2294
2295 let database = {
2297 use crate::database::Database;
2298 let database_url = std::env::var("DATABASE_URL").ok();
2299 match Database::connect_optional(database_url.as_deref()).await {
2300 Ok(db) => {
2301 if db.is_connected() {
2302 if let Err(e) = db.migrate_if_connected().await {
2304 warn!("Failed to run database migrations: {}", e);
2305 } else {
2306 info!("Database connected and migrations applied");
2307 }
2308 }
2309 Some(db)
2310 }
2311 Err(e) => {
2312 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2313 None
2314 }
2315 }
2316 };
2317
2318 let (drift_engine, incident_manager, drift_config) = {
2321 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2322 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2323 use std::sync::Arc;
2324
2325 let drift_config = DriftBudgetConfig::default();
2327 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2328
2329 let incident_store = Arc::new(IncidentStore::default());
2331 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2332
2333 (drift_engine, incident_manager, drift_config)
2334 };
2335
2336 {
2337 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2338 use crate::middleware::drift_tracking::DriftTrackingState;
2339 use mockforge_contracts::consumer_contracts::{
2340 ConsumerBreakingChangeDetector, UsageRecorder,
2341 };
2342 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2343 use std::sync::Arc;
2344
2345 let usage_recorder = Arc::new(UsageRecorder::default());
2347 let consumer_detector =
2348 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2349
2350 let diff_analyzer = if drift_config.enabled {
2352 match ContractDiffAnalyzer::new(
2353 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2354 ) {
2355 Ok(analyzer) => Some(Arc::new(analyzer)),
2356 Err(e) => {
2357 warn!("Failed to create contract diff analyzer: {}", e);
2358 None
2359 }
2360 }
2361 } else {
2362 None
2363 };
2364
2365 let spec = if let Some(ref spec_path) = spec_path {
2368 match OpenApiSpec::from_file(spec_path).await {
2369 Ok(s) => Some(Arc::new(s)),
2370 Err(e) => {
2371 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2372 None
2373 }
2374 }
2375 } else {
2376 None
2377 };
2378
2379 let drift_tracking_state = DriftTrackingState {
2381 diff_analyzer,
2382 spec,
2383 drift_engine: drift_engine.clone(),
2384 incident_manager: incident_manager.clone(),
2385 usage_recorder,
2386 consumer_detector,
2387 enabled: drift_config.enabled,
2388 };
2389
2390 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2392
2393 let drift_tracking_state_clone = drift_tracking_state.clone();
2396 app = app.layer(axum::middleware::from_fn(
2397 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2398 let state = drift_tracking_state_clone.clone();
2399 async move {
2400 if req.extensions().get::<DriftTrackingState>().is_none() {
2402 req.extensions_mut().insert(state);
2403 }
2404 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2406 .await
2407 }
2408 },
2409 ));
2410
2411 let drift_state = DriftBudgetState {
2412 engine: drift_engine.clone(),
2413 incident_manager: incident_manager.clone(),
2414 gitops_handler: None, };
2416
2417 app = app.merge(drift_budget_router(drift_state));
2418 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2419 }
2420
2421 #[cfg(feature = "pipelines")]
2423 {
2424 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2425
2426 let pipeline_state = PipelineState::new();
2427 app = app.merge(pipeline_router(pipeline_state));
2428 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2429 }
2430
2431 {
2433 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2434 use crate::handlers::forecasting::{forecasting_router, ForecastingState};
2435 use crate::handlers::semantic_drift::{semantic_drift_router, SemanticDriftState};
2436 use crate::handlers::threat_modeling::{threat_modeling_router, ThreatModelingState};
2437 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2438 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2439 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2440 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2441 use std::sync::Arc;
2442
2443 let forecasting_config = ForecastingConfig::default();
2445 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2446 let forecasting_state = ForecastingState {
2447 forecaster,
2448 database: database.clone(),
2449 };
2450
2451 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2453 let semantic_state = SemanticDriftState {
2454 manager: semantic_manager,
2455 database: database.clone(),
2456 };
2457
2458 let threat_config = ThreatModelingConfig::default();
2460 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2461 Ok(analyzer) => Arc::new(analyzer),
2462 Err(e) => {
2463 warn!("Failed to create threat analyzer: {}. Using default.", e);
2464 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2465 |_| {
2466 ThreatAnalyzer::new(ThreatModelingConfig {
2468 enabled: false,
2469 ..Default::default()
2470 })
2471 .expect("Failed to create fallback threat analyzer")
2472 },
2473 ))
2474 }
2475 };
2476 let mut webhook_configs = Vec::new();
2478 let config_paths = [
2479 "config.yaml",
2480 "mockforge.yaml",
2481 "tools/mockforge/config.yaml",
2482 "../tools/mockforge/config.yaml",
2483 ];
2484
2485 for path in &config_paths {
2486 if let Ok(config) = mockforge_core::config::load_config(path).await {
2487 if !config.incidents.webhooks.is_empty() {
2488 webhook_configs = config.incidents.webhooks.clone();
2489 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2490 break;
2491 }
2492 }
2493 }
2494
2495 if webhook_configs.is_empty() {
2496 debug!("No webhook configs found in config files, using empty list");
2497 }
2498
2499 let threat_state = ThreatModelingState {
2500 analyzer: threat_analyzer,
2501 webhook_configs,
2502 database: database.clone(),
2503 };
2504
2505 let contract_health_state = ContractHealthState {
2507 incident_manager: incident_manager.clone(),
2508 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2509 database: database.clone(),
2510 };
2511
2512 app = app.merge(forecasting_router(forecasting_state));
2514 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2515
2516 app = app.merge(semantic_drift_router(semantic_state));
2517 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2518
2519 app = app.merge(threat_modeling_router(threat_state));
2520 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2521
2522 app = app.merge(contract_health_router(contract_health_state));
2523 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2524 }
2525
2526 {
2528 use crate::handlers::protocol_contracts::{
2529 protocol_contracts_router, ProtocolContractState,
2530 };
2531 use mockforge_core::contract_drift::{
2532 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2533 };
2534 use std::sync::Arc;
2535 use tokio::sync::RwLock;
2536
2537 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2539
2540 let mut fitness_registry = FitnessFunctionRegistry::new();
2542
2543 let config_paths = [
2545 "config.yaml",
2546 "mockforge.yaml",
2547 "tools/mockforge/config.yaml",
2548 "../tools/mockforge/config.yaml",
2549 ];
2550
2551 let mut config_loaded = false;
2552 for path in &config_paths {
2553 if let Ok(config) = mockforge_core::config::load_config(path).await {
2554 if !config.contracts.fitness_rules.is_empty() {
2555 if let Err(e) =
2556 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2557 {
2558 warn!("Failed to load fitness rules from config {}: {}", path, e);
2559 } else {
2560 info!(
2561 "Loaded {} fitness rules from config: {}",
2562 config.contracts.fitness_rules.len(),
2563 path
2564 );
2565 config_loaded = true;
2566 break;
2567 }
2568 }
2569 }
2570 }
2571
2572 if !config_loaded {
2573 debug!("No fitness rules found in config files, using empty registry");
2574 }
2575
2576 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2577
2578 let consumer_mapping_registry =
2582 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2583 let consumer_analyzer =
2584 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2585
2586 let protocol_state = ProtocolContractState {
2587 registry: contract_registry,
2588 drift_engine: Some(drift_engine.clone()),
2589 incident_manager: Some(incident_manager.clone()),
2590 fitness_registry: Some(fitness_registry),
2591 consumer_analyzer: Some(consumer_analyzer),
2592 };
2593
2594 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2595 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2596 }
2597
2598 #[cfg(feature = "behavioral-cloning")]
2600 {
2601 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2602 use std::path::PathBuf;
2603
2604 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2606 .ok()
2607 .map(PathBuf::from)
2608 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2609
2610 let bc_middleware_state = if let Some(path) = db_path {
2611 BehavioralCloningMiddlewareState::with_database_path(path)
2612 } else {
2613 BehavioralCloningMiddlewareState::new()
2614 };
2615
2616 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2618 .ok()
2619 .and_then(|v| v.parse::<bool>().ok())
2620 .unwrap_or(false);
2621
2622 if enabled {
2623 let bc_state_clone = bc_middleware_state.clone();
2624 app = app.layer(axum::middleware::from_fn(
2625 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2626 let state = bc_state_clone.clone();
2627 async move {
2628 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2630 req.extensions_mut().insert(state);
2631 }
2632 crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
2634 req, next,
2635 )
2636 .await
2637 }
2638 },
2639 ));
2640 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2641 }
2642 }
2643
2644 {
2646 use crate::handlers::consumer_contracts::{
2647 consumer_contracts_router, ConsumerContractsState,
2648 };
2649 use mockforge_contracts::consumer_contracts::{
2650 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2651 };
2652 use std::sync::Arc;
2653
2654 let registry = Arc::new(ConsumerRegistry::default());
2656
2657 let usage_recorder = Arc::new(UsageRecorder::default());
2659
2660 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2662
2663 let consumer_state = ConsumerContractsState {
2664 registry,
2665 usage_recorder,
2666 detector,
2667 violations: Arc::new(RwLock::new(HashMap::new())),
2668 };
2669
2670 app = app.merge(consumer_contracts_router(consumer_state));
2671 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2672 }
2673
2674 #[cfg(feature = "behavioral-cloning")]
2676 {
2677 use crate::handlers::behavioral_cloning::{
2678 behavioral_cloning_router, BehavioralCloningState,
2679 };
2680 use std::path::PathBuf;
2681
2682 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2684 .ok()
2685 .map(PathBuf::from)
2686 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2687
2688 let bc_state = if let Some(path) = db_path {
2689 BehavioralCloningState::with_database_path(path)
2690 } else {
2691 BehavioralCloningState::new()
2692 };
2693
2694 app = app.merge(behavioral_cloning_router(bc_state));
2695 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2696 }
2697
2698 {
2700 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2701 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2702 use mockforge_core::consistency::ConsistencyEngine;
2703 use std::sync::Arc;
2704
2705 let consistency_engine = Arc::new(ConsistencyEngine::new());
2707
2708 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2710 consistency_engine.register_adapter(http_adapter.clone()).await;
2711
2712 let consistency_state = ConsistencyState {
2714 engine: consistency_engine.clone(),
2715 };
2716
2717 use crate::handlers::xray::XRayState;
2719 let xray_state = Arc::new(XRayState {
2720 engine: consistency_engine.clone(),
2721 request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
2722 });
2723
2724 let consistency_middleware_state = ConsistencyMiddlewareState {
2726 engine: consistency_engine.clone(),
2727 adapter: http_adapter,
2728 xray_state: Some(xray_state.clone()),
2729 };
2730
2731 if let Some(reality_cfg) = crate::reality_proxy::RealityProxyConfig::from_env() {
2739 tracing::info!(
2740 upstream = %reality_cfg.upstream_base,
2741 "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2742 );
2743 app =
2744 app.layer(axum::middleware::from_fn(
2745 move |req: axum::extract::Request, next: axum::middleware::Next| {
2746 let cfg = reality_cfg.clone();
2747 async move {
2748 crate::reality_proxy::reality_proxy_middleware(cfg, req, next).await
2749 }
2750 },
2751 ));
2752 }
2753
2754 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2756 app = app.layer(axum::middleware::from_fn(
2757 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2758 let state = consistency_middleware_state_clone.clone();
2759 async move {
2760 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2762 req.extensions_mut().insert(state);
2763 }
2764 consistency::middleware::consistency_middleware(req, next).await
2766 }
2767 },
2768 ));
2769
2770 app = app.merge(consistency_router(consistency_state));
2772 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2773
2774 #[cfg(feature = "scenario-engine")]
2782 {
2783 use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
2784 let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
2785 Ok(s) => s,
2786 Err(e) => {
2787 tracing::warn!(
2788 error = %e,
2789 "Failed to init scenario storage; runtime scenarios API will list empty"
2790 );
2791 let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
2794 mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
2795 .expect("temp scenario storage")
2796 }
2797 };
2798 if let Err(e) = scenario_storage.load().await {
2799 tracing::warn!(
2800 error = %e,
2801 "Failed to load installed scenarios; API will list empty until scenarios are installed"
2802 );
2803 }
2804 let scenarios_state =
2805 ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
2806 app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
2807 debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
2808 }
2809
2810 {
2812 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2813 let fidelity_state = FidelityState::new();
2814 app = app.merge(fidelity_router(fidelity_state));
2815 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2816 }
2817
2818 {
2820 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2821 let scenario_studio_state = ScenarioStudioState::new();
2822 app = app.merge(scenario_studio_router(scenario_studio_state));
2823 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2824 }
2825
2826 {
2828 use crate::handlers::performance::{performance_router, PerformanceState};
2829 let performance_state = PerformanceState::new();
2830 app = app.nest("/api/performance", performance_router(performance_state));
2831 debug!("Performance mode endpoints mounted at /api/performance");
2832 }
2833
2834 {
2836 use crate::handlers::world_state::{world_state_router, WorldStateState};
2837 use mockforge_world_state::WorldStateEngine;
2838 use std::sync::Arc;
2839 use tokio::sync::RwLock;
2840
2841 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
2842 let world_state_state = WorldStateState {
2843 engine: world_state_engine,
2844 };
2845 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
2846 debug!("World state endpoints mounted at /api/world-state");
2847 }
2848
2849 {
2851 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2852 use mockforge_core::snapshots::SnapshotManager;
2853 use std::path::PathBuf;
2854
2855 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2856 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2857
2858 let snapshot_state = SnapshotState {
2859 manager: snapshot_manager,
2860 consistency_engine: Some(consistency_engine.clone()),
2861 workspace_persistence: None, vbr_engine: None, recorder: None, };
2865
2866 app = app.merge(snapshot_router(snapshot_state));
2867 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2868
2869 {
2871 use crate::handlers::xray::xray_router;
2872 app = app.merge(xray_router((*xray_state).clone()));
2873 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2874 }
2875 }
2876
2877 {
2879 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2880 use crate::middleware::ab_testing::ab_testing_middleware;
2881
2882 let ab_testing_state = ABTestingState::new();
2883
2884 let ab_testing_state_clone = ab_testing_state.clone();
2886 app = app.layer(axum::middleware::from_fn(
2887 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2888 let state = ab_testing_state_clone.clone();
2889 async move {
2890 if req.extensions().get::<ABTestingState>().is_none() {
2892 req.extensions_mut().insert(state);
2893 }
2894 ab_testing_middleware(req, next).await
2896 }
2897 },
2898 ));
2899
2900 app = app.merge(ab_testing_router(ab_testing_state));
2902 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2903 }
2904 }
2905
2906 {
2908 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
2909 use mockforge_core::pr_generation::{PRGenerator, PRProvider};
2910 use std::sync::Arc;
2911
2912 let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
2914
2915 let generator = if pr_config.enabled && pr_config.token.is_some() {
2916 let token = pr_config.token.as_ref().unwrap().clone();
2917 let generator = match pr_config.provider {
2918 PRProvider::GitHub => PRGenerator::new_github(
2919 pr_config.owner.clone(),
2920 pr_config.repo.clone(),
2921 token,
2922 pr_config.base_branch.clone(),
2923 ),
2924 PRProvider::GitLab => PRGenerator::new_gitlab(
2925 pr_config.owner.clone(),
2926 pr_config.repo.clone(),
2927 token,
2928 pr_config.base_branch.clone(),
2929 ),
2930 };
2931 Some(Arc::new(generator))
2932 } else {
2933 None
2934 };
2935
2936 let pr_state = PRGenerationState {
2937 generator: generator.clone(),
2938 };
2939
2940 app = app.merge(pr_generation_router(pr_state));
2941 if generator.is_some() {
2942 debug!(
2943 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
2944 pr_config.provider
2945 );
2946 } else {
2947 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
2948 }
2949 }
2950
2951 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
2953
2954 if let Some(mt_config) = multi_tenant_config {
2956 if mt_config.enabled {
2957 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
2958 use std::sync::Arc;
2959
2960 info!(
2961 "Multi-tenant mode enabled with {} routing strategy",
2962 match mt_config.routing_strategy {
2963 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
2964 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
2965 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
2966 }
2967 );
2968
2969 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
2971
2972 let default_workspace =
2974 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
2975 if let Err(e) =
2976 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
2977 {
2978 warn!("Failed to register default workspace: {}", e);
2979 } else {
2980 info!("Registered default workspace: '{}'", mt_config.default_workspace);
2981 }
2982
2983 let registry = Arc::new(registry);
2985
2986 let _workspace_router = WorkspaceRouter::new(registry);
2988 info!("Workspace routing middleware initialized for HTTP server");
2989 }
2990 }
2991
2992 let mut final_cors_config = cors_config;
2994 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
2995 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
2997 let mut rate_limit_config = middleware::RateLimitConfig {
2998 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
2999 .ok()
3000 .and_then(|v| v.parse().ok())
3001 .unwrap_or(1000),
3002 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3003 .ok()
3004 .and_then(|v| v.parse().ok())
3005 .unwrap_or(2000),
3006 per_ip: true,
3007 per_endpoint: false,
3008 };
3009
3010 if let Some(deploy_config) = &deceptive_deploy_config {
3011 if deploy_config.enabled {
3012 info!("Deceptive deploy mode enabled - applying production-like configuration");
3013
3014 if let Some(prod_cors) = &deploy_config.cors {
3016 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3017 enabled: true,
3018 allowed_origins: prod_cors.allowed_origins.clone(),
3019 allowed_methods: prod_cors.allowed_methods.clone(),
3020 allowed_headers: prod_cors.allowed_headers.clone(),
3021 allow_credentials: prod_cors.allow_credentials,
3022 });
3023 info!("Applied production-like CORS configuration");
3024 }
3025
3026 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3028 rate_limit_config = middleware::RateLimitConfig {
3029 requests_per_minute: prod_rate_limit.requests_per_minute,
3030 burst: prod_rate_limit.burst,
3031 per_ip: prod_rate_limit.per_ip,
3032 per_endpoint: false,
3033 };
3034 info!(
3035 "Applied production-like rate limiting: {} req/min, burst: {}",
3036 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3037 );
3038 }
3039
3040 if !deploy_config.headers.is_empty() {
3042 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3043 production_headers = Some(std::sync::Arc::new(headers_map));
3044 info!("Configured {} production headers", deploy_config.headers.len());
3045 }
3046
3047 if let Some(prod_oauth) = &deploy_config.oauth {
3049 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3050 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3051 oauth2: Some(oauth2_config),
3052 ..Default::default()
3053 });
3054 info!("Applied production-like OAuth configuration for deceptive deploy");
3055 }
3056 }
3057 }
3058
3059 let rate_limit_disabled = middleware::is_rate_limit_disabled();
3061 let rate_limiter =
3062 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3063
3064 let mut state = HttpServerState::new();
3065 if rate_limit_disabled {
3066 info!(
3067 "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3068 );
3069 } else {
3070 state = state.with_rate_limiter(rate_limiter.clone());
3071 }
3072
3073 if let Some(headers) = production_headers.clone() {
3075 state = state.with_production_headers(headers);
3076 }
3077
3078 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3080
3081 if state.production_headers.is_some() {
3083 app =
3084 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3085 }
3086
3087 if let Some(auth_config) = deceptive_deploy_auth_config {
3089 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3090 use std::collections::HashMap;
3091 use std::sync::Arc;
3092 use tokio::sync::RwLock;
3093
3094 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3096 match create_oauth2_client(oauth2_config) {
3097 Ok(client) => Some(client),
3098 Err(e) => {
3099 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3100 None
3101 }
3102 }
3103 } else {
3104 None
3105 };
3106
3107 let auth_state = AuthState {
3109 config: auth_config,
3110 spec: None, oauth2_client,
3112 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3113 };
3114
3115 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3117 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3118 }
3119
3120 #[cfg(feature = "runtime-daemon")]
3122 {
3123 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3124 use std::sync::Arc;
3125
3126 let daemon_config = RuntimeDaemonConfig::from_env();
3128
3129 if daemon_config.enabled {
3130 info!("Runtime daemon enabled - auto-creating mocks from 404s");
3131
3132 let management_api_url =
3134 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3135 let port =
3136 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3137 format!("http://localhost:{}", port)
3138 });
3139
3140 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3142
3143 let detector = NotFoundDetector::new(daemon_config.clone());
3145 detector.set_generator(generator).await;
3146
3147 let detector_clone = detector.clone();
3149 app = app.layer(axum::middleware::from_fn(
3150 move |req: axum::extract::Request, next: axum::middleware::Next| {
3151 let detector = detector_clone.clone();
3152 async move { detector.detect_and_auto_create(req, next).await }
3153 },
3154 ));
3155
3156 debug!("Runtime daemon 404 detection middleware added");
3157 }
3158 }
3159
3160 {
3162 let routes_state = HttpServerState::with_routes(captured_routes);
3163 let routes_router = Router::new()
3164 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3165 .with_state(routes_state);
3166 app = app.merge(routes_router);
3167 }
3168
3169 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3171
3172 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3177
3178 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3181
3182 app = apply_cors_middleware(app, final_cors_config);
3184
3185 app
3186}
3187
3188#[test]
3192fn test_route_info_clone() {
3193 let route = RouteInfo {
3194 method: "POST".to_string(),
3195 path: "/users".to_string(),
3196 operation_id: Some("createUser".to_string()),
3197 summary: None,
3198 description: None,
3199 parameters: vec![],
3200 };
3201
3202 let cloned = route.clone();
3203 assert_eq!(route.method, cloned.method);
3204 assert_eq!(route.path, cloned.path);
3205 assert_eq!(route.operation_id, cloned.operation_id);
3206}
3207
3208#[test]
3209fn test_http_server_state_new() {
3210 let state = HttpServerState::new();
3211 assert_eq!(state.routes.len(), 0);
3212}
3213
3214#[test]
3215fn test_http_server_state_with_routes() {
3216 let routes = vec![
3217 RouteInfo {
3218 method: "GET".to_string(),
3219 path: "/users".to_string(),
3220 operation_id: Some("getUsers".to_string()),
3221 summary: None,
3222 description: None,
3223 parameters: vec![],
3224 },
3225 RouteInfo {
3226 method: "POST".to_string(),
3227 path: "/users".to_string(),
3228 operation_id: Some("createUser".to_string()),
3229 summary: None,
3230 description: None,
3231 parameters: vec![],
3232 },
3233 ];
3234
3235 let state = HttpServerState::with_routes(routes.clone());
3236 assert_eq!(state.routes.len(), 2);
3237 assert_eq!(state.routes[0].method, "GET");
3238 assert_eq!(state.routes[1].method, "POST");
3239}
3240
3241#[test]
3242fn test_http_server_state_clone() {
3243 let routes = vec![RouteInfo {
3244 method: "GET".to_string(),
3245 path: "/test".to_string(),
3246 operation_id: None,
3247 summary: None,
3248 description: None,
3249 parameters: vec![],
3250 }];
3251
3252 let state = HttpServerState::with_routes(routes);
3253 let cloned = state.clone();
3254
3255 assert_eq!(state.routes.len(), cloned.routes.len());
3256 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3257}
3258
3259#[tokio::test]
3260async fn test_build_router_without_openapi() {
3261 let _router = build_router(None, None, None).await;
3262 }
3264
3265#[tokio::test]
3266async fn test_build_router_with_nonexistent_spec() {
3267 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3268 }
3270
3271#[tokio::test]
3272async fn test_build_router_with_auth_and_latency() {
3273 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3274 }
3276
3277#[tokio::test]
3278async fn test_build_router_with_latency() {
3279 let _router = build_router_with_latency(None, None, None).await;
3280 }
3282
3283#[tokio::test]
3284async fn test_build_router_with_auth() {
3285 let _router = build_router_with_auth(None, None, None).await;
3286 }
3288
3289#[tokio::test]
3290async fn test_build_router_with_chains() {
3291 let _router = build_router_with_chains(None, None, None).await;
3292 }
3294
3295#[test]
3296fn test_route_info_with_all_fields() {
3297 let route = RouteInfo {
3298 method: "PUT".to_string(),
3299 path: "/users/{id}".to_string(),
3300 operation_id: Some("updateUser".to_string()),
3301 summary: Some("Update user".to_string()),
3302 description: Some("Updates an existing user".to_string()),
3303 parameters: vec!["id".to_string(), "body".to_string()],
3304 };
3305
3306 assert!(route.operation_id.is_some());
3307 assert!(route.summary.is_some());
3308 assert!(route.description.is_some());
3309 assert_eq!(route.parameters.len(), 2);
3310}
3311
3312#[test]
3313fn test_route_info_with_minimal_fields() {
3314 let route = RouteInfo {
3315 method: "DELETE".to_string(),
3316 path: "/users/{id}".to_string(),
3317 operation_id: None,
3318 summary: None,
3319 description: None,
3320 parameters: vec![],
3321 };
3322
3323 assert!(route.operation_id.is_none());
3324 assert!(route.summary.is_none());
3325 assert!(route.description.is_none());
3326 assert_eq!(route.parameters.len(), 0);
3327}
3328
3329#[test]
3330fn test_http_server_state_empty_routes() {
3331 let state = HttpServerState::with_routes(vec![]);
3332 assert_eq!(state.routes.len(), 0);
3333}
3334
3335#[test]
3336fn test_http_server_state_multiple_routes() {
3337 let routes = vec![
3338 RouteInfo {
3339 method: "GET".to_string(),
3340 path: "/users".to_string(),
3341 operation_id: Some("listUsers".to_string()),
3342 summary: Some("List all users".to_string()),
3343 description: None,
3344 parameters: vec![],
3345 },
3346 RouteInfo {
3347 method: "GET".to_string(),
3348 path: "/users/{id}".to_string(),
3349 operation_id: Some("getUser".to_string()),
3350 summary: Some("Get a user".to_string()),
3351 description: None,
3352 parameters: vec!["id".to_string()],
3353 },
3354 RouteInfo {
3355 method: "POST".to_string(),
3356 path: "/users".to_string(),
3357 operation_id: Some("createUser".to_string()),
3358 summary: Some("Create a user".to_string()),
3359 description: None,
3360 parameters: vec!["body".to_string()],
3361 },
3362 ];
3363
3364 let state = HttpServerState::with_routes(routes);
3365 assert_eq!(state.routes.len(), 3);
3366
3367 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3369 assert!(methods.contains(&"GET"));
3370 assert!(methods.contains(&"POST"));
3371}
3372
3373#[test]
3374fn test_http_server_state_with_rate_limiter() {
3375 use std::sync::Arc;
3376
3377 let config = crate::middleware::RateLimitConfig::default();
3378 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
3379
3380 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3381
3382 assert!(state.rate_limiter.is_some());
3383 assert_eq!(state.routes.len(), 0);
3384}
3385
3386#[tokio::test]
3387async fn test_build_router_includes_rate_limiter() {
3388 let _router = build_router(None, None, None).await;
3389 }