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