1pub mod ai_handler;
172pub mod auth;
173pub mod chain_handlers;
174pub mod consistency;
176pub mod contract_diff_middleware;
178pub mod coverage;
179pub mod database;
180pub mod file_generator;
182pub mod file_server;
184pub mod health;
186pub mod http_tracing_middleware;
187pub mod latency_profiles;
189pub mod management;
191pub mod management_ws;
193pub mod metrics_middleware;
194pub mod middleware;
195pub mod op_middleware;
196pub mod protocol_server;
198pub mod proxy_server;
200pub mod quick_mock;
202pub mod rag_ai_generator;
204pub mod replay_listing;
206pub mod request_logging;
207pub mod spec_import;
209pub mod sse;
211pub mod state_machine_api;
213pub mod tls;
215pub mod token_response;
217pub mod ui_builder;
219pub mod verification;
221
222pub mod handlers;
224
225pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
227pub use health::{HealthManager, ServiceStatus};
229
230pub use management::{
232 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
233 ServerConfig, ServerStats,
234};
235
236pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
238
239pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
241
242pub use verification::verification_router;
244
245pub use metrics_middleware::collect_http_metrics;
247
248pub use http_tracing_middleware::http_tracing_middleware;
250
251pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
253
254async fn load_persona_from_config() -> Option<Arc<Persona>> {
257 use mockforge_core::config::load_config;
258
259 let config_paths = [
261 "config.yaml",
262 "mockforge.yaml",
263 "tools/mockforge/config.yaml",
264 "../tools/mockforge/config.yaml",
265 ];
266
267 for path in &config_paths {
268 if let Ok(config) = load_config(path).await {
269 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
272 {
273 tracing::info!(
274 "Loaded active persona '{}' from config file: {}",
275 persona.name,
276 path
277 );
278 return Some(Arc::new(persona.clone()));
279 } else {
280 tracing::debug!(
281 "No active persona found in config file: {} (personas count: {})",
282 path,
283 config.mockai.intelligent_behavior.personas.personas.len()
284 );
285 }
286 } else {
287 tracing::debug!("Could not load config from: {}", path);
288 }
289 }
290
291 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
292 None
293}
294
295use axum::extract::State;
296use axum::middleware::from_fn_with_state;
297use axum::response::Json;
298use axum::Router;
299use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
300use mockforge_core::intelligent_behavior::config::Persona;
301use mockforge_foundation::latency::LatencyInjector;
302use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
303use mockforge_openapi::openapi_routes::ValidationOptions;
304use mockforge_openapi::OpenApiSpec;
305use std::sync::Arc;
306use tower_http::cors::{Any, CorsLayer};
307
308#[cfg(feature = "data-faker")]
309use mockforge_data::provider::register_core_faker_provider;
310use mockforge_foundation::latency::LatencyProfile;
311use std::collections::HashMap;
312use std::ffi::OsStr;
313use std::path::Path;
314use tokio::fs;
315use tokio::sync::RwLock;
316use tracing::*;
317
318#[derive(Clone)]
320pub struct RouteInfo {
321 pub method: String,
323 pub path: String,
325 pub operation_id: Option<String>,
327 pub summary: Option<String>,
329 pub description: Option<String>,
331 pub parameters: Vec<String>,
333}
334
335#[derive(Clone)]
337pub struct HttpServerState {
338 pub routes: Vec<RouteInfo>,
340 pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
342 pub production_headers: Option<Arc<HashMap<String, String>>>,
344}
345
346impl Default for HttpServerState {
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352impl HttpServerState {
353 pub fn new() -> Self {
355 Self {
356 routes: Vec::new(),
357 rate_limiter: None,
358 production_headers: None,
359 }
360 }
361
362 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
364 Self {
365 routes,
366 rate_limiter: None,
367 production_headers: None,
368 }
369 }
370
371 pub fn with_rate_limiter(
373 mut self,
374 rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
375 ) -> Self {
376 self.rate_limiter = Some(rate_limiter);
377 self
378 }
379
380 pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
382 self.production_headers = Some(headers);
383 self
384 }
385}
386
387async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
389 let route_info: Vec<serde_json::Value> = state
390 .routes
391 .iter()
392 .map(|route| {
393 serde_json::json!({
394 "method": route.method,
395 "path": route.path,
396 "operation_id": route.operation_id,
397 "summary": route.summary,
398 "description": route.description,
399 "parameters": route.parameters
400 })
401 })
402 .collect();
403
404 Json(serde_json::json!({
405 "routes": route_info,
406 "total": state.routes.len()
407 }))
408}
409
410async fn get_docs_handler() -> axum::response::Html<&'static str> {
412 axum::response::Html(include_str!("../static/docs.html"))
413}
414
415pub async fn build_router(
417 spec_path: Option<String>,
418 options: Option<ValidationOptions>,
419 failure_config: Option<FailureConfig>,
420) -> Router {
421 build_router_with_multi_tenant(
422 spec_path,
423 options,
424 failure_config,
425 None,
426 None,
427 None,
428 None,
429 None,
430 None,
431 None,
432 )
433 .await
434}
435
436fn apply_cors_middleware(
438 app: Router,
439 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
440) -> Router {
441 use http::Method;
442 use tower_http::cors::AllowOrigin;
443
444 if let Some(config) = cors_config {
445 if !config.enabled {
446 return app;
447 }
448
449 let mut cors_layer = CorsLayer::new();
450 let is_wildcard_origin;
451
452 if config.allowed_origins.contains(&"*".to_string()) {
454 cors_layer = cors_layer.allow_origin(Any);
455 is_wildcard_origin = true;
456 } else if !config.allowed_origins.is_empty() {
457 let origins: Vec<_> = config
459 .allowed_origins
460 .iter()
461 .filter_map(|origin| {
462 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
463 })
464 .collect();
465
466 if origins.is_empty() {
467 warn!("No valid CORS origins configured, using permissive CORS");
469 cors_layer = cors_layer.allow_origin(Any);
470 is_wildcard_origin = true;
471 } else {
472 if origins.len() == 1 {
475 cors_layer = cors_layer.allow_origin(origins[0].clone());
476 is_wildcard_origin = false;
477 } else {
478 warn!(
480 "Multiple CORS origins configured, using permissive CORS. \
481 Consider using '*' for all origins."
482 );
483 cors_layer = cors_layer.allow_origin(Any);
484 is_wildcard_origin = true;
485 }
486 }
487 } else {
488 cors_layer = cors_layer.allow_origin(Any);
490 is_wildcard_origin = true;
491 }
492
493 if !config.allowed_methods.is_empty() {
495 let methods: Vec<Method> =
496 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
497 if !methods.is_empty() {
498 cors_layer = cors_layer.allow_methods(methods);
499 }
500 } else {
501 cors_layer = cors_layer.allow_methods([
503 Method::GET,
504 Method::POST,
505 Method::PUT,
506 Method::DELETE,
507 Method::PATCH,
508 Method::OPTIONS,
509 ]);
510 }
511
512 if !config.allowed_headers.is_empty() {
514 let headers: Vec<_> = config
515 .allowed_headers
516 .iter()
517 .filter_map(|h| h.parse::<http::HeaderName>().ok())
518 .collect();
519 if !headers.is_empty() {
520 cors_layer = cors_layer.allow_headers(headers);
521 }
522 } else {
523 cors_layer =
525 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
526 }
527
528 let should_allow_credentials = if is_wildcard_origin {
532 false
534 } else {
535 config.allow_credentials
537 };
538
539 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
540
541 info!(
542 "CORS middleware enabled with configured settings (credentials: {})",
543 should_allow_credentials
544 );
545 app.layer(cors_layer)
546 } else {
547 debug!("No CORS config provided, using permissive CORS for development");
551 app.layer(CorsLayer::permissive().allow_credentials(false))
554 }
555}
556
557#[allow(clippy::too_many_arguments)]
559#[allow(deprecated)] pub async fn build_router_with_multi_tenant(
561 spec_path: Option<String>,
562 options: Option<ValidationOptions>,
563 failure_config: Option<FailureConfig>,
564 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
565 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
566 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
567 ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
568 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
569 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
570 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
571) -> Router {
572 use std::time::Instant;
573
574 let startup_start = Instant::now();
575
576 let mut app = Router::new();
578
579 let mut rate_limit_config = middleware::RateLimitConfig {
582 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
583 .ok()
584 .and_then(|v| v.parse().ok())
585 .unwrap_or(1000),
586 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
587 .ok()
588 .and_then(|v| v.parse().ok())
589 .unwrap_or(2000),
590 per_ip: true,
591 per_endpoint: false,
592 };
593
594 let mut final_cors_config = cors_config;
596 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
597 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
599
600 if let Some(deploy_config) = &deceptive_deploy_config {
601 if deploy_config.enabled {
602 info!("Deceptive deploy mode enabled - applying production-like configuration");
603
604 if let Some(prod_cors) = &deploy_config.cors {
606 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
607 enabled: true,
608 allowed_origins: prod_cors.allowed_origins.clone(),
609 allowed_methods: prod_cors.allowed_methods.clone(),
610 allowed_headers: prod_cors.allowed_headers.clone(),
611 allow_credentials: prod_cors.allow_credentials,
612 });
613 info!("Applied production-like CORS configuration");
614 }
615
616 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
618 rate_limit_config = middleware::RateLimitConfig {
619 requests_per_minute: prod_rate_limit.requests_per_minute,
620 burst: prod_rate_limit.burst,
621 per_ip: prod_rate_limit.per_ip,
622 per_endpoint: false,
623 };
624 info!(
625 "Applied production-like rate limiting: {} req/min, burst: {}",
626 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
627 );
628 }
629
630 if !deploy_config.headers.is_empty() {
632 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
633 production_headers = Some(std::sync::Arc::new(headers_map));
634 info!("Configured {} production headers", deploy_config.headers.len());
635 }
636
637 if let Some(prod_oauth) = &deploy_config.oauth {
639 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
640 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
641 oauth2: Some(oauth2_config),
642 ..Default::default()
643 });
644 info!("Applied production-like OAuth configuration for deceptive deploy");
645 }
646 }
647 }
648
649 let rate_limiter =
650 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
651
652 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
653
654 if let Some(headers) = production_headers.clone() {
656 state = state.with_production_headers(headers);
657 }
658
659 let spec_path_for_mgmt = spec_path.clone();
661
662 if let Some(spec_path) = spec_path {
664 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
665
666 let spec_load_start = Instant::now();
668 match OpenApiSpec::from_file(&spec_path).await {
669 Ok(openapi) => {
670 let spec_load_duration = spec_load_start.elapsed();
671 info!(
672 "Successfully loaded OpenAPI spec from {} (took {:?})",
673 spec_path, spec_load_duration
674 );
675
676 tracing::debug!("Creating OpenAPI route registry...");
678 let registry_start = Instant::now();
679
680 let persona = load_persona_from_config().await;
682
683 let registry = if let Some(opts) = options {
684 tracing::debug!("Using custom validation options");
685 if let Some(ref persona) = persona {
686 tracing::info!("Using persona '{}' for route generation", persona.name);
687 }
688 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
689 } else {
690 tracing::debug!("Using environment-based options");
691 if let Some(ref persona) = persona {
692 tracing::info!("Using persona '{}' for route generation", persona.name);
693 }
694 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
695 };
696 let registry_duration = registry_start.elapsed();
697 info!(
698 "Created OpenAPI route registry with {} routes (took {:?})",
699 registry.routes().len(),
700 registry_duration
701 );
702
703 let extract_start = Instant::now();
705 let route_info: Vec<RouteInfo> = registry
706 .routes()
707 .iter()
708 .map(|route| RouteInfo {
709 method: route.method.clone(),
710 path: route.path.clone(),
711 operation_id: route.operation.operation_id.clone(),
712 summary: route.operation.summary.clone(),
713 description: route.operation.description.clone(),
714 parameters: route.parameters.clone(),
715 })
716 .collect();
717 state.routes = route_info;
718 let extract_duration = extract_start.elapsed();
719 debug!("Extracted route information (took {:?})", extract_duration);
720
721 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
723 tracing::debug!("Loading overrides from environment variable");
724 let overrides_start = Instant::now();
725 match mockforge_core::Overrides::load_from_globs(&[]).await {
726 Ok(overrides) => {
727 let overrides_duration = overrides_start.elapsed();
728 info!(
729 "Loaded {} override rules (took {:?})",
730 overrides.rules().len(),
731 overrides_duration
732 );
733 Some(overrides)
734 }
735 Err(e) => {
736 tracing::warn!("Failed to load overrides: {}", e);
737 None
738 }
739 }
740 } else {
741 None
742 };
743
744 let router_build_start = Instant::now();
746 let overrides_enabled = overrides.is_some();
747 let response_rewriter: Option<
748 std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
749 > = Some(std::sync::Arc::new(
750 mockforge_core::openapi_rewriter::CoreResponseRewriter::new(overrides),
751 ));
752 let openapi_router = if let Some(mockai_instance) = &mockai {
753 tracing::debug!("Building router with MockAI support");
754 registry.build_router_with_mockai(Some(mockai_instance.clone()))
755 } else if let Some(ai_generator) = &ai_generator {
756 tracing::debug!("Building router with AI generator support");
757 registry.build_router_with_ai(Some(ai_generator.clone()))
758 } else if let Some(failure_config) = &failure_config {
759 tracing::debug!("Building router with failure injection and overrides");
760 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
761 registry.build_router_with_injectors_and_overrides(
762 LatencyInjector::default(),
763 Some(failure_injector),
764 response_rewriter,
765 overrides_enabled,
766 )
767 } else {
768 tracing::debug!("Building router with overrides");
769 registry.build_router_with_injectors_and_overrides(
770 LatencyInjector::default(),
771 None,
772 response_rewriter,
773 overrides_enabled,
774 )
775 };
776 let router_build_duration = router_build_start.elapsed();
777 debug!("Built OpenAPI router (took {:?})", router_build_duration);
778
779 tracing::debug!("Merging OpenAPI router with main router");
780 app = app.merge(openapi_router);
781 tracing::debug!("Router built successfully");
782 }
783 Err(e) => {
784 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
785 }
786 }
787 }
788
789 app = app.route(
791 "/health",
792 axum::routing::get(|| async {
793 use mockforge_core::server_utils::health::HealthStatus;
794 {
795 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
797 Ok(value) => Json(value),
798 Err(e) => {
799 tracing::error!("Failed to serialize health status: {}", e);
801 Json(serde_json::json!({
802 "status": "healthy",
803 "service": "mockforge-http",
804 "uptime_seconds": 0
805 }))
806 }
807 }
808 }
809 }),
810 )
811 .merge(sse::sse_router())
813 .merge(file_server::file_serving_router());
815
816 let state_for_routes = state.clone();
818
819 let routes_router = Router::new()
821 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
822 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
823 .with_state(state_for_routes);
824
825 app = app.merge(routes_router);
827
828 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
830
831 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
834 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
835
836 if Path::new(&coverage_html_path).exists() {
838 app = app.nest_service(
839 "/__mockforge/coverage.html",
840 tower_http::services::ServeFile::new(&coverage_html_path),
841 );
842 debug!("Serving coverage UI from: {}", coverage_html_path);
843 } else {
844 debug!(
845 "Coverage UI file not found at: {}. Skipping static file serving.",
846 coverage_html_path
847 );
848 }
849
850 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
853 match OpenApiSpec::from_file(sp).await {
854 Ok(s) => Some(Arc::new(s)),
855 Err(e) => {
856 debug!("Failed to load OpenAPI spec for management API: {}", e);
857 None
858 }
859 }
860 } else {
861 None
862 };
863 let mgmt_port = std::env::var("PORT")
864 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
865 .ok()
866 .and_then(|p| p.parse().ok())
867 .unwrap_or(3000);
868 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
869
870 use std::sync::Arc;
872 let ws_state = WsManagementState::new();
873 let ws_broadcast = Arc::new(ws_state.tx.clone());
874 let management_state = management_state.with_ws_broadcast(ws_broadcast);
875
876 #[cfg(feature = "smtp")]
880 let management_state = {
881 if let Some(smtp_reg) = smtp_registry {
882 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
883 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
884 Err(e) => {
885 error!(
886 "Invalid SMTP registry type passed to HTTP management state: {:?}",
887 e.type_id()
888 );
889 management_state
890 }
891 }
892 } else {
893 management_state
894 }
895 };
896 #[cfg(not(feature = "smtp"))]
897 let management_state = management_state;
898 #[cfg(not(feature = "smtp"))]
899 let _ = smtp_registry;
900 let management_state_for_fallback = management_state.clone();
901 app = app.nest("/__mockforge/api", management_router(management_state));
902 app = app.fallback_service(
907 axum::routing::any(crate::management::dynamic_mock_fallback)
908 .with_state(management_state_for_fallback),
909 );
910
911 app = app.merge(verification_router());
913
914 use crate::auth::oidc::oidc_router;
916 app = app.merge(oidc_router());
917
918 {
920 use mockforge_core::security::get_global_access_review_service;
921 if let Some(service) = get_global_access_review_service().await {
922 use crate::handlers::access_review::{access_review_router, AccessReviewState};
923 let review_state = AccessReviewState { service };
924 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
925 debug!("Access review API mounted at /api/v1/security/access-reviews");
926 }
927 }
928
929 {
931 use mockforge_core::security::get_global_privileged_access_manager;
932 if let Some(manager) = get_global_privileged_access_manager().await {
933 use crate::handlers::privileged_access::{
934 privileged_access_router, PrivilegedAccessState,
935 };
936 let privileged_state = PrivilegedAccessState { manager };
937 app = app.nest(
938 "/api/v1/security/privileged-access",
939 privileged_access_router(privileged_state),
940 );
941 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
942 }
943 }
944
945 {
947 use mockforge_core::security::get_global_change_management_engine;
948 if let Some(engine) = get_global_change_management_engine().await {
949 use crate::handlers::change_management::{
950 change_management_router, ChangeManagementState,
951 };
952 let change_state = ChangeManagementState { engine };
953 app = app.nest("/api/v1/change-management", change_management_router(change_state));
954 debug!("Change management API mounted at /api/v1/change-management");
955 }
956 }
957
958 {
960 use mockforge_core::security::get_global_risk_assessment_engine;
961 if let Some(engine) = get_global_risk_assessment_engine().await {
962 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
963 let risk_state = RiskAssessmentState { engine };
964 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
965 debug!("Risk assessment API mounted at /api/v1/security/risks");
966 }
967 }
968
969 {
971 use crate::auth::token_lifecycle::TokenLifecycleManager;
972 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
973 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
974 let lifecycle_state = TokenLifecycleState {
975 manager: lifecycle_manager,
976 };
977 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
978 debug!("Token lifecycle API mounted at /api/v1/auth");
979 }
980
981 {
983 use crate::auth::oidc::load_oidc_state;
984 use crate::auth::token_lifecycle::TokenLifecycleManager;
985 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
986 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
988 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
989 let oauth2_state = OAuth2ServerState {
990 oidc_state,
991 lifecycle_manager,
992 auth_codes: Arc::new(RwLock::new(HashMap::new())),
993 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
994 };
995 app = app.merge(oauth2_server_router(oauth2_state));
996 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
997 }
998
999 {
1001 use crate::auth::oidc::load_oidc_state;
1002 use crate::auth::risk_engine::RiskEngine;
1003 use crate::auth::token_lifecycle::TokenLifecycleManager;
1004 use crate::handlers::consent::{consent_router, ConsentState};
1005 use crate::handlers::oauth2_server::OAuth2ServerState;
1006 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1008 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1009 let oauth2_state = OAuth2ServerState {
1010 oidc_state: oidc_state.clone(),
1011 lifecycle_manager: lifecycle_manager.clone(),
1012 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1013 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1014 };
1015 let risk_engine = Arc::new(RiskEngine::default());
1016 let consent_state = ConsentState {
1017 oauth2_state,
1018 risk_engine,
1019 };
1020 app = app.merge(consent_router(consent_state));
1021 debug!("Consent screen endpoints mounted at /consent");
1022 }
1023
1024 {
1026 use crate::auth::risk_engine::RiskEngine;
1027 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1028 let risk_engine = Arc::new(RiskEngine::default());
1029 let risk_state = RiskSimulationState { risk_engine };
1030 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1031 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1032 }
1033
1034 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1036
1037 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1039
1040 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1042
1043 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1046
1047 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1049
1050 if state.production_headers.is_some() {
1052 app =
1053 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1054 }
1055
1056 if let Some(auth_config) = deceptive_deploy_auth_config {
1058 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1059 use std::collections::HashMap;
1060 use std::sync::Arc;
1061 use tokio::sync::RwLock;
1062
1063 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1065 match create_oauth2_client(oauth2_config) {
1066 Ok(client) => Some(client),
1067 Err(e) => {
1068 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1069 None
1070 }
1071 }
1072 } else {
1073 None
1074 };
1075
1076 let auth_state = AuthState {
1078 config: auth_config,
1079 spec: None, oauth2_client,
1081 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1082 };
1083
1084 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1086 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1087 }
1088
1089 app = apply_cors_middleware(app, final_cors_config);
1091
1092 if let Some(mt_config) = multi_tenant_config {
1094 if mt_config.enabled {
1095 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1096 use std::sync::Arc;
1097
1098 info!(
1099 "Multi-tenant mode enabled with {} routing strategy",
1100 match mt_config.routing_strategy {
1101 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1102 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1103 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1104 }
1105 );
1106
1107 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1109
1110 let default_workspace =
1112 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1113 if let Err(e) =
1114 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1115 {
1116 warn!("Failed to register default workspace: {}", e);
1117 } else {
1118 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1119 }
1120
1121 if mt_config.auto_discover {
1123 if let Some(config_dir) = &mt_config.config_directory {
1124 let config_path = Path::new(config_dir);
1125 if config_path.exists() && config_path.is_dir() {
1126 match fs::read_dir(config_path).await {
1127 Ok(mut entries) => {
1128 while let Ok(Some(entry)) = entries.next_entry().await {
1129 let path = entry.path();
1130 if path.extension() == Some(OsStr::new("yaml")) {
1131 match fs::read_to_string(&path).await {
1132 Ok(content) => {
1133 match serde_yaml::from_str::<
1134 mockforge_core::Workspace,
1135 >(
1136 &content
1137 ) {
1138 Ok(workspace) => {
1139 if let Err(e) = registry.register_workspace(
1140 workspace.id.clone(),
1141 workspace,
1142 ) {
1143 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1144 } else {
1145 info!("Auto-registered workspace from {:?}", path);
1146 }
1147 }
1148 Err(e) => {
1149 warn!("Failed to parse workspace from {:?}: {}", path, e);
1150 }
1151 }
1152 }
1153 Err(e) => {
1154 warn!(
1155 "Failed to read workspace file {:?}: {}",
1156 path, e
1157 );
1158 }
1159 }
1160 }
1161 }
1162 }
1163 Err(e) => {
1164 warn!("Failed to read config directory {:?}: {}", config_path, e);
1165 }
1166 }
1167 } else {
1168 warn!(
1169 "Config directory {:?} does not exist or is not a directory",
1170 config_path
1171 );
1172 }
1173 }
1174 }
1175
1176 let registry = Arc::new(registry);
1178
1179 let _workspace_router = WorkspaceRouter::new(registry);
1181
1182 info!("Workspace routing middleware initialized for HTTP server");
1185 }
1186 }
1187
1188 let total_startup_duration = startup_start.elapsed();
1189 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1190
1191 app
1192}
1193
1194pub async fn build_router_with_auth_and_latency(
1196 spec_path: Option<String>,
1197 _options: Option<()>,
1198 auth_config: Option<mockforge_core::config::AuthConfig>,
1199 latency_injector: Option<LatencyInjector>,
1200) -> Router {
1201 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1203
1204 if let Some(injector) = latency_injector {
1206 let injector = Arc::new(injector);
1207 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1208 let injector = injector.clone();
1209 async move {
1210 let _ = injector.inject_latency(&[]).await;
1211 next.run(req).await
1212 }
1213 }));
1214 }
1215
1216 app
1217}
1218
1219pub async fn build_router_with_latency(
1221 spec_path: Option<String>,
1222 options: Option<ValidationOptions>,
1223 latency_injector: Option<LatencyInjector>,
1224) -> Router {
1225 if let Some(spec) = &spec_path {
1226 match OpenApiSpec::from_file(spec).await {
1227 Ok(openapi) => {
1228 let registry = if let Some(opts) = options {
1229 OpenApiRouteRegistry::new_with_options(openapi, opts)
1230 } else {
1231 OpenApiRouteRegistry::new_with_env(openapi)
1232 };
1233
1234 if let Some(injector) = latency_injector {
1235 return registry.build_router_with_latency(injector);
1236 } else {
1237 return registry.build_router();
1238 }
1239 }
1240 Err(e) => {
1241 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1242 }
1243 }
1244 }
1245
1246 build_router(None, None, None).await
1247}
1248
1249pub async fn build_router_with_auth(
1251 spec_path: Option<String>,
1252 options: Option<ValidationOptions>,
1253 auth_config: Option<mockforge_core::config::AuthConfig>,
1254) -> Router {
1255 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1256 use std::sync::Arc;
1257
1258 #[cfg(feature = "data-faker")]
1260 {
1261 register_core_faker_provider();
1262 }
1263
1264 let spec = if let Some(spec_path) = &spec_path {
1266 match OpenApiSpec::from_file(&spec_path).await {
1267 Ok(spec) => Some(Arc::new(spec)),
1268 Err(e) => {
1269 warn!("Failed to load OpenAPI spec for auth: {}", e);
1270 None
1271 }
1272 }
1273 } else {
1274 None
1275 };
1276
1277 let oauth2_client = if let Some(auth_config) = &auth_config {
1279 if let Some(oauth2_config) = &auth_config.oauth2 {
1280 match create_oauth2_client(oauth2_config) {
1281 Ok(client) => Some(client),
1282 Err(e) => {
1283 warn!("Failed to create OAuth2 client: {}", e);
1284 None
1285 }
1286 }
1287 } else {
1288 None
1289 }
1290 } else {
1291 None
1292 };
1293
1294 let auth_state = AuthState {
1295 config: auth_config.unwrap_or_default(),
1296 spec,
1297 oauth2_client,
1298 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1299 };
1300
1301 let mut app = Router::new().with_state(auth_state.clone());
1303
1304 if let Some(spec_path) = spec_path {
1306 match OpenApiSpec::from_file(&spec_path).await {
1307 Ok(openapi) => {
1308 info!("Loaded OpenAPI spec from {}", spec_path);
1309 let registry = if let Some(opts) = options {
1310 OpenApiRouteRegistry::new_with_options(openapi, opts)
1311 } else {
1312 OpenApiRouteRegistry::new_with_env(openapi)
1313 };
1314
1315 app = registry.build_router();
1316 }
1317 Err(e) => {
1318 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1319 }
1320 }
1321 }
1322
1323 app = app.route(
1325 "/health",
1326 axum::routing::get(|| async {
1327 use mockforge_core::server_utils::health::HealthStatus;
1328 {
1329 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1331 Ok(value) => Json(value),
1332 Err(e) => {
1333 tracing::error!("Failed to serialize health status: {}", e);
1335 Json(serde_json::json!({
1336 "status": "healthy",
1337 "service": "mockforge-http",
1338 "uptime_seconds": 0
1339 }))
1340 }
1341 }
1342 }
1343 }),
1344 )
1345 .merge(sse::sse_router())
1347 .merge(file_server::file_serving_router())
1349 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1351 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1353
1354 app
1355}
1356
1357pub async fn serve_router(
1359 port: u16,
1360 app: Router,
1361) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1362 serve_router_with_tls(port, app, None).await
1363}
1364
1365pub async fn serve_router_with_tls(
1367 port: u16,
1368 app: Router,
1369 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1370) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1371 serve_router_with_tls_notify(port, app, tls_config, None).await
1372}
1373
1374pub async fn serve_router_with_tls_notify(
1382 port: u16,
1383 app: Router,
1384 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1385 bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1386) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1387 use std::net::SocketAddr;
1388
1389 let addr = mockforge_core::wildcard_socket_addr(port);
1390
1391 if let Some(ref tls) = tls_config {
1392 if tls.enabled {
1393 info!("HTTPS listening on {}", addr);
1394 if let Some(tx) = bound_port_tx {
1395 let _ = tx.send(port);
1396 }
1397 return serve_with_tls(addr, app, tls).await;
1398 }
1399 }
1400
1401 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1402 format!(
1403 "Failed to bind HTTP server to port {}: {}\n\
1404 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 {}",
1405 port, e, port, port
1406 )
1407 })?;
1408
1409 let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1410 info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1411 if let Some(tx) = bound_port_tx {
1412 let _ = tx.send(actual_port);
1413 }
1414
1415 let odata_app = tower::ServiceBuilder::new()
1419 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1420 .service(app);
1421 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1422 axum::serve(listener, make_svc).await?;
1423 Ok(())
1424}
1425
1426async fn serve_with_tls(
1431 addr: std::net::SocketAddr,
1432 app: Router,
1433 tls_config: &mockforge_core::config::HttpTlsConfig,
1434) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1435 use axum_server::tls_rustls::RustlsConfig;
1436 use std::net::SocketAddr;
1437
1438 tls::init_crypto_provider();
1440
1441 info!("Loading TLS configuration for HTTPS server");
1442
1443 let server_config = tls::load_tls_server_config(tls_config)?;
1445
1446 let rustls_config = RustlsConfig::from_config(server_config);
1449
1450 info!("Starting HTTPS server on {}", addr);
1451
1452 let odata_app = tower::ServiceBuilder::new()
1456 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1457 .service(app);
1458 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1459
1460 axum_server::bind_rustls(addr, rustls_config)
1462 .serve(make_svc)
1463 .await
1464 .map_err(|e| format!("HTTPS server error: {}", e).into())
1465}
1466
1467pub async fn start(
1469 port: u16,
1470 spec_path: Option<String>,
1471 options: Option<ValidationOptions>,
1472) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1473 start_with_latency(port, spec_path, options, None).await
1474}
1475
1476pub async fn start_with_auth_and_latency(
1478 port: u16,
1479 spec_path: Option<String>,
1480 options: Option<ValidationOptions>,
1481 auth_config: Option<mockforge_core::config::AuthConfig>,
1482 latency_profile: Option<LatencyProfile>,
1483) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1484 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1485 .await
1486}
1487
1488pub async fn start_with_auth_and_injectors(
1490 port: u16,
1491 spec_path: Option<String>,
1492 options: Option<ValidationOptions>,
1493 auth_config: Option<mockforge_core::config::AuthConfig>,
1494 _latency_profile: Option<LatencyProfile>,
1495 _failure_injector: Option<FailureInjector>,
1496) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1497 let app = build_router_with_auth(spec_path, options, auth_config).await;
1499 serve_router(port, app).await
1500}
1501
1502pub async fn start_with_latency(
1504 port: u16,
1505 spec_path: Option<String>,
1506 options: Option<ValidationOptions>,
1507 latency_profile: Option<LatencyProfile>,
1508) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1509 let latency_injector =
1510 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1511
1512 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1513 serve_router(port, app).await
1514}
1515
1516pub async fn build_router_with_chains(
1518 spec_path: Option<String>,
1519 options: Option<ValidationOptions>,
1520 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1521) -> Router {
1522 build_router_with_chains_and_multi_tenant(
1523 spec_path,
1524 options,
1525 circling_config,
1526 None,
1527 None,
1528 None,
1529 None,
1530 None,
1531 None,
1532 None,
1533 false,
1534 None, None, None, None, )
1539 .await
1540}
1541
1542async fn apply_route_chaos(
1550 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1551 method: &http::Method,
1552 uri: &http::Uri,
1553) -> Option<axum::response::Response> {
1554 use axum::http::StatusCode;
1555 use axum::response::IntoResponse;
1556
1557 if let Some(injector) = injector {
1558 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1560 let mut response = Json(serde_json::json!({
1562 "error": fault_response.error_message,
1563 "fault_type": fault_response.fault_type,
1564 }))
1565 .into_response();
1566 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1567 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1568 return Some(response);
1569 }
1570
1571 if let Err(e) = injector.inject_latency(method, uri).await {
1573 tracing::warn!("Failed to inject latency: {}", e);
1574 }
1575 }
1576
1577 None }
1579
1580#[allow(clippy::too_many_arguments)]
1582#[allow(deprecated)] pub async fn build_router_with_chains_and_multi_tenant(
1584 spec_path: Option<String>,
1585 options: Option<ValidationOptions>,
1586 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1587 multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1588 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1589 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1590 _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1591 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1592 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1593 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1594 traffic_shaping_enabled: bool,
1595 health_manager: Option<Arc<HealthManager>>,
1596 _mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1597 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1598 proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1599) -> Router {
1600 use crate::latency_profiles::LatencyProfiles;
1601 use crate::op_middleware::Shared;
1602 use mockforge_core::Overrides;
1603
1604 let template_expand =
1606 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1607 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1608 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1609 .unwrap_or(false)
1610 });
1611
1612 let _shared = Shared {
1613 profiles: LatencyProfiles::default(),
1614 overrides: Overrides::default(),
1615 failure_injector: None,
1616 traffic_shaper,
1617 overrides_enabled: false,
1618 traffic_shaping_enabled,
1619 };
1620
1621 let mut app = Router::new();
1623 let mut include_default_health = true;
1624 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1625
1626 if let Some(ref spec) = spec_path {
1628 match OpenApiSpec::from_file(&spec).await {
1629 Ok(openapi) => {
1630 info!("Loaded OpenAPI spec from {}", spec);
1631
1632 let persona = load_persona_from_config().await;
1634
1635 let mut registry = if let Some(opts) = options {
1636 tracing::debug!("Using custom validation options");
1637 if let Some(ref persona) = persona {
1638 tracing::info!("Using persona '{}' for route generation", persona.name);
1639 }
1640 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1641 } else {
1642 tracing::debug!("Using environment-based options");
1643 if let Some(ref persona) = persona {
1644 tracing::info!("Using persona '{}' for route generation", persona.name);
1645 }
1646 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1647 };
1648
1649 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1651 .unwrap_or_else(|_| "/app/fixtures".to_string());
1652 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1653 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1654 .unwrap_or(true); if custom_fixtures_enabled {
1657 use mockforge_openapi::CustomFixtureLoader;
1658 use std::path::PathBuf;
1659 use std::sync::Arc;
1660
1661 let fixtures_path = PathBuf::from(&fixtures_dir);
1662 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1663
1664 if let Err(e) = custom_loader.load_fixtures().await {
1665 tracing::warn!("Failed to load custom fixtures: {}", e);
1666 } else {
1667 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1668 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1669 }
1670 }
1671
1672 if registry
1673 .routes()
1674 .iter()
1675 .any(|route| route.method == "GET" && route.path == "/health")
1676 {
1677 include_default_health = false;
1678 }
1679 captured_routes = registry
1681 .routes()
1682 .iter()
1683 .map(|r| RouteInfo {
1684 method: r.method.clone(),
1685 path: r.path.clone(),
1686 operation_id: r.operation.operation_id.clone(),
1687 summary: r.operation.summary.clone(),
1688 description: r.operation.description.clone(),
1689 parameters: r.parameters.clone(),
1690 })
1691 .collect();
1692
1693 {
1696 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1697 captured_routes
1698 .iter()
1699 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1700 method: r.method.clone(),
1701 path: r.path.clone(),
1702 operation_id: r.operation_id.clone(),
1703 summary: r.summary.clone(),
1704 description: r.description.clone(),
1705 parameters: r.parameters.clone(),
1706 })
1707 .collect();
1708 mockforge_core::request_logger::set_global_routes(global_routes);
1709 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1710 }
1711
1712 let spec_router = if let Some(ref mockai_instance) = _mockai {
1714 tracing::debug!("Building router with MockAI support");
1715 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1716 } else {
1717 registry.build_router()
1718 };
1719 app = app.merge(spec_router);
1720 }
1721 Err(e) => {
1722 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1723 }
1724 }
1725 }
1726
1727 let route_chaos_injector: Option<
1731 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1732 > = if let Some(ref route_configs) = route_configs {
1733 if !route_configs.is_empty() {
1734 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1737 route_configs.to_vec();
1738 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1739 Ok(injector) => {
1740 info!(
1741 "Initialized advanced routing features for {} route(s)",
1742 route_configs.len()
1743 );
1744 Some(std::sync::Arc::new(injector)
1747 as std::sync::Arc<
1748 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1749 >)
1750 }
1751 Err(e) => {
1752 warn!(
1753 "Failed to initialize advanced routing features: {}. Using basic routing.",
1754 e
1755 );
1756 None
1757 }
1758 }
1759 } else {
1760 None
1761 }
1762 } else {
1763 None
1764 };
1765
1766 if let Some(route_configs) = route_configs {
1767 use axum::http::StatusCode;
1768 use axum::response::IntoResponse;
1769
1770 if !route_configs.is_empty() {
1771 info!("Registering {} custom route(s) from config", route_configs.len());
1772 }
1773
1774 let injector = route_chaos_injector.clone();
1775 for route_config in route_configs {
1776 let status = route_config.response.status;
1777 let body = route_config.response.body.clone();
1778 let headers = route_config.response.headers.clone();
1779 let path = route_config.path.clone();
1780 let method = route_config.method.clone();
1781
1782 let expected_method = method.to_uppercase();
1787 let injector_clone = injector.clone();
1791 app = app.route(
1792 &path,
1793 #[allow(clippy::non_send_fields_in_send_ty)]
1794 axum::routing::any(move |req: http::Request<axum::body::Body>| {
1795 let body = body.clone();
1796 let headers = headers.clone();
1797 let expand = template_expand;
1798 let expected = expected_method.clone();
1799 let status_code = status;
1800 let injector_for_chaos = injector_clone.clone();
1802
1803 async move {
1804 if req.method().as_str() != expected.as_str() {
1806 return axum::response::Response::builder()
1808 .status(StatusCode::METHOD_NOT_ALLOWED)
1809 .header("Allow", &expected)
1810 .body(axum::body::Body::empty())
1811 .unwrap()
1812 .into_response();
1813 }
1814
1815 if let Some(fault_response) = apply_route_chaos(
1819 injector_for_chaos.as_deref(),
1820 req.method(),
1821 req.uri(),
1822 )
1823 .await
1824 {
1825 return fault_response;
1826 }
1827
1828 let mut body_value = body.unwrap_or(serde_json::json!({}));
1830
1831 if expand {
1835 use mockforge_template_expansion::RequestContext;
1836 use serde_json::Value;
1837 use std::collections::HashMap;
1838
1839 let method = req.method().to_string();
1841 let path = req.uri().path().to_string();
1842
1843 let query_params: HashMap<String, Value> = req
1845 .uri()
1846 .query()
1847 .map(|q| {
1848 url::form_urlencoded::parse(q.as_bytes())
1849 .into_owned()
1850 .map(|(k, v)| (k, Value::String(v)))
1851 .collect()
1852 })
1853 .unwrap_or_default();
1854
1855 let headers: HashMap<String, Value> = req
1857 .headers()
1858 .iter()
1859 .map(|(k, v)| {
1860 (
1861 k.to_string(),
1862 Value::String(v.to_str().unwrap_or_default().to_string()),
1863 )
1864 })
1865 .collect();
1866
1867 let context = RequestContext {
1871 method,
1872 path,
1873 query_params,
1874 headers,
1875 body: None, path_params: HashMap::new(),
1877 multipart_fields: HashMap::new(),
1878 multipart_files: HashMap::new(),
1879 };
1880
1881 let body_value_clone = body_value.clone();
1885 let context_clone = context.clone();
1886 body_value = match tokio::task::spawn_blocking(move || {
1887 mockforge_template_expansion::expand_templates_in_json(
1888 body_value_clone,
1889 &context_clone,
1890 )
1891 })
1892 .await
1893 {
1894 Ok(result) => result,
1895 Err(_) => body_value, };
1897 }
1898
1899 let mut response = Json(body_value).into_response();
1900
1901 *response.status_mut() =
1903 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
1904
1905 for (key, value) in headers {
1907 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
1908 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
1909 response.headers_mut().insert(header_name, header_value);
1910 }
1911 }
1912 }
1913
1914 response
1915 }
1916 }),
1917 );
1918
1919 debug!("Registered route: {} {}", method, path);
1920 }
1921 }
1922
1923 if let Some(health) = health_manager {
1925 app = app.merge(health::health_router(health));
1927 info!(
1928 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1929 );
1930 } else if include_default_health {
1931 app = app.route(
1933 "/health",
1934 axum::routing::get(|| async {
1935 use mockforge_core::server_utils::health::HealthStatus;
1936 {
1937 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1939 Ok(value) => Json(value),
1940 Err(e) => {
1941 tracing::error!("Failed to serialize health status: {}", e);
1943 Json(serde_json::json!({
1944 "status": "healthy",
1945 "service": "mockforge-http",
1946 "uptime_seconds": 0
1947 }))
1948 }
1949 }
1950 }
1951 }),
1952 );
1953 }
1954
1955 app = app.merge(sse::sse_router());
1956 app = app.merge(file_server::file_serving_router());
1958
1959 let mgmt_spec = if let Some(ref sp) = spec_path {
1962 match OpenApiSpec::from_file(sp).await {
1963 Ok(s) => Some(Arc::new(s)),
1964 Err(e) => {
1965 debug!("Failed to load OpenAPI spec for management API: {}", e);
1966 None
1967 }
1968 }
1969 } else {
1970 None
1971 };
1972 let spec_path_clone = spec_path.clone();
1973 let mgmt_port = std::env::var("PORT")
1974 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
1975 .ok()
1976 .and_then(|p| p.parse().ok())
1977 .unwrap_or(3000);
1978 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
1979
1980 use std::sync::Arc;
1982 let ws_state = WsManagementState::new();
1983 let ws_broadcast = Arc::new(ws_state.tx.clone());
1984 let management_state = management_state.with_ws_broadcast(ws_broadcast);
1985
1986 let management_state = if let Some(proxy_cfg) = proxy_config {
1988 use tokio::sync::RwLock;
1989 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
1990 management_state.with_proxy_config(proxy_config_arc)
1991 } else {
1992 management_state
1993 };
1994
1995 #[cfg(feature = "smtp")]
1996 let management_state = {
1997 if let Some(smtp_reg) = smtp_registry {
1998 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1999 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2000 Err(e) => {
2001 error!(
2002 "Invalid SMTP registry type passed to HTTP management state: {:?}",
2003 e.type_id()
2004 );
2005 management_state
2006 }
2007 }
2008 } else {
2009 management_state
2010 }
2011 };
2012 #[cfg(not(feature = "smtp"))]
2013 let management_state = {
2014 let _ = smtp_registry;
2015 management_state
2016 };
2017 #[cfg(feature = "mqtt")]
2018 let management_state = {
2019 if let Some(broker) = mqtt_broker {
2020 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
2021 Ok(broker) => management_state.with_mqtt_broker(broker),
2022 Err(e) => {
2023 error!(
2024 "Invalid MQTT broker passed to HTTP management state: {:?}",
2025 e.type_id()
2026 );
2027 management_state
2028 }
2029 }
2030 } else {
2031 management_state
2032 }
2033 };
2034 #[cfg(not(feature = "mqtt"))]
2035 let management_state = {
2036 let _ = mqtt_broker;
2037 management_state
2038 };
2039 let management_state_for_fallback = management_state.clone();
2040 app = app.nest("/__mockforge/api", management_router(management_state));
2041 app = app.fallback_service(
2043 axum::routing::any(crate::management::dynamic_mock_fallback)
2044 .with_state(management_state_for_fallback),
2045 );
2046
2047 app = app.merge(verification_router());
2049
2050 use crate::auth::oidc::oidc_router;
2052 app = app.merge(oidc_router());
2053
2054 {
2056 use mockforge_core::security::get_global_access_review_service;
2057 if let Some(service) = get_global_access_review_service().await {
2058 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2059 let review_state = AccessReviewState { service };
2060 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2061 debug!("Access review API mounted at /api/v1/security/access-reviews");
2062 }
2063 }
2064
2065 {
2067 use mockforge_core::security::get_global_privileged_access_manager;
2068 if let Some(manager) = get_global_privileged_access_manager().await {
2069 use crate::handlers::privileged_access::{
2070 privileged_access_router, PrivilegedAccessState,
2071 };
2072 let privileged_state = PrivilegedAccessState { manager };
2073 app = app.nest(
2074 "/api/v1/security/privileged-access",
2075 privileged_access_router(privileged_state),
2076 );
2077 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2078 }
2079 }
2080
2081 {
2083 use mockforge_core::security::get_global_change_management_engine;
2084 if let Some(engine) = get_global_change_management_engine().await {
2085 use crate::handlers::change_management::{
2086 change_management_router, ChangeManagementState,
2087 };
2088 let change_state = ChangeManagementState { engine };
2089 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2090 debug!("Change management API mounted at /api/v1/change-management");
2091 }
2092 }
2093
2094 {
2096 use mockforge_core::security::get_global_risk_assessment_engine;
2097 if let Some(engine) = get_global_risk_assessment_engine().await {
2098 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2099 let risk_state = RiskAssessmentState { engine };
2100 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2101 debug!("Risk assessment API mounted at /api/v1/security/risks");
2102 }
2103 }
2104
2105 {
2107 use crate::auth::token_lifecycle::TokenLifecycleManager;
2108 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2109 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2110 let lifecycle_state = TokenLifecycleState {
2111 manager: lifecycle_manager,
2112 };
2113 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2114 debug!("Token lifecycle API mounted at /api/v1/auth");
2115 }
2116
2117 {
2119 use crate::auth::oidc::load_oidc_state;
2120 use crate::auth::token_lifecycle::TokenLifecycleManager;
2121 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2122 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2124 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2125 let oauth2_state = OAuth2ServerState {
2126 oidc_state,
2127 lifecycle_manager,
2128 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2129 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2130 };
2131 app = app.merge(oauth2_server_router(oauth2_state));
2132 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2133 }
2134
2135 {
2137 use crate::auth::oidc::load_oidc_state;
2138 use crate::auth::risk_engine::RiskEngine;
2139 use crate::auth::token_lifecycle::TokenLifecycleManager;
2140 use crate::handlers::consent::{consent_router, ConsentState};
2141 use crate::handlers::oauth2_server::OAuth2ServerState;
2142 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2144 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2145 let oauth2_state = OAuth2ServerState {
2146 oidc_state: oidc_state.clone(),
2147 lifecycle_manager: lifecycle_manager.clone(),
2148 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2149 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2150 };
2151 let risk_engine = Arc::new(RiskEngine::default());
2152 let consent_state = ConsentState {
2153 oauth2_state,
2154 risk_engine,
2155 };
2156 app = app.merge(consent_router(consent_state));
2157 debug!("Consent screen endpoints mounted at /consent");
2158 }
2159
2160 {
2162 use crate::auth::risk_engine::RiskEngine;
2163 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2164 let risk_engine = Arc::new(RiskEngine::default());
2165 let risk_state = RiskSimulationState { risk_engine };
2166 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2167 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2168 }
2169
2170 let database = {
2172 use crate::database::Database;
2173 let database_url = std::env::var("DATABASE_URL").ok();
2174 match Database::connect_optional(database_url.as_deref()).await {
2175 Ok(db) => {
2176 if db.is_connected() {
2177 if let Err(e) = db.migrate_if_connected().await {
2179 warn!("Failed to run database migrations: {}", e);
2180 } else {
2181 info!("Database connected and migrations applied");
2182 }
2183 }
2184 Some(db)
2185 }
2186 Err(e) => {
2187 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2188 None
2189 }
2190 }
2191 };
2192
2193 let (drift_engine, incident_manager, drift_config) = {
2196 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2197 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2198 use std::sync::Arc;
2199
2200 let drift_config = DriftBudgetConfig::default();
2202 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2203
2204 let incident_store = Arc::new(IncidentStore::default());
2206 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2207
2208 (drift_engine, incident_manager, drift_config)
2209 };
2210
2211 {
2212 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2213 use crate::middleware::drift_tracking::DriftTrackingState;
2214 use mockforge_contracts::consumer_contracts::{
2215 ConsumerBreakingChangeDetector, UsageRecorder,
2216 };
2217 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2218 use std::sync::Arc;
2219
2220 let usage_recorder = Arc::new(UsageRecorder::default());
2222 let consumer_detector =
2223 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2224
2225 let diff_analyzer = if drift_config.enabled {
2227 match ContractDiffAnalyzer::new(
2228 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2229 ) {
2230 Ok(analyzer) => Some(Arc::new(analyzer)),
2231 Err(e) => {
2232 warn!("Failed to create contract diff analyzer: {}", e);
2233 None
2234 }
2235 }
2236 } else {
2237 None
2238 };
2239
2240 let spec = if let Some(ref spec_path) = spec_path {
2243 match OpenApiSpec::from_file(spec_path).await {
2244 Ok(s) => Some(Arc::new(s)),
2245 Err(e) => {
2246 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2247 None
2248 }
2249 }
2250 } else {
2251 None
2252 };
2253
2254 let drift_tracking_state = DriftTrackingState {
2256 diff_analyzer,
2257 spec,
2258 drift_engine: drift_engine.clone(),
2259 incident_manager: incident_manager.clone(),
2260 usage_recorder,
2261 consumer_detector,
2262 enabled: drift_config.enabled,
2263 };
2264
2265 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2267
2268 let drift_tracking_state_clone = drift_tracking_state.clone();
2271 app = app.layer(axum::middleware::from_fn(
2272 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2273 let state = drift_tracking_state_clone.clone();
2274 async move {
2275 if req.extensions().get::<DriftTrackingState>().is_none() {
2277 req.extensions_mut().insert(state);
2278 }
2279 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2281 .await
2282 }
2283 },
2284 ));
2285
2286 let drift_state = DriftBudgetState {
2287 engine: drift_engine.clone(),
2288 incident_manager: incident_manager.clone(),
2289 gitops_handler: None, };
2291
2292 app = app.merge(drift_budget_router(drift_state));
2293 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2294 }
2295
2296 #[cfg(feature = "pipelines")]
2298 {
2299 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2300
2301 let pipeline_state = PipelineState::new();
2302 app = app.merge(pipeline_router(pipeline_state));
2303 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2304 }
2305
2306 {
2308 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2309 use crate::handlers::forecasting::{forecasting_router, ForecastingState};
2310 use crate::handlers::semantic_drift::{semantic_drift_router, SemanticDriftState};
2311 use crate::handlers::threat_modeling::{threat_modeling_router, ThreatModelingState};
2312 use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2313 use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2314 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2315 use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2316 use std::sync::Arc;
2317
2318 let forecasting_config = ForecastingConfig::default();
2320 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2321 let forecasting_state = ForecastingState {
2322 forecaster,
2323 database: database.clone(),
2324 };
2325
2326 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2328 let semantic_state = SemanticDriftState {
2329 manager: semantic_manager,
2330 database: database.clone(),
2331 };
2332
2333 let threat_config = ThreatModelingConfig::default();
2335 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2336 Ok(analyzer) => Arc::new(analyzer),
2337 Err(e) => {
2338 warn!("Failed to create threat analyzer: {}. Using default.", e);
2339 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2340 |_| {
2341 ThreatAnalyzer::new(ThreatModelingConfig {
2343 enabled: false,
2344 ..Default::default()
2345 })
2346 .expect("Failed to create fallback threat analyzer")
2347 },
2348 ))
2349 }
2350 };
2351 let mut webhook_configs = Vec::new();
2353 let config_paths = [
2354 "config.yaml",
2355 "mockforge.yaml",
2356 "tools/mockforge/config.yaml",
2357 "../tools/mockforge/config.yaml",
2358 ];
2359
2360 for path in &config_paths {
2361 if let Ok(config) = mockforge_core::config::load_config(path).await {
2362 if !config.incidents.webhooks.is_empty() {
2363 webhook_configs = config.incidents.webhooks.clone();
2364 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2365 break;
2366 }
2367 }
2368 }
2369
2370 if webhook_configs.is_empty() {
2371 debug!("No webhook configs found in config files, using empty list");
2372 }
2373
2374 let threat_state = ThreatModelingState {
2375 analyzer: threat_analyzer,
2376 webhook_configs,
2377 database: database.clone(),
2378 };
2379
2380 let contract_health_state = ContractHealthState {
2382 incident_manager: incident_manager.clone(),
2383 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2384 database: database.clone(),
2385 };
2386
2387 app = app.merge(forecasting_router(forecasting_state));
2389 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2390
2391 app = app.merge(semantic_drift_router(semantic_state));
2392 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2393
2394 app = app.merge(threat_modeling_router(threat_state));
2395 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2396
2397 app = app.merge(contract_health_router(contract_health_state));
2398 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2399 }
2400
2401 {
2403 use crate::handlers::protocol_contracts::{
2404 protocol_contracts_router, ProtocolContractState,
2405 };
2406 use mockforge_core::contract_drift::{
2407 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2408 };
2409 use std::sync::Arc;
2410 use tokio::sync::RwLock;
2411
2412 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2414
2415 let mut fitness_registry = FitnessFunctionRegistry::new();
2417
2418 let config_paths = [
2420 "config.yaml",
2421 "mockforge.yaml",
2422 "tools/mockforge/config.yaml",
2423 "../tools/mockforge/config.yaml",
2424 ];
2425
2426 let mut config_loaded = false;
2427 for path in &config_paths {
2428 if let Ok(config) = mockforge_core::config::load_config(path).await {
2429 if !config.contracts.fitness_rules.is_empty() {
2430 if let Err(e) =
2431 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2432 {
2433 warn!("Failed to load fitness rules from config {}: {}", path, e);
2434 } else {
2435 info!(
2436 "Loaded {} fitness rules from config: {}",
2437 config.contracts.fitness_rules.len(),
2438 path
2439 );
2440 config_loaded = true;
2441 break;
2442 }
2443 }
2444 }
2445 }
2446
2447 if !config_loaded {
2448 debug!("No fitness rules found in config files, using empty registry");
2449 }
2450
2451 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2452
2453 let consumer_mapping_registry =
2457 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2458 let consumer_analyzer =
2459 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2460
2461 let protocol_state = ProtocolContractState {
2462 registry: contract_registry,
2463 drift_engine: Some(drift_engine.clone()),
2464 incident_manager: Some(incident_manager.clone()),
2465 fitness_registry: Some(fitness_registry),
2466 consumer_analyzer: Some(consumer_analyzer),
2467 };
2468
2469 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2470 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2471 }
2472
2473 #[cfg(feature = "behavioral-cloning")]
2475 {
2476 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2477 use std::path::PathBuf;
2478
2479 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2481 .ok()
2482 .map(PathBuf::from)
2483 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2484
2485 let bc_middleware_state = if let Some(path) = db_path {
2486 BehavioralCloningMiddlewareState::with_database_path(path)
2487 } else {
2488 BehavioralCloningMiddlewareState::new()
2489 };
2490
2491 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2493 .ok()
2494 .and_then(|v| v.parse::<bool>().ok())
2495 .unwrap_or(false);
2496
2497 if enabled {
2498 let bc_state_clone = bc_middleware_state.clone();
2499 app = app.layer(axum::middleware::from_fn(
2500 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2501 let state = bc_state_clone.clone();
2502 async move {
2503 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2505 req.extensions_mut().insert(state);
2506 }
2507 crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
2509 req, next,
2510 )
2511 .await
2512 }
2513 },
2514 ));
2515 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2516 }
2517 }
2518
2519 {
2521 use crate::handlers::consumer_contracts::{
2522 consumer_contracts_router, ConsumerContractsState,
2523 };
2524 use mockforge_contracts::consumer_contracts::{
2525 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2526 };
2527 use std::sync::Arc;
2528
2529 let registry = Arc::new(ConsumerRegistry::default());
2531
2532 let usage_recorder = Arc::new(UsageRecorder::default());
2534
2535 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2537
2538 let consumer_state = ConsumerContractsState {
2539 registry,
2540 usage_recorder,
2541 detector,
2542 violations: Arc::new(RwLock::new(HashMap::new())),
2543 };
2544
2545 app = app.merge(consumer_contracts_router(consumer_state));
2546 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2547 }
2548
2549 #[cfg(feature = "behavioral-cloning")]
2551 {
2552 use crate::handlers::behavioral_cloning::{
2553 behavioral_cloning_router, BehavioralCloningState,
2554 };
2555 use std::path::PathBuf;
2556
2557 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2559 .ok()
2560 .map(PathBuf::from)
2561 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2562
2563 let bc_state = if let Some(path) = db_path {
2564 BehavioralCloningState::with_database_path(path)
2565 } else {
2566 BehavioralCloningState::new()
2567 };
2568
2569 app = app.merge(behavioral_cloning_router(bc_state));
2570 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2571 }
2572
2573 {
2575 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2576 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2577 use mockforge_core::consistency::ConsistencyEngine;
2578 use std::sync::Arc;
2579
2580 let consistency_engine = Arc::new(ConsistencyEngine::new());
2582
2583 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2585 consistency_engine.register_adapter(http_adapter.clone()).await;
2586
2587 let consistency_state = ConsistencyState {
2589 engine: consistency_engine.clone(),
2590 };
2591
2592 use crate::handlers::xray::XRayState;
2594 let xray_state = Arc::new(XRayState {
2595 engine: consistency_engine.clone(),
2596 request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
2597 });
2598
2599 let consistency_middleware_state = ConsistencyMiddlewareState {
2601 engine: consistency_engine.clone(),
2602 adapter: http_adapter,
2603 xray_state: Some(xray_state.clone()),
2604 };
2605
2606 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2608 app = app.layer(axum::middleware::from_fn(
2609 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2610 let state = consistency_middleware_state_clone.clone();
2611 async move {
2612 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2614 req.extensions_mut().insert(state);
2615 }
2616 consistency::middleware::consistency_middleware(req, next).await
2618 }
2619 },
2620 ));
2621
2622 app = app.merge(consistency_router(consistency_state));
2624 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2625
2626 {
2628 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2629 let fidelity_state = FidelityState::new();
2630 app = app.merge(fidelity_router(fidelity_state));
2631 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2632 }
2633
2634 {
2636 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2637 let scenario_studio_state = ScenarioStudioState::new();
2638 app = app.merge(scenario_studio_router(scenario_studio_state));
2639 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2640 }
2641
2642 {
2644 use crate::handlers::performance::{performance_router, PerformanceState};
2645 let performance_state = PerformanceState::new();
2646 app = app.nest("/api/performance", performance_router(performance_state));
2647 debug!("Performance mode endpoints mounted at /api/performance");
2648 }
2649
2650 {
2652 use crate::handlers::world_state::{world_state_router, WorldStateState};
2653 use mockforge_world_state::WorldStateEngine;
2654 use std::sync::Arc;
2655 use tokio::sync::RwLock;
2656
2657 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
2658 let world_state_state = WorldStateState {
2659 engine: world_state_engine,
2660 };
2661 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
2662 debug!("World state endpoints mounted at /api/world-state");
2663 }
2664
2665 {
2667 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2668 use mockforge_core::snapshots::SnapshotManager;
2669 use std::path::PathBuf;
2670
2671 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2672 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2673
2674 let snapshot_state = SnapshotState {
2675 manager: snapshot_manager,
2676 consistency_engine: Some(consistency_engine.clone()),
2677 workspace_persistence: None, vbr_engine: None, recorder: None, };
2681
2682 app = app.merge(snapshot_router(snapshot_state));
2683 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2684
2685 {
2687 use crate::handlers::xray::xray_router;
2688 app = app.merge(xray_router((*xray_state).clone()));
2689 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2690 }
2691 }
2692
2693 {
2695 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2696 use crate::middleware::ab_testing::ab_testing_middleware;
2697
2698 let ab_testing_state = ABTestingState::new();
2699
2700 let ab_testing_state_clone = ab_testing_state.clone();
2702 app = app.layer(axum::middleware::from_fn(
2703 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2704 let state = ab_testing_state_clone.clone();
2705 async move {
2706 if req.extensions().get::<ABTestingState>().is_none() {
2708 req.extensions_mut().insert(state);
2709 }
2710 ab_testing_middleware(req, next).await
2712 }
2713 },
2714 ));
2715
2716 app = app.merge(ab_testing_router(ab_testing_state));
2718 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2719 }
2720 }
2721
2722 {
2724 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
2725 use mockforge_core::pr_generation::{PRGenerator, PRProvider};
2726 use std::sync::Arc;
2727
2728 let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
2730
2731 let generator = if pr_config.enabled && pr_config.token.is_some() {
2732 let token = pr_config.token.as_ref().unwrap().clone();
2733 let generator = match pr_config.provider {
2734 PRProvider::GitHub => PRGenerator::new_github(
2735 pr_config.owner.clone(),
2736 pr_config.repo.clone(),
2737 token,
2738 pr_config.base_branch.clone(),
2739 ),
2740 PRProvider::GitLab => PRGenerator::new_gitlab(
2741 pr_config.owner.clone(),
2742 pr_config.repo.clone(),
2743 token,
2744 pr_config.base_branch.clone(),
2745 ),
2746 };
2747 Some(Arc::new(generator))
2748 } else {
2749 None
2750 };
2751
2752 let pr_state = PRGenerationState {
2753 generator: generator.clone(),
2754 };
2755
2756 app = app.merge(pr_generation_router(pr_state));
2757 if generator.is_some() {
2758 debug!(
2759 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
2760 pr_config.provider
2761 );
2762 } else {
2763 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
2764 }
2765 }
2766
2767 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
2769
2770 if let Some(mt_config) = multi_tenant_config {
2772 if mt_config.enabled {
2773 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
2774 use std::sync::Arc;
2775
2776 info!(
2777 "Multi-tenant mode enabled with {} routing strategy",
2778 match mt_config.routing_strategy {
2779 mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
2780 mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
2781 mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
2782 }
2783 );
2784
2785 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
2787
2788 let default_workspace =
2790 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
2791 if let Err(e) =
2792 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
2793 {
2794 warn!("Failed to register default workspace: {}", e);
2795 } else {
2796 info!("Registered default workspace: '{}'", mt_config.default_workspace);
2797 }
2798
2799 let registry = Arc::new(registry);
2801
2802 let _workspace_router = WorkspaceRouter::new(registry);
2804 info!("Workspace routing middleware initialized for HTTP server");
2805 }
2806 }
2807
2808 let mut final_cors_config = cors_config;
2810 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
2811 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
2813 let mut rate_limit_config = middleware::RateLimitConfig {
2814 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
2815 .ok()
2816 .and_then(|v| v.parse().ok())
2817 .unwrap_or(1000),
2818 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
2819 .ok()
2820 .and_then(|v| v.parse().ok())
2821 .unwrap_or(2000),
2822 per_ip: true,
2823 per_endpoint: false,
2824 };
2825
2826 if let Some(deploy_config) = &deceptive_deploy_config {
2827 if deploy_config.enabled {
2828 info!("Deceptive deploy mode enabled - applying production-like configuration");
2829
2830 if let Some(prod_cors) = &deploy_config.cors {
2832 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
2833 enabled: true,
2834 allowed_origins: prod_cors.allowed_origins.clone(),
2835 allowed_methods: prod_cors.allowed_methods.clone(),
2836 allowed_headers: prod_cors.allowed_headers.clone(),
2837 allow_credentials: prod_cors.allow_credentials,
2838 });
2839 info!("Applied production-like CORS configuration");
2840 }
2841
2842 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
2844 rate_limit_config = middleware::RateLimitConfig {
2845 requests_per_minute: prod_rate_limit.requests_per_minute,
2846 burst: prod_rate_limit.burst,
2847 per_ip: prod_rate_limit.per_ip,
2848 per_endpoint: false,
2849 };
2850 info!(
2851 "Applied production-like rate limiting: {} req/min, burst: {}",
2852 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
2853 );
2854 }
2855
2856 if !deploy_config.headers.is_empty() {
2858 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
2859 production_headers = Some(std::sync::Arc::new(headers_map));
2860 info!("Configured {} production headers", deploy_config.headers.len());
2861 }
2862
2863 if let Some(prod_oauth) = &deploy_config.oauth {
2865 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
2866 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
2867 oauth2: Some(oauth2_config),
2868 ..Default::default()
2869 });
2870 info!("Applied production-like OAuth configuration for deceptive deploy");
2871 }
2872 }
2873 }
2874
2875 let rate_limiter =
2877 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
2878
2879 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
2880
2881 if let Some(headers) = production_headers.clone() {
2883 state = state.with_production_headers(headers);
2884 }
2885
2886 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
2888
2889 if state.production_headers.is_some() {
2891 app =
2892 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
2893 }
2894
2895 if let Some(auth_config) = deceptive_deploy_auth_config {
2897 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
2898 use std::collections::HashMap;
2899 use std::sync::Arc;
2900 use tokio::sync::RwLock;
2901
2902 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
2904 match create_oauth2_client(oauth2_config) {
2905 Ok(client) => Some(client),
2906 Err(e) => {
2907 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
2908 None
2909 }
2910 }
2911 } else {
2912 None
2913 };
2914
2915 let auth_state = AuthState {
2917 config: auth_config,
2918 spec: None, oauth2_client,
2920 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
2921 };
2922
2923 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
2925 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
2926 }
2927
2928 #[cfg(feature = "runtime-daemon")]
2930 {
2931 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
2932 use std::sync::Arc;
2933
2934 let daemon_config = RuntimeDaemonConfig::from_env();
2936
2937 if daemon_config.enabled {
2938 info!("Runtime daemon enabled - auto-creating mocks from 404s");
2939
2940 let management_api_url =
2942 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
2943 let port =
2944 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
2945 format!("http://localhost:{}", port)
2946 });
2947
2948 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
2950
2951 let detector = NotFoundDetector::new(daemon_config.clone());
2953 detector.set_generator(generator).await;
2954
2955 let detector_clone = detector.clone();
2957 app = app.layer(axum::middleware::from_fn(
2958 move |req: axum::extract::Request, next: axum::middleware::Next| {
2959 let detector = detector_clone.clone();
2960 async move { detector.detect_and_auto_create(req, next).await }
2961 },
2962 ));
2963
2964 debug!("Runtime daemon 404 detection middleware added");
2965 }
2966 }
2967
2968 {
2970 let routes_state = HttpServerState::with_routes(captured_routes);
2971 let routes_router = Router::new()
2972 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
2973 .with_state(routes_state);
2974 app = app.merge(routes_router);
2975 }
2976
2977 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
2979
2980 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
2985
2986 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
2989
2990 app = apply_cors_middleware(app, final_cors_config);
2992
2993 app
2994}
2995
2996#[test]
3000fn test_route_info_clone() {
3001 let route = RouteInfo {
3002 method: "POST".to_string(),
3003 path: "/users".to_string(),
3004 operation_id: Some("createUser".to_string()),
3005 summary: None,
3006 description: None,
3007 parameters: vec![],
3008 };
3009
3010 let cloned = route.clone();
3011 assert_eq!(route.method, cloned.method);
3012 assert_eq!(route.path, cloned.path);
3013 assert_eq!(route.operation_id, cloned.operation_id);
3014}
3015
3016#[test]
3017fn test_http_server_state_new() {
3018 let state = HttpServerState::new();
3019 assert_eq!(state.routes.len(), 0);
3020}
3021
3022#[test]
3023fn test_http_server_state_with_routes() {
3024 let routes = vec![
3025 RouteInfo {
3026 method: "GET".to_string(),
3027 path: "/users".to_string(),
3028 operation_id: Some("getUsers".to_string()),
3029 summary: None,
3030 description: None,
3031 parameters: vec![],
3032 },
3033 RouteInfo {
3034 method: "POST".to_string(),
3035 path: "/users".to_string(),
3036 operation_id: Some("createUser".to_string()),
3037 summary: None,
3038 description: None,
3039 parameters: vec![],
3040 },
3041 ];
3042
3043 let state = HttpServerState::with_routes(routes.clone());
3044 assert_eq!(state.routes.len(), 2);
3045 assert_eq!(state.routes[0].method, "GET");
3046 assert_eq!(state.routes[1].method, "POST");
3047}
3048
3049#[test]
3050fn test_http_server_state_clone() {
3051 let routes = vec![RouteInfo {
3052 method: "GET".to_string(),
3053 path: "/test".to_string(),
3054 operation_id: None,
3055 summary: None,
3056 description: None,
3057 parameters: vec![],
3058 }];
3059
3060 let state = HttpServerState::with_routes(routes);
3061 let cloned = state.clone();
3062
3063 assert_eq!(state.routes.len(), cloned.routes.len());
3064 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3065}
3066
3067#[tokio::test]
3068async fn test_build_router_without_openapi() {
3069 let _router = build_router(None, None, None).await;
3070 }
3072
3073#[tokio::test]
3074async fn test_build_router_with_nonexistent_spec() {
3075 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3076 }
3078
3079#[tokio::test]
3080async fn test_build_router_with_auth_and_latency() {
3081 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3082 }
3084
3085#[tokio::test]
3086async fn test_build_router_with_latency() {
3087 let _router = build_router_with_latency(None, None, None).await;
3088 }
3090
3091#[tokio::test]
3092async fn test_build_router_with_auth() {
3093 let _router = build_router_with_auth(None, None, None).await;
3094 }
3096
3097#[tokio::test]
3098async fn test_build_router_with_chains() {
3099 let _router = build_router_with_chains(None, None, None).await;
3100 }
3102
3103#[test]
3104fn test_route_info_with_all_fields() {
3105 let route = RouteInfo {
3106 method: "PUT".to_string(),
3107 path: "/users/{id}".to_string(),
3108 operation_id: Some("updateUser".to_string()),
3109 summary: Some("Update user".to_string()),
3110 description: Some("Updates an existing user".to_string()),
3111 parameters: vec!["id".to_string(), "body".to_string()],
3112 };
3113
3114 assert!(route.operation_id.is_some());
3115 assert!(route.summary.is_some());
3116 assert!(route.description.is_some());
3117 assert_eq!(route.parameters.len(), 2);
3118}
3119
3120#[test]
3121fn test_route_info_with_minimal_fields() {
3122 let route = RouteInfo {
3123 method: "DELETE".to_string(),
3124 path: "/users/{id}".to_string(),
3125 operation_id: None,
3126 summary: None,
3127 description: None,
3128 parameters: vec![],
3129 };
3130
3131 assert!(route.operation_id.is_none());
3132 assert!(route.summary.is_none());
3133 assert!(route.description.is_none());
3134 assert_eq!(route.parameters.len(), 0);
3135}
3136
3137#[test]
3138fn test_http_server_state_empty_routes() {
3139 let state = HttpServerState::with_routes(vec![]);
3140 assert_eq!(state.routes.len(), 0);
3141}
3142
3143#[test]
3144fn test_http_server_state_multiple_routes() {
3145 let routes = vec![
3146 RouteInfo {
3147 method: "GET".to_string(),
3148 path: "/users".to_string(),
3149 operation_id: Some("listUsers".to_string()),
3150 summary: Some("List all users".to_string()),
3151 description: None,
3152 parameters: vec![],
3153 },
3154 RouteInfo {
3155 method: "GET".to_string(),
3156 path: "/users/{id}".to_string(),
3157 operation_id: Some("getUser".to_string()),
3158 summary: Some("Get a user".to_string()),
3159 description: None,
3160 parameters: vec!["id".to_string()],
3161 },
3162 RouteInfo {
3163 method: "POST".to_string(),
3164 path: "/users".to_string(),
3165 operation_id: Some("createUser".to_string()),
3166 summary: Some("Create a user".to_string()),
3167 description: None,
3168 parameters: vec!["body".to_string()],
3169 },
3170 ];
3171
3172 let state = HttpServerState::with_routes(routes);
3173 assert_eq!(state.routes.len(), 3);
3174
3175 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3177 assert!(methods.contains(&"GET"));
3178 assert!(methods.contains(&"POST"));
3179}
3180
3181#[test]
3182fn test_http_server_state_with_rate_limiter() {
3183 use std::sync::Arc;
3184
3185 let config = crate::middleware::RateLimitConfig::default();
3186 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
3187
3188 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3189
3190 assert!(state.rate_limiter.is_some());
3191 assert_eq!(state.routes.len(), 0);
3192}
3193
3194#[tokio::test]
3195async fn test_build_router_includes_rate_limiter() {
3196 let _router = build_router(None, None, None).await;
3197 }