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